Skip to content

Releases: objectstack-ai/objectstack

@objectstack/service-cluster@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Patch Changes

@objectstack/service-cluster-redis@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Patch Changes

@objectstack/service-cache@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Patch Changes

@objectstack/service-automation@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Minor Changes

  • 780b4b5: feat(automation): schema-aware flow-condition validation at registration (#1928)

    registerFlow now runs the same schema-aware condition checks as
    objectstack build — so a flow registered dynamically (via the API / Studio,
    bypassing the build lint) still gets the guardrail. When the host wires an
    object-schema resolver, a flow condition that references an unknown field,
    likely-typos a field name, or does arithmetic/ordering on a text/boolean field
    against a number is surfaced as an advisory warning (logged), pointing at
    the object's real schema.

    • New AutomationEngine.setObjectSchemaResolver(resolver) bridge (mirrors
      setFunctionResolver); AutomationServicePlugin wires it to
      objectql.registry.getObject in start(), before the flow pull, so
      registry-sourced flows are covered too.
    • Strictly additive / zero regression: the fatal set is unchanged (syntax,
      brace-in-CEL, unknown-function still throw); everything the schema pass adds is
      logged, never thrown, and the whole thing is a no-op when no resolver is wired.
      Flow conditions bind fields flat, so the check runs in flattened scope
      (flow variables stay dyn and are never flagged; equality is runtime-safe).

    Builds on the tier-4 type-soundness check in @objectstack/formula /
    @objectstack/lint (#1928).

  • 2ea08ee: Flow trigger observability — kill the four-layer silence around record-change flows that never fire (2026-07-17 third-party eval).

    A misauthored auto-launched flow (wrong objectName, missing requires: ['automation','triggers'], failing start condition) produced ZERO output at every layer: the engine's own registration/binding logs land inside the CLI's boot-quiet stdout window (which swallows debug/info/warn — only error/fatal reach stderr), and each "didn't happen" path was itself silent. Fixes:

    • Startup banner Flows: section (os serve/os dev/os start): flow count, bound-to-trigger count, registered trigger types, draft count — plus loud lines for flows declared with no automation engine enabled (requires missing), flows whose trigger type has no registered trigger, and bound record-change flows targeting an unknown object (dead binding). Printed after stdout is restored, so it is immune to the boot-quiet window.
    • Trigger-fired run failures now log at ERROR (stderr — always visible): the automation engine no longer drops the AutomationResult of a trigger-fired execution; condition-evaluation faults and node failures surface with the flow name. Condition-not-met skips stay at debug (high-frequency, intentional).
    • RecordChangeTrigger probes object existence at bind time and warns when a flow's objectName matches no registered object (exact-name matching), instead of silently arming a hook that can never fire.
    • kernel:bootstrapped binding audit in the automation plugin: warns per enabled-but-unbound triggered flow with the reason, and reports registered/bound/draft counts (AutomationEngine.getTriggerBindingAudit(), extended getFlowRuntimeStates() with status/triggerType/object).
    • os validate flow-wiring advisories (@objectstack/lint validateFlowTriggerReadiness): warns when a record-triggered flow targets an object the stack does not define, and when an auto-triggered flow's status is draft (authored or defaulted — draft flows still fire; declare active or obsolete).
    • Removed leftover boot-debug writes (registerApp/AppPlugin/StandaloneStack/AuditPlugin stderr noise) that previous debugging of this same silence had left behind.
  • 1e145eb: fix(automation): region-aware run-history compaction keeps loop containers + early failures (#3234)

    compactStepsForHistory bounded a terminal run's persisted step log to the last
    MAX_PERSISTED_HISTORY_STEPS entries with a plain tail-slice. With the ADR-0031
    structured-region step logs (#1505) a single loop can emit
    iterations × body-steps entries, so the tail-slice dropped the
    loop/parallel/try_catch container step (it precedes all its body steps)
    and every early iteration — leaving getRun/listRuns (after a process restart
    or ring-buffer eviction) with body steps the Runs surface could no longer nest,
    and silently hiding an early failure.

    Compaction is now region-aware (new exported compactStepLogForHistory): over
    budget it keeps the run's structural backbone — every top-level step (including
    the region container steps) and every failure, each pulled in with its ancestor
    container chain — plus the most recent body steps, order-preserving and
    hard-capped at max so steps_json stays bounded (#2585). Every retained body
    step keeps its enclosing container(s), so the compacted log never contains an
    orphan and the observability surface's per-iteration / per-region nesting still
    reconstructs.

  • a2795f6: feat(triggers): declarative time-relative trigger — daily sweep instead of fragile date-equality (#1874)

    Time-relative business rules ("alert 60 days before a contract's end_date")
    could only be expressed as a record_change flow gated on a date-equality
    condition like end_date == daysFromNow(60). That predicate is only evaluated
    when the record happens to change, so it fires only if a record is edited on
    exactly the threshold day — i.e. almost never, unattended. The robust
    alternative was a hand-written cron + range query that every author
    re-implemented (contracts renewal_alert, hr document_expiring_soon,
    procurement po_overdue, …).

    A flow's start node can now declare a timeRelative descriptor instead:

    config: {
      timeRelative: {
        object: 'contracts',
        dateField: 'end_date',
        offsetDays: [60, 30, 7],      // T-minus reminders — fires on each threshold day
        // — or — withinDays: 30      // "expiring soon" range; negative = overdue lookback
        filter: { status: 'active' }, // optional, ANDed with the date window
      },
      schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC
    }

    The new time_relative trigger (shipped in @objectstack/trigger-schedule as
    TimeRelativeTriggerPlugin) sweeps the object on that schedule and launches the
    flow once per matching record, with the record on the automation context —
    so the start-node condition gate and {record.<field>} interpolation work
    exactly as for a record-change flow. Because the window is evaluated every day,
    a threshold is never missed regardless of when the record last changed. The
    discovery query runs as a system operation (RLS-bypassing) and is capped
    (maxRecords, default 1000) so a mis-scoped window can't fan out unboundedly;
    per-record failures are isolated so one bad row never aborts the sweep.

    The automation engine routes a start node carrying config.timeRelative to the
    time_relative trigger (ahead of the plain schedule trigger, whose behavior is
    unchanged), and os validate gains readiness checks for the new descriptor
    (unknown swept object, ambiguous draft status). New authorable spec key:
    TimeRelativeTriggerSchema (@objectstack/spec/automation).

Patch Changes

  • 22013aa: Split the overloaded managedBy: 'system' bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). The system bucket 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 as better-auth/append-only but, unlike better-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 managedBy enum value (which would fall through to fully-editable platform defaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and engine-owned is defined as a system/append-only object 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 declare userActions: { create, edit, delete: true }. The affordance is a declaration only — the DelegatedAdminGate / RLS / permission sets remain the authz.
    • Engine-owned objects locked to readsapiMethods: ['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_secret is explicitly read-locked (an empty apiMethods array fails open).
    • sys_import_job stays engine-owned: the REST import route now writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp) and the object is locked to ['get','list'].
    • New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-owned system/append-only objects, keyed off the resolved affordance; isSystem and 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 just better-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049).
    • /me/permissions clamp (plugin-hono-server) now clamps system/append-only as well as better-auth, so the client hint reflects permission ∩ guard...
Read more

@objectstack/service-analytics@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Minor Changes

  • a9459e6: Analytics drill metadata now snapshots raw grouped values for totals/subtotal rows too (#3214). The ADR-0021 D2 drill sidecar (drillRawRows, #2080) only covered result.rows, but the totals rows added in #1753 carry dimension values and go through the same label resolution — which overwrote their stored value (select option value, lookup/master_detail FK id) with the display label, leaving a subtotal drill nothing to exact-match on.

    queryDataset now also emits drillRawTotals, aligned to result.totals by index (drillRawTotals[i][j]result.totals[i].rows[j]), captured in the same pre-label-resolution pass. Each map is restricted to the drillable dimensions the grouping actually groups by, so the grand-total grouping ([]) contributes an empty map per row. Purely additive result props (same as #2080) — no spec-contract change.

  • dd9f223: feat(analytics): scope a datetime date-bucket drill to the reference-tz midnight instants (#1752 follow-up)

    Closes the one gap left by the initial #1752 change: a datetime date dimension
    bucketed under a non-UTC reference timezone previously fell back to a superset
    drill (its bucket boundary is that tz's midnight instant, which YYYY-MM-DD
    calendar bounds can't express).

    • @objectstack/core adds zonedDateStartToUtcMs(ymd, tz) — the UTC instant
      at which a calendar day begins in a reference timezone (the inverse of
      calendarPartsInTz). DST-safe: the offset is read from the platform tz
      database via Intl, with a two-pass resolution for the rare offset-boundary
      case; an unset/'UTC'/invalid zone returns plain UTC midnight.
    • @objectstack/service-analytics now emits drillRanges bounds per the
      field's temporal type (ADR-0053): a datetime field → ISO instant bounds
      at the reference tz's midnight (works under any tz, incl. DST); a date field
      YYYY-MM-DD calendar bounds (tz-naive, exact under any tz). An unknown field
      type is still emitted only under UTC and omitted (superset) under a non-UTC tz.

    No objectui change is needed — the client already forwards whatever bound values
    the server sends into the drill filter and the filter[field][gte|lt] URL.

  • 290e2f0: feat(analytics): emit a half-open date-range drill scope for granularity-bucketed date dimensions (#1752)

    A report/dashboard cell grouped by a dateGranularity date dimension ("2026-Q2")
    covers a SPAN of records, so drilling it needs a range (>= start AND < nextStart),
    which the equality drill contract (drillRawRows) can't express — date dims were
    therefore excluded from drill metadata and a drill landed on an unscoped superset.

    • @objectstack/core adds bucketKeyToCalendarRange(key, granularity), the
      inverse of bucketDateValue: it turns a canonical bucket key into its half-open
      [start, end) calendar span (YYYY-MM-DD, end exclusive). Pure, timezone-naive
      calendar arithmetic; returns null for unbucketable / out-of-range keys so the
      caller falls back to an unscoped (superset) drill rather than emit a wrong bound.
    • @objectstack/service-analytics emits a drillRanges sidecar (aligned to
      rows by index — the range companion to drillRawRows) for date +
      dateGranularity dimensions, computed from the canonical bucket key in the
      pre-label-resolution snapshot pass. A datetime field under a non-UTC reference
      timezone is omitted (host drills a superset) until instant-boundary support
      lands; a tz-naive date field is exact under any timezone (ADR-0053).

    Consumed by objectui's report drill-through to scope the drilled record list to the
    clicked time bucket.

Patch Changes

@objectstack/sdui-parser@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:26
f55be04
@objectstack/sdui-parser@16.0.0

@objectstack/runtime@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:26
f55be04

Major Changes

  • 6c270a6: BREAKING: remove the deprecated ctx.session.tenantId / ctx.user.tenantId alias from the hook & action authoring surface — converge on organizationId (#3290).

    #3280 made organizationId the blessed developer-facing name for the caller's active org across the JS authoring surface and kept tenantId as a @deprecated alias carrying the identical value. That alias is now removed from the hook ctx.session, the action-body ctx.session, and the action-body ctx.user. Read the caller's active org under the single blessed name:

    - const org = ctx.session.tenantId;   // hook or action body
    + const org = ctx.user?.organizationId ?? ctx.session?.organizationId;

    FROM → TO migration (in any *.hook.ts / *.action.ts body):

    • ctx.session.tenantIdctx.session.organizationId
    • ctx.user.tenantId (action body) → ctx.user.organizationId

    The value is unchanged — organizationId is the same active-org id, matching the organization_id column and current_user.organizationId in RLS/sharing. ctx.user is undefined for system / unauthenticated writes, so read ctx.session?.organizationId when a hook or action must work regardless of a resolved user.

    What changed internally:

    • @objectstack/specHookContextSchema.session drops the tenantId field (only organizationId remains). A stray tenantId on a constructed session is now stripped by the schema.
    • @objectstack/objectql — the engine's buildSession() no longer emits session.tenantId; the audit-stamp plugin sources the tenant_id column from session.organizationId.
    • @objectstack/runtimebuildActionSession() and the REST action ctx.user no longer emit tenantId.
    • @objectstack/trigger-record-change — reads session.organizationId (was session.tenantId) when forwarding the writer's org to a runAs:'user' flow; behavior is identical.

    Explicit non-goal (unchanged): the generic driver-layer tenancy abstraction is not touched — ExecutionContext.tenantId, DriverOptions.tenantId, SqlDriver.applyTenantScope / TenancyConfig.tenantField, and ExecutionLog.tenantId. That isolation column is configurable and legitimately carries an environment id in database-per-tenant kernels; it is a distinct axis from the developer-facing org. The build-time check:org-identifier guard now also covers packages/** to keep reference bodies off the removed name.

Minor Changes

  • 2049b6a: feat(observability): admin-gated per-request Server-Timing via X-OS-Debug-Timing (#2408)

    Perf-tuning mode was previously global-only (serverTiming option /
    OS_SERVER_TIMING), which discloses internal phase durations — a mild
    backend-fingerprinting surface — to every caller. This adds the per-request
    gating path from the design so an operator can pull a single request's
    Server-Timing breakdown on a live environment without turning the header on
    for everyone.

    • observability: a request-scoped disclosure gate (runWithPerfDisclosure,
      allowPerfDisclosure, isPerfDisclosureAllowed, PerfDisclosureGate) kept
      separate from the pure PerfTiming collector and pinned to its own
      Symbol.for store so the middleware and dispatcher share it across module
      copies.
    • plugin-hono-server: the Server-Timing middleware is registered by default
      (unless serverTiming: false). It runs the collector when timing is global
      or the request sends X-OS-Debug-Timing: 1, and emits the header only
      when the gate is open. OS_PERF_TIMING=1 now also enables global mode.
    • runtime: after resolving the execution context, the dispatcher opens the
      gate for admin/service/system principals, so ordinary callers never receive
      the header even if they send the debug header.

    Existing global-mode behavior is unchanged.

  • 92f5f19: feat(runtime): sandbox budget is script CPU-time, not wall clock (ADR-0102 D1, #3295)

    The QuickJS sandbox now meters each hook/action invocation against how much
    VM-active (CPU) time the body burns, not wall clock. Idle host-await time and
    a nested hook's own execution (which runs host-side while the caller's VM is
    parked) are no longer charged to the caller — so a slow/loaded host or a deep
    nested-write chain can't trip the budget while a script is merely waiting (the
    root cause of the #3259 CI flake). A separate, generous wall-clock ceiling
    (default 30s, max(ceiling, cpuBudget)) remains as the backstop for a body stuck
    on a host call that never settles.

    What changes for consumers (behaviour, not API signatures):

    • Meaning of the timeout knobs. body.timeoutMs, the hookTimeoutMs /
      actionTimeoutMs runner options, and OS_SANDBOX_HOOK_TIMEOUT_MS /
      OS_SANDBOX_ACTION_TIMEOUT_MS keep their names, defaults (250ms / 5000ms),
      and precedence
      — but now bound CPU-time instead of wall-clock. In practice
      this only loosens legitimate slow/nested work; a runaway synchronous script
      is still cut at the same budget.
    • Error messages. exceeded timeout of Nms → either exceeded CPU budget of Nms (script burned its CPU budget) or exceeded wall-clock ceiling of Nms while awaiting host calls (stuck on a never-settling host call). Update any
      code/tests matching the old string.

    New knobs (additive):

    • QuickJSScriptRunner option wallCeilingMs and env OS_SANDBOX_WALL_CEILING_MS
      — tune the wall ceiling (explicit option › env › 30s).
    • resolveSandboxTimeoutMs (@objectstack/types) gains a 'wallCeiling' kind.

    Also fixes a latent init bug in the new accounting where the interrupt handler
    could fire during installCtx and corrupt ctx marshalling. The nested-write
    integration suites now run at the stock 250ms budget (previously forced to 10s),
    which is itself the regression guard for the nested-charging fix.

  • 32899e6: feat(runtime): env-overridable sandbox hook/action timeout default (#3259)

    The QuickJS sandbox enforces a wall-clock deadline on every hook/action
    invocation (250ms hooks / 5000ms actions). Each invocation compiles a fresh
    WASM module, and a nested hook compiles ANOTHER one inside the parent's budget,
    so on a heavily loaded or slow host — an oversubscribed CI runner, constrained
    production hardware — that fixed VM-creation cost alone can trip the hook
    default even while the VM is still making progress. On CI this surfaced as an
    intermittent hook '…' exceeded timeout of 250ms flake on PRs that never
    touched the sandbox path.

    The per-invocation timeout DEFAULT is now resolvable from the environment via
    resolveSandboxTimeoutMs (@objectstack/types), which QuickJSScriptRunner
    consults, so an operator can raise the floor once, deployment-wide, instead of
    re-tuning every call site:

    • OS_SANDBOX_HOOK_TIMEOUT_MS — default hook budget (ms)
    • OS_SANDBOX_ACTION_TIMEOUT_MS — default action budget (ms)

    Precedence is unchanged: an explicit hookTimeoutMs / actionTimeoutMs passed
    to the runner still wins over the env var, and a body's own declared timeoutMs
    still wins over the resolved default (the smaller of the explicit values). Only
    a positive integer is honored; unset / empty / non-numeric / non-positive keeps
    the built-in 250ms / 5000ms defaults, so behaviour is byte-for-byte unchanged
    when the vars are absent — production is unaffected unless it opts in.

    CI's Test Core now sets OS_SANDBOX_HOOK_TIMEOUT_MS=10000 so the shared-runner
    load flake can't recur; genuine hangs stay bounded by each test's own timeout.

Patch Changes

  • b39c65d: Extend the blessed organizationId org name to the action-body surface (follow-up to #3280). Hooks now teach ctx.user.organizationId / ctx.session.organizationId as the blessed name for the caller's active org; action bodies — the sibling authoring surface that shares the same sandbox runner — were left behind: the REST dispatch path exposed only ctx.user.tenantId (the deprecated name) and no ctx.session at all, and the MCP run_action path exposed neither.

    Both action-dispatch sites (handleActions, MCP runAction) now populate:

    • ctx.user.organizationId — the blessed name (matches the organization_id column and current_user.organizationId in RLS); ctx.user.tenantId is kept as a deprecated alias with the identical value on the REST path.
    • ctx.session ({ userId, organizationId, tenantId, roles? }) — mirrors the hook ctx.session shape, undefined for a context-less / self-invoked call.

    Action bodies execute trusted (the ctx.engine / ctx.api facade bypasses RLS/FLS), so a body that must scope by org has to read it from ctx — now under the same name a hook author uses. Additive and behavior-preserving; the objectstack-ui skill documents the action-body ctx and the organizationId read.

  • fdc244e: Dev-loop DX fixes from the 15.1 third-party evaluation (P2 batch):

    • Hot-added objects are now queryable without a restart. Adding a *.object.ts under os dev used to recompile "green" while every query answered no such table (or not registered) until a manual restart: the artifact reload never notified the ObjectQL registry, tables were only created at boot, and seeds only loaded from the boot-time bundle. The metadata:reloaded payload now carries the parsed artifact; ObjectQL ingests the object definitions and re-runs the idempotent schema sync (same skipSchemaSync opt-out as boot), and the runtime loads seeds for first-seen objects (dev, single-tenant). os dev also prints ✚ new object(s): … on recompile.
    • Dev admin credentials stay visible. The os dev startup banner only showed admin@objectos.ai / admin123 on the boot that actually seeded it; with the persistent default DB every later boot hid it, and the Console login page never knew it existed. The hint now re-arms on every de...
Read more

@objectstack/rest@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:26
f55be04

Minor Changes

  • bfa3c3f: Broadcast a transactionalBatch capability 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's ObjectStackAdapter) had to probe: fire a /batch, read 404/405 (no route) or 501 (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.

    WellKnownCapabilitiesSchema gains a required transactionalBatch: 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 /batch handler runs its ops inside engine.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 reports false even on a transaction-capable engine (never advertise an endpoint that would 404).
    • @objectstack/plugin-hono-server (standalone discovery) — reports false: 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 hierarchical capabilities to flat booleans, so client.capabilities.transactionalBatch is exposed (and now typed) for declarative consumers.

    The bit follows the existing capability semantics: true ⟺ the /batch route is mounted and the runtime can honour a transaction — the exact condition under which the endpoint returns 200 rather than 404/405/501. Additive and behavior-preserving; only the discovery payload gains a field.

Patch Changes

  • 22013aa: Split the overloaded managedBy: 'system' bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). The system bucket 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 as better-auth/append-only but, unlike better-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 managedBy enum value (which would fall through to fully-editable platform defaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and engine-owned is defined as a system/append-only object 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 declare userActions: { create, edit, delete: true }. The affordance is a declaration only — the DelegatedAdminGate / RLS / permission sets remain the authz.
    • Engine-owned objects locked to readsapiMethods: ['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_secret is explicitly read-locked (an empty apiMethods array fails open).
    • sys_import_job stays engine-owned: the REST import route now writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp) and the object is locked to ['get','list'].
    • New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-owned system/append-only objects, keyed off the resolved affordance; isSystem and 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 just better-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049).
    • /me/permissions clamp (plugin-hono-server) now clamps system/append-only as well as better-auth, so the client hint reflects permission ∩ guard.

    Potentially breaking: a downstream/third-party system object 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. Declare userActions opening the verbs the object legitimately takes from a user context. better-auth keeps plugin-auth's identity write guard unchanged; the row-level managed_by provenance vocabulary (ADR-0066) is a different axis and is untouched.

  • 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 bulkWrite validates that writeBatch returns 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 driver
      bulkCreate return instead of padding afterInsert with undefined.
    • #3150 wraps the two remaining un-retried write points (seed
      writeRecord/resolveDeferredUpdates, import's no-createManyData
      fallback) in withTransientRetry; defaultIsTransientError short-circuits
      definitive logical errors to non-transient.
    • #3148 import resolveRef flushes 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 attempt counter through bulkWrite; seed rechecks by
      externalId and import by matchFields before re-writing, so a
      commit-then-lost-response retry cannot duplicate a batch.
    • #3147 recomputeSummaries retries transient failures and, on exhaustion,
      surfaces SummaryRecomputeError (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).
  • 43a3efb: fix(rest): gate the cross-object transactional batch by the same per-object API rules as single-record writes (#1604)

    The POST {basePath}/batch route (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 an apiEnabled: false object, or run an
    operation outside an object's apiMethods whitelist, 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
      missing object is a 400 instead of a 500;
    • enforces enable.apiEnabled / enable.apiMethods for 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 id for update / delete (400);
    • rejects an unresolvable { $ref } with 400 BATCH_UNRESOLVED_REF instead of
      silently writing a null FK;
    • rejects an explicit atomic: false (400 BATCH_NOT_ATOMIC) rather than
      silently applying atomically — non-atomic per-object batches stay on
      POST /data/:object/batch.

    enforceApiAccess is refactored to share the pure apiAccessDenialFromEnable
    check + a loadObjectItems helper with the batch route (single-record behavior
    unchanged). Adds rest-batch-endpoint.test.ts — the REST-boundary coverage
    ADR-0034 flagged as missing (commit, $ref, rollback surfacing, API-access
    denial, request validation).

  • 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 ...

Read more

@objectstack/plugin-webhooks@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:27
f55be04

Patch Changes

  • 4b6fde8: Trim the dead undelete and api webhook triggers (#3196). WebhookTriggerType declared five triggers but only three ever fired:

    • undelete had no event source — the engine has no soft-delete/restore capability (delete is a hard delete; no deleted_at convention, no restore operation, and data.record.undeleted is never emitted). The undeleted case in the auto-enqueuer's action mapper was dead code awaiting a producer that doesn't exist.
    • api ("manually triggered") had no fire path — the only webhook HTTP surface re-queues already-failed deliveries; nothing originates a manual fire.

    Both are removed from the enum (contract-first, matching #3184/#3195): authoring a webhook on a removed trigger now fails loudly at os validate / registration instead of registering a webhook that silently never fires. No shipped webhook metadata used either. The auto-enqueuer now also warns when a persisted sys_webhook row carries a trigger it can't map to an emitted record event (a drift-guard, so a dead trigger can't silently no-op again). Reintroduce undelete only alongside a real restore subsystem, and api only alongside a real manual-fire endpoint. Updated the sys_webhook trigger options, field help (all locales), docs, and reference; added rejection tests.

  • 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 [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 [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 [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/service-messaging@16.0.0
    • @objectstack/core@16.0.0

@objectstack/plugin-sharing@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:26
f55be04

Patch Changes

  • 22013aa: Split the overloaded managedBy: 'system' bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). The system bucket 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 as better-auth/append-only but, unlike better-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 managedBy enum value (which would fall through to fully-editable platform defaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and engine-owned is defined as a system/append-only object 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 declare userActions: { create, edit, delete: true }. The affordance is a declaration only — the DelegatedAdminGate / RLS / permission sets remain the authz.
    • Engine-owned objects locked to readsapiMethods: ['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_secret is explicitly read-locked (an empty apiMethods array fails open).
    • sys_import_job stays engine-owned: the REST import route now writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp) and the object is locked to ['get','list'].
    • New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-owned system/append-only objects, keyed off the resolved affordance; isSystem and 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 just better-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049).
    • /me/permissions clamp (plugin-hono-server) now clamps system/append-only as well as better-auth, so the client hint reflects permission ∩ guard.

    Potentially breaking: a downstream/third-party system object 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. Declare userActions opening the verbs the object legitimately takes from a user context. better-auth keeps plugin-auth's identity write guard unchanged; the row-level managed_by provenance vocabulary (ADR-0066) is a different axis and is untouched.

  • 62a2117: Split the overloaded managedBy: 'system' bucket with an explicit engine-owned value (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a new managedBy value would fall through to the fully-editable platform default on deployed Console clients. Both reasons against it are now retired — the server-side write guard / apiMethods reconciliation / /me/permissions clamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it, additively.

    • New enum value engine-owned with the same all-locked default affordance row as system (create/import/edit/delete: false, exportCsv: true). It joins ENGINE_OWNED_BUCKETS (the engine write guard) and GUARDED_WRITE_BUCKETS (the /me/permissions clamp); the guard, reconcileManagedApiMethods, and the clamp mechanisms are unchanged — engine-owned is an explicit member of the set they already covered by resolved affordance.
    • 20 objects relabelled system → engine-owned — the ones the engine owns end to end and that declared no write-opening userActions (the metadata store, jobs, approval runtime rows, sharing rows, sys_automation_run, the messaging delivery/receipt pipeline, sys_secret, settings). One-line, behaviour-identical per object.
    • 8 admin/user-writable objects keep managedBy: 'system' (the RBAC link tables, sys_user_preference, sys_approval_delegation, the messaging config grids) — system now reads as "engine-managed schema, writable via userActions".

    Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405 apiMethods reconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedBy is schema metadata) and no code branches on the 'system' literal. Retiring the overloaded system entirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17.

  • 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 [6b51346]

  • Updated dependencies [80273c8]

  • Updated dependencies [fdc244e]

  • Updated dependencies [bfa3c3f]

  • Updated dependencies [5e3301d]

  • Updated dependencies [dd9f223]

  • Updated dependencies [46e876c]

  • Updated dependencies [2ea08ee]

  • Updated dependencies [7125007]

  • Updated dependencies [5f05de2]

  • Updated dependencies [021ba4c]

  • Updated dependencies [158aa14]

  • Updated dependencies [62a2117]

  • Updated dependencies [d2723e2]

  • Updated dependencies [674457a]

  • 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 [04ecd4e]

  • Updated dependencies [4d5a892]

  • Updated dependencies [16cebeb]

  • Updated dependencies [86d30af]

  • Updated dependencies [8923843]

  • Updated dependencies [ea32ec7]

  • 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/objectql@16.0.0
    • @objectstack/core@16.0.0
    • @objectstack/formula@16.0.0