feat(intake): protocol v1 — uitnodigingslink, info, foutcodes en ondertekende verzoeken (fase 4, stap 1) #2254

Merged
brenno merged 2 commits from feat/intake-protocol into main 2026-10-03 18:29:23 +00:00
Owner

Wat

Fase 4, stap 1: het contract van de inzendserver (FORM_INTAKE.md §6) vastgelegd en de zuivere helft ervan gebouwd — zonder server, zonder netwerk. De server wacht nog op een genoemde beheerder (D3); dit is wat daar niet van afhangt.

  • docs/design/INTAKE_PROTOCOL.md (CC-BY-4.0, vectoren CC0 — D5): de normatieve tekst. De uitnodigingslink, de handelingen van de invuller (info, het formulier, uploaden, intrekken) en van de redactie (publiceren, token, lijst, ophalen, ack, verwijderen), ondertekende verzoeken, de foutcodes, CORS, wat de server bewaart, bekende grenzen.
  • Uitnodigingslink (parseInviteLink): https://<shell>/f/<fid>#api=…&fp=…&t=…, in vaste volgorde gelezen. Een link zonder vingerafdruk is niet compleet en gaat niet verder (§6.4) — geen prompt met een naam uit de bundel zelf.
  • GET /v1/info (parseIntakeInfo): te oud of te nieuw wordt geweigerd, nooit geraden; een pakketplafond boven dat van de client wordt tot dat van de client verlaagd.
  • Ontvangstbevestiging, intrekgeheim en uitnodigingstoken: van beide gaat alleen de hash naar de server. IntakeErrorCode: een gesloten set met wire-naam en status; een lichaam dat niet van ons is (de pagina van een proxy) geeft toch een fout, via de status.
  • Ondertekende organisatorverzoeken (intake_request.dart): Authorization: OciIntake key=…, ts=…, nonce=…, sig=…, Ed25519 over de canonieke JSON van ["ocideck-intake-req-v1", host, methode, target, body_sha256, ts, nonce]. De verifier neemt host, methode, target en body-hash die de server kent, nooit wat de header beweert; de query zit in target, dus een pagineringscursor is onderweg niet te veranderen. Venster ±5 minuten, vóór de handtekening gecontroleerd. IntakeNonceCache onthoudt een nonce pas nadat het verzoek is geautoriseerd (een vreemde met een eigen sleutel kan hem niet vullen), precies zo lang als de ts nog kan slagen, en zegt full in plaats van er een te vergeten.
  • Gitleaks: generic-api-key las token/secret plus een waarde met entropie in het vectorbestand en in een testconstante als lek (zes keer). De constante is hernoemd; het vectorbestand — dat per definitie voorbeeldwaarden onder die namen draagt — staat nu als enig bestand in de allowlist van .gitleaks.toml, met de prijs erbij (zoals bij age_testkit): intake_protocol_vector_test rekent elke waarde na en een Dart-bestand blijft gewoon gescand.
  • intake_request.dart is het derde bestand dat primitieven mag importeren (check_packages regel 10; de poort en zijn test zijn bijgewerkt).

Aangepast ten opzichte van het ontwerp (FORM_INTAKE §6.3, nu met een aanvulling)

  1. GET /v1/forms/{fid} geeft variants (een {bundle, template} per taal) in plaats van {bundle, template, assets}: een bundel bindt één tekst (§7.1, door de eigenaar bevestigd). assets vervalt — een formulier verwijst in v1 niet naar bestanden.
  2. PUT /v1/forms/{fid} publiceert de hele set varianten in één keer; de server verifieert elke bundel zelf tegen de ondertekenaar van het verzoek.
  3. Het uitnodigingstoken staat alleen als hash op de server (token_sha256); de client van de organisator maakt het.
  4. Het grafsteentje (tombstone) bewaart de hash van het intrekgeheim, zodat een intrekking waarvan het antwoord verloren ging kan worden herhaald.
  5. state in het formulierantwoord is adviserend; de server dwingt af.

