Skip to content

Commit 91ca7f3

Browse files
committed
docs(forms,app-crm): forms.mdx says what the guest_portal name does; 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
1 parent c564b0e commit 91ca7f3

2 files changed

Lines changed: 31 additions & 33 deletions

File tree

‎content/docs/ui/forms.mdx‎

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ Only the spec'd whitelist of form fields is accepted; everything else (status, o
4747
> exactly that object, never anything else and never the anonymous fall-open. So
4848
> public forms work under the unconditional anonymous-deny posture (v17
4949
> retired the `requireAuth: false` opt-out, #3963) with **no**
50-
> `guest_portal` permission set. The `guest_portal` permission set + `anonymous` flag
51-
> are still attached for **back-compat** (object hooks that detect a guest via a
52-
> falsy `ctx.user?.id`), but they are no longer the authorization mechanism.
50+
> `guest_portal` permission set. The submit's context also carries
51+
> `permissions: ['guest_portal']` and `anonymous: true`. Neither authorizes
52+
> anything and neither reaches an object hook; section 2 says the one thing the
53+
> `guest_portal` name does.
5354
5455
## 1. Declare the form view
5556

@@ -102,32 +103,27 @@ export default defineView({
102103
> - Multiple form views per object are fine — only the one(s) with `sharing.enabled === true` and `sharing.allowAnonymous === true` are exposed.
103104
> - On a **walled** tenancy posture (`group` or `isolated` in force), a form whose object is walled by an organization column is **not offered**. An anonymous submission carries no organization, and an insert without one into a walled object is refused, so both anonymous endpoints answer the form exactly as they answer a withdrawn one (`404 FORM_NOT_FOUND`). The administrator is told why, at the form's `sharing`: the view's read (`GET /api/v1/meta/view/:name`) carries it in `_diagnostics.warnings`, and a save or publish of the view answers success with a `public-form-intake-unavailable` warning in `advisories`. If the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it and the form is offered again.
104105
105-
## 2. (Optional) Create the `guest_portal` permission set
106-
107-
The submit's authorization no longer depends on this set — the
108-
declaration-derived `publicFormGrant` (see the note above) is what permits the
109-
insert. For the submit you only need a `guest_portal` permission set if you rely
110-
on the legacy back-compat path (e.g. an older runtime, or object hooks that
111-
branch on the `guest_portal` permission). When present the set is attached to
112-
the anonymous context, so keep it INSERT-only on the form's target object — the
113-
guest-safe shape (ADR-0090 D9).
114-
115-
{/* os:check */}
116-
```ts
117-
// hotcrm/src/security/guest-portal.permission.ts
118-
import { definePermissionSet } from '@objectstack/spec/security';
119-
120-
// Guest-safe capability: INSERT-only on the intake objects. (`isProfile` no
121-
// longer exists — the Profile concept was removed by ADR-0090 D2.)
122-
export default definePermissionSet({
123-
name: 'guest_portal',
124-
label: 'Public Form Submitters',
125-
objects: {
126-
lead: { allowCreate: true }, // no read/edit/delete
127-
case: { allowCreate: true },
128-
},
129-
});
130-
```
106+
## 2. No permission set to create — what the `guest_portal` name does
107+
108+
The submit is authorized by the declaration-derived `publicFormGrant` (see the
109+
note above). The grant is decided before any permission set resolves, so the
110+
form works the same whether or not a `guest_portal` permission set exists. The
111+
submit's execution context still names `guest_portal` in `permissions`, and that
112+
name reaches one thing only:
113+
114+
- **The result masker, where a deployment registers a set of that name.** Only
115+
the set's field permissions (`fields`, such as `readable: false`) and the
116+
capabilities it holds against a field's `requiredPermissions` are read, to mask
117+
the created record the server hands back to the route. The set's object
118+
permissions (`allowCreate`, `allowRead`, …) are not read on this path, so they
119+
neither grant nor limit the insert.
120+
- **The visitor never sees that record.** The submit answers with the new
121+
record's `id` only (section 4). Register a `guest_portal` set only if you want
122+
fields masked on the record the route gets back; nothing else needs one.
123+
- **No object hook can read it.** A hook's `ctx.session` and `ctx.user` carry
124+
neither `permissions` nor `anonymous`, and on a public submission both are
125+
unset because the context carries no user. Tell a public submission apart by
126+
the missing `ctx.user` (section 3), not by a permission.
131127

132128
## 3. Stamp server-controlled defaults in a hook
133129

@@ -247,7 +243,7 @@ lookup route.
247243
### Auth model
248244

249245
- Neither route calls `enforceAuth`, so both work under the always-on anonymous-deny default (there is no `requireAuth` knob to configure since v17).
250-
- The execution context the **submit** route hands to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin honors `publicFormGrant` first — a create + read-back grant scoped to exactly the declared object — so authorization holds even without a `guest_portal` profile. `permissions: ['guest_portal']` is retained for back-compat.
246+
- The execution context the **submit** route hands to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin admits on `publicFormGrant` alone — a create + read-back grant scoped to exactly the declared object, decided before any permission set resolves — so no `guest_portal` set is needed. Neither `permissions` nor `anonymous` authorizes anything or reaches an object hook; where a set named `guest_portal` is registered, the result masker reads its field permissions (section 2).
251247
- No CSRF or auth header is needed; embed the form on any domain.
252248

253249
## 5. Embedding from a front-end

‎examples/app-crm/src/objects/lead.object.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ export const Lead = ObjectSchema.create({
4141
status: Field.select({
4242
label: 'Status',
4343
required: true,
44-
// Write-path default so minimal creates (e.g. the public Web-to-Lead form,
45-
// which doesn't expose status) satisfy `required` — the option `default`
46-
// below is only a UI preselect.
44+
// The write-path default: a create that omits `status` (e.g. the public
45+
// Web-to-Lead form, which doesn't collect it) is stored as 'new', so it
46+
// satisfies `required`. A declared `defaultValue` takes precedence over
47+
// the option marked `default: true` below, which the engine falls back to
48+
// on write only for a select that declares no `defaultValue`.
4749
defaultValue: 'new',
4850
options: [
4951
{ label: 'New', value: 'new', default: true, color: '#94A3B8' },

0 commit comments

Comments
 (0)