Skip to content

Releases: objectstack-ai/objectstack

@objectstack/observability@16.0.0

Choose a tag to compare

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

Minor Changes

  • efbcfe1: feat(observability): admin-only richer per-request timing detail via X-OS-Debug-Timing: json (#2408)

    Completes the optional "richer JSON" diagnostic from #2408. In addition to the
    basic Server-Timing header, an admin/service caller can now request a
    per-query breakdown — the slowest SQL statements and a query count — by sending
    X-OS-Debug-Timing: json. The detail is returned in a separate
    X-OS-Debug-Timing-Detail response header (compact JSON) and is admin-only,
    even under global mode
    : an ordinary caller never sees SQL shapes.

    • observability: PerfTiming gains opt-in per-event detail capture
      (enableDetail / recordDetail / details) plus the ambient
      recordServerTimingDetail. The disclosure gate gains a privileged level
      (set by allowPerfDisclosure, read via isPerfDisclosurePrivileged) so the
      richer detail can be gated independently of the basic header.
    • driver-sql: when detail capture is on, the query listener additionally
      records each query's parametrized statement (knex's q.sql, ?
      placeholders) — never the bindings, so no literal row value ever enters the
      collector. Zero overhead when detail is off.
    • plugin-hono-server: X-OS-Debug-Timing: json enables detail capture; the
      middleware emits X-OS-Debug-Timing-Detail (slowest queries, capped and
      sanitized to header-safe ASCII) only when the principal is a proven admin.

    Basic and global behavior are unchanged; json is purely additive.

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