Getoetst

  • 126 tests in de kern (ook op Chrome/dart2js): elke weigering van de link met haar eigen geval, de volgorde van de controles, de grenzen (64 KiB tot op de byte, het venster van 300 s tot op de seconde), elke manier waarop een header slecht kan zijn, elk onderdeel van het verzoek dat gewijzigd wordt (host, methode, pad, query, body, ts, nonce, sleutel, handtekening), de noncecache op haar randen.
  • Vectorbestand (test/fixtures/intake_protocol_vectors.json, CC0): twee ondertekende verzoeken met preimage, body-hash en header, de hashes van een token en een geheim, en links die lezen of geweigerd worden. De handtekeningen en de vingerafdruk zijn onafhankelijk geverifieerd met Node's OpenSSL Ed25519; de test maakt ze byte voor byte opnieuw.
  • Mutatietoets over beide bestanden: 0 overlevers (de overlevers waren een equivalente grens, die ik herschreven heb zodat hij niet meer bestaat, en ontbrekende tests, die geschreven zijn).
  • make check (ongepiped, exit 0) liep over de boom vóór een laatste kleine wijziging (de hernoeming van één constante in een pakkettest en de gitleaks-allowlist); daarna draaiden check-static, check-packages, test-packages, check-registrations, check-secrets (gitleaks + trufflehog, ook de historie) en sast opnieuw: groen. De tak loopt niet achter op main.

Wat er nog niet is

De bodies van de organisatorhandelingen als types, de referentieserver, IntakeHttpTransport en de handelingen in de app (Uitnodiging openen…, acceptatie, outbox, binnenhalen van de server), en de webinvuller (die moet nog kunnen verzegelen onder dart2js).

