Repository navigation
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
Conversation
…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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #272
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_idprediction was corrected to the measurednull. The two upstream findings (the guest_portal comments, and form-levelrequirednot 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-linkedupdatedvalues 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,frauthored;zh-Hantproduced bypnpm gen:zh-hant), cover pair (cover-en.svg,cover.svg) and one diagram pair (two-nets-en.svg,two-nets.svg).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).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. Audiencebusiness, topicapp-building. Canonical T4 closes every locale verbatim fromsrc/lib/positioning.ts.Proof artifact — produced on the published
@objectstack/cli17.7.0examples/app-crmexported from tag@objectstack/cli@17.7.0(commit4e4e881427), dependencies pinned to the published 17.7.0 packages (@objectstack/cli,runtime,spec,service-i18n,plugin-hono-serverall resolve to 17.7.0), booted withobjectstack dev --no-watchon 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).guest_portal(phase A, port 49160)guest_portal(phase B, 49163)GET /api/v1/forms/contact-us, anonymousobjectSchemacarries exactlyname,email,phone,company,titleFORM_NOT_FOUNDPOST …/submitwithname,emaillY0qNhApoHwyYMtk,status: "new",source: "web",lead_score: 0,owner_id: null,organization_id: "org_mvbpxfocznjyqv4e"FORM_NOT_FOUNDPOST …/submitsmugglingstatus: "converted",lead_score: 100,owner_id: "attacker",organization_id: "org_other",notesOJINuRoRVtZC-l95; stored row read as admin:status: "new",lead_score: 0,owner_id: null,organization_id= the server's own,notes: nullkL-lplom1jyxJyCh)GET /api/v1/data/crm_lead, anonymous{"error":"UNAUTHENTICATED","code":"UNAUTHENTICATED",…}GET /api/v1/data/crm_lead/IDof the row just created, anonymousPOST …/submitwithout form-requiredemailemail: nullemail: nullPOST …/submitwithout object-requirednameVALIDATION_FAILED"Lead Name is required"Removing
GuestPortalProfilefrompermissionsfirst madeobjectstack buildrefuse:✗ 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_idstaysnullon 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-watchcompiles only when the artifact is missing or--compileis 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.0unless 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. Noguestprofile, no broad grant".:41-42— the two endpoints.:47— "Only the fields listed insections[].fieldsare accepted on submit; everything else is stripped server-side." and "derives a narrowpublicFormGrant: { 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 answer404 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 aguestprofile broad write".:53— "SetsharingModel: 'private'on the object".examples/app-crm/src/views/lead.view.ts:96-104— the comment "sharing.allowAnonymousopens the public endpoint; theguest_portalprofile (INSERT-only on crm_lead) authorizes the write";:106-128theweb_to_leadform quoted in the post;:114-120the five fields;:123-127thesharingblock.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-11comment;:41-47—statusdefaultValue: 'new'"so minimal creates (e.g. the public Web-to-Lead form, which doesn't expose status) satisfyrequired"; thirteenField.*declarations (:19-90).content/docs/permissions/explain.mdx:88-90— "since v17 anonymous access to object data is always denied anyway — therequireAuth: falseopt-out was retired".packages/core/src/security/anonymous-deny.ts:42— "TherequireAuthopt-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 theguest_portalpermission set carried on the execution context" … "falls open withpermissions: []";: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-configuredguest_portal.guest_portal+anonymousare 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-configuredguest_portal";:2351-2356— grant admitsinsert,find,findOne,counton 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.content/blog/how-to-self-host-objectstack/index.mdx:187-193— the401 UNAUTHENTICATEDprobe this post links to.build-B.log).Still true at objectstack
origin/main570c7a2d95:lead.view.ts:95-106andsales-positions.ts:63-73carry the same guest-profile comments;rest-server.ts:9066-9071still has the "falls open" docstring beside the:9481-9485"WITHOUT a deployment-configuredguest_portal" code; the submit route (:9408-9473) still builds the allow-list fromsections[].fieldswith no section-levelrequiredcheck.Gates
pnpm content:lintdate/updatedin the future (slot post)pnpm content:lint --publishedpnpm svg:lint(the four new SVGs)pnpm check(under the shared verify lock)pnpm build(under the lock, twice; final run on the committed content)--distprose, CJK breaks, quotes and links all pass; tree clean afterwards (git statusempty)pnpm seo:smokepnpm gen:zh-hant指南里) resolved by rewording the zh-Hans source to指南中Browser check (looked at, not only asserted)
astro previewon 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: theUpdated Oct 26, 2026line 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 (theGET /api/v1/data/crm_leadlabel 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
public-data-collection.mdx:13is 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-11029points to CRMlead.hook.ts/case.hook.tsas "the canonical pattern"; neither file exists at the tag (examples/app-crm/src/hooks/holds onlyopportunity.hook.ts). Stale comment; noted, not filed; carrier: whoever fixes the guest-profile comments.organization_idand system columns. That is the documented read-back (:47); the post names it as a limit rather than a defect.npm create objectstack@latestwas 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