Repository navigation
test(verify): pin the anonymous public-form door a booted stack already serves - #22543
Conversation
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… stack serves Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a6b59bee8ef600352808f19ddf18e7cd5cd98f76 && git checkout a6b59bee8ef600352808f19ddf18e7cd5cd98f76
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce78ff7bcd850f440b2bacc25a35cedbcc847c56 cc6c53e50656be5114f0fe1404d9a795bdf2a9f7 && git checkout -B drift-repro ce78ff7bcd850f440b2bacc25a35cedbcc847c56 && git merge --no-ff cc6c53e50656be5114f0fe1404d9a795bdf2a9f7
node scripts/docs-audit/affected-docs.mjs --json ce78ff7bcd850f440b2bacc25a35cedbcc847c56
|
|
Generated by Claude Code |
Part of #22301
Clause-②: no
What this is
Item 4 of #22301, the anonymous public-form door on the verify handle. Items 1-3 landed earlier, items 5-9 are not addressed here, and #22301 remains open.
I checked the premise first, and it does not hold on this base. A booted stack already serves
POST /api/v1/forms/:slug/submitanonymously, through the stack's own HTTP surface. The item therefore reduces to pins and one documentation pointer. Nothing changes inpackages/rest,packages/runtimeorpackages/objectql, and the handle gains no method.Premise reading (base
40a6ee50a, re-checked after mergingfaf634850)A fixture app declares one public form (
sharing: { enabled: true, allowAnonymous: true, publicLink: '/forms/prm-intake' }) and is booted withbootStack:stack.api('/forms/prm-intake/submit', POST), no token201{"id":"XP3f15bln6eNEnGH"}HttpDispatcher.dispatch('POST', '/forms/prm-intake/submit', ...)404ROUTE_NOT_FOUNDstack.api('/api/v1/forms/prm-intake/submit', ...)(prefix twice)404ENDPOINT_NOT_FOUNDstack.raw('/forms/prm-intake/submit', ...)(no prefix)404ENDPOINT_NOT_FOUNDstack.api('/forms/nope/submit', ...)404FORM_NOT_FOUNDstack.api('/data/prm_request', POST), no token401UNAUTHENTICATEDRestServer.registerFormEndpointsregisters it (packages/rest/src/rest-server.ts:10563-10565at the base).bootStackmounts that server throughcreateRestApiPlugin(packages/verify/src/harness.ts:1063) onto the stack's Hono app, sostack.apireaches the route./forms, and should not. That dispatcher drivesflows.*andactions.run. A second implementation of the form route would fork its invariants (AGENTS.md, Route and surface ownership, rule 1).ENDPOINT_NOT_FOUNDcomes from. It is the Hono app's not-found answer (plugin-hono-server/src/adapter.ts:1219). On this base, the only requests that get it are paths that miss the route, like the two spellings above. That is the likely origin of the 17.7.0 reading on the card.@objectstack/verify@17.7.0tag commit4e4e88142,packages/qa/dogfood/test/showcase-public-form.dogfood.test.tsalready submits throughbootStackandstack.api('/forms/contact-us/submit').{ publicFormGrant: { object: 'prm_request' }, permissions: ['guest_portal'], anonymous: true }. That is the context the route builds atrest-server.ts:10664-10668.What changes
packages/verify/src/handle.public-form-door.test.ts(new). Seven pins, all through a booted stack and the route's own path (stack.api('/forms/:slug/submit'), no token):201with the created id, and the row lands with what was submitted.registerMiddlewareseam and does not re-spell it:{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }, with no user, no system principal, and nothing else.sessionanduserare both undefined, so an app's guest branch runs (here it stampsorigin: 'web'). The control is a person's insert throughhooks.run, where the hook sees that person and stamps'internal'.record-after-createflow with no trigger user, and the flow reaches its write. The control is a person's insert, whose run carries that person.status) never reaches the engine. The engine is handed only the payload keys['email', 'subject'].404FORM_NOT_FOUND. The engine is never handed a write, no hook runs, and no row lands.allowAnonymous: false, with apublicLink) gets the same answer as 6, and nothing reaches the engine.packages/verify/src/handle.ts. Documentation only:hooks.run's JSDoc tells a reader that an anonymous write is the form's own route,api('/forms/:slug/submit', ...)with no token. The hotcrm port reported "the handle offers no way to reach that branch at all", so this is the pointer it was missing..changeset/22301-verify-public-form-door.md.@objectstack/verifypatch,Clause-②: no. The JSDoc ships: afterpnpm --filter @objectstack/verify build,packages/verify/dist/index.d.tscarries the new phrase once, with the positive control (an existing JSDoc phrase, "address the row by") also present once. The test file ships nothing (pfd_requestandpublic-form-doorhave 0 hits indist/index.jsanddist/index.d.ts). So this needs a changeset, notskip-changeset.Ablation: one-time proof through the built
@objectstack/restdist@objectstack/verifyresolves@objectstack/restthrough itsexports(todist/), with no source alias. So each leg went: mutate withscripts/ablation-replace.mjs(WRAP mode, trap-restored), runpnpm --filter @objectstack/rest build, runscripts/ablation-dist-preflight.mjs(marker indist/), then run the pin file.anonymous: true,changed toisSystem: true, anonymous: Boolean('ablation-22301-system'),. The anchor went 1 to 0 and the blob wente1a572a19260todd6fcbf0550a, with the marker in dist. Result: 2 failed, 5 passed.expected { …(4) } to deeply equal { …(3) }, with+ "isSystem": true.expected { userId: undefined, …(4) } to be undefined, because the hook receivedsession: { isSystem: true, … }.if (allowedFields.has(k)) filteredData[k] = v;changed toif (allowedFields.has(k) || (k === 'status' && 'ablation-22301-whitelist')) filteredData[k] = v;. Result: 1 failed, 6 passed. Pin 5 failed:the payload the engine was handed: expected [ [ 'email', 'status', 'subject' ] ] to deeply equal [ [ 'email', 'subject' ] ].e1a572a19260andgit diff HEADis empty.@objectstack/rest.ablation-dist-preflight --absentpasses for both markers ("marker absent from all 6 built files", "working tree clean against HEAD").ablation-replacerefused it ("the anchor count moved 1 -> 1") and restored the file. Leg A above is the corrected run.anonymousFormIntakeCandidatesin@objectstack/metadata-core, which is outside this diff's surface. Each pin also asserts that the engine was never handed a write.Tests and gates
These results are from the final head
5bf0b5ab5, which mergesorigin/mainfaf634850into the two commits above. The build state was refreshed after the merge (pnpm install --frozen-lockfile, thenpnpm --filter '@objectstack/verify...' build, the package plus its dependency closure).pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2: Test Files 25 passed (25), Tests 203 passed (203). This is the package in full: 25 test files on disk, the new one included. The same suite at the pre-merge head19465fae3also passed 25/25 files and 203/203 tests.pnpm --filter @objectstack/verify typecheck:tsc --noEmitexit 0, andcheck:test-typecheck: OK — @objectstack/verify's test layer compiles under packages/verify/tsconfig.test.json; 0 file(s) / 0 error(s). The new test file is in that program: it is listed once bytsc --noEmit -p tsconfig.test.json --listFiles.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands at5bf0b5ab5. All 63 were run, each exit code captured before any pipe, and every one is 0.--ranprinted:Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN.Some of those gates' own lines:check-nul-bytes: OK (scanned 10524 text file(s) ...; no raw ASCII control bytes)✓ doc authoring guard: 17989 customer-facing string(s) ... cleancheck-test-source-alias OK — 73 packages with tests scanned✓ check:published-files — 69 publishable package(s) ...check:cross-package-test-inputs OK: 30 package(s) read outside themselves, all declaredcheck:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(some packages had nodist/). Once those were built, it was re-run green (107 published require entry point(s) across 66 package(s) load), and it is green in the final-head run.pnpm lintis CI's to run. I ran it on the two touched TypeScript files only:eslint --no-inline-config --format json packages/verify/src/handle.ts packages/verify/src/handle.public-form-door.test.tsreturned 2 files linted, 0 errors, 0 warnings, exit 0. Why the narrowing excludes nothing:eslint.config.mjsdeclares (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']andpackages/**/*.{ts,tsx,mts,cts}).parserOptions.projectand noprojectService(0 hits), and its own docblock at lines 327-329 says the same. So this diff cannot change any untouched file's lint verdict.serveand has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301).For hotcrm (the
repo:hotcrmseat)hotcrm's matching local path is
guestInsertintest/helpers/verify-stack.ts:239-242, read read-only at hotcrmf0afcbd. It has 20 call sites in 11 test files. hotcrm declares two public forms,/forms/contact-us(crm_lead,web_to_lead) and/forms/support(crm_case,web_to_case).The door replaces that helper. Three differences matter when porting a call site:
{ id }. Read the row back withstack.rows(object, { id }).case-assignment.test.ts:339).statusinflow-sla-ownerless-case.test.ts:104,ratinginflow-record-change.test.ts:610, and the guest-strip fixtures inhooks-runtime-service.test.ts:132/:1083. Through the real door those keys are dropped before any hook runs. Such a case either drops the key, or (for a hook-level strip test) states that the route's whitelist is now what it observes.None of this needs a platform change. The hotcrm suite was not run.
Acceptance notes (not filed)
buildSessiondocblock says more than holds. It says "Every real transport resolvespositionsinto the context, so an anonymous HTTP request still yields a session and stays gated." The public-form door is a real transport, and it yields no session (measured: the hook'ssessionanduserare both undefined). So a hook cannot tell a public-form submission from a bare programmatic call bysessionalone. A guest branch keyed on "no user and no system principal" (the hotcrm shape, and pin 3's) works.git grepoverpackages/**for!ctx.sessionand similar spellings: 0 hook hits).packages/verify/README.md) is aboutason handle methods, and it is accurate for them. A pointer to the form door there would help a reader. The README is outside this item's claimed file surface, so it is left for the seat. Carrier: none.Generated by Claude Code