-
818e6a3: fix(server-timing): emit the per-request, admin-gated
Server-Timingheader on the standard server (os serve/dev) (#3361)The per-request
Server-Timingpath (#2408) — where an admin sendsX-OS-Debug-Timing: 1(orjson) and gets phase timings while an ordinary user gets nothing — never emitted on the shipped Hono server. The disclosure gate the Hono middleware opens is only ever flipped by the runtime dispatcher'stimedResolveExecutionContext, but the data (/api/v1/data/*) and metadata (/api/v1/meta/*) routes onos serve/devare served by@objectstack/rest'sRestServer(which shadows the Hono plugin's own CRUD), and its identity resolver never opened the gate. Only global mode (OS_SERVER_TIMING=true) — which discloses to every caller, not just admins — worked.- observability: the disclosure predicate
isPerfDisclosurePrincipal(ec)now lives here (the home of the gate), the single definition of "who may pull per-request timings" shared by every HTTP entry point.@objectstack/runtimere-exports it for back-compat. - rest:
RestServer.resolveExecCtxopens the gate for an admin/service principal (via the carriedposturerung), the REST-server analog of the dispatcher — this is the fix that makesos serve/devemit. - plugin-hono-server: the standalone CRUD surface's self-contained
resolveCtxopens the gate too (deriving the rung for the gate decision only, never writing it onto the enforcement context). Adds an e2e test that boots the Hono app and asserts an admin getsServer-Timingwhile a member/anon does not.
- observability: the disclosure predicate
-
Updated dependencies [212b66a]
-
Updated dependencies [d10c4dc]
-
Updated dependencies [9e45b63]
-
Updated dependencies [b20201f]
-
Updated dependencies [818e6a3]
- @objectstack/platform-objects@16.1.0
- @objectstack/spec@16.1.0
- @objectstack/core@16.1.0
- @objectstack/observability@16.1.0
- @objectstack/service-package@16.1.0
- @objectstack/types@16.1.0
-
bfa3c3f: Broadcast a
transactionalBatchcapability bit in discovery so clients negotiate the atomic cross-object batch declaratively, instead of runtime-probing 404/405/501 (#3298).The atomic cross-object batch endpoint (
POST {basePath}/batch, #1604 / ADR-0034 item 4) and its typed SDK surface (client.data.batchTransaction, #3271) already shipped, but discovery never told a client whether a backend actually supports it. Consumers (notably ObjectUI'sObjectStackAdapter) had to probe: fire a/batch, read404/405(no route) or501(no runtime transaction), and only then fall back to non-atomic client-side simulation. That is "find out by calling", not capability negotiation — it cannot be decided at connect time and cannot serve as the "minimum backend supports/batch" gate that blocks hard-deleting the non-atomic fallback downstream.WellKnownCapabilitiesSchemagains a requiredtransactionalBatch: boolean, and every discovery producer fills it honestly (declared === enforced), so it never becomes a declared-but-unpopulated bit:@objectstack/metadata-protocol(getDiscovery) — reports whether the runtime engine can honour a transaction (typeof engine.transaction === 'function'). The/batchhandler runs its ops insideengine.transaction(), which degrades to a non-atomic passthrough (or 501) without one.@objectstack/rest(/discovery) — ANDs the engine signal with whether it actually mounts the route (api.enableBatch), so a server with batch disabled reportsfalseeven on a transaction-capable engine (never advertise an endpoint that would 404).@objectstack/plugin-hono-server(standalone discovery) — reportsfalse: this minimal surface registers CRUD only and does not mount/batch(that ships with@objectstack/rest). Under-reporting is the safe direction — a client keeps its correct-but-slower fallback rather than losing atomicity.@objectstack/client— already normalizes hierarchicalcapabilitiesto flat booleans, soclient.capabilities.transactionalBatchis exposed (and now typed) for declarative consumers.
The bit follows the existing capability semantics:
true⟺ the/batchroute is mounted and the runtime can honour a transaction — the exact condition under which the endpoint returns200rather than404/405/501. Additive and behavior-preserving; only the discovery payload gains a field.
-
22013aa: Split the overloaded
managedBy: 'system'bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). Thesystembucket conflated two incompatible write policies: rows a platform service owns end to end (never user-written), and platform-defined schema whose rows are legitimately admin/user-writable. It carried the same all-false affordance row asbetter-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write these rows through the generic data API (ADR-0049 gap).Rather than add a new
managedByenum value (which would fall through to fully-editableplatformdefaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances= bucket default +userActions), and engine-owned is defined as asystem/append-onlyobject that grants no write:- Writable set declares
userActions— the RBAC link tables (sys_user_position,sys_user_permission_set,sys_position_permission_set),sys_user_preference,sys_approval_delegation, and the messaging config grids (sys_notification_preference/…_subscription/…_template) now declareuserActions: { create, edit, delete: true }. The affordance is a declaration only — theDelegatedAdminGate/ RLS / permission sets remain the authz. - Engine-owned objects locked to reads —
apiMethods: ['get','list']added where absent (jobs, notifications, approval request/approver/token/action,sys_record_share,sys_automation_run, mail/settings/secret audit, the messaging delivery pipeline).sys_secretis explicitly read-locked (an emptyapiMethodsarray fails open). sys_import_jobstays engine-owned: the REST import route now writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp) and the object is locked to['get','list'].- New engine write guard (
assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-ownedsystem/append-onlyobjects, keyed off the resolved affordance;isSystemand context-less engine/service writes bypass by construction. Wired into the security middleware alongside the other data-layer gates. reconcileManagedApiMethods(objectql registry) now runs for every managed bucket, not justbetter-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049)./me/permissionsclamp (plugin-hono-server) now clampssystem/append-onlyas well asbetter-auth, so the client hint reflectspermission ∩ guard.
Potentially breaking: a downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open behaviour will have those verbs stripped (with a warning) and user-context generic writes to it rejected. DeclareuserActionsopening the verbs the object legitimately takes from a user context.better-authkeeps plugin-auth's identity write guard unchanged; the row-levelmanaged_byprovenance vocabulary (ADR-0066) is a different axis and is untouched. - Writable set declares
-
e057f42: fix: harden the bulk-write path — retries, idempotency, contracts, and summary visibility (#3147–#3152)
Six reliability fixes to the batched seed/import +
engine.insert(array)path introduced by the #2678 bulk-write rework:- #3151
bulkWritevalidates thatwriteBatchreturns one record per input row (a short/long/non-array return is degraded per-row, not backfilled as phantom success);engine.insert(array)likewise rejects a short driverbulkCreatereturn instead of padding afterInsert withundefined. - #3150 wraps the two remaining un-retried write points (seed
writeRecord/resolveDeferredUpdates, import's no-createManyDatafallback) inwithTransientRetry;defaultIsTransientErrorshort-circuits definitive logical errors to non-transient. - #3148 import
resolveRefflushes pending creates on a same-object miss so a later row can reference an earlier same-file CREATE, and no longer negatively caches a miss. - #3149 threads an
attemptcounter throughbulkWrite; seed rechecks byexternalIdand import bymatchFieldsbefore re-writing, so a commit-then-lost-response retry cannot duplicate a batch. - #3147
recomputeSummariesretries transient failures and, on exhaustion, surfacesSummaryRecomputeError(ERR_SUMMARY_RECOMPUTE) instead of a silent warn; seed/import recover it to a warning without re-writing. - #3152 autonumbers are assigned after validation, so a batch that dies in validation consumes no sequence value (no number-range gaps).
- #3151
-
43a3efb: fix(rest): gate the cross-object transactional batch by the same per-object API rules as single-record writes (#1604)
The
POST {basePath}/batchroute (issue #1604 / ADR-0034) wraps N cross-object create/update/delete ops in one engine transaction, but it skipped the per-object API-exposure gate every single-record route applies — an authenticated caller could write to anapiEnabled: falseobject, or run an operation outside an object'sapiMethodswhitelist, straight through the batch surface (ADR-0049 / #1889 — the same "declared ≠ enforced" hole closed for the generic write path in #3220 / #3213).The route now:
- validates the body against a new
CrossObjectBatchRequestSchema(@objectstack/spec/api, Zod-First) — a malformed op, an unknown action, or a missingobjectis a400instead of a500; - enforces
enable.apiEnabled/enable.apiMethodsfor every op (metadata fetched once, each distinct(object, action)checked) BEFORE opening the transaction —404 OBJECT_API_DISABLED/405 OBJECT_API_METHOD_NOT_ALLOWED; - requires an
idforupdate/delete(400); - rejects an unresolvable
{ $ref }with400 BATCH_UNRESOLVED_REFinstead of silently writing anullFK; - rejects an explicit
atomic: false(400 BATCH_NOT_ATOMIC) rather than silently applying atomically — non-atomic per-object batches stay onPOST /data/:object/batch.
enforceApiAccessis refactored to share the pureapiAccessDenialFromEnablecheck + aloadObjectItemshelper with the batch route (single-record behavior unchanged). Addsrest-batch-endpoint.test.ts— the REST-boundary coverage ADR-0034 flagged as missing (commit,$ref, rollback surfacing, API-access denial, request validation). - validates the body against a new
-
Updated dependencies [f972574]
-
Updated dependencies [6289ec3]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [8efa395]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [bc65105]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [62a2117]
-
Updated dependencies [83e8f7d]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [06ff734]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [92f5f19]
-
Updated dependencies [32899e6]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
-
Updated dependencies [8ff9210]
- @objectstack/spec@16.0.0
- @objectstack/platform-objects@16.0.0
- @objectstack/core@16.0.0
- @objectstack/types@16.0.0
- @objectstack/service-package@16.0.0
-
bfa3c3f: Broadcast a
transactionalBatchcapability bit in discovery so clients negotiate the atomic cross-object batch declaratively, instead of runtime-probing 404/405/501 (#3298).The atomic cross-object batch endpoint (
POST {basePath}/batch, #1604 / ADR-0034 item 4) and its typed SDK surface (client.data.batchTransaction, #3271) already shipped, but discovery never told a client whether a backend actually supports it. Consumers (notably ObjectUI'sObjectStackAdapter) had to probe: fire a/batch, read404/405(no route) or501(no runtime transaction), and only then fall back to non-atomic client-side simulation. That is "find out by calling", not capability negotiation — it cannot be decided at connect time and cannot serve as the "minimum backend supports/batch" gate that blocks hard-deleting the non-atomic fallback downstream.WellKnownCapabilitiesSchemagains a requiredtransactionalBatch: boolean, and every discovery producer fills it honestly (declared === enforced), so it never becomes a declared-but-unpopulated bit:@objectstack/metadata-protocol(getDiscovery) — reports whether the runtime engine can honour a transaction (typeof engine.transaction === 'function'). The/batchhandler runs its ops insideengine.transaction(), which degrades to a non-atomic passthrough (or 501) without one.@objectstack/rest(/discovery) — ANDs the engine signal with whether it actually mounts the route (api.enableBatch), so a server with batch disabled reportsfalseeven on a transaction-capable engine (never advertise an endpoint that would 404).@objectstack/plugin-hono-server(standalone discovery) — reportsfalse: this minimal surface registers CRUD only and does not mount/batch(that ships with@objectstack/rest). Under-reporting is the safe direction — a client keeps its correct-but-slower fallback rather than losing atomicity.@objectstack/client— already normalizes hierarchicalcapabilitiesto flat booleans, soclient.capabilities.transactionalBatchis exposed (and now typed) for declarative consumers.
The bit follows the existing capability semantics:
true⟺ the/batchroute is mounted and the runtime can honour a transaction — the exact condition under which the endpoint returns200rather than404/405/501. Additive and behavior-preserving; only the discovery payload gains a field.
- Updated dependencies [6289ec3]
- Updated dependencies [8efa395]
- Updated dependencies [bfa3c3f]
- Updated dependencies [62a2117]
- Updated dependencies [06ff734]
- @objectstack/spec@16.0.0-rc.1
- @objectstack/platform-objects@16.0.0-rc.1
- @objectstack/core@16.0.0-rc.1
- @objectstack/service-package@16.0.0-rc.1
- @objectstack/types@16.0.0-rc.1
-
22013aa: Split the overloaded
managedBy: 'system'bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). Thesystembucket conflated two incompatible write policies: rows a platform service owns end to end (never user-written), and platform-defined schema whose rows are legitimately admin/user-writable. It carried the same all-false affordance row asbetter-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write these rows through the generic data API (ADR-0049 gap).Rather than add a new
managedByenum value (which would fall through to fully-editableplatformdefaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances= bucket default +userActions), and engine-owned is defined as asystem/append-onlyobject that grants no write:- Writable set declares
userActions— the RBAC link tables (sys_user_position,sys_user_permission_set,sys_position_permission_set),sys_user_preference,sys_approval_delegation, and the messaging config grids (sys_notification_preference/…_subscription/…_template) now declareuserActions: { create, edit, delete: true }. The affordance is a declaration only — theDelegatedAdminGate/ RLS / permission sets remain the authz. - Engine-owned objects locked to reads —
apiMethods: ['get','list']added where absent (jobs, notifications, approval request/approver/token/action,sys_record_share,sys_automation_run, mail/settings/secret audit, the messaging delivery pipeline).sys_secretis explicitly read-locked (an emptyapiMethodsarray fails open). sys_import_jobstays engine-owned: the REST import route now writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp) and the object is locked to['get','list'].- New engine write guard (
assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-ownedsystem/append-onlyobjects, keyed off the resolved affordance;isSystemand context-less engine/service writes bypass by construction. Wired into the security middleware alongside the other data-layer gates. reconcileManagedApiMethods(objectql registry) now runs for every managed bucket, not justbetter-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049)./me/permissionsclamp (plugin-hono-server) now clampssystem/append-onlyas well asbetter-auth, so the client hint reflectspermission ∩ guard.
Potentially breaking: a downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open behaviour will have those verbs stripped (with a warning) and user-context generic writes to it rejected. DeclareuserActionsopening the verbs the object legitimately takes from a user context.better-authkeeps plugin-auth's identity write guard unchanged; the row-levelmanaged_byprovenance vocabulary (ADR-0066) is a different axis and is untouched. - Writable set declares
-
e057f42: fix: harden the bulk-write path — retries, idempotency, contracts, and summary visibility (#3147–#3152)
Six reliability fixes to the batched seed/import +
engine.insert(array)path introduced by the #2678 bulk-write rework:- #3151
bulkWritevalidates thatwriteBatchreturns one record per input row (a short/long/non-array return is degraded per-row, not backfilled as phantom success);engine.insert(array)likewise rejects a short driverbulkCreatereturn instead of padding afterInsert withundefined. - #3150 wraps the two remaining un-retried write points (seed
writeRecord/resolveDeferredUpdates, import's no-createManyDatafallback) inwithTransientRetry;defaultIsTransientErrorshort-circuits definitive logical errors to non-transient. - #3148 import
resolveRefflushes pending creates on a same-object miss so a later row can reference an earlier same-file CREATE, and no longer negatively caches a miss. - #3149 threads an
attemptcounter throughbulkWrite; seed rechecks byexternalIdand import bymatchFieldsbefore re-writing, so a commit-then-lost-response retry cannot duplicate a batch. - #3147
recomputeSummariesretries transient failures and, on exhaustion, surfacesSummaryRecomputeError(ERR_SUMMARY_RECOMPUTE) instead of a silent warn; seed/import recover it to a warning without re-writing. - #3152 autonumbers are assigned after validation, so a batch that dies in validation consumes no sequence value (no number-range gaps).
- #3151
-
43a3efb: fix(rest): gate the cross-object transactional batch by the same per-object API rules as single-record writes (#1604)
The
POST {basePath}/batchroute (issue #1604 / ADR-0034) wraps N cross-object create/update/delete ops in one engine transaction, but it skipped the per-object API-exposure gate every single-record route applies — an authenticated caller could write to anapiEnabled: falseobject, or run an operation outside an object'sapiMethodswhitelist, straight through the batch surface (ADR-0049 / #1889 — the same "declared ≠ enforced" hole closed for the generic write path in #3220 / #3213).The route now:
- validates the body against a new
CrossObjectBatchRequestSchema(@objectstack/spec/api, Zod-First) — a malformed op, an unknown action, or a missingobjectis a400instead of a500; - enforces
enable.apiEnabled/enable.apiMethodsfor every op (metadata fetched once, each distinct(object, action)checked) BEFORE opening the transaction —404 OBJECT_API_DISABLED/405 OBJECT_API_METHOD_NOT_ALLOWED; - requires an
idforupdate/delete(400); - rejects an unresolvable
{ $ref }with400 BATCH_UNRESOLVED_REFinstead of silently writing anullFK; - rejects an explicit
atomic: false(400 BATCH_NOT_ATOMIC) rather than silently applying atomically — non-atomic per-object batches stay onPOST /data/:object/batch.
enforceApiAccessis refactored to share the pureapiAccessDenialFromEnablecheck + aloadObjectItemshelper with the batch route (single-record behavior unchanged). Addsrest-batch-endpoint.test.ts— the REST-boundary coverage ADR-0034 flagged as missing (commit,$ref, rollback surfacing, API-access denial, request validation). - validates the body against a new
-
Updated dependencies [f972574]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [bc65105]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [83e8f7d]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [92f5f19]
-
Updated dependencies [32899e6]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
- @objectstack/spec@16.0.0-rc.0
- @objectstack/platform-objects@16.0.0-rc.0
- @objectstack/core@16.0.0-rc.0
- @objectstack/types@16.0.0-rc.0
- @objectstack/service-package@16.0.0-rc.0
- @objectstack/spec@15.1.1
- @objectstack/core@15.1.1
- @objectstack/types@15.1.1
- @objectstack/platform-objects@15.1.1
- @objectstack/service-package@15.1.1
-
f531a26: feat(discovery): honest capabilities — standardized stub/fallback marker + realtime route honesty (ADR-0076 D12/A1.5 framework slice, #2462)
Spec — new service self-description marker for honest discovery (ADR-0076 D12):
SERVICE_SELF_INFO_KEY(__serviceInfo),ServiceSelfInfoSchema/ServiceSelfInfo, andreadServiceSelfInfo(), which also normalizes plugin-dev's legacy_dev: trueflag to{ status: 'stub', handlerReady: false }. A registered service that is a stub / dev fake / degraded fallback self-identifies via this marker; a fully real service carries no marker.Runtime + metadata-protocol — both discovery builders (
HttpDispatcher.getDiscoveryInfoand the protocol shim'sgetDiscovery) now honor the marker instead of hardcodingstatus: 'available', handlerReady: truefor every registered service. Dev stubs reportstub, the ObjectQL analytics fallback reportsdegraded(it keeps serving — no/analytics404), and consumers can finally truststatus === 'available'/handlerReady === true.Realtime honesty fix — discovery no longer advertises a
/realtimeroute orwebsockets: true:service-realtimeis an in-process pub/sub bus, no dispatcher branch or plugin mounts any/realtimeHTTP surface, so the advertised route always 404'd. The registered service now reportsstatus: 'degraded', handlerReady: falsewith no route (clients using the SDK are unaffected — it falls back to the conventional path, which behaves exactly as before). Also corrects the advertised realtime provider from the nonexistentplugin-realtimetoservice-realtime.REST (A1.5) — the REST layer's protocol dependency is narrowed from the
ObjectStackProtocolgod-union to the newRestProtocol = DataProtocol & MetadataProtocolslice (exported from@objectstack/rest), per the ADR-0076 D9 incremental narrowing guidance. Type-level only; no runtime change. -
f531a26: feat(attachments): sys_file orphan lifecycle + parent-derived attachment access (#2755)
Orphan lifecycle (ADR-0057). Deleting a
sys_attachmentjoin row used to orphan the backingsys_filerow and its storage bytes forever.sys_filenow declares a lifecycle (ttl 30don a newdeleted_attombstone for orphans;retention 7d onlyWhen status=pendingfor abandoned uploads), the storage plugin's new hooks tombstone a file when its LAST join row is deleted (attachments scope only —Field.file/Field.image/avatar scopes are never touched) and un-tombstone on re-attach, and a new LifecycleService reap guard seam (registerReapGuard) re-verifies zero references at sweep time and deletes the storage bytes before confirming each row reap. A guarded object is never blind-deleted; an erroring guard fails safe (rows retained).Attachment access (ADR-0049, Salesforce parent-derived semantics).
sys_attachmentcreate now requires caller READ visibility of the parent record (403ATTACHMENT_PARENT_ACCESS) and server-stampsuploaded_byfrom the session (client value ignored); delete requires uploader-or-parent-editor (403ATTACHMENT_DELETE_DENIED). The storage upload routes require an authenticated session when an auth service is wired (401AUTH_REQUIRED; bare kernels stay open) and stampowner_idon new files.REMOVED —
sys_attachment.share_type/sys_attachment.visibility. Both fields were modeled in v1 with zero runtime consumers (ADR-0049 parsed-but-unenforced). There is no replacement key: attachment access is derived from the parent record by the hooks above. Writers of these fields should simply stop sending them (unknown-field validation will reject them); existing DB columns are left as unmanaged leftovers, no migration needed.@objectstack/verifygainsBootOptions.extraPluginsfor booting optional service pairs (e.g. storage + audit) in dogfood fixtures. -
f531a26: refactor(security): converge the anonymous-deny decision into one shared function + a source-enumerating ratchet (#2567 Phase 2)
Phase 1 gated every HTTP surface (REST
/data, dispatcher/graphql+/meta, raw-hono/data) against the secure-by-defaultrequireAuthposture, but each seam hand-rolled the same!userId && !isSystem → 401check. Phase 2 removes that duplication and pins the surfaces so a new ungated entry point fails CI.- New
shouldDenyAnonymousin@objectstack/core(security/anonymous-deny.ts) — the single anonymous-deny decision + shared 401 body/constants, mirroring theauth-gate.tspattern (pure function so the seams can never drift). All five seams — RESTenforceAuth, dispatcherhandleGraphQL/handleMetadata/handleAI, honodenyAnonymous— now delegate to it. Pure refactor: no runtime behavior change (verified by the unchanged Phase-1 handler + e2e proofs). Identity resolution and the dynamic exemptions (public-form grants, share-link tokens) are untouched — they run upstream and only ever hand the seam an already-resolved context. - A
discover()ratchet on the authz-conformance matrix — it statically enumerates the data/meta/graphql HTTP entry points from source (curated per-file probes, control-plane routes excluded) and asserts each is classified by a matrixcoverskey. A new/data//meta//graphqlroute (or a removed/stalecovers) now fails CI as UNCLASSIFIED / STALE, not in review. A companion negative test proves the ratchet bites.
A design trap is guarded:
isAuthGateAllowlisted(undefined)returnstrue, so a body-routed seam (GraphQL, which has no request path) must pass no path — the shared function's non-empty-path guard denies anonymous unconditionally there, never falling through to the control-plane allowlist. - New
-
f531a26: fix(authz): carry the derived posture rung on ExecutionContext (#2947)
The ADR-0095 D2 posture ladder (
PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL) is derived once by the shared authz resolver from capability grants, but both HTTP/MCP entry points that build theExecutionContextdropped it — so any enforcement-side reader ofcontext.posturealways sawundefined(the same drop that forced the explain layer to re-derive it, #2949).ExecutionContextSchemanow carries an optionalposturefield, and bothrest-serverand the runtimeresolveExecutionContextplumb the resolver's value through. Additive and behavior-preserving: no enforcement decision consumespostureyet — whether the hot path evaluates by posture remains a larger ADR-level decision — this only stops the already-computed value from being discarded, so enforcement and explain read the same derived rung. -
f531a26: fix(import): make async import-job cancellation actually stop the worker (#2824)
Cancelling a running async import used to have no effect on a synchronous storage driver (better-sqlite3 / wasm fallback): every
awaitin the row loop resolved as a microtask, so a 50k-row import monopolized the Node event loop for minutes — the cancel route's HTTP handler (and every progress poll) could never run, so the in-memory flagshouldCancelpolls was never set. The job then finishedsucceededwith all rows written despite the user's cancel.Three-part fix:
runImportyields one macrotask at every progress boundary (everyprogressEveryrows), so pending I/O — the cancel request, progress polls, any other traffic — gets serviced during a large import. This is the root-cause fix; it also unblocks progress polling for the wizard.- The worker's
shouldCancelnow also reads the durable job row as a fallback: a cancel accepted by another process (or after a restart dropped the in-memory flag) still stops the worker. - A late cancel wins the terminal state: the worker's final patch no
longer overwrites the cancel route's durable
cancelledwithsucceeded, and a job cancelled while stillpendingdoesn't start at all. Counts stay truthful — they reflect what was actually written.
-
f531a26: fix(rest): split multi-value fields on import so
multiple: truecolumns resolve per-token (#3063)The bulk-import coercion (
import-coerce.ts) resolved a reference cell as a single value regardless of the field'smultipleflag: amultiple: truelookup/user cell like张焊工;李质检was passed whole to name resolution and always failed withno <object> matches "张焊工;李质检", so every multi-value association had to be back-filled by hand in the record UI after import.Coercion now mirrors objectql's
isMultiValueFieldpredicate. A field whose stored value is an array — an inherently-multi type (multiselect/checkboxes/tags) or a multi-capable type flaggedmultiple: true(per the spec: select, lookup, file, image;radioshares select's branch andusershares lookup's) — has its cell split on the export separator (,/;/、/ newline) and each token coerced individually:- lookup / user (
multiple: true) — resolve each name token to an id, store the id array; an unmatched/ambiguous token reports the specific token (no sys_user matches "查无此人") instead of the whole string. - select / radio (
multiple: true) — match each token against the options, store the option-value array. - file / image (
multiple: true) — split into an id/url array.
Single-value fields and the non-multi-capable reference types (master_detail / reference / tree) are unchanged — a stray
multiple: trueon them stays a single resolved value, matching the engine. - lookup / user (
-
f531a26: fix(security): public-form submissions can no longer forge server-managed anchors (#3022)
The anonymous public-form surface (ADR-0056 Option A,
POST /forms/:slug/submit) is authorized by the declaration-derivedpublicFormGrant, which short-circuits the security middleware BEFORE every write gate (CRUD, FLS, the owner anchor guard, the tenant CHECK). The only field-side defense was the route's declared-field allow-list — and a FormView with zero declared section fields fell back to merging the raw body wholesale, so an unauthenticated visitor couldPOST owner_id=<victim>(ororganization_id, audit columns,id) and attach the record to another user or tenant — the #3004 insert-forge, with no credentials at all.Server-managed anchors are now enforced on this surface at BOTH layers, from a single shared definition (
PUBLIC_FORM_SERVER_MANAGED_FIELDS, new in@objectstack/spec/security):- Data layer (authoritative) — the
publicFormGrantbranch in@objectstack/plugin-securitystripsid/owner_id/organization_id/tenant_id/ audit columns / soft-delete state /__searchfrom every row of a granted insert (batch included) before admitting the write, so the boundary holds no matter what any route lets through. Ownership stays NULL for object hooks / the first-admin bootstrap to assign, as for other anonymous-seeded rows. - Route layer — the submit allow-list excludes the same set
unconditionally: an explicitly declared
owner_idsection field no longer passes, and the zero-declared-sections fallback keeps its documented all-fields behavior for business columns while refusing the managed set. The resolve route (GET /forms/:slug) drops the managed fields from the rendered sections and the embedded object schema so a form never collects a value the submit refuses, andGET /forms/:slug/lookup/:fieldrefuses apublicPickerdeclared on a managed anchor (which would have opened anonymoussys_usersearch throughowner_id).
Authenticated writes are unaffected — this is the anonymous-surface rule only;
owner_idtransfer semantics for signed-in callers stay governed by the transfer grant (#3004 / PR #3018). - Data layer (authoritative) — the
-
f531a26: fix(rest): mapDataError now honors an explicit 4xx
error.status/error.codecarried by domain errors (#2926 ⑦). Record-scope authorization denials from plugin-sharing (status 403, code FORBIDDEN) previously degraded to a bare 400 with no machine-readable code because the generic data routes bypass sendError's status passthrough. Structured 409 envelopes (CONCURRENT_UPDATE, DELETE_RESTRICTED) keep their dedicated branches; 5xx statuses still go through the message-sanitizing heuristics. -
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [3fe9df1]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [4109153]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [627f225]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
- @objectstack/spec@15.1.0
- @objectstack/service-package@15.1.0
- @objectstack/platform-objects@15.1.0
- @objectstack/core@15.1.0
- @objectstack/types@15.1.0
-
a581a65: feat(plugin-security): C2-β — explain 引擎 record 粒度行级归因 (#2920)
explain(principal, object, operation, recordId?)现支持记录级解释。透传recordId时,引擎在对象级流水线之上叠加行级归因,全部复用 enforcement 同一批函数(explained-by-construction):tenant_isolationLayer 0:作为永远最先的层被 prepend;每层打上kernelTier(layer_0_tenantvslayer_1_business),可区分「租户墙挡的」还是「业务 RLS 挡的」。- 每层
record归因(tenant / owd_baseline / sharing / rls):outcome(admitted/excluded/not_evaluated)、有效rowFilter、matchesRecord(用@objectstack/formula的matchesFilterCondition对同一条 FilterCondition 求值)、命中的rules[](tenant_filter/owd_baseline/ownership/record_share/sharing_rule/team/rls_policy,含 grants/via/effect)。 - 顶层
record判定:visible+decidedBy决定性层。读走复合行过滤匹配,写走 sharing service 的canEdit(均为 enforcement 原语)。 principal.posture:ADR-0095 D2 档位(PLATFORM_ADMIN/TENANT_ADMIN/MEMBER/EXTERNAL)的 B2 stand-in 派生(复用resolveAuthzContext已投影的 platform_admin / org 角色证据),待 B2 合并后替换。computeRlsFilter重构为computeLayeredRlsFilter(暴露{ layer0, layer1 }拆分)+ 薄 andCompose 包装,单一代码路径,行级归因不会与执行漂移。- REST
security.explain(GET/POST)接受可选recordId。
向后兼容:无
recordId的对象级请求输出 byte-identical——无tenant_isolation层、无kernelTier、无posture、无record。 -
31d04d4: Fix the data-import automation chain (#2922). Batch
engine.insertnow firesbeforeInsert/afterInsertonce per row with single-record hook contexts, so flat-input proxies, declarative hook conditions, audit writers, and record-change triggers see real records instead of arrays. A newExecutionContext.skipAutomationsflag (mirrored intoHookContext.session) lets callers suppress metadata-bound automation hooks and flow dispatch while code-registered system hooks (audit, security, sharing) still run — making the import wizard's "run automations & triggers" checkbox and import undo actually effective. The REST import default flips to running automations unless the request explicitly opts out (runAutomations: false), matching historical behavior. -
Updated dependencies [02a014b]
-
Updated dependencies [28b7c28]
-
Updated dependencies [13749ec]
-
Updated dependencies [e62c233]
-
Updated dependencies [ed61c9b]
-
Updated dependencies [31d04d4]
- @objectstack/platform-objects@15.0.0
- @objectstack/spec@15.0.0
- @objectstack/core@15.0.0
- @objectstack/service-package@15.0.0
- @objectstack/types@15.0.0
-
607aaf4: 导出文件名本地化 + 系统字段标签内置多语言回退。
@objectstack/rest— 导出下载文件名:GET /data/:object/export的Content-Disposition不再是裸的<对象名>.<扩展名>,改为「对象显示名-时间戳」:ASCII 兜底用 API 名(filename="contracts-20260714-153045.xlsx"),本地化标签(如中文)按 RFC 5987/6266 编码进filename*=UTF-8''…(浏览器直接下载得到合同-20260714-153045.xlsx)。新增导出exportContentDisposition(objectName, label, ext, now?)。@objectstack/spec— 系统字段标签回退:ObjectQL 注册表给每个对象注入的系统字段(owner_id/created_at/created_by/updated_at/updated_by)只带英文标签,自定义对象又没有对应的翻译条目,导致中文界面的列表表头、导出文件、导入模板里漏出 "Owner"/"Created At" 等英文。translateObject现内置这五个字段的 en/zh-CN/ja-JP/es-ES 标签表(措辞与平台生成的翻译包一致),仅当字段仍是注入的英文默认值时套用——作者自定义的标签绝不覆盖;无翻译包时也生效(translateObject不再因缺 bundle 而提前返回,REST 元数据翻译路径同步放宽,缓存 ETag 本就按 locale 分键,无缓存串味风险)。@objectstack/plugin-reports— 附件文件名:定时报表附件的文件名清洗从「非 ASCII 全部替换成_」改为按 Unicode 字母/数字保留(\p{L}\p{N}),中文计划名不再变成一串下划线。@objectstack/rest— 导入接受翻译后的选项标签(导出 ↔ 导入闭环):导出与导入模板写出的是翻译后的选项标签(如待规划),但导入强制转换只认作者原始 schema 的标签/值,导致用户把自己刚导出的本地化文件原样导回时 select 字段全部报invalid_option。prepareImportRequest新增localizeSchema钩子(REST 导入路由传入translateMetaItem),把当前 locale 的翻译标签合并进字段选项作为匹配同义词——作者标签与选项 code 照常匹配,非法值照常报错,翻译失败时降级为仅作者标签匹配。新增导出mergeLocalizedOptionSynonyms(metaMap, localizedMetaMap)。 -
e46169c: 面向最终用户的错误消息去掉调试噪音:REST 数据路由(
mapDataError)对沙箱 hook/action 抛错解包SandboxError.innerMessage(并对丢失实例的情况正则剥离hook 'x' threw: Error:包装,保留TypeError:等非默认错误名);客户端 SDK 的error.message不再拼[ObjectStack] CODE:前缀(code 仍在error.code上可编程读取)。控制台报错 toast 从[ObjectStack] hook 'pm_ref_base' threw: Error: 制作基地被…变为只显示业务消息本身;完整调试包装仍写入服务端日志。 -
Updated dependencies [16b4bf6]
-
Updated dependencies [16b4bf6]
-
Updated dependencies [10e8983]
-
Updated dependencies [607aaf4]
-
Updated dependencies [bb71321]
- @objectstack/spec@14.8.0
- @objectstack/platform-objects@14.8.0
- @objectstack/core@14.8.0
- @objectstack/service-package@14.8.0
- @objectstack/types@14.8.0
- Updated dependencies [d6a72eb]
- Updated dependencies [824a395]
- @objectstack/spec@14.7.0
- @objectstack/types@14.7.0
- @objectstack/core@14.7.0
- @objectstack/platform-objects@14.7.0
- @objectstack/service-package@14.7.0
- Updated dependencies [609cb13]
- Updated dependencies [ce6d151]
- @objectstack/spec@14.6.0
- @objectstack/platform-objects@14.6.0
- @objectstack/core@14.6.0
- @objectstack/service-package@14.6.0
- @objectstack/types@14.6.0
-
4d9dd7b: fix(rest): validate required fields in import dry-run to match the real insert
The bulk-import dry run (
POST /data/:object/import,dryRun:true) only ran cell coercion and reported every coercible CREATE row as ok — so a row missing a required NOT-NULL field with no default was green-lit, then died on the real insert withNOT NULL constraint failed. The ImportWizard shows the dry-run result, so it promised imports that then failed.Add a required-field pre-check to the shared import runner (CREATE rows only), mirroring the engine's insert-time validation (
objectql/record-validator.ts+applyFieldDefaults): a required field is unsatisfied only when it has no value AND no default;system/readonly/autonumberand the engine-owned lifecycle columns are exempt.ExportFieldMetagainsrequired/system/readonly/hasDefault(populated bybuildFieldMetaMap). Applied to both dry-run and real paths so they stay identical and a real insert returns a readable<field> is requiredinstead of a raw driver error; skipped whenrunAutomationsis set (a beforeInsert hook may populate the field). -
Updated dependencies [526805e]
-
Updated dependencies [d79ca07]
-
Updated dependencies [33ebd34]
-
Updated dependencies [c044f08]
-
Updated dependencies [01274eb]
-
Updated dependencies [8f23746]
-
Updated dependencies [b97af7e]
-
Updated dependencies [6da03ee]
- @objectstack/spec@14.5.0
- @objectstack/platform-objects@14.5.0
- @objectstack/core@14.5.0
- @objectstack/service-package@14.5.0
- @objectstack/types@14.5.0
- Updated dependencies [7953832]
- Updated dependencies [82e745e]
- Updated dependencies [f3035bd]
- Updated dependencies [82c0d94]
- Updated dependencies [7449476]
- @objectstack/spec@14.4.0
- @objectstack/platform-objects@14.4.0
- @objectstack/core@14.4.0
- @objectstack/service-package@14.4.0
- @objectstack/types@14.4.0
-
2a71f48: feat(auth): admin direct user management, phone sign-in, and identity bulk import (#2766, re-scoped #2758)
sys_useris managed by better-auth and its generic CRUD is suppressed, so until now the only way to add a teammate was the email-dependent invite flow. This ships three staged capabilities:- Admin direct user management —
POST /api/v1/auth/admin/create-userand a wrappedPOST /api/v1/auth/admin/set-user-password(ADR-0068 platform-admin gate; better-auth pipeline so credentials are real). Optional generated temporary password (returned once, never persisted or logged) and a newsys_user.must_change_passwordflag enforced through the ADR-0069 authGate (403 PASSWORD_EXPIREDuntil the user changes it). Newcreate_useraction and upgradedset_user_passwordaction on the Users list — pure schema, no frontend changes. - Phone sign-in (opt-in
auth.plugins.phoneNumber) — better-auth phoneNumber plugin, phone+password only (POST /sign-in/phone-number); OTP flows stay off until SMS infrastructure exists. Addssys_user.phone_number(unique) /phone_number_verified. Phone-only accounts get an undeliverable placeholder email (u-<random>@placeholder.invalid, never derived from the phone number); all auth mail callbacks refuse placeholder recipients. - Identity bulk import —
POST /api/v1/auth/admin/import-usersaccepts the same payloads as the generic import routes (rows/csv/xlsx, dryRun, upsert by email or phone) but writes every row through better-auth. Password policies:invite(reset-link email per created user; requires an EmailService) andtemporary(per-row one-time passwords + forced change). Sync only, ≤500 rows per request; no undo; upsert updates touch profile fields only and can never reset an existing user's password.prepareImportRequestand the CSV/xlsx parsers moved from rest-server.ts to an exportedimport-prepare.tsmodule (behavior unchanged).
- Admin direct user management —
-
02f6af4: ADR-0090 follow-through wave: enforce book audience at the read layer; finish the D2/D3 cleanup the P1 rename missed.
- rest:
/meta/book,/meta/doc, and/meta/book/:name/treenow ENFORCE the ADR-0046 §6.7 audience model (ADR-0049 — no unenforced security properties): anonymous callers see onlypublicbooks/docs;{ permissionSet }-gated books require the caller to hold the named set; a doc's effective audience is the union over the books that CLAIM it (unclaimed docs default toorg; orphan rendering never inheritspublic). Gated evaluation fails CLOSED when holdings cannot be resolved.doc/booksingle-item reads bypass the shared meta cache (per-caller gate vs shared ETag). - spec: new pure helpers powering that gate —
audienceAllows,resolveDocAudiences,docAudienceAllows,resolveBookClaimedDocs(+AudienceCaller/AudienceBooktypes). BREAKING but ships as aminorper the launch-window convention (pre-1.0 semantics — breaking changes do not burn a major version number while the whole stack is in lockstep):METADATA_FORM_REGISTRYkeysrole/profileare gone —positionis the registered form (thepositiontype had LOST its form layout in the P1 rename);EnvironmentArtifactMetadataSchemadeclarespositionsinstead of retiredroles/profiles. - plugin-security: the
securityservice exposesresolvePermissionSetNames(ctx)— the same resolution as data-plane enforcement, for the docs gate. - metadata: artifact ingestion maps
positions → 'position'(the staleroles → 'role'mapping matched nothing since the P1 rename, silently dropping compiled positions from metadata registration). - lint: books join the D3 role-word scan (their
audienceis a permission-model reference now), and a new advisory rulesecurity-book-audience-unknown-setflags a{ permissionSet }audience naming a set the stack does not declare (runtime fails closed — the typo cost is "nobody can read the book", so say it at author time). - platform-objects: metadata-form translations regain
position(all four locales) and drop the retiredrole/profilegroups, with a vocabulary regression test.
- rest:
-
bea4b92: feat(rest): colour select/radio cells in xlsx exports with their option colour
The data export route (
GET /data/:object/export) now carries a select / radio field's optioncolorinto the generated Excel workbook as the cell's font colour (white cell background), so an exported sheet reads like the in-app coloured badges instead of plain black text. csv / json output is unchanged.export-format.tsgainstoArgb()(hex#RGB/#RRGGBB→ exceljs ARGBFFRRGGBB,undefinedfor anything not plain hex) andcellFontColor()(resolves the matched select/radio option's colour for one cell; returnsundefined— i.e. leave it unstyled — for non-option fields, unmatched values, colourless options, or invalid hex).ExportFieldMeta.optionsnow carries the optioncolor.createXlsxStream(res, useStyles)takes the flag through to exceljs'WorkbookWriter; the route enables styling and setscell.font.colorper-cell only for xlsx.
Styling is heavier than a bare value dump, so it is gated behind a 10 000-row cap (
STYLE_ROW_CAP): exports whose effective limit exceeds it stream without colours (all rows intact) and setX-Export-Styles: dropped; coloured exports setX-Export-Styles: applied. This mirrors the "formatted export has a lower ceiling than a raw dump" pattern used by Salesforce / ServiceNow. The existing 50 000-row hard cap is unchanged.Closes #2757.
- Updated dependencies [2a71f48]
- Updated dependencies [02f6af4]
- Updated dependencies [c1064f1]
- @objectstack/platform-objects@14.3.0
- @objectstack/spec@14.3.0
- @objectstack/core@14.3.0
- @objectstack/service-package@14.3.0
- @objectstack/types@14.3.0
- Updated dependencies [ac8f029]
- Updated dependencies [4ab9958]
- @objectstack/spec@14.2.0
- @objectstack/platform-objects@14.2.0
- @objectstack/core@14.2.0
- @objectstack/service-package@14.2.0
- @objectstack/types@14.2.0
- Updated dependencies [5a8465f]
- Updated dependencies [7f8620b]
- Updated dependencies [82ba3a6]
- @objectstack/spec@14.1.0
- @objectstack/core@14.1.0
- @objectstack/platform-objects@14.1.0
- @objectstack/service-package@14.1.0
- @objectstack/types@14.1.0
-
ac08698: ADR-0090 D6 — the explain engine gets its REST face (#2696).
@objectstack/rest: newGET/POST /api/v1/security/explain(object/operation/userId, validated against the spec'sExplainRequestSchema) delegating to thesecurityservice'sexplain(request, callerContext)— the same code paths the enforcement middleware runs, so the returnedExplainDecisionis explained by construction. The route is authenticated-only (401 even onrequireAuth=falsedeployments), returns 501 when no security service exposesexplain, and maps the service'sPermissionDeniedErrorto 403. Registered on scoped (/environments/:environmentId) and unscoped base paths; the env kernel's ownsecurityservice is preferred, with a new host-kernelsecurityServiceProviderfallback wired by the REST plugin.@objectstack/plugin-security:explainAccessForCallernow honors delegated administration (D12) — explaining ANOTHER user is authorized bymanage_usersor a delegatedadminScopewhose business-unit subtree covers the target user (newDelegatedAdminGate.scopesCoverUser, fail-closed on unresolvable scopes/memberships). Self-explain still needs neither. -
bd39dc5: ADR-0090 D5/D9 — suggested audience bindings become a queryable, confirmable surface.
A package permission set declaring
isDefault: trueis an install-time SUGGESTION to bind the set to the built-ineveryoneposition — never auto-bound. Until now the flag was only read at bootstrap as the fallback-set name; after an install there was no way to see or act on the suggestion.@objectstack/plugin-security: newsys_audience_binding_suggestionsystem object (read-only over the data API; unique per package × set × anchor) plus a convergent reconciler (syncAudienceBindingSuggestions) that reads every declaredisDefaultset — boot-declared stack metadata AND installed package manifests, so a runtimePOST /api/v1/packagesinstall is visible immediately — and keeps the table honest: undeclared → pending row pruned, bound out-of-band → markedconfirmed(observed). Thesecurityservice gainslistAudienceBindingSuggestions/confirmAudienceBindingSuggestion/dismissAudienceBindingSuggestion, all pre-gated on tenant-level admin (ADR-0066 superuser wildcard — anchors stay tenant-level only per D12). Confirm writes thesys_position_permission_setrow with the caller's execution context, so the D5/D9 audience-anchor gate (no high-privilege set oneveryone/guest) and the D12 delegated-admin gate enforce the binding; a set not yet materialized (installed this session) is first seeded through the same provenance-checked upsert as the boot seeder (ADR-0086 D4).@objectstack/restand@objectstack/runtime: the HTTP surface, registered on both API layers (the RestServer thatobjectstack dev/hono serves, and the runtime HttpDispatcher used by the adapters) —GET /api/v1/security/suggested-bindings?status=&packageId=,POST /api/v1/security/suggested-bindings/:id/confirm,POST /api/v1/security/suggested-bindings/:id/dismiss(401 unauthenticated, 403/404/409 mapped from the service's typed errors, 501/503 without plugin-security).
-
e2fa074: feat(data): make object
enable.feeds/enable.activitiesreal opt-out gates; define theenable.trackHistorycontract (#2707)ObjectSchema.enable.{files,trackHistory,activities,feeds}were parsed but (mostly) unconsumed — an author setting them got nothing, silently. Per the enforce-or-remove doctrine, each flag now has a defined enforcement contract:enable.activities— opt-OUT writer gate. Spec default flipsfalse → true; plugin-audit keeps mirroring CRUD into thesys_activitytimeline unless the object declares an explicitactivities: false(behavior-preserving for every existing stack; the off-switch is the per-object lever for activity-row growth, ADR-0057). The compliancesys_audit_logrow is NOT gated.enable.feeds— opt-OUT with server-side enforcement. Spec default flipsfalse → true; an explicitfeeds: falsenow rejectssys_commentcreation targeting that object at the engine hook seam (403FEEDS_DISABLED, fail-closed likeCLONE_DISABLED).enable.trackHistory— was misclassifieddeadin the liveness ledger: the console has gated the record History tab on it since 2026-05. Reclassified live with the two-grain contract documented (object flag = History-tab master switch; per-fieldtrackHistory= diff selector; audit capture stays unconditional as a compliance ledger).enable.files— stays dead + authorWarn (reserved for the future generic Attachments panel; useField.file/Field.imagemeanwhile). Itsdescribe()now says so instead of advertising a capability that doesn't exist.
The default flips can't be avoided: with
default(false), compiled output materializesfalsefor every object with anenableblock, making "author explicitly opted out" indistinguishable from "schema default" — so opt-out semantics require the default to betrue(same posture astrash/mru/clone). Liveness ledger + reference docs regenerated; compile-time authorWarn now fires only forenable.files. -
23c8668: feat(data):
enable.filesgoes live — opt-in gate for the generic Attachments surface (#2727)The last dead ObjectCapabilities flag gets its enforcement contract.
enable.filesis opt-IN (spec default staysfalse): the generic record Attachments panel is a new surface, not an existing behavior.- plugin-audit registers a
sys_attachmentbeforeInsert hook: attachment join rows may only target objects that explicitly declareenable: { files: true }— anything else (absent block, absent flag, explicit false, unknown object) rejects fail-closed with 403FILES_DISABLED(CLONE_DISABLED / FEEDS_DISABLED pattern). mapDataErrormapsFILES_DISABLED→ 403 with the gated target object (generic data routes bypasssendError's.statuspassthrough — the #2707 lesson, applied at introduction time).Field.file/Field.imageare deliberately independent: they store the file URL in the record's own column and never createsys_attachmentrows, so field-level attachments work regardless of this flag.- Liveness ledger:
enable.filesdead→live, authorWarn dropped — ObjectCapabilities is now 100% live. The compile-time liveness-dead-property warning no longer fires for it;describe()and the reference docs state the real contract.
Companion objectui PR ships
RecordAttachmentsPanel(upload/list/ download/delete over the presigned three-step storage flow), rendered on record pages when the flag is true. - plugin-audit registers a
-
1056c5f: Package uninstall now revokes the package's data-plane permission rows (#2747, ADR-0086 D3 / ADR-0090 D5 "no ghost grants").
@objectstack/metadata-protocol:deletePackagegains an uninstall-cleanup seam — the exact mirror of the publish materializer: domain plugins register named cleanups viaregisterUninstallCleanup(name, fn)and every cleanup runs with the uninstalled package id, its outcome reported on the newcleanupsarray of the response (a failed revocation is visible, never silent).deletePackagealso unregisters the package from the in-memory SchemaRegistry (best-effort), so the running kernel stops serving it without waiting for a restart.@objectstack/plugin-security: registers thesecurity.package-permissionscleanup — deletes the package's ownsys_permission_setrows (managed_by: 'package'+ matchingpackage_idonly; env-authored and foreign-package rows are never touched, ADR-0086 D4), theirsys_position_permission_set/sys_user_permission_setbindings (bindings first, so no dangling grants), and the package'ssys_audience_binding_suggestionrows (a reinstall re-prompts fresh). Also fixes the engine-call signature in the suggestion module:find/deletereadcontextfrom their second argument — the previous trailing{ context }argument was ignored, so deletes ran principal-less.@objectstack/rest:DELETE /api/v1/packages/:id(no version pin) now goes throughprotocol.deletePackage— one uninstall semantic instead of a baresys_packagesrow delete — removing the package's metadata, durable record, registry entry, and running the cleanups; the response carriesdeletedCount+cleanups. A version-scoped delete keeps the narrow durable-registry semantics. -
Updated dependencies [0a8e685]
-
Updated dependencies [afa8115]
-
Updated dependencies [80f12ca]
-
Updated dependencies [332b711]
-
Updated dependencies [e2fa074]
-
Updated dependencies [23c8668]
-
Updated dependencies [29f017d]
-
Updated dependencies [216fa9a]
-
Updated dependencies [6c22b12]
-
Updated dependencies [d0531c4]
-
Updated dependencies [cff5aac]
- @objectstack/spec@14.0.0
- @objectstack/platform-objects@14.0.0
- @objectstack/core@14.0.0
- @objectstack/service-package@14.0.0
- @objectstack/types@14.0.0
-
6d83431: ADR-0090 P1 breaking wave — permission model v2 concept convergence.
Pre-launch one-step renames and secure defaults (no compatibility aliases, per ADR-0090 D3/D4 superseding ADR-0057 D5/D7's alias discipline):
sys_role→sys_position,sys_user_role→sys_user_position(fieldrole→position),sys_role_permission_set→sys_position_permission_set(fieldrole_id→position_id);RoleSchema/defineRole→PositionSchema/definePositionwith noparent(positions are flat; hierarchy lives on the business-unit tree).ExecutionContext.roles[]→positions[]; the EvalUser/CEL contractcurrent_user.roles→current_user.positions(formula validators updated); stack propertyroles:→positions:; metadata kindsrole/profile→position(profile kind removed).isProfileremoved fromPermissionSetSchema(ADR-0090 D2);isDefaultnarrows to an install-time suggestion;appDefaultProfileName→appDefaultPermissionSetName(isDefault-only).- OWD enum drops legacy aliases
read/read_write/full; new optionalexternalSharingModel(external dial,privatedefault) lands as P1 spec shape (ADR-0090 D11). - Secure default (D1): a custom object with an owner field and NO
sharingModelnow resolvesprivate(was: fully public). System objects keep their explicit posture. Unrecognised stored values fail closed. - ExecutionContext gains the P1 principal-taxonomy shape (D10):
principalKind/audience/onBehalfOf(optional, semantics phase in later). - Sharing recipients:
role→position(expanded viasys_user_position∪ the better-auth membership transition source);role_and_subordinatesremoved —unit_and_subordinatesnow expands the business-unit subtree (finishes ADR-0057 D5's re-homing).
-
57b89b4: feat(mcp): the MCP surface is now default-on — a core platform capability (#2698)
/api/v1/mcpis served (and advertised in/discovery) out of the box; the OAuth 2.1 authorization track and Dynamic Client Registration follow it, so a fresh deployment is connectable by any MCP client with zero configuration. Operators opt OUT withOS_MCP_SERVER_ENABLED=false.- New single decision point
isMcpServerEnabled()in@objectstack/types(default on; explicitfalse/0/off/nodisables). The runtime dispatcher's/mcproute gate, the CLI's MCP plugin auto-load, the REST/discoveryadvertisement, and the auth service's OAuth/DCR follow-defaults all delegate to it — the served route, the advertised route, and the authorization track can never disagree. - The env var is now effectively tri-state: unset → HTTP surface on;
explicit
true→ additionally auto-start the long-lived stdio transport at boot (unchanged, still opt-in — a default must not claim the process's stdin/stdout); explicitfalse→ everything off, fail-closed (404, no metadata, no DCR). - The OAuth 2.1 TLS rule is unaffected: on a plain-HTTP non-loopback origin the OAuth track stays dark and the default-on surface remains API-key-only.
- New single decision point
- Updated dependencies [6d83431]
- Updated dependencies [01917c2]
- Updated dependencies [b271691]
- Updated dependencies [a5a1e41]
- Updated dependencies [466adf6]
- Updated dependencies [57b89b4]
- Updated dependencies [5be00c3]
- Updated dependencies [466adf6]
- Updated dependencies [2bee609]
- Updated dependencies [9fa84f9]
- Updated dependencies [fc7e7f7]
- @objectstack/spec@13.0.0
- @objectstack/core@13.0.0
- @objectstack/platform-objects@13.0.0
- @objectstack/types@13.0.0
- @objectstack/service-package@13.0.0
-
21420d9: Seed loader and data-import now route bulk writes through the engine's array-form
insert()(one round-trip per batch, with parent-deduplicated summary recompute) instead of oneinsert()/createData()call per record, and both retry transient driver errors instead of silently dropping the row (#2678).A new shared helper,
bulkWrite(@objectstack/core), batches rows through a caller-supplied batch-write function, retries a whole-batch transient failure (network blip / timeout) with exponential backoff, and degrades to per-row writes (each itself retried) when a batch fails for a non-transient reason — so one bad row can't drop the other N-1.withTransientRetrywraps a single write (e.g. an update) with the same retry behavior.SeedLoaderService.loadDataset()(@objectstack/metadata-protocol) buffers insert-mode records and flushes them in batches of 200 via the engine's arrayinsert(). Datasets with a self-referencing field (e.g.employee.manager_id -> employee) keep the historical per-record write path, since a later record may need an earlier one's freshly-assigned id.runImport()(@objectstack/rest) buffers create-resolved rows and flushes them viaprotocol.createManyData()when the protocol supports it, falling back to the original per-rowcreateData()call otherwise.Protocol.createManyData(@objectstack/metadata-protocol) now forwardscontexttoengine.insert()likecreateDataalready did, so tenant-scoped bulk creates work correctly.
Previously, a 1000-row seed or import into an object with a rollup summary issued 1000+ round-trips and up to 1000 summary recomputes; a single transient network error on any one row silently dropped it with no retry (the 2026-07-06 HotCRM first-boot incident). A
bulkCreate-capable driver now sees roughlyceil(N/batch)writes, and a transient error is retried before a row is ever reported as failed.Fix (
@objectstack/driver-sql):SqlDriver.bulkCreate()never generated a client-side id for a row missing one, unlikecreate()— a latent gap that this change is the first to exercise at scale (a bulk-inserted row without a driver-native id default silently landed withid: NULL).bulkCreate()now mirrorscreate()'s id/_idnormalization per row.
- Updated dependencies [6cebf22]
- Updated dependencies [21420d9]
- @objectstack/spec@12.6.0
- @objectstack/core@12.6.0
- @objectstack/platform-objects@12.6.0
- @objectstack/service-package@12.6.0
- Updated dependencies [8b3d363]
- @objectstack/spec@12.5.0
- @objectstack/core@12.5.0
- @objectstack/platform-objects@12.5.0
- @objectstack/service-package@12.5.0
- Updated dependencies [60dc3ba]
- @objectstack/spec@12.4.0
- @objectstack/core@12.4.0
- @objectstack/platform-objects@12.4.0
- @objectstack/service-package@12.4.0
- Updated dependencies [e7eceec]
- @objectstack/spec@12.3.0
- @objectstack/core@12.3.0
- @objectstack/platform-objects@12.3.0
- @objectstack/service-package@12.3.0
- fce8ff4: feat(rest,spec): named import mappings (#2611) —
POST /data/:object/importacceptsmappingName, resolving a registereddefineMappingartifact (stackmappings:) and applying its fieldMapping pipeline (rename + constant/map/split/join; lookup delegates to the built-in reference resolution) as a strict projection before coercion. The artifact'smode/upsertKeyserve as writeMode/matchFields defaults; explicit request values win. Errors are loud and specific:MAPPING_NOT_FOUND,MAPPING_TARGET_MISMATCH,MAPPING_FORMAT_MISMATCH,CONFLICTING_MAPPING(mutually exclusive with the inline rename), andUNSUPPORTED_TRANSFORMforjavascript(no server-side sandbox — never silently skipped).defineStackcross-reference validation now rejects mappings targeting undefined objects andjavascripttransforms at build time.
- Updated dependencies [fce8ff4]
- Updated dependencies [3962023]
- Updated dependencies [2bb193d]
- Updated dependencies [0426d27]
- Updated dependencies [da807f7]
- Updated dependencies [4f5b791]
- @objectstack/spec@12.2.0
- @objectstack/core@12.2.0
- @objectstack/platform-objects@12.2.0
- @objectstack/service-package@12.2.0
- Updated dependencies [93e6d02]
- @objectstack/spec@12.1.0
- @objectstack/core@12.1.0
- @objectstack/platform-objects@12.1.0
- @objectstack/service-package@12.1.0
-
7c09621: feat(security)!:
api.requireAuthnow defaults totrue— anonymous access to the data API is denied by default (ADR-0056 D2 flip)BREAKING. The global
requireAuthdefault flipped FROMfalseTOtrue(RestApiConfigSchema.requireAuthin@objectstack/spec, mirrored byRestServer.normalizeConfigin@objectstack/rest). Anonymous requests to the/data/*CRUD + batch endpoints are now rejected with HTTP 401 unless the deployment explicitly opts out. (Scope note: this gate covers the REST/data/*surface — the metadata read/write endpoints and the dispatcher GraphQL route have their own pre-existing anonymous posture, tracked separately; this flip does not change them.)Migration (one line): a deployment that intentionally serves data publicly (demo / playground / kiosk) sets the flag on the stack config — now a declared
ObjectStackDefinitionSchema.apifield, so it survivesdefineStackstrict parsing (previously an undeclared top-levelapikey was silently stripped):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.requireAuthat the plugin level (one nesting short) is now also called out with a boot warning instead of being silently 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); noguest_portalprofile needed. - 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.
- Updated dependencies [a8df396]
- Updated dependencies [e695fe0]
- Updated dependencies [07f055c]
- Updated dependencies [7c09621]
- Updated dependencies [7709db4]
- Updated dependencies [2082109]
- Updated dependencies [7c09621]
- Updated dependencies [9860de4]
- Updated dependencies [069c205]
- @objectstack/spec@12.0.0
- @objectstack/platform-objects@12.0.0
- @objectstack/core@12.0.0
- @objectstack/service-package@12.0.0
- Updated dependencies [6a9397e]
- Updated dependencies [c0efe5d]
- @objectstack/spec@11.10.0
- @objectstack/core@11.10.0
- @objectstack/platform-objects@11.10.0
- @objectstack/service-package@11.10.0
- Updated dependencies [d3595d9]
- @objectstack/spec@11.9.0
- @objectstack/core@11.9.0
- @objectstack/platform-objects@11.9.0
- @objectstack/service-package@11.9.0
- Updated dependencies [53d491a]
- Updated dependencies [b84726b]
- @objectstack/platform-objects@11.8.0
- @objectstack/spec@11.8.0
- @objectstack/core@11.8.0
- @objectstack/service-package@11.8.0
- Updated dependencies [5178906]
- @objectstack/spec@11.7.0
- @objectstack/platform-objects@11.7.0
- @objectstack/core@11.7.0
- @objectstack/service-package@11.7.0
- @objectstack/spec@11.6.0
- @objectstack/core@11.6.0
- @objectstack/platform-objects@11.6.0
- @objectstack/service-package@11.6.0
- Updated dependencies [6ee4f04]
- Updated dependencies [c1e3a65]
- @objectstack/spec@11.5.0
- @objectstack/core@11.5.0
- @objectstack/platform-objects@11.5.0
- @objectstack/service-package@11.5.0
- Updated dependencies [5821c51]
- Updated dependencies [a0fce3f]
- @objectstack/spec@11.4.0
- @objectstack/core@11.4.0
- @objectstack/service-package@11.4.0
- Updated dependencies [58e8e31]
- Updated dependencies [b4a5df0]
- @objectstack/spec@11.3.0
- @objectstack/core@11.3.0
- @objectstack/service-package@11.3.0
- Updated dependencies [d0f4b13]
- Updated dependencies [302bdab]
- @objectstack/spec@11.2.0
- @objectstack/core@11.2.0
- @objectstack/service-package@11.2.0
-
ce0b4f6: Auth: password expiry — the session-validation gate (ADR-0069 D1, P1)
Builds the authentication-policy session gate ADR-0069 needs and uses it for password expiry. When
password_expiry_days(newauthsetting, 0 = off) is exceeded, an authenticated user is blocked from protected REST resources with403 PASSWORD_EXPIREDuntil they change their password — while auth + remediation paths stay reachable.- core: new pure
evaluateAuthGate/isAuthGateAllowlistedhelper (@objectstack/core/security) — single source of truth for the allow-list (auth endpoints, change-password, health, UI-bootstrap reads). - plugin-auth:
customSessioncomputes the gate posture once and attachesuser.authGate;computeAuthGatereadssys_user.password_changed_atvs the configured window;password_changed_atis stamped on sign-up / change / reset;isAuthGateActive()keeps the gate zero-overhead when off. - platform-objects: new
sys_user.password_changed_atcolumn. - rest:
resolveExecCtxcarriesauthGate;enforceAuthblocks gated sessions (independent ofrequireAuth) using the core allow-list. - service-settings: new
password_expiry_daysfield.
Default-off / additive (no upgrade behavior change); a null
password_changed_atnever expires (existing users). Per ADR-0049 the setting ships with its enforcement; timestamps written asDate(ADR-0074).This gate is the shared seam for enforced MFA (ADR-0069 D3), which lands next as a small addition (a second
authGatebranch). The dispatcher/MCP path is a follow-up (tracked in #2375); the REST surface the Console uses is fully gated here. - core: new pure
-
9ccfcd6: perf(core): authenticated requests issued ~16 sequential queries — duplicate authz + repeated localization — now request-scoped memoized
An authenticated REST request resolves its execution context (identity + RBAC/RLS + localization) many times in a single handler — the data operation itself, app-nav RBAC filtering, dashboard widget gating, the ADR-0069 auth gate. Each
resolveExecCtxpass is the fullresolveAuthzContextaggregation plus the localization read (~16 sequential queries), and nothing memoized it, so a request that resolves twice paid for duplicate authz and repeated localization.@objectstack/rest—resolveExecCtxis now memoized per request, keyed by the request object (aWeakMap, so the entry is collected with the request — no TTL, no cross-request leak) and the inputenvironmentId. The in-flight Promise is cached so concurrent callers share one resolution. The heavy path moved tocomputeExecCtx. Anonymous (undefined) resolutions are cached too.@objectstack/core— within a singleresolveAuthzContextpass,sys_useris now read at most once (the email fallback and theai_seatsynthesis shared a duplicate query on the API-key path);resolveLocalizationContext's direct-read fallback batchestimezone/locale/currencyinto onesys_settingquery ($inonkey) instead of three sequential reads.
No authorization-behavior change — the same roles/permissions/RLS context is resolved, just without the redundant reads. The
sys_memberreads (per-user roles vs. all-org-members) are intentionally left distinct (different filters/limits).Tests: query-counting regressions assert
sys_userreads once and localization reads once; new rest-server tests pin the per-request/per-environment memo contract. -
Updated dependencies [ce0b4f6]
-
Updated dependencies [9ccfcd6]
-
Updated dependencies [ecf193f]
-
Updated dependencies [51bec81]
-
Updated dependencies [3e593a7]
-
Updated dependencies [63d5403]
- @objectstack/core@11.1.0
- @objectstack/spec@11.1.0
- @objectstack/service-package@11.1.0
-
359c0aa: fix(objectql,rest): single-item meta reads must revalidate (no
max-age=3600)GET /api/v1/meta/object/:name(and the other single-item meta reads served by the cached path) sentCache-Control: public, max-age, max-age=3600. Two bugs:-
Stale metadata for up to an hour. Object metadata is invalidated by publish, but a one-hour TTL let browsers (and any CDN/proxy) serve a stale schema without revalidating — e.g. the AI-build "New" create form kept rendering pre-publish fields until the TTL lapsed. The list endpoint
GET /api/v1/meta/objectis uncached, which is why list views updated but single-object reads didn't.getMetaItemCachednow returnsdirectives: ['private', 'no-cache']with nomaxAge, so the ETag validator (which already changes on publish) gates freshness: a cheap304when unchanged, fresh fields the instant a publish bumps the ETag.privatealso keeps per-tenant metadata out of shared caches. -
Malformed header. The directives array carried a bare
max-ageplaceholder and the REST layer appendedmax-age=3600from themaxAgefield, concatenating intopublic, max-age, max-age=3600. The header builder now strips the baremax-agetoken before appending the real value, so amaxAgeis emitted once as a well-formedmax-age=N.
-
-
9a810f8: fix(rest): register static data-action routes before the greedy
:object/:idmatcherThe REST router matches first-registered-wins with no specificity sorting, but
registerDataActionEndpoints(which holdsGET /data/:object/export) ran AFTERregisterCrudEndpoints(which holds the greedyGET /data/:object/:id). A request toGET /data/<object>/exportwas therefore captured by:object/:id—"export"treated as a record id — returning404 RECORD_NOT_FOUNDinstead of streaming the export. The data-action registration now runs first, mirroring the existing/meta/:type/:name/references-before-/meta/:type/:nameconvention. Reordering is safe both ways:registerDataActionEndpointscontains no greedy 2-segment:object/:idroutes, so it cannot shadow any CRUD literal. A regression test asserts the export route registers ahead of the get-by-id route. -
a619a3a: fix(setup): first-run admin polish — pin Company/Localization, gate dashboard widgets by
requiresService, i18n + settings PUT envelopeDogfooding the Setup app as a brand-new system administrator surfaced a cluster of small first-run gaps, now fixed:
- platform-objects: pin Localization and Company in the Setup sidebar's Configuration group — both are registered
service-settingsmanifests (the two lowest-orderWorkspace settings) but were reachable only via the "All Settings" hub. Translate the previously-English nav labels Cloud Connection (云连接), Datasources (数据源) and Capabilities (能力). Tag the System Overviewwidget_organizationsKPI withrequiresService: 'org-scoping'. - rest: extend the ADR-0057 D10 server-side visibility gate to dashboard widgets — strip widgets whose
requiresServicenames an unregistered kernel service (mirrors the existing app-nav gate;resolveRegisteredServicesnow also discovers gates declared on widgets). In a single-tenant runtime this removes the orphan "Organizations" KPI, matching the already-hidden org nav entries. - service-settings: add the missing zh
helpstrings for the Localization manifest (number/currency/first-day-of-week/fiscal-year fields), and accept the{ values: { … } }envelope onPUT /api/settings/:nssymmetrically with whatGETreturns.
- platform-objects: pin Localization and Company in the Setup sidebar's Configuration group — both are registered
-
aa33b02: fix(security): single-source the request authorization resolver — REST no longer drops sys_user_position
The REST server and the runtime dispatcher each carried their own copy of the request → ExecutionContext identity/role resolver, and they drifted on a security path. The REST copy silently omitted
sys_user_position(so custom roles granted via the ADR-0057 D4 platform-RBAC path did not apply over REST),sys_position_permission_set, theowner→org_ownermembership normalization, the platform-admin derivation, and theai_seatsynthesis — fail-closed (legitimate access denied), not an escalation.Both entry points now delegate to a single shared resolver,
resolveAuthzContextin@objectstack/core/security(joining the API-key verifier that already lived there). A contract test locks every authorization source and a lint gate (check:authz-resolver) prevents a future duplicate resolver or a dropped delegation. -
Updated dependencies [ab5718a]
-
Updated dependencies [4845c12]
-
Updated dependencies [c1a754a]
-
Updated dependencies [6fbe91f]
-
Updated dependencies [715d667]
-
Updated dependencies [5eef4cf]
-
Updated dependencies [72759e1]
-
Updated dependencies [6c4fbd9]
-
Updated dependencies [ef3ed67]
-
Updated dependencies [cd51229]
-
Updated dependencies [7697a0e]
-
Updated dependencies [e7e04f1]
-
Updated dependencies [cfd5ac4]
-
Updated dependencies [2be5c1f]
-
Updated dependencies [ad143ce]
-
Updated dependencies [5c4a8c8]
-
Updated dependencies [3afaeed]
-
Updated dependencies [8801c02]
-
Updated dependencies [3d04e06]
-
Updated dependencies [4a84c98]
-
Updated dependencies [c715d25]
-
Updated dependencies [aa33b02]
-
Updated dependencies [d980f0d]
-
Updated dependencies [a658523]
-
Updated dependencies [82ff91c]
-
Updated dependencies [638f472]
- @objectstack/spec@11.0.0
- @objectstack/core@11.0.0
- @objectstack/service-package@11.0.0
- @objectstack/spec@10.3.0
- @objectstack/core@10.3.0
- @objectstack/service-package@10.3.0
- Updated dependencies [b496498]
- @objectstack/spec@10.2.0
- @objectstack/core@10.2.0
- @objectstack/service-package@10.2.0
-
517dad9: Schema drift detection +
os migratefor non-additive metadata changes (#2186).The metadata→DB schema sync was additive-only: it created tables and added columns but never altered/dropped existing ones, so relaxing
required, changing a type/length, or dropping a field silently diverged from an existing database. The physical column won at write time, surfacing a misleadingorganization_id is required400 even though/metareported the field optional.- driver-sql — the SQL driver now detects managed-schema drift (metadata is
the source of truth) and categorises each divergence
safe/needs_confirm/destructive.initObjectswarns once per divergence with an actionable hint. A new opt-inSqlDriverConfig.autoMigrate: 'safe'auto-applies the loosening subset (relaxNOT NULL, widen varchar) so an existing dev DB self-heals on restart — never destructive, force-disabled underNODE_ENV=production. New public methodsdetectManagedDrift()/applyMigrationEntries(). SQLite reconciles via the official table-rebuild (copy → swap), preserving data; Postgres/MySQL alter in place. - cli — new
os migrate plan(dry-run, categorised diff) andos migrate apply(--allow-destructivefor drops/tightenings, confirm gate,--json).os dev/servenow passautoMigrate: 'safe'in dev only. - rest — a
NOT NULLviolation that reaches the driver (metadata validation already passed) now carries a drift-awarehintpointing atos migrate, instead of only the misleading "field is required" message. TheVALIDATION_FAILED/fieldsenvelope is unchanged for back-compat.
- driver-sql — the SQL driver now detects managed-schema drift (metadata is
the source of truth) and categorises each divergence
-
Updated dependencies [49da36e]
-
Updated dependencies [ac79f16]
- @objectstack/spec@10.1.0
- @objectstack/core@10.1.0
- @objectstack/service-package@10.1.0
-
2256e93: Setup nav: gate Organizations/Invitations on multi-org; enforce
requiresServiceserver-side (ADR-0057 addendum D10).rest-server'sfilterAppForUsernow honoursNavigationItem.requiresService— entries whose named kernel service isn't registered are dropped from the served app metadata (fail-open when the kernel can't be probed; previously the field was a frontend-only hint). AppliesrequiresService: 'org-scoping'to the Setup app's Organizations and Invitations entries, so they surface only in multi-org (multi-tenant) deployments and disappear in single-tenant. Business Units is intentionally left ungated — it is open per the open/paid seam + D12 ("pick people by BU"); only the hierarchy rollup capability is enterprise. -
220ce5b: Resolve the tenant default currency onto ExecutionContext.
Adds
ExecutionContext.currency(ISO 4217) and resolves it from thelocalization.currencysetting alongsidetimezone/locale— in both the runtimeresolveExecutionContextand the REST mirror. This is the foundation for the documented "applied when a currency field omits its own" fallback: the tenant default is now carried on every request context, so analytics enrichment, formatters, and renderers can resolve a measure/field currency down to the org default instead of hard-coding it. Undefined when no tenant default is configured (consumers then render a plain number).
-
3754f80: Fix: the Setup-nav capability gate (
requiresService, ADR-0057 D10) was a no-op on the single-item app-meta path.GET /meta/app/:namereturns a metadata envelope{ type, name, item: <app>, ... }, butfilterAppForUserwas applied to the envelope — whose.navigationis undefined — so it returned it untouched, silently bypassing BOTH therequiredPermissionsgate and the D10requiresServicegate. Organizations/Invitations therefore still appeared in the Setup app even in single-tenant deployments.filterAppForUserandresolveRegisteredServicesnow unwrap the envelope (the list path already passed the raw app). Verified against a liveos dev: single-tenant hides Organizations/Invitations; multi-tenant shows them. -
Updated dependencies [d7ff626]
-
Updated dependencies [2a1b16b]
-
Updated dependencies [e16f2a8]
-
Updated dependencies [e411a82]
-
Updated dependencies [a581385]
-
Updated dependencies [d5f6d29]
-
Updated dependencies [220ce5b]
-
Updated dependencies [3efe334]
-
Updated dependencies [feead7e]
-
Updated dependencies [6ca20b3]
-
Updated dependencies [5f875fe]
-
Updated dependencies [b469950]
- @objectstack/spec@10.0.0
- @objectstack/core@10.0.0
- @objectstack/service-package@10.0.0
-
e7f6539: feat(rest): warn on fail-open anonymous posture (ADR-0056 D2, warn→enforce)
Secure-by-default work for the data API. The deny capability already exists (
api.requireAuth=truerejects anonymous viaenforceAuth, and share-link /guest_portal/ control-plane routes are exempt) — but the default is fail-open (requireAuth=false), so an object with no OWD/RLS is world-readable with no signal. This adds a boot-time WARN when running in that posture, making it explicit (consistent with D4/D8 honesty). The global default is deliberately NOT flipped here — that is a release-gated decision; flipping it would 401 deployments that rely on anonymous reads. Proven by theshowcase-anonymous-denydogfood test (anonymous read+write → 401, authenticated → 200, control-plane open). -
751f5cf: feat(security): declaration-derived public-form authorization (ADR-0056, Option A)
Public form submissions are now authorized by the declaration, not by a deployment-configured
guest_portalprofile. The form-submit route derives a narrowpublicFormGrant: { object }from the matched form's target object; the SecurityPlugin honors it as a least-privilege capability — create + the immediate read-back on THAT object only, with no userId, and crucially NOT the anonymous fall-open. This makes public forms work under secure-by-default (requireAuth) without a hand-configuredguest_portal, scoped to exactly the declared object (the field allow-list is still enforced at the route;guest_portal/anonymousare kept on the context for back-compat with guest-detection hooks). It is the prerequisite that unblocks the eventualrequireAuthdefault flip, and generalizes the platform principle "public access = declared + runtime-derived scoped grant" (the same shape share-links already use). Proven byform-self-authdogfood (create on target allowed; cross-object + update/delete denied). plugin-security 108, rest 121, full dogfood 98 — no regression. -
2afb612: feat(security): resolve
current_user.emailin RLS owner policiesRLS
usingpredicates can now referencecurrent_user.email— a unique, human-readable, seedable owner anchor (owner = current_user.email). Previously the RLS compiler resolved onlycurrent_user.id/organization_id/roles/org_user_ids, so any owner-by-name/email predicate silently compiled to the deny sentinel (fail-closed → the user saw nothing). Email is sourced for free from the auth session (with a boundedsys_userfallback for the API-key path) and threaded onto theExecutionContextin both identity resolvers — the REST data path (rest-server) and the dispatcher path (resolve-execution-context).Display
nameis deliberately not exposed to RLS: names collide, and a collision on an ownership predicate is an access-control leak. Only unique identifiers (id,email) are resolvable.This makes owner-scoped row-level security work with seed data (no per-user ids needed) and, combined with
controlled_by_parent(ADR-0055), lets a master's owner scoping flow to its detail records. The example-showcase demonstrates it:showcase_invoicecarries anowneremail + an owner RLS policy, its lines are controlled-by-parent, and invoices/lines are seeded per owner. It also fixes the showcase's previously inert owner predicates (they used==andcurrent_user.name, neither of which the compiler accepts) to= current_user.email. -
Updated dependencies [e7f6539]
-
Updated dependencies [2365d07]
-
Updated dependencies [6595b53]
-
Updated dependencies [fa8964d]
-
Updated dependencies [36138c7]
-
Updated dependencies [a8e4f3b]
-
Updated dependencies [4c213c2]
-
Updated dependencies [2afb612]
- @objectstack/spec@9.11.0
- @objectstack/core@9.11.0
- @objectstack/service-package@9.11.0
-
fd07027: fix(analytics): make organization timezone actually drive date-dimension bucketing (ADR-0053 Phase 2, #1982)
Date-bucketed analytics silently ignored the reference timezone end-to-end. Three independent seams were broken:
- service-analytics —
NativeSQLStrategy(priority 10) won every cube/dataset query on a SQL driver, but it groups by the raw column (nodate_trunc) and ignorestimezone, so a date dimension never bucketed (one row per raw timestamp) and a non-UTC zone was dropped. It now declines queries that carry atimeDimensions[].granularity, handing them toObjectQLStrategy→engine.aggregate(native bucketing when UTC-safe, uniform in-memory bucketing when non-UTC). - objectql — the in-memory
countaggregation treated the*count-all sentinel (the Cubecountmeasure / a fieldless datasetcount, both compiled tosql: '*') as a column name, counting non-null of a non-existent property →0for every bucket. The driver'sCOUNT(*)masked it; the in-memory path (non-UTC date buckets,driver-rest/driver-memory) returned zeros.*is now counted as all rows. - rest —
resolveExecCtxnever resolved the localization timezone/locale, so/analytics/dataset/queryalways ran withtimezone: 'UTC'. It now resolves them through thesettingsservice (honouring the 4-tier cascade incl. theOS_LOCALIZATION_TIMEZONEenv override), mirroring the dispatcher path.
- service-analytics —
-
Updated dependencies [db02bd5]
-
Updated dependencies [641675d]
-
Updated dependencies [94e9040]
-
Updated dependencies [1f88fd9]
-
Updated dependencies [1f88fd9]
- @objectstack/spec@9.10.0
- @objectstack/core@9.10.0
- @objectstack/service-package@9.10.0
- @objectstack/spec@9.9.1
- @objectstack/core@9.9.1
- @objectstack/service-package@9.9.1
-
44c5348: fix: two runtime gaps found by driving the CRM example end-to-end.
Delete of a parent with a required-FK child no longer fails with a misleading " is required" error.
cascadeDeleteRelationsdefaulted alookupFK toset_null; for a required FK that issued an UPDATE clearing the column, which the child's validator rejected with a400 "<field> is required"naming a field that isn't even on the object being deleted (e.g. deleting acrm_accountwith opportunities →"account is required"). A required FK can't be nulled, so a defaultedset_nullnow escalates torestrict: the delete is refused with a clear409 DELETE_RESTRICTEDcarrying the dependent object + count ("Cannot delete crm_account (…): 4 dependent crm_opportunity record(s) reference it via account … set deleteBehavior:'cascade'"). Explicitcascade/restrictand optional (nullable) lookups are unchanged.Removed the hardcoded
POST /data/lead/:id/convertendpoint +convertLeadprotocol method. It hardcoded bare object names (lead/account/contact/opportunity) and a fixed Salesforce field mapping into the framework runtime, so it was unreachable by any real (namespaced) app —/data/crm_lead/:id/convert404s, and the literalleadobject doesn't exist. Lead conversion is an app concern modeled correctly as a flow (the CRM ships acrm_convert_lead_wizardscreen flow); baking a CRM-specific workflow into the framework was false surface. Untested, undocumented, unused by the example. Removed.
- Updated dependencies [84249a4]
- Updated dependencies [11af299]
- Updated dependencies [d5774b5]
- Updated dependencies [134043a]
- Updated dependencies [90108e0]
- Updated dependencies [9afeb2d]
- Updated dependencies [6bec07e]
- Updated dependencies [601cc11]
- Updated dependencies [575448d]
- @objectstack/spec@9.9.0
- @objectstack/core@9.9.0
- @objectstack/service-package@9.9.0
-
7fe0b91: feat(rest): enforce object-level API exposure (
enable.apiEnabled/enable.apiMethods) on the REST data surface (ADR-0049 #1889). Previously these flags were parsed but unenforced — an object could not be hidden from the automatic API, a false sense of security. Now:apiEnabled: false→ the object's/api/v1/data/{object}routes return 404 (existence not revealed); a non-emptyapiMethodswhitelist → operations outside it return 405. Enforced across list/get/create/query/update/delete/import/export/batch/createMany/updateMany/deleteMany. Default-allow (objects with noenableblock, orapiEnabledunset/true and noapiMethods) behave exactly as before — no regression. This is the external API boundary only; internal callers (hooks, flows, objectql) are unaffected. -
884bf2f: feat: record clone — wire the
object.enable.clonecapability to a real runtime (previously a parsed-but-dead flag).- objectql: new
protocol.cloneData({ object, id, overrides?, context? })— reads the source record, drops engine-owned columns (id+ auditcreated_at/created_by/updated_at/updated_by, plussystem-flagged,autonumber,formulaandsummaryfields) so the insert path re-derives them, applies calleroverrideslast, and inserts the copy. Shallow by design (duplicates the record's own fields, not its child records). Gated byschema.enable.clone: default-on, an explicitenable.clone === falsethrows403 CLONE_DISABLED. - rest: new
POST /api/v1/data/:object/:id/clone(201 →{ object, id, sourceId, record }). Optional body{ overrides }(or a bare field map) overrides copied values, e.g. a newnameor a cleared unique field. Honors the same auth +enable.apiEnabled/apiMethodsgates as the rest of the data surface;enable.clone === false→ 403.
Reclassifies
object.enable.clonedead → livein the spec liveness ledger. - objectql: new
- Updated dependencies [97c55b3]
- Updated dependencies [1b1f490]
- @objectstack/spec@9.8.0
- @objectstack/core@9.8.0
- @objectstack/service-package@9.8.0
- @objectstack/spec@9.7.0
- @objectstack/core@9.7.0
- @objectstack/service-package@9.7.0
-
71578f2: feat(book): documentation navigation as a
bookelement — spine + derived membership (ADR-0046 §6)Adds the
bookmetadata element: a navigation spine (ordered groups +audience+ identity) whose membership is derived by rule (includeglob/tag) plus optional per-docorder/group, never a central array. This keeps AI authoring create-and-forget (no central-array read-modify-write) and runtime overlay merge-safe (RFC 7396 treats arrays atomically).BookSchema+resolveBookTree()derived-membership resolver +defineBook()+ additivedoc.order/doc.group.- Register
bookas a render-time metadata type (allowOrgOverride: true); wire it through the runtime type enumerations (PLURAL_TO_SINGULAR, engine registration, artifact field map, type-schema map). - REST
GET /meta/book/:name/treeresolves the tree; read-layeraudiencegating (public≡ anonymous;org/{profile}require sign-in).
- Updated dependencies [d1e930a]
- Updated dependencies [71578f2]
- Updated dependencies [5e3a301]
- Updated dependencies [5db2742]
- @objectstack/spec@9.6.0
- @objectstack/core@9.6.0
- @objectstack/service-package@9.6.0
- Updated dependencies [ee72aae]
- @objectstack/spec@9.5.1
- @objectstack/core@9.5.1
- @objectstack/service-package@9.5.1
-
d08551c: feat(ADR-0046): per-locale documentation content (doc i18n)
Docs can now ship localized bodies. Authors add sibling locale-variant files
src/docs/<name>.<locale>.md(e.g.crm_lead_guide.zh.md,..pt-BR.md) next to the base<name>.md; the base stays the default and the fallback. Flatness is preserved — variants are flat siblings, not subdirectories.- spec:
DocSchemagains an optionaltranslationsmap (locale → {label?, description?, content}) plusresolveDocLocale(doc, locale), which collapses a doc to the best-matching locale (exact → primary subtagzh-CN→zh→ base) with per-field fallback and strips thetranslationsmap. - cli (collect-docs): variant files are folded into the base doc's
translations; orphan/duplicate variants and the v1 MDX/image bans are linted on variant content too. - rest:
/meta/doc(list + single) resolves the request locale from the existingAccept-Language/?localenegotiation, returns one localized body, and never ships thetranslationsmap. Doc detail bypasses the response cache so a language switch can't return a stale-locale body. - setup / studio: the built-in overview docs now ship
zhtranslations (TS-first inlinetranslations), so a Chinese console renders Chinese docs.
The console already sends the active UI language as
Accept-Language, so doc content localizes on a language switch with no client change. - spec:
- Updated dependencies [d08551c]
- Updated dependencies [707aeed]
- Updated dependencies [7a103d4]
- Updated dependencies [4b01250]
- @objectstack/spec@9.5.0
- @objectstack/core@9.5.0
- @objectstack/service-package@9.5.0
-
0856476: feat(metadata): package-scoped single-item resolution via
?package=(ADR-0048)A single-item metadata GET (
/meta/:type/:name?package=<id>) now resolves package-scoped (prefer-local): when two installed packages ship an item of the sametype/name, the requester's own package wins. Previously only the list endpoint was package-aware; a single-item fetch was context-free, so a cross-package collision always resolved to whichever package registered first.The fix threads
packageIdend-to-end:@objectstack/rest— the cacheable single-item path calledgetMetaItemCached(ETag keyed on type+name only) and dropped?package=. A?package=read now bypasses that cache and takes the disambiguatinggetMetaItem(type, name, packageId)path, so two same-named items never share one cache entry.@objectstack/objectql—protocol.getMetaItemforwardspackageIdto the overlay query (sys_metadata.package_id),MetadataFacade.get, andregistry.getItem;MetadataFacade.getgained an optionalcurrentPackageId.@objectstack/runtime— the parallel HTTP dispatcher threads?package=too.
This lets the doc viewer (
/apps/:packageId/docs/:name) resolve one doc scoped to its app, sodocnames no longer need a namespace prefix for uniqueness (the prefix becomes a recommended convention, likepage/dashboard/report);doc.zoddoc-comments updated accordingly.
-
3e675f6: fix(metadata): package-scope the layered (Studio editor) read via
?package=(ADR-0048)The
?layers=truesingle-item read (the Studio metadata editor's 3-state code/overlay/effective view) ignoredpackageId, so editing one of two same-named items from different packages resolved ambiguously (first match).protocol.getMetaItemLayerednow threadspackageIdinto the code layer (metadataService.get+lookupArtifactItem+registry.getItem) and thesys_metadataoverlay query (package_idprefer-local).registry.getArtifactItem(type, name, currentPackageId?)andlookupArtifactItemgained the optional package-scope hint.rest-serverthreads?package=into the layered branch.
This completes the per-route package-scoped resolution audit: the runtime render surface (dashboard/report/page/doc) was already scoped; this closes the Studio editor (
/apps/:appName/metadata/:type/:name). Frontend counterpart sends?package=from the metadata list row's owning package. -
Updated dependencies [060467a]
-
Updated dependencies [0856476]
-
Updated dependencies [b678d8c]
-
Updated dependencies [b678d8c]
-
Updated dependencies [b678d8c]
- @objectstack/spec@9.4.0
- @objectstack/core@9.4.0
- @objectstack/service-package@9.4.0
- 290f631: ADR-0044 flow-level send-back-for-revision (#1744). The approval node gains a third flow movement beyond approve/reject:
sendBack()finalizes the pending request asreturned(newApprovalStatus), resumes the run down itsreviseedge to a wait point where the record lock releases, and the submitter'sresubmit()re-enters the approval node over a declared back-edge, opening the next round's request (fresh approver slate, re-locked,roundstamped via the config snapshot). Engine:FlowEdgeSchema.typegains'back'— cycle validation now requires the graph minus back-edges to be a DAG (unmarked cycles still rejected), node re-entry overwrites outputs/appends steps, a 100-re-entry runaway guard backstops misauthored loops, andcancelRun(runId, reason)lands as the first run-cancel primitive (recall crossing a revise window cancels the parked run).maxRevisions(default 3) on the approval node config auto-rejects send-backs past the budget. REST:POST /approvals/requests/:id/reviseand/resubmit. Audit kindsrevise/resubmitjoinApprovalActionKindand thesys_approval_actionenum. - 50b7b47: Approvals server-side pagination + search pushdown (#1745).
listRequestsacceptsq/limit/offset— free-text search pushes into the engine query as an$orof$containsterms (thepayload_jsonsnapshot carries record titles, so titles match without a join), and the page window pushes down whenever the filter is fully pushable; approver/status-array filters still post-filter their bounded scan and window in memory (the documented residual until the approver join-table follow-up). NewcountRequestsreturns the unwindowed total (enginecountwhen pushable). REST:GET /approvals/requestsgainsq/limit/offsetand returns{data, total}when paging. - f8684ea: Approvals thread interactions — the collaboration layer between submit and decide.
reassign()hands a pending-approver slot to someone else (audit-first ordering, new approver notified via the optionalmessagingservice),remind()nudges every pending approver with a 4h per-request throttle (THROTTLED→ HTTP 429),requestInfo()sends a request back to the submitter for more material while it stays pending, andcomment()adds free-form thread replies. Rows exposesla_due_at(created_at + escalation.timeoutHours, display-only) and single reads attachflow_steps(the owning flow's approval trunk with done/current/upcoming states). REST grows the four matching POST routes; thesys_approval_action.actionenum gains the new kinds.
- b08d08d: ADR-0046:
GET /meta/doclist responses omitcontentby default (?include=contentopts back in;GET /meta/doc/:namealways returns the full body). The runtime dispatcher's/metadata/docroute already slims docs (#1789) — this applies the same rule on the REST/meta/:typeroute the console actually reads, keeping unbounded manuals off the list surface. - Updated dependencies [1ada658]
- Updated dependencies [3219191]
- Updated dependencies [290f631]
- Updated dependencies [50b7b47]
- Updated dependencies [f15d6f6]
- Updated dependencies [f8684ea]
- Updated dependencies [b4765be]
- @objectstack/spec@9.3.0
- @objectstack/core@9.3.0
- @objectstack/service-package@9.3.0
- Updated dependencies [2f57b75]
- Updated dependencies [2f57b75]
- @objectstack/spec@9.2.0
- @objectstack/core@9.2.0
- @objectstack/service-package@9.2.0
- Updated dependencies [b9062c9]
- @objectstack/spec@9.1.0
- @objectstack/core@9.1.0
- @objectstack/service-package@9.1.0
- Updated dependencies [1817845]
- @objectstack/spec@9.0.1
- @objectstack/core@9.0.1
- @objectstack/service-package@9.0.1
- Updated dependencies [4c3f693]
- Updated dependencies [0bf39f1]
- Updated dependencies [f533f42]
- Updated dependencies [1c83ee8]
- @objectstack/spec@9.0.0
- @objectstack/core@9.0.0
- @objectstack/service-package@9.0.0
- @objectstack/spec@8.0.1
- @objectstack/core@8.0.1
- @objectstack/service-package@8.0.1
- 345e189: Robust multi-write transactions (ADR-0034).
engine.transaction()now establishes an ambient transaction (AsyncLocalStorage) so every data operation during the callback — including internal reads performed while a write runs — binds to the active transaction's connection instead of asking the pool for another one and deadlocking on SQLite's single-connection pool. Adds a cross-object transactional batch endpoint (POST /api/v1/data/batch) with intra-batch{ $ref: <opIndex> }parent references, so a parent and its children can be created atomically in one transaction.
-
0a6438e: perf(rest): cache hostname→environment resolution; document cluster pub/sub durability (P1-4, P1-5)
- rest (P1-4):
resolveByHostname()ran on every unscoped request — a control-plane lookup (typically a DB query) in the hot path.RestServernow cacheshostname → environmentIdin-memory with a 30s TTL across all three resolution sites, caching negative results too so unknown hosts don't hammer the registry. Registry errors are not cached, so a transient blip self-heals. - service-cluster-redis (P1-5): recorded the durability contract for
metadata.changedinpubsub.ts. Redis pub/sub is at-most-once by design; the event is a cache-invalidation hint only — the durable source of truth is the transactionalsys_metadata(+sys_metadata_history) write, so a missed event causes a stale cache until the next reload, never data loss. No code change to the delivery semantics; risk accepted and documented.
- rest (P1-4):
-
ae7fb3f: fix(rest): advertise
routes.mcpin /discovery when MCP is enabled (cloud#152)The objectui Integrations page reads
discovery.routes.mcpto show the "Connect an AI agent" card, but it stayed absent on live envs even with MCP enabled. Root cause (NOT a cache, as first suspected):@objectstack/restserves its OWN/discovery(protocol.getDiscovery()), separate from the dispatcher'sgetDiscoveryInfowhere themcpfield was added — so the REST-served discovery never advertised it.The REST discovery handler now adds
routes.mcp(pointing at the unscoped/api/v1/mcp, since the MCP route is mounted bare) whenOS_MCP_SERVER_ENABLED=true, and omits it otherwise — mirroring the dispatcher discovery and the opt-in gate. 2 tests (enabled → advertised, disabled → absent). -
c262301: fix(rest): REST data API honors sys_api_key — one shared verifier with MCP (closes #1633)
Staging e2e found the MCP surface authenticated a
sys_api_keybut the REST data API (@objectstack/rest) returned 401 for the same key — itsresolveExecCtxonly checked the better-auth session, never the API key.Converged both surfaces onto ONE verifier so they can't drift:
@objectstack/core/securitynow owns the sharedsys_api_keyprimitives (hashApiKey,generateApiKey,extractApiKey,parseScopes,isExpired) plus a newresolveApiKeyPrincipal(ql, headers, nowMs?)that hashes the inbound key, looks it up by the indexed at-rest hash, and rejects unknown / revoked / expired / owner-less keys (fail-closed).coreis the natural home: bothrestandruntimedepend on it, it depends on neither (no cycle), and it's server-side (already usesnode:crypto).@objectstack/runtime—security/api-key.tsre-exports the primitives from core (stable import surface) andresolveExecutionContextnow delegates its API-key branch toresolveApiKeyPrincipal.@objectstack/rest—resolveExecCtxresolves the data engine once and triesresolveApiKeyPrincipal(x-api-key /Authorization: ApiKey) BEFORE the session, so/api/v1/data+/api/v1/metanow authenticate an API key under the key's permissions + RLS, exactly like the dispatcher/MCP path.
Tests: core
api-key.test.ts(primitives + verifier: valid / revoked / expired / unknown / owner-less / plaintext-not-matched / fail-closed-ql). runtime + rest suites green. -
e1478fe: fix(rest): map schema-mismatch & not-null driver errors to structured 4xx
mapDataErrorcollapsed any SQL-looking driver error into a generic500 DATABASE_ERROR, so a bad write payload to the data API leaked a 500 instead of a fixable 4xx (e.g.POST /data/sys_teamwith an unknown field, or omitting a required column). It now maps unknown-column errors to400 INVALID_FIELD { field }and not-null violations to400 VALIDATION_FAILED { fields:[{required}] }across SQLite/Postgres/MySQL phrasings, placed before the unknown-object branch so Postgrescolumn … of relation … does not existis not mis-mapped to 404. Genuine driver faults still return 500; unique violations still return 409. -
Updated dependencies [a46c017]
-
Updated dependencies [b990b89]
-
Updated dependencies [99111ec]
-
Updated dependencies [d5a8161]
-
Updated dependencies [5cf1f1b]
-
Updated dependencies [9ef89d4]
-
Updated dependencies [3306d2f]
-
Updated dependencies [c262301]
-
Updated dependencies [bc44195]
-
Updated dependencies [9e2e229]
- @objectstack/spec@8.0.0
- @objectstack/core@8.0.0
- @objectstack/service-package@8.0.0
-
ac1fc4c: feat(metadata): optional storage teardown on delete so "publish to preview" leaves no orphan table
Object storage was create-only:
publishMetaItemcreates a table (ensureObjectStorage) but nothing ever dropped one —deleteMetaItemonly tombstones the metadata row, leaving the physical table behind. That made the pragmatic "publish an object just to preview it with real data, then discard if wrong" loop leave residue.Adds the inverse path, opt-in and guarded:
engine.dropObjectSchema(name)— inverse ofsyncObjectSchema; resolves the table name + driver and calls the driver's existingdropTable(DROP TABLE IF EXISTS / drop collection).deleteMetaItem({ …, dropStorage })— whentrue, drops the object's physical table after the metadata is removed. DESTRUCTIVE, so it is gated:objecttype only (others have no table),activestate only (drafts were never materialised), and never asys_-prefixed platform table. Defaultfalsekeeps delete non-destructive to data. Best-effort: a drop failure is logged, not thrown.- REST:
DELETE /meta/:type/:name?dropStorage=truethreads the flag.
This makes "publish to preview → discard" cleanly reversible. Combined with the draft-overlay read mode, it backs the team's chosen approach: lean on publish (into a dev sandbox) for data-level confirmation rather than building a full draft-data preview, and make that publish safely undoable.
- @objectstack/spec@7.9.0
- @objectstack/core@7.9.0
- @objectstack/service-package@7.9.0
-
a75823a: feat(metadata): expose pending DRAFT metadata (ADR-0033 draft discoverability)
AI-authored metadata lands as drafts (
sys_metadatarows withstate='draft', bound to an app package), but the only list path —getMetaItems— reads the active registry, so drafts were invisible: a just-built app package looked empty and there was no "pending changes" surface.SysMetadataRepository.listDrafts({type?, packageId?})lists draft rows (mirrorslist()but scoped tostate='draft', optionally narrowed by package), returning a light header projection (no body) withpackageId.protocol.listDrafts({packageId?, type?, organizationId?})exposes it over the overlay repo.GET /api/v1/meta/_drafts?packageId=&type=surfaces it to the console. Registered in the REST server before the greedy/meta/:typeroute (and mirrored in the dispatcher) so_draftsis never captured as a metadata type name.
Read-only; no behavior change to existing list/publish paths. Powers the upcoming Studio "drafts/pending changes" view and draft-aware package contents.
-
Updated dependencies [06f2bbb]
-
Updated dependencies [4fbb86a]
-
Updated dependencies [36719db]
-
Updated dependencies [424ab26]
- @objectstack/spec@7.8.0
- @objectstack/service-package@7.8.0
- @objectstack/core@7.8.0
- Updated dependencies [b391955]
- Updated dependencies [f06b64e]
- Updated dependencies [023bf93]
- @objectstack/spec@7.7.0
- @objectstack/core@7.7.0
- @objectstack/service-package@7.7.0
- Updated dependencies [955d4c8]
- Updated dependencies [c4a4cbd]
- Updated dependencies [b046ec2]
- Updated dependencies [2170ad9]
- Updated dependencies [02d6359]
- Updated dependencies [7648242]
- Updated dependencies [8fa1e7f]
- Updated dependencies [55866f5]
- Updated dependencies [60f9c45]
- @objectstack/spec@7.6.0
- @objectstack/core@7.6.0
- @objectstack/service-package@7.6.0
- @objectstack/spec@7.5.0
- @objectstack/core@7.5.0
- @objectstack/service-package@7.5.0
- @objectstack/spec@7.4.1
- @objectstack/core@7.4.1
- @objectstack/service-package@7.4.1
-
2faf9f2: External Datasource Federation (ADR-0015) — REST surface.
Adds
registerExternalDatasourceRoutes, mounting/api/v1/datasources/:name/ external/*—GET tables,POST tables/:remote/draft,POST refresh-catalog,POST validate— served by theexternal-datasourceservice and wired into the REST API plugin. Routes return503 external_service_unavailablewhen the service is not registered, so they are safe to mount unconditionally.
-
58b450b: Make metadata labels follow the active UI language without a page refresh (#1319).
The client now carries the active locale on every request (
Accept-Language,setLocale/getLocale), the protocol ETag is locale-aware so cached metadata no longer collides across languages, and theclient-reactmetadata hooks refetch when the locale changes. Theapps/accountconsole wires its router locale through so a language switch relabels server-resolved object/field/view labels in place instead of leaving the UI half-translated until reload. -
82eb6cf: Fix system-metadata translations: locale fallback, app/dashboard localization, and coverage gaps.
Switching the UI language left many surfaces in English. Three root causes are addressed:
-
Locale fallback (server). The metadata translation resolver (
@objectstack/speci18n-resolver) now resolves a requested locale against the locales actually present in the bundle (exact → case-insensitive → base-language → variant), so a request forzhcorrectly hits thezh-CNbundle instead of falling back to English. This mirrorsresolveLocalein@objectstack/coreand benefits every resolver (objects, views, actions, settings, metadata forms). -
App & dashboard localization (server). Added
translateAppandtranslateDashboardresolvers and wiredapp/dashboardinto the REST/metatranslation path. App labels, sidebar/navigation group labels, and dashboard titles/widgets were previously never localized at the API boundary even though the translation data existed. -
Coverage & quality (data). Added translations for the previously untranslated platform objects
sys_share_link,sys_view_definition, andsys_metadata_audit(and registered them in the i18n-extract config so future extractions keep them). Replaced English placeholder strings left in thezh-CN/ja-JP/es-ESobject and metadata-form bundles (notably actionconfirmText/successMessageprompts). Added the missinges-ESbuilt-in Settings bundle in@objectstack/service-settings.
-
-
Updated dependencies [23c7107]
-
Updated dependencies [c72daad]
-
Updated dependencies [f115182]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [2faf9f2]
-
Updated dependencies [58b450b]
-
Updated dependencies [82eb6cf]
-
Updated dependencies [13d8653]
-
Updated dependencies [ff3d006]
-
Updated dependencies [5e831de]
- @objectstack/spec@7.4.0
- @objectstack/core@7.4.0
- @objectstack/service-package@7.4.0
- Updated dependencies [5e7c554]
- @objectstack/spec@7.3.0
- @objectstack/core@7.3.0
- @objectstack/service-package@7.3.0
- @objectstack/spec@7.2.1
- @objectstack/core@7.2.1
- @objectstack/service-package@7.2.1
- @objectstack/spec@7.2.0
- @objectstack/core@7.2.0
- @objectstack/service-package@7.2.0
- Updated dependencies [47a92f4]
- @objectstack/spec@7.1.0
- @objectstack/core@7.1.0
- @objectstack/service-package@7.1.0
- Updated dependencies [74470ad]
- Updated dependencies [d29617e]
- Updated dependencies [dc72172]
- @objectstack/spec@7.0.0
- @objectstack/core@7.0.0
- @objectstack/service-package@7.0.0
- @objectstack/spec@6.9.0
- @objectstack/core@6.9.0
- @objectstack/service-package@6.9.0
- @objectstack/spec@6.8.1
- @objectstack/core@6.8.1
- @objectstack/service-package@6.8.1
-
c8b9f57: Metadata Admin engine — protocol foundations.
This is the backend half of the unified Metadata Admin shipped in the Setup app. The framework now exposes everything the engine needs to render a directory tile, schema-driven form, layered diff, references graph, and destructive-change confirmation for every registered metadata type.
GET /api/v1/meta/typesis now type-rich. Each entry includes{ icon, domain, schema (JSONSchema), allowOrgOverride, allowRuntimeCreate, supportsOverlay, ui? }so the client can render without a second round-trip per type.GET /api/v1/meta/:type/:name/referencesscans every registered metadata type for pointers to the given item (object fields, view sources, flow targets, permission objects, …) and returns the inbound edges so the UI can warn before deletes.GET /api/v1/meta/:type/:name?layers=code,overlay,effectivereturns each layer separately rather than the merged effective document, powering the 3-state diff editor (code source / overlay / effective).- Destructive-change detection on
PUT /api/v1/meta/object/:nameandPUT /api/v1/meta/field/:name: rejects field type narrowing, required toggled on without a default, removed enum values, etc., unless the client opts in withforce=true. - Env-var registry patch:
OBJECTSTACK_METADATA_WRITABLE=object,field,permission,view,…flipsallowOrgOverrideon for the listed types at boot, enabling runtime overlays for production without re-deploying spec. - New guide: Adding a Metadata Type walks through registry entry + Zod schema + optional custom editor.
Setup app navigation now uses the new component-route variant (
{ type: 'component', componentRef: 'metadata:directory' }) — the temporary/dev/metaroute is removed.
- Updated dependencies [6e88f77]
- Updated dependencies [c8b9f57]
- @objectstack/spec@6.8.0
- @objectstack/core@6.8.0
- @objectstack/service-package@6.8.0
- @objectstack/spec@6.7.1
- @objectstack/core@6.7.1
- @objectstack/service-package@6.7.1
- Updated dependencies [430067b]
- Updated dependencies [4f9e9d4]
- @objectstack/spec@6.7.0
- @objectstack/core@6.7.0
- @objectstack/service-package@6.7.0
- Updated dependencies [a49cfc2]
- @objectstack/spec@6.6.0
- @objectstack/core@6.6.0
- @objectstack/service-package@6.6.0
- @objectstack/spec@6.5.1
- @objectstack/core@6.5.1
- @objectstack/service-package@6.5.1
- @objectstack/spec@6.5.0
- @objectstack/core@6.5.0
- @objectstack/service-package@6.5.0
- Updated dependencies [f8651cc]
- Updated dependencies [f8651cc]
- Updated dependencies [0bf6f9a]
- @objectstack/spec@6.4.0
- @objectstack/core@6.4.0
- @objectstack/service-package@6.4.0
- @objectstack/spec@6.3.0
- @objectstack/core@6.3.0
- @objectstack/service-package@6.3.0
- Updated dependencies [b4c74a9]
- @objectstack/spec@6.2.0
- @objectstack/core@6.2.0
- @objectstack/service-package@6.2.0
- @objectstack/spec@6.1.1
- @objectstack/core@6.1.1
- @objectstack/service-package@6.1.1
- Updated dependencies [93c0589]
- @objectstack/spec@6.1.0
- @objectstack/core@6.1.0
- @objectstack/service-package@6.1.0
-
944f187: # v5.0 —
project→environmenthard renameThe runtime concept previously called "project" (per-tenant business workspace; Org → Project → Branch hierarchy; per-project ObjectKernel, per-project DB, per-project artifact) is now uniformly called "environment".
This is a hard rename with no aliases, deprecation shims, or compatibility layer. Upgrade requires a coordinated update of CLI, runtime, server, and any clients calling the REST API.
Note: "project" in the npm / monorepo sense (the framework itself,
package.json, tsconfig project references, vitestprojectsconfig) is unchanged.- Flags renamed:
--project/-p→--environment/-e(os publish,os rollback)--project-id→--environment-id(os dev)
- Default local env id:
proj_local→env_local. - Env var:
OS_PROJECT_ID→OS_ENVIRONMENT_ID. - Command group renamed:
os projects ...→os environments ...(bind,create,list,show,switch). - Persisted auth-config key:
activeProjectId→activeEnvironmentId.
- Scoped routes:
/api/v1/projects/:projectId/...→/api/v1/environments/:environmentId/.... - Cloud control-plane routes:
/api/v1/cloud/projects/...→/api/v1/cloud/environments/...(including/cloud/environments/:id/artifact,/cloud/environments/:id/metadata,/cloud/environments/:id/credentials/rotate, etc.). - Header:
X-Project-Id(and lowercasex-project-id) →X-Environment-Id(x-environment-id). - Route param name in handlers:
req.params.projectId→req.params.environmentId. - Hostname-routing and tenant-resolution code-paths use
environmentIdend-to-end.
- Exported symbols (no aliases):
createSystemProjectPlugin→createSystemEnvironmentPluginSYSTEM_PROJECT_ID→SYSTEM_ENVIRONMENT_IDProjectArtifactSchema→EnvironmentArtifactSchemaPROJECT_ARTIFACT_SCHEMA_VERSION→ENVIRONMENT_ARTIFACT_SCHEMA_VERSIONObjectOSProjectPlugin→ObjectOSEnvironmentPlugincreateSingleProjectPlugin→createSingleEnvironmentPlugin
- Plugin identifier strings:
com.objectstack.runtime.objectos-project→objectos-environmentcom.objectstack.studio.single-project→single-environmentcom.objectstack.multi-project→multi-environmentcom.objectstack.runtime.system-project→system-environment
- Provisioning hook:
provisionSystemProject→provisionSystemEnvironment.
- Column renames on
sys_metadataandsys_metadata_history:project_id→environment_id. - Column renames on
sys_activity:project_id→environment_id(plus index). - Object renames in platform-objects metadata:
sys_project→sys_environment(lookup targets),sys_project_member→sys_environment_member,sys_project_credential→sys_environment_credential. - Auth-context field:
active_project_id→active_environment_id. - JSON schemas under
packages/spec/json-schema/system/:ProjectArtifact*.json→EnvironmentArtifact*.json(regenerated at build).
A new migration
migrateProjectIdToEnvironmentId(packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts) auto-runs fromDatabaseLoader.ensureSchema()on bootstrap and rewrites any existingproject_idcolumn onsys_metadata/sys_metadata_historytoenvironment_id(idempotent, best-effort). Existing rows are preserved.The legacy reverse migration
migrateEnvIdToProjectIdis retained verbatim for historical / disaster-recovery use; it is not auto-run.-os publish --project proj_xyz +os publish --environment env_xyz -curl -H "X-Project-Id: env_xyz" https://api.example.com/api/v1/data/customer +curl -H "X-Environment-Id: env_xyz" https://api.example.com/api/v1/data/customer -OS_PROJECT_ID=env_xyz os dev +OS_ENVIRONMENT_ID=env_xyz os dev -import { createSystemProjectPlugin, SYSTEM_PROJECT_ID } from "@objectstack/runtime"; +import { createSystemEnvironmentPlugin, SYSTEM_ENVIRONMENT_ID } from "@objectstack/runtime"; -import { ProjectArtifactSchema } from "@objectstack/spec"; +import { EnvironmentArtifactSchema } from "@objectstack/spec";
If you maintain a Cloud control-plane deployment, the
cloudrepository must be updated in lockstep to pick up the new plugin identifier strings (single-environment,multi-environment,objectos-environment). - Flags renamed:
- Updated dependencies [629a716]
- Updated dependencies [dbc4f7d]
- Updated dependencies [944f187]
- @objectstack/spec@6.0.0
- @objectstack/core@6.0.0
- @objectstack/service-package@6.0.0
-
b806f58: Scope
sys_uservisibility to fellow organization members.The default RLS policy on
sys_userwasid = current_user.id, which meant @-mention pickers, owner/assignee lookups, reviewer selectors and the user roster all returned just the current user. The RLS compiler doesn't support subqueries, so aid IN (SELECT user_id FROM sys_member ...)policy isn't expressible.This change:
- Pre-resolves
org_user_ids(the IDs of all users in the active org) intoExecutionContextin all three REST entry-point resolvers (@objectstack/rest,@objectstack/runtime,@objectstack/plugin-hono-server). - Adds the field to
ExecutionContextSchemaso it survives Zod parsing. - Adds an
org_user_idsfield to the RLS compiler's user context. - Adds a new
sys_user_org_memberspolicy (id IN (current_user.org_user_ids)) to bothmember_defaultandviewer_readonlypermission sets, alongside the existingsys_user_selfpolicy. The RLS compiler OR-combines them, so users see themselves AND their org collaborators.
Capped at 1000 members per request. Large enterprises should plug in a directory cache or split per workspace.
- Pre-resolves
-
Updated dependencies [bab2b20]
-
Updated dependencies [fa011d8]
-
Updated dependencies [b806f58]
- @objectstack/spec@5.2.0
- @objectstack/core@5.2.0
- @objectstack/service-package@5.2.0
- Updated dependencies [75f4ee6]
- Updated dependencies [823d559]
- @objectstack/spec@5.1.0
- @objectstack/core@5.1.0
- @objectstack/service-package@5.1.0
-
5cfdc85: PR-10d.4 — REST plumbing for the metadata repository write path.
PUT /api/v1/meta/:type/:name(and the compound:type/:section/:namevariant) now forwards theIf-Matchheader tosaveMetaItemasparentVersion, andX-Actor(orreq.user.id) asactor. ETag-style quotes are stripped.- A failed optimistic-lock check surfaces as HTTP 409 with body
{ "error": "...", "code": "metadata_conflict" }(no protocol changes —sendErroralready honourederror.status+error.code). - Added a real-engine integration test for the repository write path
(
protocol-save-meta-repo-path-real-engine.test.ts) — addresses the PR-10d.3 rubber-duck stub-drift concern by exercisingObjectStackProtocolImplementation.saveMetaItemthroughnew ObjectQL()with an inline in-memory driver. Covers insert→update version bump, parentVersion conflict, checksum length, and plural→singular normalization.
Default behaviour unchanged: the repository write path remains opt-in via
options.useRepositoryWritePath/OBJECTSTACK_USE_REPOSITORY_WRITE_PATH=1. Flag flip and legacy path removal will follow in a separate post-soak PR.
- Updated dependencies [2f9073a]
- @objectstack/spec@5.0.0
- @objectstack/core@5.0.0
- @objectstack/service-package@5.0.0
-
2869891: feat: Optimistic Concurrency Control (OCC) via
If-MatchUpdate and Delete requests now accept an optional version token. When supplied, the protocol compares it against the record's current
updated_at(orversioncolumn when available) and rejects with409 CONCURRENT_UPDATEon mismatch, preventing silent overwrites when two clients edit the same record.Wire formats (opt-in, all server- and client-backward-compatible):
PATCH /data/{object}/{id}— supportsIf-Match: "<token>"header orexpectedVersion: "<token>"body field (body wins when both present).DELETE /data/{object}/{id}— supportsIf-Matchheader or?expectedVersion=...query param.- Conflict response:
409 { error, code: 'CONCURRENT_UPDATE', currentVersion, currentRecord }so the client can offer Reload / Overwrite / Cancel UX.
Behaviour
- Missing/empty version → no check (legacy callers unaffected).
- Record not found during the version probe → no check; the downstream write
produces a normal
404. - Object has no
updated_atcolumn → no check (explicit opt-out for objects without timestamps). - Quoted RFC-7232 tokens (
"…") are accepted and unquoted before comparison.
Client
client.data.update(resource, id, data, { ifMatch })andclient.data.delete(resource, id, { ifMatch })now forward the token as anIf-Matchheader.Application-level CAS (findOne + compare in protocol.ts) is used in this slice to avoid touching every storage driver. A small TOCTOU window remains; for the B2B record-editing latencies this protects against, it is more than sufficient. Drivers may later be upgraded to atomic
WHERE id=? AND updated_at=?writes for true CAS without changing the public API.Tests: 7 new cases in
protocol-data.test.tscover opt-in, match, mismatch, quote-stripping, no-timestamps, empty-token, and the delete path.
- Updated dependencies [2869891]
- @objectstack/spec@4.2.0
- @objectstack/core@4.2.0
- @objectstack/service-package@4.2.0
- @objectstack/spec@4.1.1
- @objectstack/core@4.1.1
- @objectstack/service-package@4.1.1
- Updated dependencies [2108c30]
- Updated dependencies [23db640]
- @objectstack/spec@4.1.0
- @objectstack/core@4.1.0
- @objectstack/service-package@4.1.0
- 15e0df6: chore: unify all package versions to a single patch release
- Updated dependencies [15e0df6]
- @objectstack/spec@4.0.5
- @objectstack/core@4.0.5
- @objectstack/service-package@4.0.5
- Updated dependencies [326b66b]
- @objectstack/spec@4.0.4
- @objectstack/core@4.0.4
- @objectstack/spec@4.0.3
- @objectstack/core@4.0.3
- Updated dependencies [5f659e9]
- @objectstack/spec@4.0.2
- @objectstack/core@4.0.2
- Updated dependencies [f08ffc3]
- Updated dependencies [e0b0a78]
- @objectstack/spec@4.0.0
- @objectstack/core@4.0.0
- @objectstack/spec@3.3.1
- @objectstack/core@3.3.1
- @objectstack/spec@3.3.0
- @objectstack/core@3.3.0
- @objectstack/spec@3.2.9
- @objectstack/core@3.2.9
- @objectstack/spec@3.2.8
- @objectstack/core@3.2.8
- @objectstack/spec@3.2.7
- @objectstack/core@3.2.7
- @objectstack/spec@3.2.6
- @objectstack/core@3.2.6
- @objectstack/spec@3.2.5
- @objectstack/core@3.2.5
- @objectstack/spec@3.2.4
- @objectstack/core@3.2.4
- @objectstack/spec@3.2.3
- @objectstack/core@3.2.3
- Updated dependencies [46defbb]
- @objectstack/spec@3.2.2
- @objectstack/core@3.2.2
- Updated dependencies [850b546]
- @objectstack/spec@3.2.1
- @objectstack/core@3.2.1
- Updated dependencies [5901c29]
- @objectstack/spec@3.2.0
- @objectstack/core@3.2.0
- Updated dependencies [953d667]
- @objectstack/spec@3.1.1
- @objectstack/core@3.1.1
- Updated dependencies [0088830]
- @objectstack/spec@3.1.0
- @objectstack/core@3.1.0
- Updated dependencies [92d9d99]
- @objectstack/spec@3.0.11
- @objectstack/core@3.0.11
- Updated dependencies [d1e5d31]
- @objectstack/spec@3.0.10
- @objectstack/core@3.0.10
- Updated dependencies [15e0df6]
- @objectstack/spec@3.0.9
- @objectstack/core@3.0.9
- Updated dependencies [5a968a2]
- @objectstack/spec@3.0.8
- @objectstack/core@3.0.8
- Updated dependencies [0119bd7]
- Updated dependencies [5426bdf]
- @objectstack/spec@3.0.7
- @objectstack/core@3.0.7
- Updated dependencies [5df254c]
- @objectstack/spec@3.0.6
- @objectstack/core@3.0.6
- Updated dependencies [23a4a68]
- @objectstack/spec@3.0.5
- @objectstack/core@3.0.5
- Updated dependencies [d738987]
- @objectstack/spec@3.0.4
- @objectstack/core@3.0.4
- c7267f6: Patch release for maintenance updates and improvements.
- Updated dependencies [c7267f6]
- @objectstack/spec@3.0.3
- @objectstack/core@3.0.3
- Updated dependencies [28985f5]
- @objectstack/spec@3.0.2
- @objectstack/core@3.0.2
- Updated dependencies [389725a]
- @objectstack/spec@3.0.1
- @objectstack/core@3.0.1
- Release v3.0.0 — unified version bump for all ObjectStack packages.
- Updated dependencies
- @objectstack/spec@3.0.0
- @objectstack/core@3.0.0
- Updated dependencies
- @objectstack/spec@2.0.7
- @objectstack/core@2.0.7
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.6
- @objectstack/core@2.0.6
- Updated dependencies
- @objectstack/spec@2.0.5
- @objectstack/core@2.0.5
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.4
- @objectstack/core@2.0.4
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.3
- @objectstack/core@2.0.3
- Updated dependencies [1db8559]
- @objectstack/spec@2.0.2
- @objectstack/core@2.0.2
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.1
- @objectstack/core@2.0.1
- Updated dependencies [38e5dd5]
- Updated dependencies [38e5dd5]
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
- Updated dependencies
- @objectstack/spec@1.1.1
- @objectstack/core@1.1.1
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
-
- @objectstack/rest (new package): Extracted REST server, route management, and
createRestApiPlugininto a dedicated package - @objectstack/runtime: Add
createDispatcherPluginfor structured route management (auth, graphql, analytics, packages, hub, storage, automation) - @objectstack/cli: Dev mode (
--dev) now auto-enables Studio UI at/_studio/— no need for--uiflag; use--no-uito disable - @objectstack/cli: Root URL
/redirects to/_studio/in dev mode for convenience - @objectstack/cli: Removed Vite dev server fallback — always serves pre-built dist, no extra port
- @objectstack/studio: Interactive API Console in Object Explorer (request builder, response viewer, history)
- @objectstack/spec: Studio Plugin schema, MCP Protocol schemas, API versioning, Dispatcher protocol
- @objectstack/spec: Comprehensive
.describe()annotations across all Zod schemas - @objectstack/core: Production hot reload and dynamic plugin loading protocol
// Before (1.1.0) — implicit const runtime = new Runtime({ api: { basePath: "/api/v1" } }); // After (1.2.0) — explicit import { createRestApiPlugin } from "@objectstack/rest"; const runtime = new Runtime(); runtime.use(createRestApiPlugin({ basePath: "/api/v1" }));
Fields like
metadata,defaultValue,filters,config,datanow usez.unknown(). Add type narrowing where needed.Barrel imports via
Hub.*still work. Direct path imports (hub/license.zod.ts→system/license.zod.ts) need updating.MetricType(analytics) →AggregationMetricType,MetricType(licensing) →LicenseMetricTypeHttpDispatcher→createDispatcherPlugin()createHonoApp→HonoServerPlugin
- @objectstack/rest (new package): Extracted REST server, route management, and
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0
-
- @objectstack/rest (new package): Extracted REST server, route management, and
createRestApiPlugininto a dedicated package - @objectstack/runtime: Add
createDispatcherPluginfor structured route management (auth, graphql, analytics, packages, hub, storage, automation) - @objectstack/cli: Dev mode (
--dev) now auto-enables Studio UI at/_studio/— no need for--uiflag; use--no-uito disable - @objectstack/cli: Root URL
/redirects to/_studio/in dev mode for convenience - @objectstack/cli: Removed Vite dev server fallback — always serves pre-built dist, no extra port
- @objectstack/studio: Interactive API Console in Object Explorer (request builder, response viewer, history)
- @objectstack/spec: Studio Plugin schema (
Studio.PluginManifest) - @objectstack/spec: MCP (Model Context Protocol) schemas for AI tools, resources, prompts, transport
- @objectstack/spec: API versioning schema with multiple strategies
- @objectstack/spec: Dispatcher protocol schema
- @objectstack/spec: Comprehensive
.describe()annotations across all Zod schemas for JSON Schema generation - @objectstack/core: Production hot reload and dynamic plugin loading protocol
REST API is now opt-in. If you relied on automatic REST registration:
// Before (1.1.0) — implicit const runtime = new Runtime({ api: { basePath: "/api/v1" } }); // After (1.2.0) — explicit import { createRestApiPlugin } from "@objectstack/rest"; const runtime = new Runtime(); runtime.use(createRestApiPlugin({ basePath: "/api/v1" }));
Fields like
metadata,defaultValue,filters,config,datain spec schemas changed fromz.any()toz.unknown(). If you consume inferred types, add type narrowing:// Before — worked silently const val: string = record.metadata.foo; // After — requires narrowing const meta = record.metadata as Record<string, string>; const val = meta.foo;
hub/composer.zod.ts,hub/marketplace.zod.ts,hub/space.zod.ts,hub/hub-federation.zod.ts— removedhub/plugin-registry.zod.ts→kernel/plugin-registry.zod.tshub/license.zod.ts→system/license.zod.tshub/tenant.zod.ts→system/tenant.zod.ts
Barrel imports via
Hub.*namespace still work. Direct path imports need updating.MetricType(data analytics) →AggregationMetricTypeMetricType(hub licensing) →LicenseMetricType
HttpDispatcher→ usecreateDispatcherPlugin()insteadcreateHonoApp→ useHonoServerPlugininstead
- @objectstack/rest (new package): Extracted REST server, route management, and
- Updated dependencies
- @objectstack/spec@2.0.0
- @objectstack/core@2.0.0