| title | v12.0.0 |
|---|---|
| description | Anonymous data access denied by default (ADR-0056 D2), adaptive record surfaces, an enforced protocol-version handshake, and a wave of author-time correctness lints. |
Released July 2026. All @objectstack/* packages are published at 12.0.0.
This major flips the platform's default security posture — anonymous requests
to the data API are now denied — and lands a wave of author-time correctness
features: adaptive record surfaces derived from metadata, package-shipped
permission sets, an enforced engines.protocol handshake, and build-gating
lints for view references, form layouts, and import mappings.
TL;DR for upgraders: if any deployment serves data anonymously (a demo,
kiosk, or public playground), you must now set api: { requireAuth: false }
on the stack or every /data/* call returns HTTP 401. Everything else in this
release is additive — existing metadata keeps loading.
The global requireAuth default flipped from false to true
(RestApiConfigSchema.requireAuth in @objectstack/spec, mirrored by
RestServer.normalizeConfig in @objectstack/rest). Anonymous requests to the
/data/* CRUD and batch endpoints are now rejected with HTTP 401 unless
the deployment explicitly opts out.
Migration (one line). A deployment that intentionally serves data publicly
sets the flag on the stack config — now a declared
ObjectStackDefinitionSchema.api field, so it survives defineStack strict
parsing (an undeclared top-level api key was previously stripped silently):
export default defineStack({
// …
api: { requireAuth: false },
});The REST plugin logs a boot warning for the explicit opt-out, so a fail-open
posture is always visible. A misplaced api.requireAuth at the plugin level
(one nesting short) is also called out with a boot warning instead of being
ignored.
What keeps working with no action:
- Share links — validate their token, then read under a system context.
- Public forms — self-authorizing via the declaration-derived
publicFormGrant(create + read-back on the declared target object only). - Control plane —
/auth,/health,/discoveryare exempt. objectstack servewith an auth-less stack — the CLI passes an explicitrequireAuth: falsefor stacks whose tier set has noauth(nothing could authenticate against them), with the boot warning.
Scope note: the deny posture applies uniformly to every HTTP surface that reaches object data (#2567) — the REST
/data/*CRUD + batch routes, the/metametadata endpoints, the dispatcher's GraphQL endpoint, and the raw-hono/dataroutes share one decision (shouldDenyAnonymousin@objectstack/core), so a caller denied on/datacannot read the same rows through a sibling door.
Because all metadata is AI-authored, the design goal is to make the model unable to get layout wrong — so these features are derived, not authored:
deriveRecordSurface(ADR-0085 §5) — a record's default surface (fullpagevs.drawer/modaloverlay) is derived from how heavy the record is (visible, non-system field count; mobile always pages). No new object key; an explicit form/navigation config still wins.FormField.span: 'auto' | 'full'— replaces absolutecolSpanas the primary primitive. Under a per-surface derived column count (mobile 1 / modal 2 / page 3–4),spanis decoupled from the count so a field lays out correctly at any width.colSpanis retained and clamped for back-compat.NavigationConfig.size— a T-shirt bucket (auto/sm/md/lg/xl/full) replacing the pixelwidth/drawerWidth, which an AI author cannot choose blind.autolets the renderer size from field count and clamp to the viewport.validateFormLayoutlint — advisoryform-field-unknown(a section references a field not on the bound object) andabsolute-colspan-discouraged.
deriveRecordFlowSurface (12.2.0) extends this to be flow-aware: view keeps
the shipped behavior, while the create/edit/child-* task flows are always
overlays (a task's URL is a false promise — a refresh loses the draft).
Field.relatedList on a child's lookup/master_detail FK becomes a
tri-state boolean | 'primary'. 'primary' marks a core relationship that the
detail page promotes to its own tab; non-primary children collapse into a
single shared Related tab. RecordRelatedListProps.columns becomes
optional — when omitted, columns derive from the child object's
highlightFields. Purely additive and opt-in per relationship.
Packages now ship working default access for their own objects, with a machine-checkable metadata↔config boundary:
PermissionSetSchema.packageId(owning package) and per-record provenancemanagedBy: 'package' | 'platform' | 'user', persisted onsys_permission_setaspackage_id/managed_by.bootstrapDeclaredPermissionsmaterializesstack.permissionsintosys_permission_setat boot (managed_by: 'package'). Idempotent and upgrade-aware: seeder-owned rows are re-seeded to the shipped declaration; rows owned by a different package are refused loudly; env-authored rows are never clobbered. Closes the inert-metadata gap where declared sets were enforced at runtime but never materialized (invisible to the admin surface).
PluginEnginesSchema.protocol was declared, documented, and checked by no
loader or installer. Now:
@objectstack/specexportsPROTOCOL_VERSION/PROTOCOL_MAJOR— the single source of truth, kept in lockstep with the package major by a drift test.@objectstack/metadata-coreaddscheckProtocolCompat()/assertProtocolCompat()and the structuredProtocolIncompatibleError(OS_PROTOCOL_INCOMPATIBLE, carrying both versions and theobjectstack migrate meta --from Ncommand).installPackageruns the handshake before writing to the registry — an incompatible package is refused with a machine-actionable diagnostic instead of crashing later in a schema.parse().
Additive: packages that declare no engines.protocol range keep loading (with
a warning).
POST /data/:object/import accepts mappingName, resolving a registered
defineMapping artifact (stack mappings:) and applying its fieldMapping
pipeline (rename + constant/map/split/join; lookups delegate to the built-in
reference resolution) as a strict projection before coercion. Errors are loud
and specific (MAPPING_NOT_FOUND, MAPPING_TARGET_MISMATCH,
UNSUPPORTED_TRANSFORM for javascript, …). defineStack cross-reference
validation rejects mappings targeting undefined objects at build time.
ObjectNavItem.filters— declarative URL filter conditions (filter[field]=value) targeting the parameterized bare data surface, for one-off / parameterized slices (dashboard drill-throughs, "assigned to me"). Values support the{current_user_id}/{current_org_id}template variables. CombiningfilterswithrecordId/viewNameis now unwritable (correct-by-construction).SelectOption.visibleWhen— a per-option CEL visibility predicate forselect/multiselect/radio, expressing cascading / dependent options (record.country == 'cn') and context gating without a bespoke matrix.Action.order— an explicit ordering lever (lower = more prominent) for which action holds the record-header primary-button slot, instead of relying on fragile cross-file registration order. Fully backward compatible (unset =0, stable sort).- Retired placeholder metadata kinds (ADR-0088) —
trigger,router,function,serviceare removed fromMetadataTypeSchema(30 → 26 kinds); each had no authoring surface. The delivered replacements arehook/record_changeflows, plugincontributes.routes+ declarativeapis:,defineStack({ functions }), and the plugin/service registry.
lint-view-refs(#2554) — atype: 'form'action whosetargetresolves to a list view now failsos compile(the concrete breakage: a blank form, a silently no-op submit). Silent<object>.<key>_2rename collisions are surfaced as warnings. Shifts objectui's runtimeviewKindguard left to compile time.validateListViewMode(ADR-0047) — atabs-styleuserFilters(element: 'tabs') on an object list view is now atscerror viaObjectListViewSchema(dropdown value chips remain allowed), stripped at parse for back-compat, and reported pre-parse with a fix hint.transfer/restore/purgepre-mapped to RBAC bits (#1883) — the permission evaluator maps the destructive lifecycle operations toallowTransfer/allowRestore/allowPurge(with themodifyAllRecordsbypass), so the gate exists ahead of the operations themselves (roadmap M2). Descriptions moved from[EXPERIMENTAL — not enforced]to[RBAC-gated; operation pending M2].
The Console frozen into @objectstack/console across the 12.x line (objectui
77826989..b0aa2512, 58 commits) is a broad Studio and record-UX push:
- Studio authoring surfaces — metadata-driven Validations / Hooks / Actions config panels, per-object API / Hooks / Actions tabs, a form designer whose column density matches the runtime form, and a review-then-publish confirmation with field-level change detail.
- Security authoring UI (pairing with the v12–v13 security work) — an OWD
sharing-model control in Studio Settings, ADR-0066 posture /
per-operation
requiredPermissions/ capability-lint surfaced in the authoring UI, and ADR-0090 D2 provenance + default badges replacing the old profile toggles. - Record UX — create/edit task flows as derived overlays with a lossless
return to origin (framework#2604), the
/:objectName/dataparameterized bare data surface with removable filter chips, URL-driven detail tabs (?tab=survives remounts), per-user persisted list filters,Action.orderhonored in the record header, cascadingvisibleWhenselect options, and registered import mappings in the import wizard. - Infrastructure — a data-invalidation bus (refresh data, don't rebuild the UI) behind the snappier navigation.
Per-commit detail: the objectui CHANGELOG.
- Package seed can no longer wedge the platform via record-change
automation (12.5.0). A seeded record whose lifecycle flow self-triggered
looped forever because a boolean re-entry guard never tripped (booleans
persist as integer
1on SQLite/libsql and CEL1 != true). Fixed at three layers:ExecutionContext.skipTriggersfor seed/bulk writes,coerceBooleanFields()on the after-hook view, and anactiveRecordFlowsself-trigger backstop in the automation engine. Field.relatedListJSDoc corrected (12.1.0) — non-primary related lists stack under one shared "Related" tab; there is no count-based auto-split.
- Update all
@objectstack/*dependencies to^12.0.0(they are version-locked — upgrade them together). - If any deployment serves data anonymously, add
api: { requireAuth: false }to itsdefineStackconfig and confirm the boot warning appears. - Re-run
os compile/os validate— the new view-reference, list-view-mode, and form-layout lints may flag pre-existing authoring mistakes. - Declare
engines.protocolon any package you ship so the handshake advertises a real range instead of grandfathering with a warning.