176 lines
7.4 KiB
Markdown
176 lines
7.4 KiB
Markdown
# 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 8–10.
|
||
**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 mediator–party 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 30–45 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 mediator–party
|
||
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
|