Repository navigation
Commit 37c7114
test(verify): pin the anonymous public-form door a booted stack already serves (#22543)
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/submit`
anonymously, through the stack's own HTTP surface. The item therefore
reduces to pins and one documentation pointer. Nothing changes in
`packages/rest`, `packages/runtime` or `packages/objectql`, and the
handle gains no method.
## Premise reading (base `40a6ee50a`, re-checked after merging
`faf634850`)
A fixture app declares one public form (`sharing: { enabled: true,
allowAnonymous: true, publicLink: '/forms/prm-intake' }`) and is booted
with `bootStack`:
| request | answer |
|---|---|
| `stack.api('/forms/prm-intake/submit', POST)`, no token | `201`
`{"id":"XP3f15bln6eNEnGH"}` |
| the handle's in-process `HttpDispatcher.dispatch('POST',
'/forms/prm-intake/submit', ...)` | `404` `ROUTE_NOT_FOUND` |
| `stack.api('/api/v1/forms/prm-intake/submit', ...)` (prefix twice) |
`404` `ENDPOINT_NOT_FOUND` |
| `stack.raw('/forms/prm-intake/submit', ...)` (no prefix) | `404`
`ENDPOINT_NOT_FOUND` |
| `stack.api('/forms/nope/submit', ...)` | `404` `FORM_NOT_FOUND` |
| `stack.api('/data/prm_request', POST)`, no token | `401`
`UNAUTHENTICATED` |
- **The route has one owner.** `RestServer.registerFormEndpoints`
registers it (`packages/rest/src/rest-server.ts:10563-10565` at the
base). `bootStack` mounts that server through `createRestApiPlugin`
(`packages/verify/src/harness.ts:1063`) onto the stack's Hono app, so
`stack.api` reaches the route.
- **The handle's in-process dispatcher does not serve `/forms`, and
should not.** That dispatcher drives `flows.*` and `actions.run`. A
second implementation of the form route would fork its invariants
(AGENTS.md, Route and surface ownership, rule 1).
- **Where `ENDPOINT_NOT_FOUND` comes 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.
- I did not run 17.7.0 itself (NOT MEASURED). However, at the
`@objectstack/verify@17.7.0` tag commit `4e4e88142`,
`packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts` already
submits through `bootStack` and `stack.api('/forms/contact-us/submit')`.
- **What the engine receives.** A middleware on the engine recorded
exactly `{ publicFormGrant: { object: 'prm_request' }, permissions:
['guest_portal'], anonymous: true }`. That is the context the route
builds at `rest-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):
1. An anonymous submit answers `201` with the created id, and the row
lands with what was submitted.
2. The engine receives the door's own execution context, exactly. The
pin reads the context through the engine's own `registerMiddleware` seam
and does not re-spell it: `{ publicFormGrant: { object }, permissions:
['guest_portal'], anonymous: true }`, with no user, no system principal,
and nothing else.
3. The bound hook sees a guest: `session` and `user` are both undefined,
so an app's guest branch runs (here it stamps `origin: 'web'`). The
control is a person's insert through `hooks.run`, where the hook sees
that person and stamps `'internal'`.
4. The record-change trigger fires the object's `record-after-create`
flow with no trigger user, and the flow reaches its write. The control
is a person's insert, whose run carries that person.
5. A key the form does not collect (`status`) never reaches the engine.
The engine is handed only the payload keys `['email', 'subject']`.
6. Refusal control: an unknown slug gets `404` `FORM_NOT_FOUND`. The
engine is never handed a write, no hook runs, and no row lands.
7. Refusal control: a form that is shared but not anonymous
(`allowAnonymous: false`, with a `publicLink`) gets the same answer as
6, and nothing reaches the engine.
- **`packages/verify/src/handle.ts`.** Documentation only:
- The header's door roster now names the anonymous form door and says
why it is not a handle method.
- `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/verify` `patch`, `Clause-②: no`. The JSDoc ships: after
`pnpm --filter @objectstack/verify build`,
`packages/verify/dist/index.d.ts` carries 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_request` and
`public-form-door` have 0 hits in `dist/index.js` and
`dist/index.d.ts`). So this needs a changeset, not `skip-changeset`.
## Ablation: one-time proof through the built `@objectstack/rest` dist
`@objectstack/verify` resolves `@objectstack/rest` through its `exports`
(to `dist/`), with no source alias. So each leg went: mutate with
`scripts/ablation-replace.mjs` (WRAP mode, trap-restored), run `pnpm
--filter @objectstack/rest build`, run
`scripts/ablation-dist-preflight.mjs` (marker in `dist/`), then run the
pin file.
- **Leg A: the route's context becomes a system write.** The mutation is
`anonymous: true,` changed to `isSystem: true, anonymous:
Boolean('ablation-22301-system'),`. The anchor went 1 to 0 and the blob
went `e1a572a19260` to `dd6fcbf0550a`, with the marker in dist. Result:
**2 failed, 5 passed.**
- Pin 2 failed: `expected { …(4) } to deeply equal { …(3) }`, with `+
"isSystem": true`.
- Pin 3 failed: `expected { userId: undefined, …(4) } to be undefined`,
because the hook received `session: { isSystem: true, … }`.
- The others stayed green, as predicted.
- **Leg B: the field whitelist admits a key the form does not collect.**
The mutation is `if (allowedFields.has(k)) filteredData[k] = v;` changed
to `if (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' ] ]`.
- **Restore leg.**
- Both legs restored the source: the blob equals HEAD `e1a572a19260` and
`git diff HEAD` is empty.
- I then rebuilt `@objectstack/rest`. `ablation-dist-preflight --absent`
passes for both markers ("marker absent from all 6 built files",
"working tree clean against HEAD").
- The pin file passes 7 of 7.
- The first attempt at leg A was a no-op. Its replacement contained the
anchor, so `ablation-replace` refused it ("the anchor count moved 1 ->
1") and restored the file. Leg A above is the corrected run.
- The refusal pins (6, 7) were not ablated. Their subject is
`anonymousFormIntakeCandidates` in `@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 merges
`origin/main` `faf634850` into the two commits above. The build state
was refreshed after the merge (`pnpm install --frozen-lockfile`, then
`pnpm --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 head `19465fae3` also passed 25/25 files and
203/203 tests.
- `pnpm --filter @objectstack/verify typecheck`: `tsc --noEmit` exit 0,
and `check: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 by
`tsc --noEmit -p tsconfig.test.json --listFiles`.
- The new pin file on its own: 7 passed (7).
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 63 commands at `5bf0b5ab5`. All 63
were run, each exit code captured before any pipe, and every one is 0.
`--ran` printed: `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) ... clean`
- `check-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 declared`
- At the pre-merge head, `check:dual-build-cjs-loads` first answered
`PREREQUISITE NOT MET` (some packages had no `dist/`). 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.
- **Lint, narrowed and proved.** `pnpm lint` is 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.ts` returned **2 files
linted, 0 errors, 0 warnings**, exit 0. Why the narrowing excludes
nothing:
- Both files are in the population `eslint.config.mjs` declares (`files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` and
`packages/**/*.{ts,tsx,mts,cts}`).
- The config never enables type-aware linting: no
`parserOptions.project` and no `projectService` (0 hits), and its own
docblock at lines 327-329 says the same. So this diff cannot change any
untouched file's lint verdict.
- **CI:** read on the PR head after opening (see the report on #22301).
## For hotcrm (the `repo:hotcrm` seat)
hotcrm's matching local path is `guestInsert` in
`test/helpers/verify-stack.ts:239-242`, read read-only at hotcrm
`f0afcbd`. 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:
1. **It takes a slug, not an object.**
2. **It answers `{ id }`.** Read the row back with `stack.rows(object, {
id })`.
3. **It keeps only the fields the form collects.**
- Call sites whose keys are all collected map one-for-one (for example
`case-assignment.test.ts:339`).
- Call sites that pass uncollected keys model a form that does not
exist. Examples: `status` in `flow-sla-ownerless-case.test.ts:104`,
`rating` in `flow-record-change.test.ts:610`, and the guest-strip
fixtures in `hooks-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)
- **The engine's `buildSession` docblock says more than holds.** It says
"Every real transport resolves `positions` into 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's `session` and `user` are both undefined). So a
hook cannot tell a public-form submission from a bare programmatic call
by `session` alone. A guest branch keyed on "no user and no system
principal" (the hotcrm shape, and pin 3's) works.
- I found no platform hook that gates on a missing session (`git grep`
over `packages/**` for `!ctx.session` and similar spellings: 0 hook
hits).
- Read-only observation, so it is not filed. Carrier: none.
- **The README sentence "There is no way to run as \"nobody\""
(`packages/verify/README.md`) is about `as` on 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](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>1 parent 3d886ee commit 37c7114
3 files changed
Lines changed: 382 additions & 0 deletions
File tree
- .changeset
- packages/verify/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
38 | 46 | | |
39 | 47 | | |
40 | 48 | | |
| |||
156 | 164 | | |
157 | 165 | | |
158 | 166 | | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
159 | 173 | | |
160 | 174 | | |
161 | 175 | | |
| |||
0 commit comments