chore: project scaffold, specs, and working record
Build and deploy / build-and-deploy (push) Failing after 5s

This commit is contained in:
Pouya Lajevardi
2026-08-26 08:51:16 -04:00
commit 19f7226661
26 changed files with 3206 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
# 05 — Intake, booking, and data handling
Authority: `AGENTS.md` §3 D10 — rebuilt intake form plus calendar booking.
Existing infrastructure is documented in `AWS-Hosting-Guide.md` Parts 810.
**Read that guide before changing anything**; the resources already exist and
were built by hand in the console.
---
## What exists today
API Gateway (HTTP API) → Lambda → DynamoDB, with SES for notification email and
a verified sender on `smlcompany.ca`. `[verified 2026-08-25 — AWS-Hosting-Guide.md]`
The shape is right. This is a hardening and rework pass, not a replacement.
## What this data actually is
The form collects, in a live legal dispute: the inquirer's identity and contact
details, the names of opposing parties and their counsel, the nature of the
dispute, and often the amounts at issue.
That is **personal information about identifiable third parties who have not
consented and do not know the submission happened.** It is more sensitive than a
typical contact form by a wide margin, and it is potentially conflict-relevant.
Design accordingly. Nothing in this section is optional.
---
## Form fields
| Field | Type | Required | Notes |
|---|---|---|---|
| Name | text | yes | |
| Email | email | yes | Validated server-side, not only in the browser |
| Phone | tel | no | |
| Role | select | yes | Counsel · In-house · Party · Institution · Other |
| Firm / organisation | text | no | |
| Process sought | select | yes | Mediation · Arbitration · Med-Arb · ENE · Not sure |
| Practice area | select | yes | The six areas plus Other |
| Other parties | text | no | Surfaced for conflicts screening |
| Opposing counsel | text | no | Same |
| Matter summary | textarea | yes | 2000 char cap. Hint: *no privileged or confidential detail* |
| Timing | select | no | Urgent · 30 days · 90 days · Exploring |
| Preferred contact | radio | no | Email · Phone |
| Consent | checkbox | **yes** | Explicit, unchecked by default, links to `/legal/privacy/` |
**Do not collect** dollar amounts, document uploads, or anything the inquirer
might reasonably treat as privileged. The intake call is for that.
### Consent text
> I consent to Pouya Lajevardi storing and using the information in this form to
> respond to my inquiry and to run a conflicts check. I understand that
> submitting this form does not create a retainer, does not appoint a neutral,
> and does not itself establish a mediatorparty relationship.
## Validation and abuse control
Client-side validation is a convenience. **The Lambda re-validates everything.**
- Required fields present; email well-formed; lengths within bounds
- Reject any field over its cap rather than truncating silently
- **Honeypot** field, hidden from sighted and screen-reader users, must be empty
- **Timestamp check** — reject submissions completed in under 3 seconds
- **Rate limit** by source IP at API Gateway: 5 requests / 5 minutes
- No CAPTCHA. It is a third-party script on a page collecting legal information,
and the two controls above stop the traffic that matters
- CORS restricted to `https://adr.smlcompany.ca` — no wildcard
- Strip HTML from every field before storage and before it enters an email body
## Storage
DynamoDB, `ca-central-1` — **Canadian data residency is a real selling point for
a Canadian legal practice, and the privacy policy will say so.** Confirm the
existing table's region and migrate if it is elsewhere (**Q10**).
| Attribute | |
|---|---|
| `pk` | `INTAKE#<uuid>` |
| `sk` | `<ISO-8601 timestamp>` |
| fields | as above |
| `sourceIp`, `userAgent` | abuse investigation only |
| `ttl` | epoch seconds — **automatic deletion** |
**Encryption at rest** with a customer-managed KMS key. **Point-in-time recovery
on.** Table access limited to the Lambda role and one named administrative
principal.
### Retention
**24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a
mechanism that runs whether anyone remembers or not.
Rationale: long enough to serve conflicts screening across a normal matter
lifecycle; short enough to be defensible under PIPEDA's requirement to retain
personal information only as long as necessary. Whatever number ships must match
`/legal/privacy/` exactly.
## Notification
SES on submission:
- **To Pouya:** the full submission, plainly formatted, replyable to the inquirer.
- **To the inquirer:** confirmation of receipt, expected response time, a repeat
of the no-retainer language, and a link to the privacy policy. This email is
the reason the form beats a `mailto:` link.
SES must have SPF, DKIM, and DMARC aligned on `smlcompany.ca` or these land in
spam. The guide covers domain verification; **DMARC needs confirming (Q3)**.
Failure handling: SES failure must never lose the submission. Write to DynamoDB
first, then send. A dead-letter queue on the Lambda, and a CloudWatch alarm on
DLQ depth ≥ 1.
## Booking
An embedded scheduler for the 3045 minute confidential intake call
(**Q5** — tool not yet chosen).
- Prefer a provider with Canadian or EU data residency and no advertising
business. Cal.com self-hosted is the strongest privacy posture; Cal.com cloud
or Calendly are acceptable.
- **Lazy-load behind a click.** No third-party iframe on first paint, and no
third-party script on any other page.
- Provide a plain link fallback that works with JavaScript disabled.
- The booking page must carry the same no-retainer language.
- Disclose the provider by name in `/legal/privacy/`.
## Security headers
Set at CloudFront via a response-headers policy:
```
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; img-src 'self' data:;
style-src 'self' 'unsafe-inline'; script-src 'self';
frame-src <booking-provider>; form-action 'self' <api-endpoint>;
base-uri 'self'; frame-ancestors 'none'
```
Tighten CSP once the booking provider is chosen. `unsafe-inline` on styles is
tolerable for critical CSS; `unsafe-inline` on scripts is not — use a hash or
nonce for the reveal script.
## Privacy policy must state
Written to match what is actually built, not what is typical:
What is collected · why · lawful basis (consent) · where it is stored (DynamoDB,
region, encrypted at rest) · **the retention period and that deletion is
automatic** · who can access it · third parties involved (AWS, SES, the booking
provider, analytics if any) · how to request access or deletion and the address
to use · that submitting the form creates no retainer and no mediatorparty
relationship · cookie and analytics disclosure · last-updated date.
If analytics ship, prefer a cookieless privacy-preserving tool (Plausible,
Fathom). GA4 on a page collecting legal-dispute information is a poor fit for a
practice whose privacy posture is part of its offer (**Q11**).
## Definition of done
- [ ] Server-side validation independent of the client
- [ ] Honeypot and timing checks live; rate limit configured
- [ ] CORS restricted to the production origin
- [ ] TTL set and verified by test record
- [ ] KMS encryption and PITR enabled
- [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested
- [ ] DLQ and CloudWatch alarm configured
- [ ] Form usable by keyboard only; errors announced with `role="alert"`
- [ ] Form degrades to a `mailto:` fallback with JavaScript disabled
- [ ] Privacy policy matches the implementation line for line