fix(rest): resolve public forms from flattened view metadata#1989
Merged
Conversation
`findPublicFormView` (the /forms/:slug + /forms/:slug/submit resolver) only
matched the *authoring* view shape — `view.form.sharing` and
`view.formViews.{key}.sharing`. But `getMetaItems({type:'view'})` returns the
*registered* shape: one flattened item per view, `{ name, object, viewKind,
config: { data, sections, sharing } }`. The form's `sharing` (allowAnonymous +
publicLink) lives under `config`, which the resolver never inspected — so every
standard public form silently failed with FORM_NOT_FOUND, making Web-to-Lead /
Web-to-Case non-functional despite being fully declared.
Add a third candidate source: a `viewKind === 'form'` item whose `config` carries
`sharing`. Object name resolves via `config.data.object` / the item's `object`.
Verified end-to-end (HotCRM): anonymous GET /api/v1/forms/contact-us → 200 form
spec; POST /api/v1/forms/contact-us/submit (no auth) → creates a crm_lead;
/forms/support → crm_case; guest reads still 401 (INSERT-only enforced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
added a commit
that referenced
this pull request
Jun 17, 2026
The app-crm example showed Web-to-Lead only via `customer.portal` `anonymousEntry` — a spec property with NO runtime consumer, so it never worked (404). Add the mechanism that actually does, and make the example coherent: - lead.view.ts: `web_to_lead` form view with `sharing.allowAnonymous` → live `GET/POST /api/v1/forms/contact-us` (+ /submit). - sales-roles.ts: `GuestPortalProfile` (isProfile, INSERT-only on crm_lead, keyed by FULL object name — the anonymous permission path requires it). - lead.object.ts: `status` gets `defaultValue: 'new'` so a minimal public create satisfies `required` (the option-level `default` is only a UI preselect). - customer.portal.ts: drop the dead `anonymousEntry` routes; point to the working form view instead (re-add when the runtime mounts anonymousEntry). - security/index.ts + objectstack.config.ts: export + register the guest profile. Doubles as a CI regression test for the public-form path (builds against the workspace) — the #1989 flattened-metadata resolution bug would have been caught here. Verified end-to-end (app-crm, no auth): GET /forms/contact-us → 200 form spec; POST /forms/contact-us/submit → creates a crm_lead (status=new); GET /data/crm_lead → 401 (guests can't read). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jun 17, 2026
…bjects (#1997) * feat(crm): working Web-to-Lead public form example The app-crm example showed Web-to-Lead only via `customer.portal` `anonymousEntry` — a spec property with NO runtime consumer, so it never worked (404). Add the mechanism that actually does, and make the example coherent: - lead.view.ts: `web_to_lead` form view with `sharing.allowAnonymous` → live `GET/POST /api/v1/forms/contact-us` (+ /submit). - sales-roles.ts: `GuestPortalProfile` (isProfile, INSERT-only on crm_lead, keyed by FULL object name — the anonymous permission path requires it). - lead.object.ts: `status` gets `defaultValue: 'new'` so a minimal public create satisfies `required` (the option-level `default` is only a UI preselect). - customer.portal.ts: drop the dead `anonymousEntry` routes; point to the working form view instead (re-add when the runtime mounts anonymousEntry). - security/index.ts + objectstack.config.ts: export + register the guest profile. Doubles as a CI regression test for the public-form path (builds against the workspace) — the #1989 flattened-metadata resolution bug would have been caught here. Verified end-to-end (app-crm, no auth): GET /forms/contact-us → 200 form spec; POST /forms/contact-us/submit → creates a crm_lead (status=new); GET /data/crm_lead → 401 (guests can't read). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(spec): cross-validate permission/profile object grants against declared objects `validateCrossReferences` checked hook/view/seed/app-nav/action references against the declared object set — but NOT permission-set/profile `objects` grants, even though `validateNamespacePrefix`'s doc already assumed it did. So a profile that grants on a non-existent object (e.g. a short `lead` instead of the namespaced `crm_lead`) passed build/validate/test silently. The grant then applies to nothing: the authenticated path may namespace-resolve the short name, but the anonymous / explicit-permission-set path does not — so e.g. a public Web-to-Lead INSERT is denied for "roles []", with no diagnostic anywhere. Add the missing check: every `permissions[].objects` key must reference a declared object, or strict validation fails loudly at build time. - stack.zod.ts: validate permission/profile object grants → object references - stack.test.ts: +2 cases (short/undefined name fails; full name passes) 82/82 spec stack tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
Fix
findPublicFormViewso the public-form endpoints (/forms/:slug,/forms/:slug/submit) actually resolve standard form views.Bug
The resolver only matched the authoring view shape (
view.form.sharing,view.formViews.{key}.sharing). ButgetMetaItems({type:'view'})returns the registered/flattened shape — one item per view:{ name: 'crm_lead.web_to_lead', object: 'crm_lead', viewKind: 'form', config: { data, sections, sharing } }. Thesharing(allowAnonymous + publicLink) sits underconfig, which the resolver never inspected.Result: every standard Web-to-Lead / Web-to-Case form returned
FORM_NOT_FOUND— the capability was fully declared (profile + view + spec + UI component) but non-functional at runtime. An unenforced/untested surface.Fix
Add a third candidate: a
viewKind === 'form'item whoseconfigcarriessharing. Object name resolves viaconfig.data.object(or the item'sobject).Verification (HotCRM, end-to-end)
GET /api/v1/forms/contact-us(no auth) → 200 form specPOST /api/v1/forms/contact-us/submit(no auth) → creates acrm_leadPOST /api/v1/forms/support/submit(no auth) → creates acrm_caseGET /api/v1/data/crm_lead(no auth) → 401 (guests still can't read — INSERT-only enforced)@objectstack/rest121/121 tests pass.🤖 Generated with Claude Code