Patch Changes

  • ce468c8: feat(observability): decompose Server-Timing into auth / db / hooks / serialize spans (perf-tuning mode)

    The opt-in Server-Timing header now breaks a request's server time into the phases that actually explain it, so an operator can open DevTools → Network → Timing and see where the time went without standing up an external tracing backend:

    • db — total SQL time with a query count. The SQL driver wires knex's query / query-response events (keyed by __knexQueryUid) and folds each query into one aggregate member (db;dur=210;desc="6 queries") — the query count is the number most useful for spotting N sequential round-trips. Timing is attributed to the originating request via AsyncLocalStorage, so it is correct under concurrency and never cross-attributes. SQL text is never emitted, only durations and a count.
    • auth — identity / session resolution in the dispatcher, the prime suspect for unexplained data-API overhead.
    • hooks — total business-hook execution time with a hook count, fed through the engine's existing HookMetricsRecorder seam (wired from the runtime, so @objectstack/objectql's lean core tier stays observability-free).
    • serialize — response JSON encoding in the HTTP adapter.

    Adds countServerTiming(name, dur, unit) (and PerfTiming.count) to fold high-frequency phases into a single aggregate member instead of flooding the header. Every phase is a no-op when perf-tuning is off (serverTiming: true / OS_SERVER_TIMING=true), so there is zero measurable overhead on the normal path.

    Closes #2408.

  • Updated dependencies [f972574]

  • Updated dependencies [6289ec3]

  • Updated dependencies [22013aa]

  • Updated dependencies [3ad3dd5]

  • Updated dependencies [8efa395]

  • Updated dependencies [3a18b60]

  • Updated dependencies [a8aa34c]

  • Updated dependencies [a3823b2]

  • Updated dependencies [43a3efb]

  • Updated dependencies [524696a]

  • Updated dependencies [bfa3c3f]

  • Updated dependencies [5e3301d]

  • Updated dependencies [46e876c]

  • 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 [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/objectql@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:25
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

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

  • 04ecd4e: feat(validation): state_machine.initialStates enforces the FSM entry point on INSERT (#3165)

    A state_machine rule's transitions only governs UPDATE — on INSERT the rule
    was a no-op, and a select field permits ANY declared option as the initial
    value. So a record could be born mid-flow (created already approved), skipping
    the whole state machine. This was the gap #3043's mitigation idea assumed didn't
    exist (declared ≠ enforced, ADR-0049).

    state_machine rules gain an optional initialStates: string[] — the states a
    record may be CREATED in. When set, an insert whose (defaulted) state-field value
    is outside the list is rejected server-side with code: 'invalid_initial_state'.
    Omit it to keep the legacy behavior (no initial-state check on insert). A missing
    / empty value is left to required-validation; transitions (UPDATE) is
    unaffected. Enforced at the same evaluateValidationRules(..., 'insert') seam the
    engine already runs after field defaults.

  • 4d5a892: feat(objectql): roll-up summary fields can filter which child rows they aggregate (#1868)

    summaryOperations gains an optional filter — a query where FilterCondition
    evaluated against each child row, so a summary aggregates only the matching
    children instead of the whole collection. This is what lets a single child object
    feed several distinct parent totals, which the cross-object rollup templates need:

    // One `engagement` child → distinct filtered totals.
    total_signups: {
      type: 'summary',
      summaryOperations: { object: 'engagement', field: 'id', function: 'count', filter: { type: 'signup' } },
    }
    // Sum only received receipt lines (3-way match).
    received_amount: {
      type: 'summary',
      summaryOperations: { object: 'procurement_receipt', field: 'amount', function: 'sum', filter: { status: 'received' } },
    }

    The engine ANDs the predicate with the parent-FK match when it recomputes, and
    because the whole filtered aggregate is re-run on every child write, a child that
    moves in or out of the predicate (e.g. a status change) keeps the parent current
    with no extra wiring. Operator and compound forms work too
    (filter: { type: { $in: ['signup', 'trial'] }, amount: { $gte: 100 } }).

    Purely additive: omitting filter aggregates every child exactly as before.

Patch Changes

  • a8aa34c: Enforce validation rules, requiredWhen, and per-option visibleWhen on multi-row updates (#3106). The bulk branch of engine.update (options.multidriver.updateMany) previously never called evaluateValidationRules, so every object-level rule (script, state_machine, format, cross_field, json_schema, conditional), field-level requiredWhen, and per-option visibleWhen check was a silent no-op there. The engine now reads the row-scoped match set (the same AST the write binds, one query shared with the readonlyWhen bulk strip) and evaluates the payload against each matched row's prior state; any error-severity violation rejects the whole batch with ValidationError (annotated with the failing record id) before anything is written. Schemas needing no prior state (format/json_schema-only) are evaluated once against the payload with no fetch, and rule-free schemas are unaffected. Behavior change: bulk writes that previously slipped past declared rules now throw. Doc comments in rule-validator.ts and validation.zod.ts no longer overstate coverage and name the remaining events: ['delete'] gap (tracked separately).

  • 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-`createManyDa...
Read more

@objectstack/metadata@16.0.0

Choose a tag to compare

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

Patch Changes

  • 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 dev boot for as long as the account still verifies against the default password, and GET /api/v1/auth/config exposes a dev-gated devSeedAdmin field (never present outside NODE_ENV=development) so the login page can show it.
    • os doctor reference analysis understands current metadata shapes. Objects bound through defineView containers (list/listViews/form/formViewsdata.object, subform childObject, lookup form fields) and app navigation (objectName, nested children, areas) were reported as "defined but not referenced". The collector now walks the canonical shapes (plus flow node config.object/objectName) and the orphan-view check descends into containers.
  • d2723e2: MetadataManager.register() / unregister() now announce to subscribe() watchers. Both updated the registry, persisted to writable loaders and published to realtime, but never fired the watch callbacks — so subscribe() looked like it covered every write while silently missing all of them. Only the saveMetaItem path (via the repository watch stream) and the filesystem watcher ever reached a subscriber. Runtime consumers that cache metadata — notably ObjectQL's SchemaRegistry bridge, the component that decides what is queryable — went stale on every other write until the process restarted.

    Announcing is now the default, so a new call site is correct without knowing this contract exists. This is a contract fix rather than a bug fix: the one live behavior change is that runtime datasource writes (datasource-admin) now reach the HMR SSE stream, which subscribes to every registered type. unregisterPackage() / bulkUnregister() also announce their deletes now — correct, but latent, since neither has a production caller today.

    Bulk ingest opts out explicitly with the new MetadataWriteOptions ({ notify: false }) — boot-time filesystem priming, artifact ingest, and ObjectQL's registry bridge, each of which either runs before consumers cache anything or announces the whole batch once (as the artifact reload path does via metadata:reloaded). The bridge in particular MUST stay silent: it copies objects out of the SchemaRegistry, and announcing would feed them back through a handler that re-registers under _packageId ?? 'metadata-service', overwriting the true package provenance of every object whose body carries no _packageId.

    Additive only — register(type, name, data) and unregister(type, name) keep working unchanged.

    Fixes #3112.

  • 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 [06cb319]

  • 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/metadata-core@16.0.0
    • @objectstack/types@16.0.0
    • @objectstack/metadata-fs@16.0.0

@objectstack/metadata-protocol@16.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 06:25
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.

  • 668dd17: Breaking (npm type surface): retire the vestigial feed contracts + protocol surface (ADR-0052 §5 follow-up, #1959).

    The service-feed runtime was deleted in #1955; sys_comment / sys_activity
    are the canonical record-collaboration/timeline backend. This removes the dead
    type surface that still pointed at the deleted runtime — every removed method was
    already unreachable (the feed REST route was never mounted → 404; the protocol
    implementation was never wired with a feed service, so requireFeedService()
    could only throw). No behavior changes.

    No authorable metadata key is removed (the feeds: object capability flag and
    the RecordActivity UI component config are unchanged), so PROTOCOL_MAJOR
    stays 15 and this ships as minor rather than a protocol major.

    FROM → TO migration for every removed export:

    • @objectstack/spec/contractsIFeedService, CreateFeedItemInput,
      UpdateFeedItemInput, ListFeedOptions, FeedListResultremoved, no
      replacement
      . Comments/activity are plain records: write sys_comment / read
      sys_activity via the data engine or the REST data API.
    • @objectstack/spec/apiFeedApiContracts, FeedApiErrorCode,
      FeedProtocol, and all feed request/response schemas + types (GetFeed*,
      CreateFeedItem*, UpdateFeedItem*, DeleteFeedItem*, AddReaction*,
      RemoveReaction*, PinFeedItem*, UnpinFeedItem*, StarFeedItem*,
      UnstarFeedItem*, SearchFeed*, GetChangelog*, ChangelogEntry,
      SubscribeRequest/Response, FeedUnsubscribeRequest, UnsubscribeResponse,
      FeedPathParams, FeedItemPathParams, FeedListFilterType) → removed. Use
      the data API against sys_comment / sys_activity (/api/v1/data/sys_comment/…);
      reactions and threaded replies are fields on sys_comment.
    • @objectstack/spec/dataFeedItemSchema/FeedItem, FeedActorSchema/FeedActor,
      MentionSchema/Mention, ReactionSchema/Reaction,
      FieldChangeEntrySchema/FieldChangeEntry, FeedVisibility,
      RecordSubscriptionSchema/RecordSubscription, SubscriptionEventType, and the
      data-namespace NotificationChannelremoved. FeedItemType and
      FeedFilterMode are kept (live UI activity-timeline config). For notification
      channels use NotificationChannelSchema from @objectstack/spec/system.
    • @objectstack/clientclient.feed.* (list / create / update / delete /
      addReaction / removeReaction / pin / unpin / star / unstar / search /
      getChangelog / subscribe / unsubscribe) and the re-exported feed response
      types → removed. One-line fix: use client.data.* on sys_comment /
      sys_activity, e.g. client.data.create('sys_comment', { object, record_id, body })
      and client.data.find('sys_activity', { filters: [['record_id', '=', id]] }).
    • @objectstack/metadata-protocolObjectStackProtocolImplementation no longer
      implements the 14 feed methods; its constructor
      (engine, getServicesRegistry?, getFeedService?, environmentId?) becomes
      (engine, getServicesRegistry?, environmentId?). One-line fix: delete the third
      argument.

Patch Changes

  • 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).
  • 0e41302: fix(metadata-protocol): unscoped metadata list dedupes package-aware, not by bare name (ADR-0048 #1828)

    getMetaItems merged registry items, sys_metadata overlay rows, draft-preview
    rows, and MetadataService items into Maps keyed by bare name, so two installed
    packages shipping the same type/name (e.g. page/home) collapsed to one row
    (last-write-wins) on an unscoped GET /meta/:type whenever either package had an
    overlay — and the frontend prefer-local resolution, which reads that list, could
    no longer tell the two packages' rows apart.

    The three merge sites (plus the env/org pre-merge) now key by (package, name),
    mirroring getMetaItem's scoped-then-global-fallback resolution: colliding rows
    stay distinct each with its own _packageId, a package-less (env-wide) overlay
    still wins over the single artifact it customizes (ADR-0005 precedence and
    single-package behaviour unchanged), and the registry-hydration artifact graft is
    scoped to each row's own package_id so a collision no longer mislabels provenance.

  • b8a21ad: Publish/discard package drafts in the draft's own org scope, fixing no_draft after saving a draft via Studio.

    Studio "Save Draft" (PUT /meta/:type/:name?mode=draft) never threads the session's activeOrganizationId, so the draft row is written env-wide (organization_id = NULL). "Publish" (POST /packages/:id/publish-drafts) resolves the active org and passed it to promoteDraft, which looked the draft up with a strict organization_id = <org> equality — so it 404'd ([no_draft] No pending draft exists …) on the env-wide row it could never match, even though listDrafts had already surfaced that draft to the publish CTA (PR #1852's $or). discardPackageDrafts had the same latent gap.

    listDrafts now projects each draft's own organizationId, and publishPackageDrafts / discardPackageDrafts promote / delete each draft in that scope (env-wide stays env-wide, per-org stays per-org). Seed-body capture and the ADR-0067 revert-plan pre-state read are scoped the same way.

    Fixes #3115.

  • beaf2de: fix(metadata-protocol): strip static readonly on INSERT at the data-write ingress (#3043)

    #2948/#3003 made static readonly: true fields server-enforced on UPDATE (a
    non-system PATCH forging approval_status: 'approved' is silently stripped in
    the engine), but INSERT was exempt. For approval/status/verdict columns that
    exemption was the shorter attack: instead of the #3003 draft-then-PATCH move, a
    non-system caller could POST a record already approval_status: 'approved' in
    one step — and the UPDATE-only strip never reached it.

    The...

Read more

@objectstack/metadata-fs@16.0.0

Choose a tag to compare

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

Patch Changes

  • Updated dependencies [62a2117]
  • Updated dependencies [06cb319]
    • @objectstack/metadata-core@16.0.0

@objectstack/metadata-core@16.0.0

Choose a tag to compare

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

Patch Changes

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

  • 06cb319: fix(identity): close the generic-write apiMethods hole on sys_presence and sys_metadata (#3220)

    Follow-through on #1591/#3213 (better-auth apiMethods reconciliation) for two
    non-better-auth managed objects that shipped the same contradiction: their
    enable.apiMethods advertised generic create/update/delete while their
    managedBy bucket forbids user-context writes, leaving the generic /data
    route open to a write the bucket does not permit.

    • sys_presence (managedBy: 'append-only') advertised create/update/delete
      (update/delete on an append-only object at that) but is written only over the
      realtime websocket/in-memory path, never through ObjectQL. Narrowed to
      ['get', 'list'].
    • sys_metadata (managedBy: 'system') advertised full CRUD but customization
      overlays are authored only through the metadata-protocol RPC (engine writes
      carry a transaction context, not a user session); neither the framework nor
      the Console (objectui) POSTs /data/sys_metadata. Narrowed to ['get', 'list'].

    Reads stay open. The metadata-protocol / realtime write paths are engine-level
    and bypass the HTTP exposure gate, so they are unaffected — verified by the
    metadata-authoring dogfood and the objectql overlay tests.

    A blast-radius audit confirmed the broader system/append-only buckets are NOT
    safe to guard wholesale: several system objects (sys_user_position,
    sys_user_permission_set, sys_position_permission_set, sys_user_preference,
    sys_import_job) are legitimately user-writable by design (delegated
    administration, user preferences, imports). Generalizing the engine write guard
    to those buckets is intentionally NOT done here — see #3220 for the bucket-taxonomy
    root cause.

  • Updated dependencies [f972574]

  • Updated dependencies [6289ec3]

  • Updated dependencies [22013aa]

  • Updated dependencies [3ad3dd5]

  • Updated dependencies [8efa395]

  • Updated dependencies [3a18b60]

  • Updated dependencies [a8aa34c]

  • Updated dependencies [a3823b2]

  • Updated dependencies [43a3efb]

  • Updated dependencies [524696a]

  • Updated dependencies [bfa3c3f]

  • Updated dependencies [5e3301d]

  • Updated dependencies [46e876c]

  • 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 [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/mcp@16.0.0

Choose a tag to compare

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

Minor Changes

  • 15dbe18: feat(mcp)!: stdio transport requires an API-key principal — fail-closed, no unscoped bridge (ADR-0101, #3246)

    The long-lived MCP stdio transport no longer reads data unscoped. It now runs
    under an env-supplied identity, closing the platform's last identity-less
    execution surface (the mcp-stdio-authority conformance row graduates
    experimentalenforced).

    • OS_MCP_STDIO_API_KEY=osk_... supplies the stdio identity, resolved through
      the SAME @objectstack/core verify + authorization chain as the HTTP/REST
      surfaces; the record_by_id resource reads via ql.find(obj, { where:{id}, context }), so RLS/FLS/tenant apply exactly as on REST /data. Re-resolved
      per read, so a revoked/expired key stops working on a live session.
    • Fail-closed — enabling stdio auto-start (OS_MCP_STDIO_ENABLED=true /
      autoStart) without a resolvable key throws and refuses to start. There is no
      unscoped fallback and deliberately no system bypass; full authority is a key
      minted on a platform-admin or dedicated service identity.

    BREAKING (stdio auto-start only): previously OS_MCP_STDIO_ENABLED=true
    (or the plugin autoStart option) started stdio with full, unscoped authority
    and no credential. It now requires OS_MCP_STDIO_API_KEY; without it, boot
    fails closed. The default-on HTTP surface and any deployment that never enables
    stdio auto-start are unaffected.

  • 83e8f7d: feat(mcp): decouple the stdio auto-start switch from the HTTP surface + surface the MCP endpoint on os dev boot (#3167)

    The MCP HTTP surface (/api/v1/mcp) and the long-lived stdio transport used to
    share one env var: OS_MCP_SERVER_ENABLED=true turned the HTTP surface on and
    silently auto-started the stdio transport — which bridges the raw metadata service

    • data engine with no per-request principal (unscoped). An operator setting it to
      "make sure MCP is on" got an unscoped transport as a side effect.
    • @objectstack/types — new resolveMcpStdioAutoStart(). Stdio auto-start is
      now its own switch, OS_MCP_STDIO_ENABLED (default off); OS_MCP_SERVER_ENABLED
      governs only the HTTP surface. The legacy OS_MCP_SERVER_ENABLED=true trigger
      still starts stdio for one release, flagged as deprecated. =false is unchanged
      (it only ever gated HTTP).
    • @objectstack/mcpMCPServerPlugin.start() gates stdio on the new switch
      and logs a one-time deprecation warning when started via the legacy alias.
    • @objectstack/clios dev now prints the MCP endpoint, the agent-skill
      URL, and a ready-to-paste claude mcp add command on boot (gated on the HTTP
      surface being on), so the "an agent operates the app it's building" loop is
      discoverable at dev time.
    • create-objectstack — the blank scaffold README documents that the app is
      itself an MCP server (the serve side), distinct from the consume-side connector.
  • 230358c: feat(mcp): validate_expression tool — validate a CEL expression against a schema before authoring (#1928)

    Adds an agent-callable MCP tool that runs the same build-time expression checks
    as objectstack build, so an AI can validate a formula / predicate / flow
    condition while authoring instead of shipping one that silently evaluates to
    null. Given { objectName, expression, site? } it resolves the object's real
    schema (field names + types, via the principal-bound describeObject bridge)
    and returns:

    • errors — bare field refs (amountrecord.amount), unknown fields
      (with a did-you-mean), unknown functions;
    • warnings — text/boolean fields misused in arithmetic, date-equality
      pitfalls;
    • inScope — the fields, stdlib functions, and namespace roots available, so
      the model can self-correct;
    • inferredType for a formula site.

    site (formula | validation | flow_condition | template, default
    formula) maps to the validator's role + scope — flow_condition binds fields
    bare, the rest bind record.<field>. Read-only, gated by the data:read OAuth
    scope, and fail-closed on sys_* objects like the other schema tools. This is
    the authoring-time surface the guardrail series (#1928) always pointed at;
    @objectstack/mcp gains a @objectstack/formula dependency (acyclic; formula is
    a leaf).

Patch Changes

@objectstack/lint@16.0.0

Choose a tag to compare

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

Minor Changes

  • 3a18b60: feat(approvals): rename the role approver type to org_membership_level (#3133)

    ApproverType.role was the last platform surface projecting the reserved word
    "role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
    exception protects better-auth's own sys_member.role column, which we do
    not own — ApproverType is our own enum, an authoring surface, and D3 mandates
    that the projection of that concept is spelled org_membership_level and
    labelled "organization membership", never "role".

    The sentence licensing the leak was also false: ADR-0090 D3 claims
    sys_member.role is "already relabelled org_membership_level in the platform
    projection", but org_membership_level existed nowhere in the codebase and
    ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
    projection never landed, so the word reached authors.

    The name manufactured a real, silent failure — "hotcrm class": every other
    surface renamed to position (sys_role, ShareRecipientType.role,
    ctx.roles[]), so { type: 'role', value: 'sales_manager' } reads as the
    legacy spelling of a position. It resolves against the membership tier, finds
    no member row, falls back to an inert role:sales_manager literal, and the
    request waits forever on an approver that cannot exist.

    • spec: ApproverType gains org_membership_level; role stays as a
      deprecated alias for one window (a published 15.x flow keeps loading) with
      DEPRECATED_APPROVER_TYPES + canonicalApproverType() as the single source
      for the mapping. Removed in the next major.
    • plugin-approvals: resolves on the canonical type and warns on the
      deprecated spelling. The type:value fallback literal keeps the authored
      spelling — stored sys_approval_approver rows and pending_approvers slots
      from 15.x carry role:<v>, and rewriting it would orphan them.
    • lint: approval-role-not-membership-tierapproval-approver-not-membership-tier
      (the rule id carried the reserved word too), plus a new
      approval-approver-type-deprecated. The two are mutually exclusive: a bad
      value wins, because prescribing org_membership_level for a position name
      would be wrong advice — the fix there is position.

    Authoring type: 'role' keeps working and now says so out loud. Rewrite it as
    org_membership_level; if the value is an org position, the fix is position.

  • 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.
  • ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)

    Closes the last open guardrail from #1928. A Field.formula or record-scoped
    predicate that uses a text or boolean field with an arithmetic (+ - * / %)
    or ordering (< > <= >=) operator against a number
    faults the runtime
    overload and silently evaluates to null (e.g. record.title * 2,
    record.is_active + 1). The build now surfaces this as a non-blocking
    warning
    with the offending field and a corrective message.

    Honours the ADR-0032 design law — the checker only flags what the runtime
    would also fail:

    • Number / currency / percent / date / datetime fields are declared dyn, so
      the cases the runtime rescues never warn — record.amount / 100 (the #1930
      registerOperator fix), record.due == today() and numeric-string / ISO-date
      values (the string-hydration retry), and numeric-coded select option values.
    • Equality (== / !=) is excluded: a heterogeneous equality is runtime-safe
      (evaluates to false), never a fault.

    New firstTypeMismatch(source, fieldCelTypes, scope) export in
    @objectstack/formula (and an optional fieldTypes hint on
    validateExpression); @objectstack/lint's validateStackExpressions threads
    each object's field types into every checked site:

    • record-scoped sites (record.<field>) — formula fields, validation rules,
      action / hook / sharing predicates;
    • flattened flow / automation conditions (bare field) — where flow
      variables stay dyn and are never flagged, and equality stays runtime-safe.

    Warnings are advisory in objectstack build / validate (fatal only under
    --strict), matching the tier-3 channel.

  • 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

  • 524696a: feat(spec)!: DashboardWidgetSchema.strict() — reject undeclared widget keys (framework#3251)

    The ADR-0021 analytics endpoint. DashboardWidgetSchema now rejects any
    undeclared top-level key instead of silently stripping it, moving a whole class
    of author error (a hallucinated or legacy key that renders as a silent no-op)
    from fallible human review to deterministic CI. options: z.unknown() remains
    the escape hatch for renderer-specific extras.

    A custom error map names the offending key(s) and, when a key is a removed
    pre-ADR-0021 inline-analytics key (object / categoryField / valueField /
    aggregate, pivot rowField / columnField) or an objectui-internal prop
    (component, inline data), points the author at the dataset shape
    (dataset + dimensions + values).

    Recorded as protocol-16 migration step16
    (dashboard-widget-strict-unknown-keys), mirroring protocol-15's step15
    strict flip on the form/page schemas (ADR-0089 D3a). The inline-analytics shape
    itself was already removed at protocol 9 (single-form cutover), so there is no
    ...

Read more

@objectstack/knowledge-ragflow@16.0.0

Choose a tag to compare

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

Patch Changes

@objectstack/knowledge-memory@16.0.0

Choose a tag to compare

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

Patch Changes