## Wat Fase 4, stap 1: het **contract van de inzendserver** (FORM_INTAKE.md §6) vastgelegd en de **zuivere helft ervan** gebouwd — zonder server, zonder netwerk. De server wacht nog op een genoemde beheerder (D3); dit is wat daar niet van afhangt. - **`docs/design/INTAKE_PROTOCOL.md`** (CC-BY-4.0, vectoren CC0 — D5): de normatieve tekst. De uitnodigingslink, de handelingen van de invuller (`info`, het formulier, uploaden, intrekken) en van de redactie (publiceren, token, lijst, ophalen, `ack`, verwijderen), ondertekende verzoeken, de foutcodes, CORS, wat de server bewaart, bekende grenzen. - **Uitnodigingslink** (`parseInviteLink`): `https://<shell>/f/<fid>#api=…&fp=…&t=…`, in vaste volgorde gelezen. **Een link zonder vingerafdruk is niet compleet en gaat niet verder** (§6.4) — geen prompt met een naam uit de bundel zelf. - **`GET /v1/info`** (`parseIntakeInfo`): te oud of te nieuw wordt geweigerd, nooit geraden; een pakketplafond boven dat van de client wordt tot dat van de client verlaagd. - **Ontvangstbevestiging**, **intrekgeheim** en **uitnodigingstoken**: van beide gaat alleen de hash naar de server. **`IntakeErrorCode`**: een gesloten set met wire-naam en status; een lichaam dat niet van ons is (de pagina van een proxy) geeft toch een fout, via de status. - **Ondertekende organisatorverzoeken** (`intake_request.dart`): `Authorization: OciIntake key=…, ts=…, nonce=…, sig=…`, Ed25519 over de canonieke JSON van `["ocideck-intake-req-v1", host, methode, target, body_sha256, ts, nonce]`. De verifier neemt host, methode, target en body-hash die **de server kent**, nooit wat de header beweert; de query zit in `target`, dus een pagineringscursor is onderweg niet te veranderen. Venster ±5 minuten, vóór de handtekening gecontroleerd. **`IntakeNonceCache`** onthoudt een nonce pas nadat het verzoek is geautoriseerd (een vreemde met een eigen sleutel kan hem niet vullen), precies zo lang als de `ts` nog kan slagen, en zegt `full` in plaats van er een te vergeten. - **Gitleaks**: `generic-api-key` las `token`/`secret` plus een waarde met entropie in het vectorbestand en in een testconstante als lek (zes keer). De constante is hernoemd; het vectorbestand — dat per definitie voorbeeldwaarden onder die namen draagt — staat nu als enig bestand in de allowlist van `.gitleaks.toml`, met de prijs erbij (zoals bij `age_testkit`): `intake_protocol_vector_test` rekent elke waarde na en een Dart-bestand blijft gewoon gescand. - **`intake_request.dart` is het derde bestand dat primitieven mag importeren** (`check_packages` regel 10; de poort en zijn test zijn bijgewerkt). ## Aangepast ten opzichte van het ontwerp (FORM_INTAKE §6.3, nu met een aanvulling) 1. `GET /v1/forms/{fid}` geeft **`variants`** (een `{bundle, template}` per taal) in plaats van `{bundle, template, assets}`: een bundel bindt één tekst (§7.1, door de eigenaar bevestigd). `assets` vervalt — een formulier verwijst in v1 niet naar bestanden. 2. `PUT /v1/forms/{fid}` publiceert **de hele set varianten in één keer**; de server verifieert elke bundel zelf tegen de ondertekenaar van het verzoek. 3. Het uitnodigingstoken staat alleen als **hash** op de server (`token_sha256`); de client van de organisator maakt het. 4. Het grafsteentje (tombstone) bewaart de **hash van het intrekgeheim**, zodat een intrekking waarvan het antwoord verloren ging kan worden herhaald. 5. `state` in het formulierantwoord is **adviserend**; de server dwingt af. ## Getoetst - 126 tests in de kern (ook op Chrome/dart2js): elke weigering van de link met haar eigen geval, de volgorde van de controles, de grenzen (64 KiB tot op de byte, het venster van 300 s tot op de seconde), elke manier waarop een header slecht kan zijn, elk onderdeel van het verzoek dat gewijzigd wordt (host, methode, pad, query, body, `ts`, nonce, sleutel, handtekening), de noncecache op haar randen. - **Vectorbestand** (`test/fixtures/intake_protocol_vectors.json`, CC0): twee ondertekende verzoeken met preimage, body-hash en header, de hashes van een token en een geheim, en links die lezen of geweigerd worden. De handtekeningen en de vingerafdruk zijn **onafhankelijk geverifieerd met Node's OpenSSL Ed25519**; de test maakt ze byte voor byte opnieuw. - Mutatietoets over beide bestanden: 0 overlevers (de overlevers waren een equivalente grens, die ik herschreven heb zodat hij niet meer bestaat, en ontbrekende tests, die geschreven zijn). - `make check` (ongepiped, exit 0) liep over de boom vóór een laatste kleine wijziging (de hernoeming van één constante in een pakkettest en de gitleaks-allowlist); daarna draaiden `check-static`, `check-packages`, `test-packages`, `check-registrations`, `check-secrets` (gitleaks + trufflehog, ook de historie) en `sast` opnieuw: groen. De tak loopt niet achter op main. ## Wat er nog niet is De bodies van de organisatorhandelingen als types, de referentieserver, `IntakeHttpTransport` en de handelingen in de app (*Uitnodiging openen…*, acceptatie, outbox, binnenhalen van de server), en de webinvuller (die moet nog kunnen verzegelen onder dart2js).
De zuivere helft van het contract van de inzendserver in ocideck_form_core:
parseInviteLink (een link zonder vingerafdruk is niet compleet), parseIntakeInfo,
IntakeArrivalNote, intrekgeheim en uitnodigingstoken (alleen hun hash gaat naar de server),
IntakeErrorCode en ondertekende organisatorverzoeken met een noncecache.
intake_request.dart is het derde bestand dat primitieven mag importeren. Vectoren: CC0,
onafhankelijk geverifieerd met Node's OpenSSL Ed25519; het vectorbestand staat in de
allowlist van gitleaks (voorbeeldwaarden onder de namen token en secret).
brenno merged commit 1438e6008a into main 2026-10-03 18:29:23 +00:00
brenno deleted branch feat/intake-protocol 2026-10-03 18:29:23 +00:00
Sign in to join this conversation.
No description provided.