Skip to content

@objectstack/plugin-hono-server@16.0.0

Choose a tag to compare

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

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.

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

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

  • 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 [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 [83e8f7d]

  • Updated dependencies [d2723e2]

  • Updated dependencies [fefcd54]

  • Updated dependencies [efbcfe1]

  • Updated dependencies [2049b6a]

  • Updated dependencies [beaf2de]

  • Updated dependencies [369eb6e]

  • Updated dependencies [06ff734]

  • Updated dependencies [b659111]

  • Updated dependencies [5754a23]

  • Updated dependencies [6c270a6]

  • Updated dependencies [290e2f0]

  • Updated dependencies [668dd17]

  • Updated dependencies [8abf133]

  • Updated dependencies [e0859b1]

  • Updated dependencies [92f5f19]

  • Updated dependencies [32899e6]

  • Updated dependencies [ce468c8]

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