Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/verify-ww-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ node scripts/verify/dev.mjs kv get app-store-ratings:summary
node scripts/verify/dev.mjs kv delete buddies:enabled
```

- For the isolated local worker, send a unique `cf-connecting-ip` per request (`198.18.x.y` or similar). Never send synthetic caller IPs to a deployed Worker: Cloudflare rejects them before the request reaches the API. Without one, every request shares the same per-IP key, and you hit the local rate limiters: 60/min per family (`/geocode` + `/autocomplete`, `/notes-import*`, `/route-planning/*`, and `/admin/*` each count separately), and the Buddies per-caller tiers (IPv6 grouped by /64): 120/min for `invite/fetch` + `invite/claim`, 60/min for `inbox/register`, 600/min for `inbox/sync` + `inbox/live`, 60/min for `blob/put`, 600/min for `blob/get`, 600/min for other signed ops. The e2e helpers and the fuzzer already do this.
- For the isolated local worker, send a unique `cf-connecting-ip` per request (`198.18.x.y` or similar). Never send synthetic caller IPs to a deployed Worker: Cloudflare rejects them before the request reaches the API. Without one, every request shares the same per-IP key, and you hit the local rate limiters: 60/min per family (`/geocode` + `/autocomplete`, `/notes-import*`, `/attestation/*`, `/route-planning/*`, and `/admin/*` each count separately), and the Buddies per-caller tiers (IPv6 grouped by /64): 120/min for `invite/fetch` + `invite/claim`, 60/min for `inbox/register`, 600/min for `inbox/sync` + `inbox/live`, 60/min for `blob/put`, 600/min for `blob/get`, 600/min for other signed ops. The e2e helpers and the fuzzer already do this.
- Signed Buddies ops need Ed25519 envelopes. Use `RelayOwner` and `RelayWriter` in `src/test/e2e.ts` (built on `src/test/buddiesClient.ts`) rather than hand-rolling curl. Photo blobs: `photoBlob()` makes sealed-looking bytes with their id and token, `RelayOwner.putBlob` and `getBlob` send them (raw bytes, envelope in headers).
- KV edits take effect on the next request (no 60 s edge cache locally).

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/verify-ww-api/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ Keep implementation details out of the map. Name only user paths, stable handles
## Features

- [Health and status](./health-and-status.md) covers `/health`, `/notes-import/status`, App Store ratings, and the JSON 404.
- [Notes Import](./notes-import.md) covers the dev-bypass verify probe, the App Attest challenge, kickoff's auth gate, and the opt-in paid kickoff.
- [Notes Import](./notes-import.md) covers the attestation verify probe and challenge (`/attestation/v1/*` and their old `/notes-import/*` paths), kickoff's auth gate, and the opt-in paid kickoff.
- [Buddies relay](./buddies-relay.md) covers pairing end to end, sync read-backs, auth failures, caps, the kill switch, and fuzzing.
- [Universal link pages](./universal-link-pages.md) covers the AASA, the `/c` and `/c/:payload` contact pages, and the `/b` invite page.
- [Universal link pages](./universal-link-pages.md) covers the AASA, the `/contacts` (`/c`, `/c/:payload`) contact pages, and the `/buddies/invite` (`/b`) invite page.
- [Admin reset](./admin.md) covers the maintainer usage reset and its 404 fail-closed auth.

Not mapped yet: supporter route planning (`/route-planning`, needs `HERE_API_KEY`). Map it with `maintain-verification-skill` before relying on it.
2 changes: 1 addition & 1 deletion .agents/skills/verify-ww-api/features/buddies-relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Buddies lets two publishers share upcoming Plans through a blind relay. Each per

## How to get to it (user POV)

- In the app: Buddies → Invite a buddy creates an invite and shares `https://ww-proxy.leviwilkerson.com/b#1<secret>`.
- In the app: Buddies → Invite a buddy creates an invite and shares `https://ww-proxy.leviwilkerson.com/b#1<secret>` (`/buddies/invite#1<secret>` once the app generates the readable path).
- The buddy taps the link. The app opens, fetches and claims the invite, and the inviter confirms.
- Opening the Buddies tab or the calendar syncs the inbox, and editing Plans writes a card. Removing a buddy runs leave. Deleting Buddies data deletes the inbox.

Expand Down
18 changes: 10 additions & 8 deletions .agents/skills/verify-ww-api/features/notes-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ Notes Import turns a publisher's free-form notes into contacts, visits, and time

## Sub-features

- `verify-v1` accepts `{}` with a valid `x-ww-dev-bypass` and returns `{"ok":true}`.
- `verify-v1` (`POST /attestation/v1/verify`) accepts `{}` with a valid `x-ww-dev-bypass` and returns `{"ok":true}`.
- `verify-v2` echoes `operationId` for a v2 probe whose `requestHash` equals `contentHash`.
- `verify-v2-mismatch` returns 400 when the two hashes differ.
- `verify-wrong-bypass` treats a wrong token as unattested and returns 400 `Missing uuid, …`.
- `challenge` issues an App Attest challenge.
- `challenge` (`POST /attestation/v1/challenge`) issues an App Attest challenge.
- `legacy-paths`: `/notes-import/challenge`, `/attest`, and `/verify` answer like their `/attestation/v1/` successors, adding `Deprecation` and a `successor-version` `Link`.
- `kickoff-gate` returns 401 `attestation_required` for kickoff without credentials (503 when no provider is healthy). No inference runs.
- `kickoff-paid` checks dates, duration conversion, existing contact/category reuse, and publisher role, then applies a correction and checks the complete result. It makes two real model calls, spends money, and is opt-in.

## How to get to it (user POV)

- In the app: Tools → Notes Import, paste notes, and import. The app calls `challenge`, `attest`, `kickoff`, `events`, then `result`.
- The app's dev-tools diagnostics screen calls `POST /notes-import/verify` to prove the attestation path.
- In the app: Tools → Notes Import, paste notes, and import. The app calls the attestation `challenge` and `register` (`attest` on the old paths), then `kickoff`, `events`, and `result`.
- The app's dev-tools diagnostics screen calls `POST /attestation/v1/verify` (older builds: `/notes-import/verify`) to prove the attestation path.
- Simulators and emulators can't attest, so dev builds on both platforms send the `x-ww-dev-bypass` header to the dev worker.

## Driving it with verify-ww-api
Expand All @@ -25,18 +26,19 @@ Preconditions:
- `doctor` passes, including `ok dev bypass configured (value hidden)`.
- `URL` and `BYPASS` are exported (see README).

- **v1 probe.** Run `curl -s -X POST $URL/notes-import/verify -H "x-ww-dev-bypass: $BYPASS" -H "cf-connecting-ip: 198.18.2.1" -d '{}'`. The body is `{"ok":true}`.
- **v2 probe.** Run `H=$(printf verify | shasum -a 256 | cut -d' ' -f1); curl -s -X POST $URL/notes-import/verify -H "x-ww-dev-bypass: $BYPASS" -H "cf-connecting-ip: 198.18.2.2" -d "{\"protocolVersion\":2,\"operation\":\"assert\",\"operationId\":\"op_verify01\",\"purpose\":\"notes-import-verify\",\"contentHash\":\"$H\",\"requestHash\":\"$H\"}"`. The body is `{"ok":true,"protocolVersion":2,"operationId":"op_verify01"}`.
- **v1 probe.** Run `curl -s -X POST $URL/attestation/v1/verify -H "x-ww-dev-bypass: $BYPASS" -H "cf-connecting-ip: 198.18.2.1" -d '{}'`. The body is `{"ok":true}`.
- **v2 probe.** Run `H=$(printf verify | shasum -a 256 | cut -d' ' -f1); curl -s -X POST $URL/attestation/v1/verify -H "x-ww-dev-bypass: $BYPASS" -H "cf-connecting-ip: 198.18.2.2" -d "{\"protocolVersion\":2,\"operation\":\"assert\",\"operationId\":\"op_verify01\",\"purpose\":\"attestation-verify\",\"contentHash\":\"$H\",\"requestHash\":\"$H\"}"`. The body is `{"ok":true,"protocolVersion":2,"operationId":"op_verify01"}`.
- **Wrong token.** Repeat the v1 probe with `-H "x-ww-dev-bypass: nope"`. You get 400 with `Missing uuid, keyId, challenge, assertion, or contentHash`.
- **Challenge.** Run `curl -s -X POST $URL/notes-import/challenge -H "cf-connecting-ip: 198.18.2.3" -d '{}'`. The body includes a `challenge` string.
- **Challenge.** Run `curl -s -X POST $URL/attestation/v1/challenge -H "cf-connecting-ip: 198.18.2.3" -d '{}'`. The body includes a `challenge` string.
- **Old path.** Run `curl -si -X POST $URL/notes-import/challenge -H "cf-connecting-ip: 198.18.2.5" -d '{}'`. You get 200 with a `challenge`, `deprecation: @1792022400`, and `link: </attestation/v1/challenge>; rel="successor-version"`.
- **Kickoff gate.** Run `curl -s -X POST $URL/notes-import/kickoff -H "cf-connecting-ip: 198.18.2.4" -d '{"uuid":"u1","notesText":"x","context":{"now":"2026-10-01T00:00:00Z","timeZone":"UTC","existingContacts":[],"existingCategories":[]}}'`. You get 401 `attestation_required`, and `.verify/wrangler.log` shows no OpenRouter call.
- **Paid kickoff (opt-in only).** Run `WW_API_E2E_ALLOW_PAID=1 pnpm test:e2e src/e2e/notesImport.e2e.test.ts` after `up --secrets-from <file with OPENROUTER_API_KEY>`. The `PAID:` test checks both complete results and their records. For a deployed dev worker, set `WW_API_URL` and `WW_API_DEV_BYPASS_TOKEN`; its existing provider secret is used and no local OpenRouter key is required. `WW_API_VERSION_OVERRIDES` optionally supplies Cloudflare's dictionary header for a 0%-traffic dev version, e.g. `ww-proxy-dev="<version-id>"`.
- **Proof.** Run `pnpm test:e2e src/e2e/notesImport.e2e.test.ts`. The free tests pass, the `PAID:` test shows as skipped unless opted in, and the transcript is in `.verify/artifacts/e2e-notes-import-*.json`.

## Gotchas

- The bypass works only with `APP_ATTEST_ENVIRONMENT=development` (`--env dev`). Plain `pnpm dev` uses prod vars, and Notes Import routes then return 500 when a bypass token is present.
- `/notes-import*` is rate limited at 60/min per `cf-connecting-ip` (its own bucket, apart from place search). Reusing one IP across a long run produces 429s (`rate_limited`, `Retry-After: 60`) that look like bugs.
- `/notes-import*` is rate limited at 60/min per `cf-connecting-ip` (its own bucket, apart from place search; `/attestation/*` has another). Reusing one IP across a long run produces 429s (`rate_limited`, `Retry-After: 60`) that look like bugs.
- While a run is live, the SSE stream sends a `:` comment line every 15 s; finished, cancelled, and unknown runs close right after the replay.
- `/notes-import/:id/events` is SSE. Use `/result` for assertions.
- The bypass skips attestation only. Real App Attest and Play Integrity flows need a physical device against the deployed dev worker (see AGENTS.md); don't claim them from this loop.
24 changes: 12 additions & 12 deletions .agents/skills/verify-ww-api/features/universal-link-pages.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Universal link pages

Shared contacts (`/c#<payload>`) and buddy invites (`/b#1<secret>`) are universal links. On iOS, the AASA file routes them into the app. Without the app, the worker serves localized fallback pages that build the "Open app" link client-side, so the payload never reaches the server or link previews.
Shared contacts (`/contacts#<payload>`), buddy invites (`/buddies/invite#1<secret>`), and Set Up Another Device codes (`/accounts/handoff#1…`) are universal links. Older app versions shared the short `/c`, `/b`, and `/h`, which serve the same pages for good. On iOS, the AASA file routes them into the app. Without the app, the worker serves localized fallback pages that build the "Open app" link client-side, so the payload never reaches the server or link previews.

## Sub-features

- `aasa` returns JSON with three app ids (`<team>.com.leviwilkerson.jwtime`, `…jwtimebeta`, `…jwtimedev`) and the components `/c#?*`, `/c/*`, and `/b#1?*`.
- `contact-page` serves the `/c` HTML with a hidden `#open-app` link and the `witnesswork://import-contact/` script.
- `contact-locale` localizes `/c?lang=es` and `Accept-Language` (`Content-Language` header, `<html lang>`).
- `aasa` returns JSON with three app ids (`<team>.com.leviwilkerson.jwtime`, `…jwtimebeta`, `…jwtimedev`) and seven components: `/contacts#?*`, `/buddies/invite#1?*`, `/accounts/handoff#1?*`, then the short `/c#?*`, `/c/*`, `/b#1?*`, `/h#1?*`.
- `contact-page` serves the `/contacts` (and `/c`) HTML with a hidden `#open-app` link, the `witnesswork://import-contact/` script, and `og:url` set to the path it was served on.
- `contact-locale` localizes `/contacts?lang=es` and `Accept-Language` (`Content-Language` header, `<html lang>`).
- `contact-legacy` makes `/c/<payload>` render an `href="witnesswork://import-contact/<payload>"` that never appears in a meta tag, and escapes non-base64url payloads.
- `invite-page` serves `/b` with a strict CSP (`default-src 'none'`), `X-Robots-Tag: noindex, nofollow`, and `Referrer-Policy: no-referrer`.
- `invite-page` serves `/buddies/invite` (and `/b`) with a strict CSP (`default-src 'none'`), `X-Robots-Tag: noindex, nofollow`, and `Referrer-Policy: no-referrer`.

## How to get to it (user POV)

- Someone taps a shared contact link in Messages. With the app installed, iOS opens it. Without the app, the browser shows the `/c` page.
- A buddy invite link opens the app, or the `/b` page in a browser.
- Someone taps a shared contact link in Messages. With the app installed, iOS opens it. Without the app, the browser shows the `/contacts` page (`/c` for older links).
- A buddy invite link opens the app, or the `/buddies/invite` page (`/b` for older links) in a browser.
- iOS downloads `/.well-known/apple-app-site-association` when the app is installed or updated.

## Driving it with verify-ww-api
Expand All @@ -22,13 +22,13 @@ Preconditions:

- `doctor` passes and `URL` is exported.

- **AASA.** Run `curl -si $URL/.well-known/apple-app-site-association`. You get 200 with `content-type: application/json`, the app ids prefixed `VERIFY0000.` (or the real team id when passed), and the three components.
- **Contact page.** Run `curl -s $URL/c | grep -c 'id="open-app"'`. It prints `1`.
- **Locale.** Run `curl -si "$URL/c?lang=es" | grep -i content-language`. The header starts with `es`.
- **AASA.** Run `curl -si $URL/.well-known/apple-app-site-association`. You get 200 with `content-type: application/json`, the app ids prefixed `VERIFY0000.` (or the real team id when passed), and the seven components.
- **Contact page.** Run `curl -s $URL/contacts | grep -c 'id="open-app"'`, then the same for `$URL/c`. Each prints `1`.
- **Locale.** Run `curl -si "$URL/contacts?lang=es" | grep -i content-language`. The header starts with `es`.
- **Legacy link.** Run `curl -s $URL/c/abc_DEF-123 | grep -o 'witnesswork://import-contact/abc_DEF-123'`. The deep link is printed.
- **Hostile payload.** Run `curl -s "$URL/c/%22%3E%3Cscript%3Ex%3C%2Fscript%3E" | grep -c '<script>x</script>'`. It prints `0`.
- **Invite page.** Run `curl -si $URL/b`. You get 200 with a `content-security-policy` that starts `default-src 'none'` and the title `A WitnessWork buddy invite`.
- **Proof.** Run `pnpm test:e2e src/e2e/public.e2e.test.ts -t "universal links"`. All four tests pass, with the transcript in `.verify/artifacts/e2e-public-*.json`.
- **Invite page.** Run `curl -si $URL/buddies/invite` (and `$URL/b`). You get 200 with a `content-security-policy` that starts `default-src 'none'` and the title `A WitnessWork buddy invite`.
- **Proof.** Run `pnpm test:e2e src/e2e/public.e2e.test.ts -t "universal links"`. All tests pass, with the transcript in `.verify/artifacts/e2e-public-*.json`.

## Gotchas

Expand Down
Loading
Loading