Skip to content

content(blog): how to let anonymous visitors submit a form into a governed business app (slot 2026-10-22) - #280

Open
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-272
Open

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-272

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #272

Re-slotted to Thu 2026-10-22 (head 40fbc0f). Daily cadence switched on 2026-10-17 (ruling B, #267), and the queued posts were re-dated onto consecutive days from 10-18. The re-date changes the post's date and the back-linked updated stamps, with zh-Hant regenerated. Gates are green on the new head. Slot dates further down this body are superseded by this line. Merge on 2026-10-22 only.

Slot: Mon 2026-10-26. Merge on the slot day only. The slot is on the shared calendar on #267. Seat review. The seat read the post and looked at the two-nets diagram at native size and the zh-Hans 360 dark hero. All probes ran on 17.7.0. The card's guest-profile question was settled by measurement, not by assertion: the run with the profile removed is byte-identical to the run with it. The owner_id prediction was corrected to the measured null. The two upstream findings (the guest_portal comments, and form-level required not enforced on the public path) are routed to objectstack by the seat. ACCEPT.

Slot Mon 2026-10-26 (date: 2026-10-26). Merge on the slot day only; if the daily switch-over slips, re-date the post and move the three back-linked updated values with it.

What this adds

  • content/blog/how-to-let-anonymous-visitors-submit-a-form/ — the post in all eight locales (index.mdx, zh-Hans, ja, ko, de, es, fr authored; zh-Hant produced by pnpm gen:zh-hant), cover pair (cover-en.svg, cover.svg) and one diagram pair (two-nets-en.svg, two-nets.svg).
  • Back-links with updated: 2026-10-26, one link sentence each: how-to-self-host-objectstack (en only exists), objectos-agent-permission-boundaries (en, zh-Hans; zh-Hant regenerated), is-lovable-safe-for-production (all eight locales).
  • Title is query-shaped; the one number is five (the form's whitelist: five of crm_lead's thirteen business fields; zero of the twelve server-managed names). Ownable concept: the form-shaped grant — authorization computed from the form at request time, so it cannot be configured wider than the form. Audience business, topic app-building. Canonical T4 closes every locale verbatim from src/lib/positioning.ts.

Proof artifact — produced on the published @objectstack/cli 17.7.0

examples/app-crm exported from tag @objectstack/cli@17.7.0 (commit 4e4e881427), dependencies pinned to the published 17.7.0 packages (@objectstack/cli, runtime, spec, service-i18n, plugin-hono-server all resolve to 17.7.0), booted with objectstack dev --no-watch on ports 49160–49164. Logs and the raw responses live in the scratchpad (issue-272/proof/phaseA.log, phaseA2.log, phaseA3.log, phaseB-valid.log, phaseC-valid.log, build-B.log, build-C.log).

Probe With guest_portal (phase A, port 49160) Without guest_portal (phase B, 49163) Form withdrawn (phase C, 49164)
GET /api/v1/forms/contact-us, anonymous 200; objectSchema carries exactly name, email, phone, company, title 200, same 404 FORM_NOT_FOUND
POST …/submit with name, email 201, id lY0qNhApoHwyYMtk, status: "new", source: "web", lead_score: 0, owner_id: null, organization_id: "org_mvbpxfocznjyqv4e" 201, identical shape 404 FORM_NOT_FOUND
POST …/submit smuggling status: "converted", lead_score: 100, owner_id: "attacker", organization_id: "org_other", notes 201, id OJINuRoRVtZC-l95; stored row read as admin: status: "new", lead_score: 0, owner_id: null, organization_id = the server's own, notes: null 201, identical (id kL-lplom1jyxJyCh) not run (door closed)
GET /api/v1/data/crm_lead, anonymous 401 {"error":"UNAUTHENTICATED","code":"UNAUTHENTICATED",…} 401, same 401
GET /api/v1/data/crm_lead/ID of the row just created, anonymous 401 401 —
POST …/submit without form-required email 201 with email: null 201 with email: null —
POST …/submit without object-required name 400 VALIDATION_FAILED "Lead Name is required" 400, same —
Lead count read as admin 15 after the probes 15 15 before and after

Removing GuestPortalProfile from permissions first made objectstack build refuse: ✗ Access matrix drift (1 change) … 'guest_portal' loses ALL access to 'crm_lead' (entry removed); accepted with --update-access-matrix, after which the artifact lists one permission set (crm_sales_user). The compiled artifact was inspected before every measured phase.

Card prediction corrected by measurement: the card expected the smuggled row to show "the admin-stamped owner". On 17.7.0 owner_id stays null on a public submission (security plugin: "Ownership stays NULL for object hooks / the first-admin bootstrap to assign"); the post says so.

Void first attempts, disclosed in the post: my first phase B and phase C boots served the previous dist/objectstack.json — objectstack dev --no-watch compiles only when the artifact is missing or --compile is passed (dev.ts:382) and printed ⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build (dev.ts:457). Both phases were redone on a fresh build before anything was written down.

Sources (tag @objectstack/cli@17.7.0 unless noted; line numbers at the tag)

  • content/docs/ui/public-data-collection.mdx:9 — the scenario quote. :13 — "Declare a public form view; the submit route derives a narrow authorization from that declaration. No guest profile, no broad grant". :41-42 — the two endpoints. :47 — "Only the fields listed in sections[].fields are accepted on submit; everything else is stripped server-side." and "derives a narrow publicFormGrant: { object: 'showcase_inquiry' } — create + read-back of the row it just made". :49 — "System-managed anchors … are never accepted from a public submission — not even when a section declares them". :53 — "The public path only ever inserts; it never lists." :57 — "Both anonymous endpoints then answer 404 FORM_NOT_FOUND". :77 — "Authorization is derived from the declaration, not configured separately — so the grant can't drift wider than the form." :86 — anti-pattern "Granting a guest profile broad write". :53 — "Set sharingModel: 'private' on the object".
  • examples/app-crm/src/views/lead.view.ts:96-104 — the comment "sharing.allowAnonymous opens the public endpoint; the guest_portal profile (INSERT-only on crm_lead) authorizes the write"; :106-128 the web_to_lead form quoted in the post; :114-120 the five fields; :123-127 the sharing block.
  • examples/app-crm/src/security/sales-positions.ts:59-73 — GuestPortalProfile, "INSERT-only — guests can never read, edit, or delete any record."
  • examples/app-crm/objectstack.config.ts:101 — permissions: [SalesUserPermissionSet, GuestPortalProfile].
  • examples/app-crm/src/objects/lead.object.ts:12 — sharingModel: 'public_read_write' with the :8-11 comment; :41-47 — status defaultValue: 'new' "so minimal creates (e.g. the public Web-to-Lead form, which doesn't expose status) satisfy required"; thirteen Field.* declarations (:19-90).
  • content/docs/permissions/explain.mdx:88-90 — "since v17 anonymous access to object data is always denied anyway — the requireAuth: false opt-out was retired".
  • packages/core/src/security/anonymous-deny.ts:42 — "The requireAuth opt-out is gone"; :52-53 — "public form submission → publicFormGrant (ADR-0056 Option A), derived from the form view's own declaration".
  • packages/spec/src/security/public-form.ts:31-54 — PUBLIC_FORM_SERVER_MANAGED_FIELDS, the twelve names.
  • packages/rest/src/rest-server.ts:10702-10706 — the route docstring: "Security is delegated to the guest_portal permission set carried on the execution context" … "falls open with permissions: []"; :11024 — "Only allow the fields declared on the matched FormView"; :11072 — zero-field form refused with 400; :11090 — server-managed names skipped; :11102-11107 — "work under secure-by-default (anonymous-deny) WITHOUT a deployment-configured guest_portal. guest_portal + anonymous are kept for back-compat with object hooks", publicFormGrant: { object: match.object }, permissions: ['guest_portal'].
  • packages/plugins/plugin-security/src/security-plugin.ts:2334-2336 — "authorizes ONLY create + the immediate read-back on THAT object … WITHOUT a deployment-configured guest_portal"; :2351-2356 — grant admits insert, find, findOne, count on that object only; :2367 — "Ownership stays NULL for object hooks / the first-admin bootstrap to assign"; :2374 — the second strip of the same names.
  • packages/cli/src/commands/dev.ts:382 — needsCompile = … (flags.compile || !fs.existsSync(artifactPath)); :457 — the stale-build warning.
  • www content/blog/how-to-self-host-objectstack/index.mdx:187-193 — the 401 UNAUTHENTICATED probe this post links to.
  • The access-matrix refusal text is the CLI's own output on 17.7.0 (build-B.log).

Still true at objectstack origin/main 570c7a2d95: lead.view.ts:95-106 and sales-positions.ts:63-73 carry the same guest-profile comments; rest-server.ts:9066-9071 still has the "falls open" docstring beside the :9481-9485 "WITHOUT a deployment-configured guest_portal" code; the submit route (:9408-9473) still builds the allow-list from sections[].fields with no section-level required check.

Gates

Gate Result
pnpm content:lint pass (355 files); warnings only: title 111 cols / description 224 chars (advisory, writing rule 2), date/updated in the future (slot post)
pnpm content:lint --published pass
pnpm svg:lint (the four new SVGs) pass — 196 text boxes, 2 filtered elements, 0 findings
pnpm check (under the shared verify lock) 0 errors, 0 warnings, 0 hints (160 files)
pnpm build (under the lock, twice; final run on the committed content) 920 pages; --dist prose, CJK breaks, quotes and links all pass; tree clean afterwards (git status empty)
pnpm seo:smoke pass (910 HTML pages; 17,419 prose blocks language-checked) — first run failed on the English scenario blockquote in ja/ko/de/es/fr, fixed by keeping only the translated quote and rebuilding
pnpm gen:zh-hant 48 generated, 0 hand-maintained; one locative warning (指南里) resolved by rewording the zh-Hans source to 指南中

Browser check (looked at, not only asserted)

astro preview on 127.0.0.1:49165; Playwright with the preinstalled Chromium. en and zh-Hans article plus /en/blog/ at 1366×625, 1440×750, 1512×830, 1920×945, 390×844 and 360×800, light and dark, lazy images scrolled into view first: 210/210 assertions pass (HTTP 200, no horizontal overflow, no element past the viewport outside a scroll container, no repeated image, cover rendered, every body image loaded). Viewport crops were taken at the hero, the diagram, both tables and the closing section and inspected; the blog list was scrolled to the new card (first position, cover, date, tags) at 1366 light, 390 dark and 1920 dark. The three back-linked posts were checked at 1440×750: the Updated Oct 26, 2026 line renders and each link resolves to /en/blog/how-to-let-anonymous-visitors-submit-a-form/. Every new SVG was rendered at native size (1200×675 and 1200×720) on light and dark; one defect found and fixed before commit (the GET /api/v1/data/crm_lead label overran its window on the covers; now 11px via an inline style, since a presentation attribute would lose to the class). Screenshots: /tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/www/shots-272/ (svg/, crops/, listcard-*, backlink-*, results.json).

Acceptance notes

  • The card's "docs wrong for 17.7.0?" branch did not trigger: public-data-collection.mdx:13 is right on 17.7.0. What is wrong is the example's comment and the runtime's own route docstring — filed below for the seat.
  • rest-server.ts:11028-11029 points to CRM lead.hook.ts / case.hook.ts as "the canonical pattern"; neither file exists at the tag (examples/app-crm/src/hooks/ holds only opportunity.hook.ts). Stale comment; noted, not filed; carrier: whoever fixes the guest-profile comments.
  • The 201 echo returns the full row including organization_id and system columns. That is the documented read-back (:47); the post names it as a limit rather than a defect.
  • npm create objectstack@latest was not itself run in this session; the proof used the CRM example at the tag with dependencies pinned to the published 17.7.0 packages, which the card lists as the first option.

维护者速读(草稿)

改了什么。 新增一篇博文(八个语言版本、封面与一张示意图),讲清 ObjectStack 17.7.0 上「匿名访客怎样把表单提交进受治理应用」:唯一的门是公开表单视图,授权从表单声明推导,五个字段能进、其余一律挡下,匿名读取一律 401,撤回后 404。三篇已发布文章各加一句回链并设 updated: 2026-10-26。

为什么改。 这是 #267 日更计划第一批的第五篇(周一 10-26 档期),填补「公开表单 / web-to-lead / 匿名提交」此前没有任何文章覆盖的空白;每个数字都在已发布的 17.7.0 上实测。

风险与代价(含回滚)。 内容类改动,不触及站点代码;门禁与六视口浏览器检查全部通过。回滚只需 revert 这一个提交。注意:文章如实指出 CRM 示例注释与运行时注释关于 guest_portal 的说法在 17.7.0 上不成立(文档是对的),该发现已另行上报,不影响本文发布。

席位意见。 (留空)

你要做的。 档期日合并;若日更切换推迟,先改 date 与三处 updated 再合并。


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

…ic door, five fields wide (#272)

New post in all eight locales (zh-Hant generated by gen:zh-hant), with a
cover pair and a two-nets diagram pair. Every transcript was produced on
the published @objectstack/cli 17.7.0 against examples/app-crm at the
@objectstack/cli@17.7.0 tag: the five-field public form answers 201 for a
clean and for a smuggling POST (status, lead_score, owner_id and
organization_id stripped), 401 UNAUTHENTICATED on every anonymous read,
and 404 FORM_NOT_FOUND once sharing.enabled is false. The guest_portal
profile was removed and re-measured: identical, so the doc's "no guest
profile" is the load-bearing story on 17.7.0 and the example's comment
is not.

Back-links with updated: 2026-10-26 on how-to-self-host-objectstack (en),
objectos-agent-permission-boundaries (en, zh-Hans, zh-Hant) and
is-lovable-safe-for-production (all eight locales).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 02:19
…ho; slot moves to 2026-10-22

Limits bullet "The 201 echoes the whole row" extended in all eight locales
(zh-Hant regenerated): whatever a beforeInsert/afterInsert hook on the
target object stamps, including values derived from existing records,
reaches the anonymous submitter in the 201 on 17.7.0 — so keep hooks that
look up existing records (de-duplication, round-robin assignment) out of
the insert path of a public-intake object and do that routing after
intake, in a flow or a scheduled job. No exploit spelled out, no issue
cited, no unreleased behaviour mentioned.

Daily publishing starts 2026-10-18, so the slot moves from Mon 2026-10-26
to Thu 2026-10-22: date: 2026-10-22 in all eight locales, and
updated: 2026-10-22 on the three back-linked posts (how-to-self-host-
objectstack en; objectos-agent-permission-boundaries en/zh-Hans/zh-Hant;
is-lovable-safe-for-production all eight).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet objectstack-fleet Bot changed the title content(blog): how to let anonymous visitors submit a form into a governed business app (slot 2026-10-26) content(blog): how to let anonymous visitors submit a form into a governed business app (slot 2026-10-22) Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post (slot Thu 2026-10-22): How Do You Let Anonymous Visitors Submit a Form Into a Governed Business App? One Public Door, Five Fields Wide

2 participants