Skip to content

docs(rest,app-crm): the public form submit is authorized by its declaration, not by a guest_portal set - #24237

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24130-guest-portal-form-comments
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24130-guest-portal-form-comments

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24130

Clause-②: no

What changes

Comments, one published docstring, one docs paragraph, and a patch changeset. No behaviour of the form routes changes.

  • packages/rest/src/rest-server.ts — the JSDoc above registerFormEndpoints, and nothing else in the file. It said security is delegated to the guest_portal set, that a deployment without one "falls open", and that secure-by-default deployments must pair the routes with one. It now states the mechanism the runtime runs: the declaration-derived publicFormGrant is admitted before any permission set resolves; it says what the grant admits, refuses and strips, and what the guest_portal name on the context reaches.
  • packages/rest/src/form-routes.ts — three comments:
    • the route header ("with the guest_portal permission set attached");
    • the stale lead.hook.ts / case.hook.ts pointer;
    • the comment above the submit context ("guest_portal + anonymous are kept for back-compat with object hooks").
  • examples/app-crm/src/views/lead.view.ts — the web_to_lead comment no longer says the guest_portal profile authorizes the write.
  • examples/app-crm/src/security/sales-positions.ts — GuestPortalProfile is kept, and its comment now says why (H3 below).
  • content/docs/ui/public-data-collection.mdx — one paragraph in section 2 saying what the guest_portal name on the submit context does and does not do.
  • .changeset/24130-form-submit-docstring.md — @objectstack/rest: patch. The docstring ships; measured below.

Measured

Tree: c564b0e45, the merge of origin/main 2bcd406a2 into this branch.

H1 — what authorizes the anonymous INSERT, and what permissions: ['guest_portal'] does

I read packages/plugins/plugin-security/src/security-plugin.ts and did not change it.

  • Admission. The middleware's publicFormGrant branch admits insert / find / findOne / count on the grant's object and throws PermissionDeniedError for any other operation or object. It decides this before any permission set resolves, and it strips PUBLIC_FORM_SERVER_MANAGED_FIELDS from inserted rows.
  • The guest_portal name. The branch then calls resolvePermissionSetsForContext(opCtx.context), which requests the context's permissions names by name. The answer is used only as input to resolveCallerPosture and maskOperationResult over the operation's result.
  • What a registered set contributes. The masker reads the set's fields (getFieldPermissions) and its system permissions (getSystemPermissions, held against a field's requiredPermissions). The set's objects bits are never read on this branch.
  • What the visitor sees. The route answers { id } only.

Live check: a real boot of examples/app-crm, through bootStack from @objectstack/verify, in a throwaway dogfood probe that is not committed. It ran with and without GuestPortalProfile in the stack's permissions:

probe with GuestPortalProfile without
permission sets the stack declares crm_sales_user, guest_portal crm_sales_user
clean POST /forms/contact-us/submit 201, body { id } 201, body { id }
POST that also sends status: 'qualified', lead_score: 99, and forged owner_id / organization_id 201, body { id } 201, body { id }
both stored rows, read as system status: new, lead_score: 0, source: web, owner_id: null, forged organization not stored same
anonymous GET /data/crm_lead 401 401
anonymous GET /data/crm_lead/ID 401 401
a beforeInsert hook on crm_lead, ctx.session / ctx.user null / null null / null

The probe asserted the two shapes' answers equal and passed (Tests 1 passed). This matches the card's measurement on 17.7.0.

A second throwaway probe ran at the engine level (packages/objectql, not committed):

  • Route context. With the route's context verbatim, a beforeInsert hook's ctx.session and ctx.user are both unset.
  • Positive control. A context with userId: 'u_ctl', positions: ['p_ctl'], permissions: ['guest_portal'], anonymous: true reaches the hook as session: { userId, positions } and user: { id }. Neither permissions nor anonymous gets through, because buildSession / buildUser in packages/objectql/src/engine.ts copy neither key.

So "kept for back-compat with object hooks" was false: no hook can read either key.

H2 — the hook-file pointer

examples/app-crm/src/hooks/ holds index.ts and opportunity.hook.ts only. Neither lead.hook.ts nor case.hook.ts exists. The comment now names the three patterns that do exist:

  • a field's defaultValue (crm_lead.status);
  • on a select that declares no defaultValue, its default: true option. packages/objectql/src/engine.ts (about :7086): "A field that declares NO defaultValue falls back to the option marked default: true." This is how source lands as web in the table above;
  • a beforeInsert hook where the value needs logic: StampInquiryDefaultsHook in examples/app-showcase/src/data/hooks/index.ts, the hook on the showcase's public-form target.

H3 — GuestPortalProfile: kept, and the example says why

The dispatch preferred dropping the profile if nothing reads it. Two things read it:

  • The grant's masker resolves the set by name on every form submit (H1).
  • ADR-0138 (Accepted) records this exact declaration in its fate table: "the form doors' guest_portal set name | packages/rest/src/rest-server.ts#RestServer.registerFormEndpoints; the reference declaration is examples/app-crm/src/security/sales-positions.ts#GuestPortalProfile | live: the form doors' context names that set, and the result masker resolves it where a deployment registers one … | keep".

Dropping the profile would leave an accepted record citing a deleted symbol, and correcting the ADR is a Tier H edit outside this card's surface. So the example keeps it.

Its comment now says:

  • what it does not do: authorize the form;
  • what the name reaches: the masker's field permissions;
  • that, as declared here (object bits only), it changes nothing on the form path. The table above shows this.

security/index.ts, objectstack.config.ts and access-matrix.json are unchanged.

Changeset — patch, because the docstring ships

registerFormEndpoints is private, but its docstring is in the published files. After the build:

  • the new phrase "admits on that grant alone" appears once each in packages/rest/dist/index.d.ts, index.d.cts, index.js and index.cjs;
  • the old phrase "falls open with" appears zero times;
  • control: a pre-existing form-routes.ts body comment (Symmetric with #6601) is in src once and in dist zero times, and so is the new form-routes.ts text.

So a reader of the published @objectstack/rest sees the corrected docstring, and that takes a patch, not skip-changeset. examples/app-crm is private: true.

Tests and gates (head c564b0e45)

  • ① Build. pnpm turbo run build --filter=@objectstack/dogfood --filter=@objectstack/example-crm --concurrency=2 covers rest's dependency closure plus rest, plugin-security and verify. Result: Tasks: 66 successful, 66 total, lock VERDICT command-exit 0.
    • The CRM's objectstack build ran in the same run: ✓ Build complete, Security: 3 Positions 2 Permissions, with no access-matrix drift.
  • ② Package tests.
    • @objectstack/rest vitest run --project local: Test Files 294 passed (294), Tests 6028 passed | 326 skipped (6354), exit 0.
    • @objectstack/rest typecheck (tsc --noEmit && check:test-typecheck): exit 0.
    • @objectstack/example-crm vitest run: Test Files 6 passed (6), Tests 46 passed (46), exit 0.
    • @objectstack/example-crm typecheck: exit 0.
  • ③ Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 commands. All 93 exit 0.
    • On the first pass, two exited 3 (PREREQUISITE NOT MET, unbuilt packages outside this closure): check:skill-examples and check:dual-build-cjs-loads. I built the eight packages they named, re-ran both, and both exited 0.
    • --ran: "Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN."
  • Lint on the changed files. pnpm exec eslint --no-inline-config over the four changed .ts files: exit 0, no output.
  • Not run locally. test:repo and the repo-wide pnpm lint are CI's.

Patch round 1

The seat's review of c564b0e45 folded two adjacent sites into this PR. The claim's file surface was revised in place: comment 6243651386, "Revised 2026-10-18T01:09Z". Head: 91ca7f3b7.

main moved 5 commits since 2bcd406a2, none on a file this PR touches, so it was not merged in. Each round-0 sentence above still describes c564b0e45, and nothing in it changed.

  • content/docs/ui/forms.mdx, the guest_portal passages only.
    • The ADR-0056 callout no longer says the set and the anonymous flag are attached "for back-compat" with object hooks. It now says neither key authorizes anything or reaches an object hook.
    • Section 2, "(Optional) Create the guest_portal permission set", is kept, retitled and rewritten: "No permission set to create — what the guest_portal name does". It now says what is true:
      • admission is the declaration-derived publicFormGrant, decided before any set resolves;
      • a registered set's object bits are not read on this path;
      • only its field permissions, and the capabilities it holds against a field's requiredPermissions, feed the result masker over the record the route gets back, and the visitor gets the id only;
      • so a deployment registers one only to mask fields on that record, and nothing else needs it;
      • no hook can read permissions or anonymous, so a public submission is told apart by the missing ctx.user.
    • Section 2's os:check snippet, which declared an INSERT-only set, is removed: its whole content was object bits that nothing reads. The section stays because a reader who sees permissions: ['guest_portal'] in the context shape needs to know what that name does.
    • The auth-model bullet no longer says permissions: ['guest_portal'] "is retained for back-compat".
  • examples/app-crm/src/objects/lead.object.ts, the status comment only. It no longer says an option default is "only a UI preselect". It now says the declared defaultValue: 'new' is what the write uses, and that this takes precedence over the default: true option, which the engine falls back to on write only for a select that declares no defaultValue.

Evidence:

  • The two probes in H1 above. On the CRM with and without GuestPortalProfile, the answers were identical, and the beforeInsert hook saw ctx.session / ctx.user unset. On the engine, the positive control showed that permissions and anonymous never reach a hook. The CRM probe also stored source: web for a select that declares no defaultValue.
  • packages/objectql/src/engine.ts, about :7086: "A field that declares NO defaultValue falls back to the option marked default: true ({@link resolveOptionDefault}, [finding] SelectOption.default is an authorable key that nothing on the insert path ever reads — the select idiom is inert #7246)". The applyFieldDefaults branch reaches resolveOptionDefault(f) only when f.defaultValue == null, so a declared defaultValue wins.

Tests and gates on 91ca7f3b7, every exit captured before any pipe:

  • Build. pnpm turbo run build over the round-0 closure plus the eight packages the two dist-reading gates need: Tasks: 74 successful, 74 total, lock VERDICT command-exit 0. The CRM's objectstack build re-ran on the new lead.object.ts: ✓ Build complete, Security: 3 Positions 2 Permissions.
  • CRM tests and typecheck. @objectstack/example-crm vitest run: Test Files 6 passed (6), Tests 46 passed (46), exit 0. typecheck: exit 0.
  • Lint. pnpm exec eslint --no-inline-config examples/app-crm/src/objects/lead.object.ts: exit 0, no output.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave the same 93 commands as round 0, and all 93 exit 0 on the first pass. check:skill-examples reads "262 prose examples type-check" (263 before: the removed snippet); check:doc-anchors reads "536 internal #fragment link(s) … all resolve". --ran: "Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN".
  • Stale-tree note. The derivation warned that the tree is 5 commits behind origin/main and that one derived gate, scripts/check-position-name-fold-loaders.mjs, changed there (fix(scripts): name-fold loader gate rule 2 resolves the fixtures directory, not one spelling of it #24236). I also ran main's copy of that script at its own path against this tree, restored by git checkout HEAD and proved by blob hash (bd90f10a6, the HEAD blob): scan exit 0, --self-test exit 0 (39 cases).

Patch round 2

Patch round 2 fixes the 403 PERMISSION_DENIED row of content/docs/ui/forms.mdx's submit error table, and nothing else. The claim was revised for it (comment 6243651386, "Revised 2026-10-18T01:33Z … patch round 2"). Head: 860ca38a9. main did not move on forms.mdx, so it was not merged.

The row said "the resolved profile does not allow create on the target object", which contradicted section 2 on the same page. It now names the conditions the submit answers 403 for, each measured by a throwaway dogfood probe (deleted, never committed) through the real POST /api/v1/forms/:slug/submit on one bootStack boot:

case answer
baseline submit 201
permission-set resolution throws (fault injected on the boot's own SecurityPlugin instance) 403 PERMISSION_DENIED "[Security] Access denied: permission subsystem unavailable for operation 'insert' …"; after restore 201
the object's security posture unresolved (fault injected) 403 PERMISSION_DENIED "… the security posture of object '…' could not be resolved …"; after restore 201
a code-registered beforeInsert hook throws PermissionDeniedError 403 PERMISSION_DENIED, the hook's message
a code-registered hook throws a plain Error (control) 500 INTERNAL_ERROR
an afterInsert hook writes a different object through ctx.api, unelevated 403 PERMISSION_DENIED "[Security] Access denied: public-form grant permits only create/read-back on 'pfx_cross', not 'insert' on 'pfx_other'"
a declarative (sandboxed) beforeInsert hook throws 400, the hook's message

The first two arms are the grant branch's fail-closed throws in security-plugin.ts; the cross-object arm is its refusal of any operation outside the grant, and it shows that a hook's nested write carries the submission's context. A permission set's object bits appear in none of them.

Gates on 860ca38a9, every exit captured before any pipe: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave the same 93 commands as round 1, and all 93 exit 0. --ran: "Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN". check:doc-anchors and check:doc-authoring are green. The same stale-tree note as round 1 applies, and main's copy of scripts/check-position-name-fold-loaders.mjs, run again at its own path with a trap restore proved by blob hash (bd90f10a6), exits 0 on scan and on --self-test. The build for the probe was Tasks: 74 successful, 74 total (all cached), lock VERDICT exit 0.

Acceptance notes

None of these sites is changed here. Each needs a carrier. The forms.mdx and lead.object.ts items that were listed here in round 0 are fixed in patch round 1 above, and the forms.mdx 403 row listed here in round 1 is fixed in patch round 2.

  • skills/objectstack-api/SKILL.md about :87 describes the submit context as { permissions: ['guest_portal'], anonymous: true } and omits publicFormGrant. This is a governed surface (Tier H); the seat files it as a card to its lane.
  • anonymous: true on the submit context has no reader. There are zero non-test property reads of anonymous in packages/ (pattern (\.|\?\.)anonymous\b|\[['"]anonymous['"]\]), against 1 for the control key publicFormGrant with the same pattern, at 5a9a80bd2. The card changes no behaviour, so the key stays.
  • ADR-0096's census row for rest-server.ts#enforceAuth reads "fall open when guest_portal unregistered (partly mitigated by the publicFormGrant)". It is an accepted record's census, a governed surface, and left as is.

Generated by Claude Code

…ration, not by a guest_portal set

The submit route's docstring said a deployment without a `guest_portal`
profile "falls open", and the CRM example said that profile authorizes the
anonymous write. Neither is what runs: the SecurityPlugin admits on the
declaration-derived `publicFormGrant` before any permission set resolves.
The comments now state that mechanism, what the `guest_portal` name on the
submit context does reach (the grant's result masker), and why the CRM
keeps `GuestPortalProfile` (ADR-0138's reference declaration). The stale
`lead.hook.ts` / `case.hook.ts` pointer now names the patterns that exist.

Comments only; no behaviour changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…y what guest_portal reaches, add the rest patch changeset

The registerFormEndpoints docstring ships in @objectstack/rest's published
declarations and JS (measured in dist), so the correction is a patch
changeset. The CRM lead form's defaults include `source`, filled from its
`default: true` select option. public-data-collection.mdx now states that
the guest_portal name on the submit context authorizes nothing and is read
only as the result masker's field permissions.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 4 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx (via guest_portal (literal, a string literal in a comment in registerFormEndpoints), /forms/:slug/submit (route, a path literal in a comment in registerFormEndpoints))
  • content/docs/ui/public-data-collection.mdx (via guest_portal (literal, a string literal in a comment in registerFormEndpoints), /forms/:slug/submit (route, a path literal in a comment in registerFormEndpoints))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v12.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via /forms/:slug/submit (route, a path literal in a comment in registerFormEndpoints))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7be3cc7280b9625af914b7b4a978396368f0ee92 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 96de960758d46a45b259b0a4c91c58c579e84df3 — the merge of head 860ca38a92bb1c8cb1b7d63fb193a3bea528cf86 into base 7be3cc7280b9625af914b7b4a978396368f0ee92, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 96de960758d46a45b259b0a4c91c58c579e84df3 && git checkout 96de960758d46a45b259b0a4c91c58c579e84df3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7be3cc7280b9625af914b7b4a978396368f0ee92 860ca38a92bb1c8cb1b7d63fb193a3bea528cf86 && git checkout -B drift-repro 7be3cc7280b9625af914b7b4a978396368f0ee92 && git merge --no-ff 860ca38a92bb1c8cb1b7d63fb193a3bea528cf86

node scripts/docs-audit/affected-docs.mjs --json 7be3cc7280b9625af914b7b4a978396368f0ee92

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7be3cc7280b9625af914b7b4a978396368f0ee92 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…crm_lead.status says which default the write uses

forms.mdx said the guest_portal set and the anonymous flag are attached
"for back-compat" with object hooks, that hooks may branch on the
guest_portal permission, and that a registered set must be kept
INSERT-only. No hook sees either key, and a registered set's object bits
are not read on the submit path; only its field permissions feed the
result masker over the record the route gets back, and the visitor gets
the id only. Section 2 is retitled and rewritten to say that, and drops
the INSERT-only snippet whose object bits nothing reads.

The crm_lead.status comment called a select option's `default` "only a
UI preselect"; the engine falls back to it on write when no defaultValue
is declared. The comment now says the declared defaultValue is what the
write uses and why it takes precedence.

Text only; no behaviour changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…not a profile

The error table said `403 PERMISSION_DENIED` means "the resolved profile
does not allow create on the target object". No permission set admits or
refuses the public submit's create. Measured through the real route, it
answers 403 when the security layer fails closed (permission sets or the
object's security posture cannot be resolved), when a write under the
submission's context leaves the grant (an afterInsert hook writing another
object through ctx.api), or when a code-registered hook throws a 403
refusal; a declarative hook's throw answers 400.

Text only; no behaviour changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · PR #24237 at 860ca38a9 (#24130)

domain:cli seat 2 · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T01:54Z · holder of claim 6243651386 (revised in place 2026-10-18T01:09Z and 2026-10-18T01:33Z). Dev reports on #24130: 6244037824 (round 0), 6244251208 (patch round 1), 6244452174 (patch round 2). The seat reviewed against this PR and the code, not the reports' own account.

  1. Shape. Draft → main. The first line is Fixes #24130, the only closing keyword. Clause-②: no is line-initial, and the assignee is marchtian. 8 files, +114 / −60. Every file is inside the revised claim surface. In rest-server.ts only the registerFormEndpoints JSDoc changes (:8273–:8295). No behaviour changes: every hunk outside the changeset is a comment or prose.
  2. The mechanism, read on the code (packages/plugins/plugin-security/src/security-plugin.ts :2412–:2470 on 7be3cc728):
    • Admission is decided on publicFormGrant: insert / find / findOne / count on the grant's object only. The server-managed anchors are stripped before the write.
    • resolvePermissionSetsForContext runs after that decision, for the result masker only, and fails closed.
    • Every new comment and doc sentence states this and nothing more.
    • The dev's two round-0 probes showed that a CRM boot with and without GuestPortalProfile gives the same 201 / 401 answers and the same stored rows, and that no hook sees permissions or anonymous. The engine's resolveOptionDefault (engine.ts about :7086) backs the status comment.
  3. H3, decided against the PM's preferred drop, with evidence: accepted. ADR-0138's fate table (:509) names examples/app-crm/src/security/sales-positions.ts#GuestPortalProfile as the reference declaration for the form doors' guest_portal name, with fate keep. Dropping it would mean editing an Accepted ADR (Tier H), which is outside this surface. The profile's comment now says why it is kept and that it changes nothing on the form path.
  4. Docs and changeset, read sentence by sentence:
    • public-data-collection.mdx: the new paragraph matches item 2 ("authorizes nothing … object permissions are not read … field permissions and capabilities … to mask the created record … id only").

    • forms.mdx, round 1:

      • the callout ("Neither authorizes anything and neither reaches an object hook");
      • section 2, retitled "No permission set to create — what the guest_portal name does", with its three bullets;
      • the auth-model bullet;
      • removing the os:check snippet, whose only content was object bits nothing reads.
    • forms.mdx, round 2: the 403 PERMISSION_DENIED cell. Each clause is a condition the dev reproduced through the real route:

      • permission-set resolution failing closed;
      • the object's security posture failing closed;
      • an afterInsert hook's nested write leaving the grant;
      • a code hook's own PermissionDeniedError.

      Controls: 201 at baseline, 500 for a plain Error, and 400 for a declarative hook's throw.

    • The @objectstack/rest patch changeset describes only the docstring. The dev measured that the JSDoc ships in dist (.d.ts, .d.cts, .js, .cjs), with a control comment absent from dist. So a changeset is owed, not skip-changeset, and its sentences match the JSDoc.

  5. Gates. Round 0: 93 derived, 93 run, 0 NOT-MEASURED. rest local passed 294 files; the example-crm tests passed 46; the typechecks exited 0. Rounds 1 and 2 each re-derived the same 93, all exit 0. ESLint --no-inline-config exited 0 over every changed .ts file. CI on round 1's head 91ca7f3b7: 41 success, 7 skipped, 0 failed.
  6. Cross-lane: devx seat 6244081579 on [PM seat] domain:devx · seat 2 — 🟢 os-sales · session_016PVjjuVPte5XEJvaQx1E4t · serial (maintainer) #20163, covering public-data-collection.mdx and forms.mdx. No objection at this reading.
  7. Accepted deviations:
    • the H3 keep (item 3);
    • the patch changeset (item 4);
    • the 8 out-of-closure packages built so two dist-reading gates could measure;
    • origin/main not re-merged in rounds 1–2, since it did not move on these files;
    • the probes were throwaway and deleted.
  8. Out of scope, routed:
  9. Landing. Ordinary queue landing once every check on 860ca38a9 is green or a roster skip. Fixes #24130 closes the card, and the seat strips pm:dispatched in the same act.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants