diff --git a/.changeset/pre.json b/.changeset/pre.json index fd630823fb..5067272a82 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -82,10 +82,17 @@ }, "changesets": [ "action-alias-conflict-warning", + "action-body-execution-context", + "action-crash-vs-rejection", "action-execute-target-precedence", + "action-governance-engine-owned", "action-param-inline-lookup-reference", "action-param-strict-unknown-keys", + "action-required-permissions-server-scope-docs", "action-undoable-liveness-corrected", + "actions-empty-object-segment-route", + "actions-failures-speak-http", + "actions-global-key-and-failure-status", "adr-0044-revise-service-owned-note", "adr-0104-d1-media-strict-per-deployment", "adr-0104-d1-value-shape-contract", @@ -106,9 +113,26 @@ "adr-0105-group-posture-entitlement", "adr-0105-group-tenancy-phase-0-1", "adr-0105-group-tenancy-posture", + "adr-0105-status-phase2-d8-d9", "adr-0106-metadata-fls-masking", + "adr-0110-action-declaration-admission", + "adr-0110-draft-only", + "adr-0111-depth-share-management", + "adr-0111-draft-only", + "adr-0111-share-link-reshare", + "adr-0111-sharing-authorization-face", + "adr-0111-verb-boundary-delete", + "adr-0112-batch-1-screaming-codes", + "adr-0112-batch-2-lowercase-sweep", + "adr-0112-error-code-vocabulary", + "adr-0113-required-write-contract", + "adr-0114-field-error-catalog", + "adr-0114-field-errors-rename", + "adr-0115-plugin-dev-stub-table-verdict", "agent-knowledge-alias-and-experimental-markers", "agents-pd12-alias-retirement-path", + "agents-pd12-shim-retired", + "agents-spec-generated-artifacts-map", "aggregate-temporal-output", "ai-agent-authoring-and-tools-removal", "ai-agents-pending-actions-sdk", @@ -116,6 +140,7 @@ "ai-surface-affinity-lint", "ai-tool-registry-and-lint", "ai-wildcard-to-zero", + "analytics-capability-conditional-mounting", "analytics-client-dispatcher-alignment", "analytics-cube-gate-and-error-leak", "analytics-effective-granularity", @@ -123,7 +148,9 @@ "analytics-label-read-scope", "analytics-objectql-read-scope", "analytics-order-by-display-label", + "analytics-query-bare-shape-entry-validation", "analytics-read-scope-bridge-order", + "analytics-timedimension-projection", "analytics-widget-query-options", "api-exposure-failopen-observability", "api-methods-derivation-contract", @@ -148,6 +175,7 @@ "approver-value-sources-and-dead-slot-warning", "array-form-triggertype-not-silent", "attachment-read-visibility-real-filter-semantics", + "auth-catchall-yields-unowned-paths", "auth-route-ledger", "auth-validationerror-4xx-mapping", "authorable-surface-ratchet", @@ -159,6 +187,12 @@ "batch-dropped-fields-observability", "better-auth-1-7-0-rc-2-and-prod-dep-batch", "better-auth-team-member-count", + "boot-api-merge", + "bulk-batch-size-cap", + "bulk-writes-bind-to-path-object", + "calendar-day-primitive-to-spec", + "calendar-day-upper-bound-memory-mongodb", + "calendar-day-upper-bound", "chartconfig-trim-zoom-clickaction", "ci-cache-tier1-optimizations", "ci-node-22-pin", @@ -167,11 +201,14 @@ "ci-test-completeness-guard", "cli-json-pipe-truncation-sweep", "client-actions-surface", + "client-error-envelope-normalisation", "client-keys-sharelinks-security", "client-meta-automation-descriptors", "client-packages-lifecycle", + "client-retires-parking-spot-read", "client-url-conformance-capstone", "close-approvals-and-record-shares-gaps", + "close-out-sweep-inert-keys", "close-sharing-rules-explain-search-gaps", "close-the-eight-reports-rest-gaps", "close-the-final-nine-rest-gaps", @@ -180,32 +217,60 @@ "console-1bb77aa24514", "console-2cb8d78e24ad", "console-4a4829d0ef39", + "console-96ee72e85439", + "console-a136322f8723", + "console-c6cfdf1288b6-backfill", + "console-e651c936870e", + "control-flow-form-zod-ledger", "control-plane-guard-crossref", "conversion-notice-channel", + "criteria-json-declaratively-required", + "cross-object-batch-501-code", "data-path-object-existence-gate", + "data-query-path-object", "datasource-availability-observability", "datasource-bound-connect-failfast", + "datasource-teardown-ownership", "date-bucket-parity-gate", + "date-now-default-utc", + "datetime-canonical-utc-storage", + "datetime-storage-form-memory-mongodb", "decision-outputs-surface-3447", + "declared-unique-index-not-legacy", + "default-datasource-adopt-seam", "default-datasource-declared", + "degraded-boot-stderr-premise", "delegable-scope-read-surface", + "delete-many-id-predicate", "department-approver-env-wide-business-unit", "deprecate-kernel-assignment-notifications", "deprecated-alias-conflict-rules", + "discovery-metadata-slot-computed", + "dispatcher-envelope-shared-predicate", + "dispatcher-error-code-is-semantic", + "dispatcher-handler-ready-gate", "dispatcher-returned-error-leak", + "dispatcher-validation-error-fields", "docs-audience-first-ia", + "docs-drift-skip-test-files", "docs-fieldschema-extend-rot", "dogfood-gate-cancelled-not-failure", "dogfood-shared-boot", + "domain-error-passthrough", "driver-connect-bound-and-reconnect-correction", "driver-sql-logicalop-retention-note", "driver-sql-or-branch-and-semantics", "drop-dead-env-template-flag", "drop-dead-list-templates", + "drop-require-auth", + "drop-undeclared-actions-valve", "dropped-fields-bulk-graphql-client", "empty-group-bucket-key-null", + "empty-state-gate-object-surface", + "empty-state-semantics-gate", "enforce-user-level-export-axis", "engines-node-22", + "envelope-violations-predicate", "export-axis-opt-in", "export-empty-result-header", "expression-approvers-3447-p2", @@ -213,15 +278,21 @@ "fault-edge-label-lint", "field-conditional-required-fold", "field-readonly-doc-preserveaudit", + "field-time-canonical-storage", "file-access-delegate", "filter-context-tokens-gate", "filter-logic-conformance-single-source", + "filter-no-silent-drop", "filter-tokens-runtime-resolver", + "find-data-wire-context", "fix-stale-scaffolder-changeset-refs", "fix-unmounted-local-file-url", + "flow-action-record-id-seeding", "flow-error-object-serialization", "flow-filter-collapse-and-write-path-tokens", "flow-lookup-expand", + "flow-node-config-alias-graduation", + "flow-node-expression-ledger", "flow-template-filter-position-severity", "flow-template-lint-and-hydrate-guards", "flow-template-paths-into-reference-integrity-suite", @@ -234,7 +305,11 @@ "group-union-driver-scope", "guard-refusal-chokepoint", "historical-import-audit-docs", + "honest-service-self-description", + "hono-current-user-endpoints-ungated", + "hono-standalone-discovery-computed", "i18n-bundle-drift-sweep", + "i18n-consolidate-success-builder", "i18n-coverage-ratchet", "i18n-extract-check-flag", "i18n-field-labels-emit-declared-shape", @@ -251,6 +326,7 @@ "import-sanitize-row-errors", "import-undo-preserveaudit", "index-drift-migrate-plan", + "inert-rule-warn-dedupe", "invitation-accepted-host-seam", "isLikelyEmail-no-control-char", "job-retry-timeout-3494", @@ -258,21 +334,32 @@ "lint-flag-record-change-trap", "lint-reference-integrity-suite", "lint-translation-reference-integrity", + "lint-unique-double-declaration", "list-column-prefix-summary-object", "liveness-evidence-path-resolution", "liveness-ledger-ai-scope-honesty", "liveness-register-orphan-proofs", "liveness-ten-preview-claims", "liveness-verified-at-clock", + "localized-field-validation-messages", "manifest-bridge-arm-on-project-kernels", "marketplace-objects-bridge-metadata-service", "marketplace-rehydrate-seed-heal", + "mcp-discovery-service-aware", "membership-grade-not-capability-channel", + "memory-datasource-ephemeral-per-pool", + "meta-type-gate-plural", "metadata-unresolvable-posture-fail-closed", + "migrate-occupancy-and-deferred-ddl", + "migrate-occupancy-file-descriptor-signal", + "migrate-plan-lists-datetime-convergence", + "migrate-search-companion-parity", + "modal-actions-are-client-only", "mongodb-single-tenant-boot-guard", "naming-drift-recheck", "nav-access-lint", "notifications-redos-fix", + "notify-source-shape-conversion", "objectchart-aggregate-result-columns", "objectchart-contract-back-to-spec-shape", "objectql-crossobj-capability", @@ -280,11 +367,14 @@ "objectql-driver-connect-failfast", "objectql-strategy-daterange", "osv-batch-2026-07-dep-bumps", + "packages-envelope-suite-comment", "page-field-and-chart-binding-lint", "page-header-i18n-3589", + "pin-control-flow-designer-forms", "platform-objects-app-i18n-phantom-debt", "plugin-page-i18n-drift-guard", "preserveaudit-test-and-docs", + "preview-omits-virtual-fields", "previous-null-on-create-leg", "prose-example-gate-covers-docs", "prune-aspirational-config-3494", @@ -296,6 +386,7 @@ "prune-report-aria-performance", "prune-report-column-grouping-schemas", "prune-skill-permissions", + "public-book-grant", "purge-webhook-delivery-i18n-and-bundle-ownership-guards", "rbac-objects-bulk-primitive", "readme-fde-audience", @@ -313,15 +404,29 @@ "remove-dead-sdk-surface", "remove-enable-trash-mru", "remove-graphql-surface", + "remove-unenforced-plugin-loading-config", "report-chart-dataset-describe", + "report-order-liveness-live", + "report-ordering-and-time-axis-default", + "required-decision-outputs", + "rest-actions-type-dispatch", "rest-env-resolution-kernel-resolver-seam", "rest-patch-data-dropped-fields", "rest-route-ledger-audit-guard", "resume-gate-map-chain-and-reserved-vars", "resume-signal-chokepoint", + "retire-batch-validate-only", "retire-default-dispatcher-routes", + "retire-degraded-analytics-shim", + "retire-dev-analytics-stub", + "retire-dispatcher-storage-bridge", "retire-three-deprecated-aliases", + "retire-three-orphan-operator-vocabularies", + "rls-enabled-enforced-security-audit", + "rls-priority-removed", "route-audit-tranche-3-service-mounts", + "route-envelope-four-more-modules", + "route-envelope-guard-dispatcher-domains", "route-ledger-audit-guard", "runtime-action-execution-module", "runtime-actions-mcp-extraction", @@ -333,6 +438,8 @@ "runtime-meta-data-extraction", "runtime-packages-extraction", "runtime-share-links-extraction", + "sandbox-structured-error-passthrough", + "savemeta-persists-normalized-operators", "scim-provider-key-and-sso-scim-parity", "scoped-invitation-placement", "screen-field-visible-when-on-the-wire", @@ -342,45 +449,72 @@ "seed-datasets-multitenant-replay-union", "seed-insert-replay-lint", "seed-loader-composite-external-id", + "seed-loader-dropped-reference-counter", "seed-loader-engine-schema-fallback", + "seed-loader-multi-value-lookup", "seed-state-machine-lint", "seed-summary-banner", "seed-summary-marketplace", "seed-writes-exempt-state-machine", + "serve-boot-log-visibility", + "serve-boots-without-artifact", "serve-fallback-declared-default", "service-error-envelope-conformance", "service-storage-success-envelope", + "share-link-routes-envelope", + "share-links-dispatcher-dual-key", "sharing-access-level-full-removed", + "sharing-rule-criteria-required", "sharing-rule-recipient-reconcile", "sharing-rule-unknown-sort-and-stale-help", "showcase-action-disabled-specimen", + "showcase-approver-and-picker-specimens", + "showcase-global-action-specimen", + "showcase-legacy-rowactions-specimen", "showcase-nav-affordance-specimen", + "spec-changes-manifest-catchup", + "spec-property-retirement-skill", "sql-driver-dialect-connect-timeout", "sqlite-datetime-date-bucket", + "sqlite-wal-journal-mode", "startup-log-noise-cleanup", "step2-metadata-protocol-plugin", "step2-prc-single-source", "storage-download-filename", "sys-view-definition-default-open", + "temporal-conformance-matrix", + "temporal-docs-accuracy", + "temporal-dogfood-gate", + "temporal-hooks-on-contract", "tombstone-agent-tools", + "tool-inert-keys-removed", "tool-requires-confirmation-not-enforced", "tool-requires-confirmation-removed", "two-factor-lockout-and-object-translations", "two-factor-lockout-extension", "two-factor-lockout-follows-settings", "typed-decision-outputs-3447", + "ui-vocabularies-derive-not-restate", "unique-tenant-scoped-materialization", + "unknown-key-strictness-step2", + "unknown-key-strictness-step3", + "unknown-key-strictness-tier-a", + "unknown-node-config-key-warning", "update-record-dropped-field-warnings", "url-field-accepts-relative-urls", "user-less-run-data-ops-refused", "user-level-export-axis", "v17-dissolve-protocol-alias", + "v17-page-console-gap-and-nav", "v17-rc-anchor", + "v17-release-page", "validate-runs-build-authoring-lints", "verify-multitenant-requests-isolated-posture", + "view-ast-operator-parity", "webhook-authoring-surface-bridge", "webhook-liveness-ledger-flip", "webhooks-drop-dead-delivery-i18n", + "wildcard-fallthrough-guard", "withdraw-adr-0107-drop-writes-proposal" ] } diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 056d4f619d..f675650f0c 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/example-crm +## 4.0.92-rc.1 + +### Patch Changes + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c8124e5] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + ## 4.0.92-rc.0 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index 64d06256b9..ced777f267 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.92-rc.0", + "version": "4.0.92-rc.1", "description": "Minimal CRM example — a smoke-test workspace that exercises the metadata loading pipeline (objects → views → app → dashboard → hook → flow → seed). For a full-featured enterprise CRM see https://github.com/objectstack-ai/hotcrm.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index e69653ef5a..1aeca54f53 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/example-showcase +## 0.3.14-rc.1 + +### Patch Changes + +- d6bfb3d: refactor(spec)!: remove the RLS-policy `priority` key — it promised conflict resolution that cannot exist (#3896 audit) + + `RowLevelSecurityPolicySchema.priority` was documented as _"Policy priority for + conflict resolution"_. The 2026-07-30 security-subset liveness re-verification + found that **nothing ever read it** — and, stronger, that nothing ever could: + applicable policies **OR-combine** (any match allows access, most permissive + wins — the schema's own describe said so), so there is never a conflict to + order and evaluation order cannot change an outcome. A semantically-void knob + on a security policy is worse than dead: an author — very often an AI + (ADR-0033) — reads it as a precedence lever and reasons about policy + interactions that do not exist. + + Removed per the `tool.requiresConfirmation` (#3715) / `DynamicLoadingConfig` + (#3950) precedent, inside the v17 breaking window: + + - **Tombstoned, not silently stripped** (`retiredKey`, #3855 pattern): an + authored `priority` fails `tsc` (the input type is `never`) and rejects at + parse with the prescription itself — _"policies OR-combine (most permissive + wins), so there is no conflict to order. Delete the key — policy outcomes are + unchanged."_ + - **ADR-0087 D2 conversion + D3 chain step** (`permission-rls-priority-removed`): + `os migrate meta` deletes the key from authored sources mechanically — a pure + lossless delete, no semantic residue. spec-changes.json and the protocol + upgrade guide carry the entry. + - The policy factory helpers (`ownerPolicy`, `tenantPolicy`, …), the showcase + example's permission sets, and `content/docs/permissions/rls.mdx` no longer + author it; the docs table's `enabled` row now states the (since-enforced) + contract instead. + - Liveness ledger entry updated to record the removal; the tombstone and entry + age out ~two majors from now. + + Dropping the key changes **no policy outcome anywhere** — that impossibility of + effect is the entire reason for the removal. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [42e3b01] +- Updated dependencies [c8124e5] +- Updated dependencies [39eb01b] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [974c6d4] +- Updated dependencies [33a5ff4] +- Updated dependencies [9e01213] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [3fe0ff1] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [c53aa53] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/cloud-connection@17.0.0-rc.1 + - @objectstack/driver-sql@17.0.0-rc.1 + - @objectstack/service-datasource@17.0.0-rc.1 + - @objectstack/connector-mcp@17.0.0-rc.1 + - @objectstack/connector-openapi@17.0.0-rc.1 + - @objectstack/connector-rest@17.0.0-rc.1 + - @objectstack/connector-slack@17.0.0-rc.1 + ## 0.3.14-rc.0 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 2e3c3f9bdd..b2f9d3b936 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.14-rc.0", + "version": "0.3.14-rc.1", "description": "Kitchen-sink showcase workspace — exercises every metadata type, every view type, every chart type, and the major end-to-end capability chains (security, automation, analytics). Built for demonstration, debugging, and coverage-driven verification.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-todo/CHANGELOG.md b/examples/app-todo/CHANGELOG.md index 592a6987e0..301b2b5b2e 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,114 @@ # @objectstack/example-todo +## 4.0.92-rc.1 + +### Patch Changes + +- c5c78bb: **[#3959] `app-todo`'s `defer_task` / `set_reminder` are `type: 'script'`, not `type: 'modal'`.** + + Both declared `type: 'modal'` with a `target` naming a modal page that does not + exist (`defer_task_modal`, `set_reminder_modal`), while their handlers sat + registered under `deferTask` / `setReminder` — keys no declaration could + address. A `modal` action has no server dispatch (`headlessActionTypeError` + rejects it over REST), so neither handler had ever executed: the example + shipped business logic that could not run, and ADR-0110 D5's boot inventory + flagged both on its first pass. + + Both already declared the `params` their handlers read, so they were always + "collect input, then run server-side" actions — which is `type: 'script'` with + `params`. The runner collects the same dialog and the handler now actually runs. + + The action-type table in `content/docs/ui/actions.mdx` said `modal` meant + "collect input, then submit to a handler", contradicting the same page's own + REST table (`modal` → 400, nothing for the server to run). Corrected. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [cbc08eb] +- Updated dependencies [0c4f5b2] +- Updated dependencies [12a19a8] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c8124e5] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [c53aa53] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/client@17.0.0-rc.1 + - @objectstack/metadata@17.0.0-rc.1 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.1 + - @objectstack/mcp@17.0.0-rc.1 + - @objectstack/knowledge-memory@17.0.0-rc.1 + - @objectstack/service-knowledge@17.0.0-rc.1 + ## 4.0.92-rc.0 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index c11fbeac9a..ec2bd80645 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.92-rc.0", + "version": "4.0.92-rc.1", "description": "Example Todo App using ObjectStack Protocol", "license": "Apache-2.0", "private": true, diff --git a/examples/embed-objectql/CHANGELOG.md b/examples/embed-objectql/CHANGELOG.md index f48411528f..d5e3488255 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,64 @@ # @objectstack/example-embed-objectql +## 0.0.32-rc.1 + +### Patch Changes + +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [b3a2318] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [9e8f04d] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/driver-memory@17.0.0-rc.1 + ## 0.0.32-rc.0 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index cc02a45c00..3fbc732688 100644 --- a/examples/embed-objectql/package.json +++ b/examples/embed-objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-embed-objectql", - "version": "0.0.32-rc.0", + "version": "0.0.32-rc.1", "private": true, "description": "Embed the ObjectQL engine as a plain library via @objectstack/objectql/core — no kernel, no plugins, no metadata protocol (ADR-0076).", "type": "module", diff --git a/packages/adapters/hono/CHANGELOG.md b/packages/adapters/hono/CHANGELOG.md index 8876a06a2e..fe9f8a7f6b 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,161 @@ # @objectstack/hono +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- 4be9d99: fix(runtime,hono,plugin-dev): retire the dispatcher's `/storage` bridge — it never spoke the storage contract (#4087) + + `POST /api/v1/storage/upload` and `GET /api/v1/storage/file/:id` were a + dispatcher-side bridge to the `file-storage` service slot, written against a + service shape that does not exist: + + - **Upload** called the contract's `upload(key, data, options?)` as + `upload(file, { request })` — the parsed file object landed in the `key` + slot and `{ request }` in `data`. That is a `TypeError` against every + implementation in the repo (`S3StorageAdapter`, `LocalStorageAdapter`, + `SwappableStorageService`, plugin-dev's in-memory one), not a + near-miss: `Buffer.from({}) → ERR_INVALID_ARG_TYPE`, or an object used as + an S3 object key / `path.join` segment. + - **Download** branched on `result.url` / `result.redirect` / `result.stream` + / `result.mimeType` while the contract's `download(key)` resolves a + `Buffer`, so every branch fell through and the route answered a + JSON-serialized Buffer. + + Both routes are removed, along with `HttpDispatcher.handleStorage()`, the + `/storage` domain registration, the dispatcher-plugin mounts and the two route + ledger rows. + + **Migration.** There is nothing to migrate off in practice — neither route + could complete a request. (They were reachable: `service-storage` mounts + `/storage/upload/presigned`, not `/storage/upload`, so nothing shadowed them. + They simply had no caller — no SDK method builds those URLs.) + `/api/v1/storage` is `@objectstack/service-storage`'s surface and always was + the working one: + + - Upload — FROM `POST /api/v1/storage/upload` TO the presigned protocol + (`POST /storage/upload/presigned` → direct `PUT` to the returned URL → + `POST /storage/upload/complete`), or `client.storage.upload(file)`, which + runs all three steps. + - Download — FROM `GET /api/v1/storage/file/:id` TO + `GET /storage/files/:fileId/url` (`client.storage.getDownloadUrl(fileId)`) + for a signed URL, or `GET /storage/files/:fileId` for a stable browser URL + that 302s to it. + + Install `@objectstack/service-storage` to get those routes; without it + `/api/v1/storage` now has no handler, which is the same answer every other + uninstalled capability gives. + + Two follow-on corrections keep `declared === enforced`: + + - `@objectstack/hono` no longer mounts `app.all('/storage/*')`. That + wildcard claimed the whole `/storage` subtree for the two dead routes, so + every other path under it — service-storage's protocol above all — got the + bridge's own 404 rather than falling through. Storage is ordinary catch-all + traffic now. + - Discovery keeps gating `routes.storage` on `isServiceServeable` — the shared + `handlerReady` predicate #4058 step 2 introduced — and plugin-dev's in-memory + implementation now self-declares `handlerReady: false`. #4058 deliberately + left that one serving because the `/storage` bridge was still there to serve + it; with the bridge retired nothing routes HTTP to that slot, so `false` is + the honest value — the position `realtime` has held since ADR-0076 D12. The + implementation keeps working for in-process callers; it is simply no longer + advertised as a reachable HTTP capability. + +### Patch Changes + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [385c4b0] +- Updated dependencies [45dc446] +- Updated dependencies [43ff598] +- Updated dependencies [839982e] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [3ba8d77] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/plugin-hono-server@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 0150ebf950..f65b18d37a 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index ae724be04f..1d72d2c90e 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,56 @@ # @objectstack/account +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index a1f7ddb83a..8214413896 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 69f640dba7..b283dfd4e9 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,56 @@ # @objectstack/setup +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 731b6d8b86..90e3b3599c 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index 5740b4cc47..565bb43f0d 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,56 @@ # @objectstack/studio +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index eb713903cd..4d3c52fd58 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index aa88f29c1d..216f643924 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,639 @@ # @objectstack/cli +## 17.0.0-rc.1 + +### Minor Changes + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +- 33a5ff4: `os migrate` no longer touches the database before you confirm, and refuses a + SQLite database another process is using (#3917). + + **Nothing is written before the prompt.** `plan` called itself a dry run and + `apply` gated on `[y/N]`, but both booted the full plugin set first — and boot + schema-sync issued create-table/add-column DDL (plus the artifact's inline seed + wrote rows) against the target database before either promise was kept. + `SqlDriver` gains `setDeferredDdl` / `previewDeferredSchemaWork` / + `flushDeferredSchemaDdl`: while armed, `initObjects` still registers every + in-memory map drift detection depends on but records the physical work instead + of performing it. Both commands boot with it armed, render the held-back work + as a `New (additive)` section of the plan, and `apply` performs it only after + confirmation. `os meta resync` / `os migrate files-to-references` keep the old + behaviour — they need the tables to exist. + + **Occupancy check.** A live `os dev`/`os serve` holding the same SQLite file is + the usual way a migration goes wrong: the migration is transactional and swaps + tables inside the file, but the running server keeps prepared statements and a + schema cookie the migration invalidates. `os migrate` now probes the target + before booting — `PRAGMA locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` under + `busy_timeout = 0`, which reports `SQLITE_BUSY` when another connection is + _attached_, not merely writing. (`wal_checkpoint(TRUNCATE)` only sees an active + writer, and `-wal`/`-shm` presence cannot tell a live server from a crashed one; + both are encoded as tests.) `apply` refuses with exit 1 — `error: database_busy` + under `--json` — unless the new `--force` flag is passed; `plan` warns and + continues, since it writes nothing either way. SQLite only: Postgres and MySQL + take their own server-side locks. + + `@objectstack/runtime` also exports `resolveStandaloneDatabase()`, so a caller + can resolve the database target with the same precedence the boot uses without + building the stack, and `createStandaloneStack` accepts `skipSeedData`. + +### Patch Changes + +- c6c0d05: fix(cli): the boot merge no longer discards the authored `api` block (#4002) + + `objectstack serve` (and `dev`, which spawns it) assembled the effective config as + `{ ...authored, ...bootResult }`. `createStandaloneStack()` / + `createDefaultHostConfig()` return an `api` block carrying only the + environment-scoping decision — `{ enableProjectScoping: false, projectResolution: +'none' }` — and under a shallow spread that object REPLACED the author's entire + `api`, silently dropping every key it did not itself set. + + Two of those keys are live knobs the CLI reads a few lines later: + + - **`api.requireAuth`** — the documented one-line opt-out for serving data + publicly (ADR-0056 D2; the v12 migration note presents it as the whole + migration). Authoring it did nothing: the value never reached the REST or + dispatcher plugin, so anonymous requests kept getting `401` **and** the boot + warning that exists to make a fail-open posture visible never fired either. + - **`api.enforceProjectMembership`** — the ADR-0024 D9 opt-out from the + `sys_environment_member` 403 gate. Silently fell back to the dispatcher default. + + `api` now merges per key, via a small pure `mergeBootConfig` helper: the author's + declarations survive, and the boot builder still wins on the keys it actually + decides (environment scoping is not the author's call on a standalone host). + Every other top-level key keeps the previous whole-value semantics — the + artifact-serve path deliberately serves the boot result's `objects` / + `permissions` / `manifest` / `plugins`, so those are untouched. + + The auth-less carve-out was never affected and is unchanged: it lives in the + `?? ((tierEnabled('auth') || hasAuthPlugin) ? true : false)` fallback, which fired + precisely _because_ the authored value had gone missing. Only an explicitly + authored value was lost. + + Verified end to end: with `api: { requireAuth: false }` on the CRM example, an + anonymous `POST /data/crm_account/query` returned `401` before and returns records + after. Worth knowing what the working flag does — the same anonymous caller can + then read `sys_user` — which is the flag's documented meaning ("serve data + publicly"), and the argument for retiring it (#3963). + +- c20b875: **Correct the stale premise left behind by #4012: the degraded-boot stderr copy + survives the operator's LOG LEVEL, not `os serve`'s boot-quiet window.** + + `emitDegradedBootBanner` writes the `OS_ALLOW_DRIVER_CONNECT_FAILURE` banner to + stderr in addition to `logger.warn`, and every comment and test name explaining + why cited the same reason: `os serve` swallowed all of stdout while the kernel + booted, and `Logger` routes `warn` to stdout. #4012 fixed that — the boot window + now buffers and replays `warn`-and-above — which retires the _stated_ + justification for a duplicate that is nonetheless still load-bearing: + + `Logger.write()` returns before touching a stream when the record is below + `config.level`, so at `--log-level error`, `fatal` or `silent` the banner's + `logger.warn` reaches **no** stream at all. A production host at `error` is + exactly the deployment this escape hatch exists for, and exactly where a + logger-only banner would vanish. Removing the stderr copy on the strength of + #4012 would therefore have been a regression — so this documents the reason that + is still true, in the places someone would read before deleting it: + `degraded-boot.ts`, the engine's emit site, and all three parity tests + (objectql, runtime, service-datasource), which are renamed off "which `os serve` + boot-quiet cannot swallow" to "which the operator log level cannot filter away". + + The objectql parity test now proves the claim instead of asserting around it: it + drives a **real** `ObjectLogger` at `level: 'error'` and requires the banner on + stderr _and_ nothing on stdout. Set the level to `warn` and it fails — so the + test is pinned to the level filter rather than passing for any reason. + + Also corrected in the same sweep, all comment-only, all previously overstating + what #4012 had not yet fixed: + + - the automation wiring summary (`format.ts`, `serve.ts`, its test) claimed the + boot window swallowed the engine's binding warnings. Its real justification is + stronger and unchanged: a flow that silently fails to arm emits **no** log line + at any level, so binding state has to be read off the live engine — absence of + a warning was never evidence of a bound flow. + - the seed summary (`seed-summary.ts`, `format.ts`, its test) and `AppPlugin`'s + seed-outcome note attributed the silence to the boot window; the operative + gate is that `SeedLoader`'s result logs are `info`, under the default `warn`. + + No behavior changes. + +- 9774b78: fix(driver-sql): `Field.time` gets a canonical storage form — `HH:MM:SS[.fff]` wall-clock text on every dialect (#3994) + + `Field.time` repeated the pre-#3912 `Field.datetime` pattern: writes were never + normalised and only reads were repaired, so one SQLite column accumulated bare + time-of-day TEXT, full-timestamp TEXT and INTEGER epoch ms side by side. + `find()` looked right; everything that compared the STORED form was wrong — + measured: a business-hours window filter silently dropped 4 of 7 rows, ORDER BY + sorted 14:30 before 08:00, a full-ISO write failed the statement outright on + both Postgres and MySQL, a bound `Date` stored a process-timezone wall clock on + pg, MySQL's bare `TIME` rounded `…00.500` up to `…01`, and a `NOW()` default + resolved against three different clocks on the three dialects. + + The #3912→#3942→#3954 construction, transplanted (ADR-0053 D-C1..D-C3): + + - One `canonicalTimeOfDay` — `HH:MM:SS`, `.fff` only when non-zero; `Date`/ + epoch/full-timestamp fold to the UTC time-of-day — applied on write + (`formatInput`), to filter comparands (`coerceFilterValue`, and thereby the + `temporalFilterValue` contract hook) and on read (`toTimeOnly`). + - SQLite: legacy columns converge at schema sync (`backfillCanonicalTimes`, + same `IS NOT`-guarded UPDATE, same log-and-swallow policy); until then the + filter paths wrap the column in the repair expression — correct, just + unindexed. `os migrate plan` lists the work as `normalize_time_storage` with + a row count. + - MySQL: new time columns are `TIME(3)`; legacy `TIME(0)` columns widen at + schema sync (`migrateMysqlTimeColumns`, plan kind `widen_time_columns`), + since zero-precision TIME _rounds_ fractional writes. + - `NOW()` defaults read the UTC clock on every dialect (Postgres previously + used the server zone, MySQL the inserting session's zone — and MySQL 8.0 + rejects a plain `CURRENT_TIMESTAMP` default on TIME entirely). + - `distinct()`/`aggregate()` present time columns exactly as `find()` does. + + `HH:MM:SS` writes round-trip byte-identically (the field-zoo `f_time` + contract); a minutes-only `HH:MM` now completes to `HH:MM:00`, and uninterpretable + values still pass through untouched. + +- ec36ba8: feat(cli): lint the contradictory uniqueness double-declaration (#3991) + + New advisory rule `unique/double-declaration`, reported by `os lint` and + `os build`. It fires when one column carries BOTH a field-level `unique: true` + and an object-level single-column unique index: + + ```ts + email: Field.email({ unique: true }), // per-tenant since #3696 + indexes: [{ fields: ['email'], unique: true }], // platform-wide, verbatim + ``` + + The two spellings deliberately mean different things (see `IndexSchema`), and + each is legitimate alone. Together on one column they never are: + + - On a **tenant-scoped** object they contradict. The stricter one wins + physically, so the global index enforces uniqueness and the per-tenant + composite becomes a constraint nothing can trip — one of the two authored + intents is silently discarded. Worse, it hides the #3696 semantic change: + the switch from global to per-tenant has _no observable effect_ while the + declared index still enforces the old behaviour, so the author never learns + their tenancy model and their real constraint disagree — until a second + tenant reuses the value and is rejected. + - On a **tenancy-less** object they are the same index declared twice. + + Tenancy is deliberately not inferred at authoring time (`organization_id` is + injected by the kernel at registration, not authored), so the message names + both readings and the fix spells out the choice: `unique: 'global'` plus + dropping the index for platform-wide, or dropping the index for per-tenant + (or writing it out as `fields: ['organization_id', 'email']`). + + A field already declared `unique: 'global'` is exempt — the index restates + that intent rather than losing it. Advisory only: the artifact is well-defined, + so this never fails a build. + +- 675566f: Make the `os migrate` occupancy check actually fire, and extend it to + `files-to-references` (#3917 follow-up). + + The check shipped in #3924 relied on a SQL lock probe + (`PRAGMA locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE`), which is correct on a + WAL database — and blind on the journal mode the platform actually uses. + ObjectStack's sqlite driver runs `journal_mode = delete`, where an idle + connection holds no lock at all, so dogfooding against a real `os serve` + holding a real project database showed `os migrate apply` reporting the + database idle and running the migration unannounced: exactly the scenario the + check was added to prevent. The unit tests missed it because they built their + fixtures in WAL mode. + + The probe now leads with the signal that survives every journal mode: which + processes hold the file open (`/proc` on Linux, `lsof` on macOS). It also names + them — `is in use — it is open in pid 12367 (node)` — which is the actionable + part. The SQL probe is kept as a second signal for WAL databases on platforms + where process inspection is unavailable or the holder belongs to another user; + either signal firing counts as busy. + + `os migrate files-to-references --apply` now takes the same gate (and the same + `--force` escape hatch). It rewrites rows rather than schema, so a concurrent + writer on the same file is at least as dangerous there; a dry run only warns, + since its counts shift under a live writer but it writes nothing itself. + +- 9e01213: fix(cli,driver-sql): `os migrate plan` lists the datetime storage convergence (#3954) + + The datetime canonicalisation (#3912/#3942) added two steps to `initObjects`' + physical path: a row-rewriting backfill on SQLite and a `TIMESTAMP` → + `DATETIME(3)` column rebuild on MySQL. Both already respected the DDL deferral, + so `plan` performed neither and `apply` performed both — the behaviour was never + wrong. The reporting was. + + `PendingSchemaWork` could only express `create_table` / `add_columns`, so an + operator saw a plan listing two added columns, confirmed it, and `apply` + additionally rewrote every row of a datetime column — or took a metadata lock to + rebuild one on a large table. The plan promises to show what apply will do. + + - `PendingSchemaWork.kind` gains `normalize_datetime_storage` and + `widen_datetime_columns`, plus an optional `rows` carrying how much data the + step touches: row-writes for the backfill, the table's size for the rebuild — + the number that decides "now" versus "in a maintenance window". + - `previewDeferredSchemaWork()` measures both without performing either, reusing + the exact predicate each migration uses (the backfill's whole `WHERE`, the + widening's own `information_schema` filter) so the plan and the apply cannot + name different sets. A probe that cannot run is swallowed to "unlisted", never + to a failed plan. + - The CLI renders them under their own heading rather than folding them into the + additive section, whose "created when you apply" framing carries an implicit + promise that the work is never data-losing. `summarizePendingSchemaWork` — the + line read just before typing `y` — never omits in-place work. + +- 39eb01b: fix(runtime,cli,types): `os migrate` and the dev runtime now share one `__search` companion schema view (#3955) + + On a zh-locale deployment the dev runtime provisions the hidden `__search` + pinyin companion column (ADR-0098) on every eligible object, but the + `os migrate plan`/`apply` boot went through `createStandaloneStack`, which + never derived the locale-gated pinyin decision from the compiled artifact. + Its metadata therefore lacked every companion column, and `migrate plan` + reported each live `__search` column of a dev-created database as a + destructive orphan — with `--allow-destructive` as the printed remediation, + which would have dropped live feature columns. + + - `@objectstack/types`: new `collectConfiguredLocales(i18n)` and + `stampSearchPinyinEnabled(i18n)` — the single resolve-and-stamp helper for + `OS_SEARCH_PINYIN_ENABLED`. An explicit env value still wins; only a + positive locale-derived decision is stamped. + - `@objectstack/runtime`: `createStandaloneStack` stamps the decision from + the artifact's `i18n` before any plugin constructs a `SchemaRegistry`, and + surfaces `i18n` on its result like `requires`/`objects`/`manifest`. + - `@objectstack/cli`: the `serve`/`dev` boot now stamps through the same + shared helper (behaviour unchanged), so create/serve and plan/apply cannot + compute different schema views of the same source tree. + + A fresh CLI-created database is now also born with the same `__search` + columns the dev runtime would provision, instead of acquiring them on the + next dev boot. + +- 627b188: fix(seed-loader): count reference fields dropped from rows that were still written + + The loader had two failure outcomes and only counted one. A record it cannot + write is counted in `errored`. But an unusable **reference value** (an object + where a natural key belongs, an array on a single-value field) is removed from + the record — never written as NULL, which would sever an existing link on + upsert replay — and the row is written **without it**. Nothing counted that. + + So a load that quietly severed N associations reported `totalErrored: 0`, and + every count-driven surface read clean. The CLI boot banner — the one seed signal + that survives `os dev`'s boot-quiet window and the default `warn` level — printed + `showcase 42 rows`, and the warn line said `0 dropped record(s)`: true, and + useless ([#3932](https://github.com/objectstack-ai/objectstack/issues/3932)). + + `SeedLoadResult.referencesDropped` and `SeedLoaderSummary.totalReferencesDropped` + now count it. It is deliberately **not** folded into `errored` — the row _was_ + written, so that would break the `inserted + updated + skipped` reconciliation + against `total`. The banner names it separately: + + ``` + ⚠ Seeds: showcase 42 ok / 3 lost links ⚠ + ``` + + Both counters are additive with a `0` default, so an existing producer or + consumer of `SeedLoaderResult` is unaffected. + +- 7ac1995: **`os dev` / `os serve` stop swallowing every plugin boot-phase log line — the + boot-quiet window buffers instead of discarding (#4012).** + + `serve` blanks stdout while the kernel boots so the startup banner is readable, + and dropped what it intercepted. `ObjectLogger` routes `debug`/`info`/`warn` to + **stdout** — only `error`/`fatal` go to stderr — so that one line swallowed + every boot-phase `logger.warn` any plugin emits: the ADR-0110 D5 + `[action-governance]` inventory, the automation engine's binding warnings, + every degraded-boot notice. `os dev` spawns `serve` with inherited stdio, so a + single drain blinded both entrypoints at every log level, and it inverted the + flag's own promise — the default is `warn` precisely "so flow/hook execution + failures surface (ADR-0032)". Data-phase logging was unaffected, which is why + the hole survived: `--log-level debug` printed thousands of lines with none + from boot. + + - The intercepted bytes now land in a line-oriented, bounded `BootLogCapture` + that classifies each line against `ObjectLogger`'s pretty/text/json + renderings and retains only records at `warn` or above, so buffer size tracks + a boot's warnings rather than its chattiness. The startup chatter the window + exists to hide is still dropped. + - Retained records replay under the banner, beside the automation and seed + summaries that exist for exactly this reason — and on the two exits that + never reach the banner: `OS_MIGRATE_AND_EXIT` (a deploy pipeline must not + lose a degraded-boot warning) and serve's error path, where a boot that died + is when its warnings matter most. + - `--verbose` / `--log-level debug|info` no longer open the window at all. + Buffering a stream the operator explicitly asked to watch would be the flag + defeating itself. + + On `examples/app-todo`, `os serve` went from 25 lines with zero WARN among them + to surfacing five boot warnings, including the `[action-governance]` line + naming all eight unbound actions. This closes the loop the D5 inventory + changeset left open: the inventory was already emitted correctly and is now + visible on the platform's own dev loop. + +- 857a6cf: fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085) + + The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is + a development platform, so it has to start without one — but `os serve +objectstack.config.ts` died during boot whenever the artifact was absent: + + ``` + Loading objectstack.config.ts... + [StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT… + + ✗ Service 'manifest' is async - use await + ``` + + Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with + only its `dist/objectstack.json` moved aside), and on every freshly authored + project between `os init` and its first `os compile`. The message named neither + the missing artifact nor a fix, so it read as an internal kernel fault. + + Three separate faults, each of which alone was enough to refuse the boot: + + - **`serve` registered the config-derived `AppPlugin` before the stack's own + `plugins[]`.** Registration order _is_ the kernel's init/start order, and that + slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and + `DefaultDatasourcePlugin` (which connects the database the app seeds through). + The wrap is now **appended** to `plugins[]`, the same slot + `createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot + and artifact-boot share one plugin order. The artifact path never hit this, + which is exactly what made a plugin-**order** bug look artifact-related. + + - **`ctx.getService()` reported a never-registered service as "is async".** + `PluginLoader.getService` is an `async` method, so its return value is _always_ + a Promise and its internal "not found" rejection can never surface + synchronously — the kernel read the answer off that Promise and told every + caller to `await` a service that did not exist, while the `not found` branch + below it was unreachable. It now decides from the registry: absent ⇒ + `[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged + `Service 'x' is async - use await`. The same crash now reads + `[Kernel] Service 'manifest' not found`, which points at the layer that is + actually wrong. + + - **`MetadataPlugin` treated an absent `local-file` artifact as fatal.** + `createStandaloneStack` always points it at `dist/objectstack.json`, so a stack + with no app at all could not boot. A **missing** local artifact is now "nothing + compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so + a later `os compile` hydrates the running server. The tolerance is + ENOENT-only — a malformed or unreadable artifact stays fatal — and + `bootstrap: 'artifact-only'` (sealed runtime, where the artifact _is_ the + deployment) keeps failing loudly rather than silently serving an empty runtime. + + `[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted + at callers for whom "no artifact" is a healthy state; a present-but-unusable + artifact keeps the loud warning. + + Pinned by an e2e pair that drives the real `os serve` with **no `os compile` + anywhere**: an app defined only by `objectstack.config.ts` (asserting its object + is in the started plugin set, not merely that boot survived) and a bare + `export default {}` platform. The #4012 fixture drops the `os compile` this bug + had forced on it. + +- c53aa53: File-backed SQLite now runs `journal_mode = WAL` (#3941). + + `SqlDriver.connect()` set `auto_vacuum` and left the journal mode alone, so + every ObjectStack SQLite database ran SQLite's built-in default — a rollback + journal. That is the worst mode for the shape this platform actually has, which + is **several processes on one file**: a dev server, `os migrate`, + `os meta resync`, a test run. Measured, on the same file: + + | | rollback journal | WAL | + | :--------------------------------------------- | :------------------------------------------------- | :---------------------------------------------------------------- | + | writer while another process holds a read open | `SQLITE_BUSY` — committing needs an exclusive lock | proceeds | + | idle attached connection visible to SQL | no — a lock lasts only as long as its transaction | yes (`locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` reports busy) | + + The second row is why the `os migrate` occupancy check had to inspect file + descriptors to see a live server at all (#3940): under a rollback journal there + was nothing in the database to see. That signal stays — it names the process, + which WAL's lock probe cannot — but the SQL probe is now authoritative for + databases ObjectStack created rather than a fallback that was blind in practice. + Concurrent _writers_ still serialize; SQLite allows one at a time in any mode. + + Journal mode is a persistent property of the file, so an existing database is + converted in place on the next connect (a header change — no rows are touched) + and stays converted. Two consequences to plan for: + + - `app.db-wal` / `app.db-shm` exist beside the database while a connection is + attached, and `app.db-wal` can hold committed transactions. A clean shutdown + checkpoints them away; a naive copy of `app.db` alone while a server runs does + not. Use `sqlite3 app.db ".backup …"`. + - **WAL does not work on network filesystems** (NFS/SMB). Opt out with + `OS_DATABASE_SQLITE_JOURNAL_MODE=delete`, or per datasource with + `sqliteJournalMode: 'delete'` in the driver config (which outranks the env + var). Either form _applies_ `delete`, so it also converts a database that + already adopted WAL back — skipping would have stranded it. + + Nothing here fails a boot, and nothing is assumed: `PRAGMA journal_mode = X` + answers with the mode actually in force rather than raising on refusal, so the + reply is read back; and because a filesystem can accept WAL and then fail the + first read _through_ it, the mode is proven with a read and rolled back to + `delete` if that fails — with a warning naming the file and the escape hatch. + `synchronous` is untouched, so durability is exactly what it was. `:memory:` + databases are left alone, as is `auto_vacuum = INCREMENTAL`, which keeps + reclaiming under WAL (ADR-0057). + + `os db clean` now counts `-wal` / `-shm` as part of the database when it measures + what a `VACUUM` reclaimed, so bytes that were sitting in the log do not read as a + reclaim of zero. + + `@objectstack/driver-sqlite-wasm` deliberately stays out of WAL. Its live + database is in the WASM heap and what reaches disk is a byte image it exports, so + nothing reads the database across processes and the pragma buys it nothing — + while still being a persistent header change in the operator's file. sql.js + _accepts_ the pragma (its VFS is memory-backed), so this had to be declared + rather than discovered. + + It also now parks a `-wal` left behind by an unclean native-driver exit rather + than loading the image beside it: wasm SQLite cannot read that log, and leaving + it next to a freshly rewritten image would let a later real SQLite replay frames + that no longer belong to it. The warning names the file it parked and how to + recover what was in it. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [e5e8b10] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [3abd233] +- Updated dependencies [ea24593] +- Updated dependencies [789ad63] +- Updated dependencies [fccec22] +- Updated dependencies [2af1988] +- Updated dependencies [b3a2318] +- Updated dependencies [0af50a3] +- Updated dependencies [cbc08eb] +- Updated dependencies [0c4f5b2] +- Updated dependencies [12a19a8] +- Updated dependencies [4580597] +- Updated dependencies [eb9230c] +- Updated dependencies [bec0f9a] +- Updated dependencies [6fd0786] +- Updated dependencies [7df7c64] +- Updated dependencies [fae74b5] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [42e3b01] +- Updated dependencies [c8124e5] +- Updated dependencies [9e8f04d] +- Updated dependencies [39eb01b] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [43ff598] +- Updated dependencies [839982e] +- Updated dependencies [71af9f5] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [974c6d4] +- Updated dependencies [495019b] +- Updated dependencies [33a5ff4] +- Updated dependencies [9e01213] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [3fe0ff1] +- Updated dependencies [be7945a] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [4580597] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [c53aa53] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/client@17.0.0-rc.1 + - @objectstack/metadata@17.0.0-rc.1 + - @objectstack/plugin-sharing@17.0.0-rc.1 + - @objectstack/plugin-security@17.0.0-rc.1 + - @objectstack/rest@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/cloud-connection@17.0.0-rc.1 + - @objectstack/plugin-auth@17.0.0-rc.1 + - @objectstack/plugin-approvals@17.0.0-rc.1 + - @objectstack/plugin-webhooks@17.0.0-rc.1 + - @objectstack/service-messaging@17.0.0-rc.1 + - @objectstack/service-automation@17.0.0-rc.1 + - @objectstack/trigger-api@17.0.0-rc.1 + - @objectstack/driver-sql@17.0.0-rc.1 + - @objectstack/service-analytics@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/driver-memory@17.0.0-rc.1 + - @objectstack/driver-mongodb@17.0.0-rc.1 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.1 + - @objectstack/console@17.0.0-rc.1 + - @objectstack/service-datasource@17.0.0-rc.1 + - @objectstack/verify@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/plugin-hono-server@17.0.0-rc.1 + - @objectstack/lint@17.0.0-rc.1 + - @objectstack/service-settings@17.0.0-rc.1 + - @objectstack/plugin-pinyin-search@17.0.0-rc.1 + - @objectstack/trigger-record-change@17.0.0-rc.1 + - @objectstack/account@17.0.0-rc.1 + - @objectstack/setup@17.0.0-rc.1 + - @objectstack/mcp@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + - @objectstack/plugin-audit@17.0.0-rc.1 + - @objectstack/plugin-email@17.0.0-rc.1 + - @objectstack/plugin-reports@17.0.0-rc.1 + - @objectstack/service-cache@17.0.0-rc.1 + - @objectstack/service-job@17.0.0-rc.1 + - @objectstack/service-package@17.0.0-rc.1 + - @objectstack/service-queue@17.0.0-rc.1 + - @objectstack/service-realtime@17.0.0-rc.1 + - @objectstack/service-sms@17.0.0-rc.1 + - @objectstack/service-storage@17.0.0-rc.1 + - @objectstack/trigger-schedule@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 2a08e3ca01..296c506eac 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/client-react/CHANGELOG.md b/packages/client-react/CHANGELOG.md index 9bc4d4d776..1a24947ab9 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,64 @@ # @objectstack/client-react +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [cbc08eb] +- Updated dependencies [0c4f5b2] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/client@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 367486bf24..070c395dc7 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 1ffcec3b78..f390f1f518 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,296 @@ # @objectstack/client +## 17.0.0-rc.1 + +### Minor Changes + +- 195ad76: fix(actions)!: failures speak HTTP — business rejections are 400, success is a single wrap (#3962) + + **BREAKING (raw-HTTP callers of `POST /api/v1/actions/...` only).** The + 200-with-inner-envelope wire was never a designed contract: no ADR or doc ever + specified it, it originated as the route's catch block reusing + `deps.success()`, and `/actions` was the only route of 12 that double-wrapped. + #3962 classifies it as a bug. Five defects traced back to that one extra layer + (the console's green toast on failed actions, `redirectUrl` never firing, a + marketplace install reported as installed when it failed, the client-envelope + divergence #3927 papered over, and crashes invisible to monitoring). + + The contract now, identical to `/data`: + + | Outcome | HTTP | Body | + | :------------------------------------------------------------- | :-------------------: | :-------------------------------------------------------------------- | + | Ran, returned | **200** | `{success: true, data: }` — single wrap | + | Ran, rejected (business rule / validation) | **400** | `{success: false, error: {message, code, details: {code?, fields?}}}` | + | Never dispatched (unknown / denied / wrong type / unavailable) | 404 / 403 / 400 / 503 | unchanged (#3930/#3951) | + | Crashed (`TypeError`, driver class, sandbox timeout) | **500** | unchanged (#3951) | + + A validation rejection carries `details.code: 'VALIDATION_FAILED'` and + `details.fields[]` — the exact payload #3937 fought for, now on the same wire + shape `/data` has always used, which `@objectstack/client` normalizes to + `err.code` / `err.fields` (#3927). A rejected flow is a 400 with + `details.code: 'FLOW_FAILED'`. The crash-vs-rejection discriminator (#3951, + error `name`) now selects 400 vs 500. + + `client.actions.invoke` / `invokeGlobal` still never throw: they fold every + failure status into `{success: false, error}`, read the single wrap on + success, and keep a NARROW legacy heuristic so a current SDK talking to a + pre-#3962 server still folds the old double-wrapped 200s correctly. + + **Migration for raw-HTTP third parties:** branch on the HTTP status — a + non-2xx is the failure, `error.message` / `error.details` carry the detail; on + a 200, `data` is the handler's return value directly (one level less than + before). Callers using `@objectstack/client` need no change. + +- c2bbd97: fix(actions): reach global actions at their real registration key, and 404 an action that never dispatched (#3913) + + **1 — the registration key and the lookup key disagreed.** Both writers + register an objectName-less action under the literal `'global'`: `AppPlugin` + (`action.object || 'global'`) and `ObjectQLPlugin.actionObjectKey`. The REST + route's fallback probed `'*'`, and `engine.executeAction` is an exact-string + `Map` lookup with no wildcard semantics — so the probe could only ever miss: + + ``` + Action 'log_call' on object '*' not found + ``` + + `POST /api/v1/actions/global/log_call` worked by **accident** (the path segment + happened to spell the registration key); `POST /api/v1/actions//log_call` never + worked at all, and neither did falling back from an object-scoped route to a + global handler. `'global'` is now the canonical key + (`GLOBAL_ACTION_OBJECT_KEY`), the probe order is + `[, 'global', '*']` for both the REST route and the MCP + `run_action` bridge (`actionHandlerObjectKeys` — one list, two surfaces), and a + single-segment path (`/actions//:action`) routes at `'global'` instead of + 400-ing. A handler registered directly under `'*'` still resolves; the doc + comments that called `'global'` a "wildcard" are corrected at every site. + + **2 — "no such action" was reported as a success.** The not-found exit called + `deps.success(...)`, which always emits `{status: 200, body: {success: true, +data}}`, so a request naming an action that does not exist came back as: + + ```json + { + "success": true, + "data": { + "success": false, + "error": "Action 'log_call' on object '*' not found" + } + } + ``` + + Every caller that did not hand-unwrap the INNER envelope read the outer + `success: true` and reported a success that never happened — including the + shipped console, which showed a green toast (fixed on that side in + objectui#2963). Nothing **dispatched** there, so it is a **404** now, joining + the answers this route already gives a status: 403 denied, 400 wrong action + type, 503 unavailable. The miss also names the **routed** object rather than + whichever probe ran last (the old fallback said `on object '*'`, an object the + caller never asked for). + + A handler that **ran and rejected** is unchanged: HTTP 200 with + `data: {success: false, error, code?, fields?}`. That is a business outcome, + not a transport error, and #3937 pins it. The line is "did a handler run" — + below it the payload, above it the status. + + `client.actions.invoke` / `invokeGlobal` still do **not** throw. `client.fetch` + throws on every non-2xx, so `invoke` now catches and folds a dispatch failure + into the same `{ success, data?, error? }` result with `error` as a plain + string — otherwise the routes that just gained a status would have started + propagating exceptions into callers that only ever checked `result.success`. + +- 0c4f5b2: `err.code` no longer falls back to the pre-#3842 parking spot (`error.details.code`). The "newer SDK, older server" pairing that read served is not a supported deployment (SDK and server ship as one fixed release group), and the ADR-0112 batch-1 rename changed the code values anyway — a code dug out of an old server's parking spot would match no branch written against the current catalog (#4007). `err.category` / `err.retryable` are now read from inside `error`, where `ApiErrorSchema` declares them; the old top-level read yielded `undefined` against every conformant server (#4006). + +### Patch Changes + +- cbc08eb: fix(client): normalize both server error envelopes so `err.code` / `err.fields` mean one thing (#3918 follow-up) + + Two envelopes are in play and they disagree about where the semantic code and + the per-field list live: + + ``` + @objectstack/rest, flat: + { error, code: 'VALIDATION_FAILED', fields: [...] } + + runtime dispatcher, wrapped: + { success: false, error: { message, code: 400, + details: { code: 'VALIDATION_FAILED', fields: [...] } } } + ``` + + `error.code` in the **wrapped** form is the HTTP status, not a semantic code. + The client read it straight through, so `err.code` was the **number 400** where + the flat envelope gave `'VALIDATION_FAILED'` — meaning the branch our own docs + teach, + + ```js + if (err.code === 'VALIDATION_FAILED') err.fields.forEach(…) + ``` + + never matched on a dispatcher-served surface, and the field list (put on the + wire for those routes by #3918) was unreachable at `err.details.error.details.fields`. + + Now normalized at the throw site: + + - **`err.code` is always the semantic string.** It is read from the flat + `code`, else the wrapped `error.details.code`, else a _string_ `error.code` — + a numeric value is never reported as a code. The HTTP status is on + `err.httpStatus`, where it always was. + - **`err.fields` is the per-field list** whenever the server sent one, from + either envelope. It is left **unset** (not `[]`) when there is none, so + `if (err.fields)` is a safe test for "this failure is field-anchored". + - **`err.details`** prefers a top-level `details` (unchanged), then the wrapped + envelope's own `details`, then the whole body. The flat envelope has no + top-level `details` and so keeps falling through to the whole body exactly as + before — only the wrapped shape changes, and only from "the entire response" + to the structured object it actually carries. + + **Behaviour change worth noting:** code that read `err.code` from a + dispatcher-served route previously got a number and now gets a string (or + `undefined` where the server sent no semantic code). Nothing in this repo did — + `err.httpStatus` was always the correct source for the status, and remains + untouched — but a consumer that branched on `err.code === 400` should move to + `err.httpStatus === 400`. + +- 03d26f7: fix(runtime,spec)!: the dispatcher's `error.code` is the semantic string it always declared; the HTTP status moves to `httpStatus` (#3842) + + `HttpDispatcher.error()` took the HTTP status as its `code` argument and wrote it + straight into the field `ApiErrorSchema` reserves for a semantic string, so + `error.code` came back as `400`/`403`/`503` — a number, duplicating the response + status and occupying the one slot a caller is meant to branch on. The real code + then had to go somewhere else, and did, three somewhere-elses: `details.code` + (auth gate, permission denial, anonymous deny), `details.type` + (project-membership gate), and `error.type` (`routeNotFound`). Four sites, three + parking spots, because the declared one was full. + + **FROM → TO on the wire.** A dispatcher error body + + ```json + { + "success": false, + "error": { + "message": "…", + "code": 403, + "details": { "code": "PERMISSION_DENIED" } + } + } + ``` + + is now + + ```json + { + "success": false, + "error": { "code": "PERMISSION_DENIED", "message": "…", "httpStatus": 403 } + } + ``` + + | Reading | Was | Now | + | ------------- | ---------------------------------------------------------- | ------------------------------------------------- | + | semantic code | `error.details.code` / `error.details.type` / `error.type` | `error.code` | + | HTTP status | `error.code` | `error.httpStatus` (or the response status) | + | context | `error.details` (with the code mixed in) | `error.details` (context only, absent when empty) | + + **One-line fix for a direct reader:** replace `body.error.details?.code ?? +body.error.type` with `body.error.code`, and `body.error.code` with + `body.error.httpStatus`. **SDK callers need no change** — `ObjectStackClient` + already normalised this (`err.code` semantic, `err.httpStatus` numeric) and still + reads the old shape, so a client newer than its server is unaffected. + + Every code already on the wire moves **verbatim** — `PERMISSION_DENIED`, + `ROUTE_NOT_FOUND`, `PASSWORD_EXPIRED`, `PROJECT_MEMBERSHIP_REQUIRED`, + `VALIDATION_FAILED`, `unauthenticated`. This change moves a field; it does not + rename anything. Reconciling the repo's two code vocabularies is #3841, and this + leaves it exactly one map and one enum to sweep instead of four parking spots. + + A branch with no code of its own is served one derived from the status, via the + single declared map `HttpStatusErrorCodeMap` / `standardErrorCodeForHttpStatus` + in `@objectstack/spec/api` (`403` → `permission_denied`, `503` → + `service_unavailable`, …). Derivation is necessary because `ApiErrorSchema.code` + is required; drawing it from `StandardErrorCode` keeps a derived code a + catalogued one rather than an invented string. + + **Spec changes:** + + - `ApiErrorSchema` gains optional `httpStatus: number` — the precedent is + `EnhancedApiErrorSchema.httpStatus`. Additive. + - `StandardErrorCode` gains `method_not_allowed` and `precondition_required`, + the two statuses the runtime returns that the enum could not name. Additive. + - **Breaking — `DispatcherErrorCode`** was `'404' | '405' | '501' | '503'` (string + spellings of HTTP statuses, for matching against the numeric `error.code`). It + is now `'ROUTE_NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'NOT_IMPLEMENTED' | +'SERVICE_UNAVAILABLE'` — the same four members the removed `error.type` enum + declared, moved verbatim. FROM `DispatcherErrorCode.parse('404')` TO + `DispatcherErrorCode.parse('ROUTE_NOT_FOUND')`; to match a status, read + `error.httpStatus`. TypeScript flags every call site. + - **Breaking — `DispatcherErrorResponseSchema`**: `error.code` is `z.string()` + (was `z.number().int()`), `error.type` is **removed** (folded into `code`), and + `error.httpStatus` / `error.details` are declared. This schema is what + legitimised the deviation — it declared the opposite of `ApiErrorSchema` for + the same field. FROM `{ code: 404, type: 'ROUTE_NOT_FOUND' }` TO + `{ code: 'ROUTE_NOT_FOUND', httpStatus: 404 }`. + + **Also aligned, because they are the same wire surface:** `dispatcher-plugin`'s + `errorResponseBase` (the THROWN-error exit) and its inline 404, and the MCP 405. + `errorResponseBase` previously discarded a thrown error's `.code` outright — it + had nowhere to put it — so the two exits of one surface disagreed about what a + caller would see; they now agree. Every body on this surface is built by one + helper (`packages/runtime/src/error-envelope.ts`), guarded in both directions by + `error-envelope.conformance.test.ts`: each branch driven and parsed against the + schema imported from `packages/spec`, plus a source scan so a new branch cannot + quietly reintroduce a numeric `code` or a `type`-as-code sibling. + + This deletes the #3687 pin in `http-dispatcher.test.ts`, which asked to be + deleted rather than updated once the dispatcher was fixed. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/client/package.json b/packages/client/package.json index d1edfa1d91..9d61ec57e5 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index ffcb38af07..259e08cda7 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/cloud-connection +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- 627b188: fix(seed-loader): count reference fields dropped from rows that were still written + + The loader had two failure outcomes and only counted one. A record it cannot + write is counted in `errored`. But an unusable **reference value** (an object + where a natural key belongs, an array on a single-value field) is removed from + the record — never written as NULL, which would sever an existing link on + upsert replay — and the row is written **without it**. Nothing counted that. + + So a load that quietly severed N associations reported `totalErrored: 0`, and + every count-driven surface read clean. The CLI boot banner — the one seed signal + that survives `os dev`'s boot-quiet window and the default `warn` level — printed + `showcase 42 rows`, and the warn line said `0 dropped record(s)`: true, and + useless ([#3932](https://github.com/objectstack-ai/objectstack/issues/3932)). + + `SeedLoadResult.referencesDropped` and `SeedLoaderSummary.totalReferencesDropped` + now count it. It is deliberately **not** folded into `errored` — the row _was_ + written, so that would break the `inserted + updated + skipped` reconciliation + against `total`. The banner names it separately: + + ``` + ⚠ Seeds: showcase 42 ok / 3 lost links ⚠ + ``` + + Both counters are additive with a `0` default, so an existing producer or + consumer of `SeedLoaderResult` is unaffected. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c8124e5] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 70fad731d4..2f28ebe127 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index fc900591b4..a723875310 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/connector-mcp +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 0b4475d7db..533fbd330b 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 2e7392d5a3..ba968352db 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/connector-openapi +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 52ca123413..40f978956a 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 2fb1b6cd12..1c1d6b5566 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/connector-rest +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 5490c1dc50..9ca726f38b 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index 41024ad11e..1f196a56a1 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/connector-slack +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index b22d62db71..6c6aa4e1c6 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 63bff32873..09d4a28c97 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,81 @@ # @objectstack/console +## 17.0.0-rc.1 + +### Minor Changes + +- 4580597: Console (objectui) refreshed to `96ee72e85439`. Frontend changes in this range: + + - fix(console): render the redaction notice on the enveloped resolve body (objectstack#3983) (#2980) + - feat(sdui): guard the public contract against silent drift (#2979) + - fix(sdui): lazy public blocks reach a kind:'react' page scope; ReactRunner keeps its errors (#2976) + - fix(list,data): bridge every spec view operator onto the filter AST (#2901) (#2974) + - fix(errors): error-code branches survive the framework's ADR-0112 rename (objectstack#3841) (#2977) + - fix(fields): a select no longer wipes itself when its value outruns its options (#2968) (#2969) + - fix(approvals): decision outputs reach both decision surfaces (#2955) (#2961) + + objectui range: `e651c936870e...96ee72e85439` + +- eb9230c: Console (objectui) refreshed to `a136322f8723`. Frontend changes in this range: + + - fix(app-shell)!: a modal action is client-side only — drop the server fallthrough (objectstack#3959) (#2973) + - fix(app-shell)!: the server-action URL identifies an action by `name`, not `target` (ADR-0110 D1) (#2970) + - fix(form): a server rejection that names fields now marks those fields (#2966) + - fix(actions): one source for the /actions envelope rule, and redirectUrl finally works (#2967) + - fix(actions): apply the ADR-0066 D4 capability gate on every action surface (framework#3923) (#2965) + - fix(detail): multi-value lookup is selectable in inline edit (#2957) + - fix(actions): a failed server action no longer reports as success (green toast) (#2963) + - fix(fields): the criteria builder stops calling an empty criteria "All records" (#2962) + - feat(report): carry a report's `order` into the dataset selection (framework#3916) (#2964) + - feat(views): the list toolbar speaks one vocabulary — `userActions` (#2890) (#2948) + + objectui range: `4a4829d0ef39...a136322f8723` + + **Release-critical for v17.** The previous pin (`4a4829d0ef39`) predates the + ADR-0110 D1 client fix, so the console it builds still posts `action.target` + to `/api/v1/actions/:object/:action`. Against a v17 server — which resolves + the declaration by `name` and refuses an unresolvable one (D3) — every + target-bound script action would return 404 from the shipped console. The + lockstep the ADR called for is enforced by THIS pin, not by merging the + objectui PR, so v17 must not ship without this bump. + +- bec0f9a: Console (objectui) backfill for `2cb8d78e24ad...c6cfdf1288b6` — the one refresh in + the v17 window that landed with no changeset. + + `scripts/bump-objectui.sh` emits a `@objectstack/console` changeset on every bump + precisely so a SHA move leaves a trace (see `docs/releases-maintenance.md`). One + bump in this window did not, so 25 commits — including two breaking ones — were + absent from the release history and from the curated v17 page. This entry records + them after the fact; it declares no new SHA move (`.objectui-sha` already points + past this range at `4a4829d0ef39`). + + Frontend changes in this range: + + - feat(react)!: trim dead device/preference delegates from useClientNotifications (objectstack#3612 companion) (#2862) + - feat(types)!: drop the ObjectStack/ObjectOS/ObjectQL/ObjectUI Capabilities re-exports (#2860) + - feat: gate detail/form edit & delete on the server's effective operation set (framework#3546) (#2832) + - feat(app-shell): approver values become record lookups (framework#3508) (#2834) + - feat(console): group tenancy posture affordances — org switcher as write context + org attribution (ADR-0105 Phase 1) (#2858) + - feat(console): i18n the system-settings hub (objectui#2851 P2) (#2859) + - fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework#3574) (#2857) + - fix(grid): validate email format in the import preview (objectstack#3566) (#2840) + - fix(fields): consistent image-field rendering + click-to-zoom (#2836) (#2837) + - fix(app-shell): stop the flow-node repeater from committing during render (#2838) (#2839) + + Plus 15 dependency bumps, three of them major for the Console's own build: + `maplibre-gl` 5→6, `chalk` 5→6, `jsdom` 29→30 (dev). + + objectui range: `2cb8d78e24ad...c6cfdf1288b6` + +### Patch Changes + +- 6fd0786: Console (objectui) refreshed to `e651c936870e`. Frontend changes in this range: + + - fix(app-shell): unwrap the declared response envelope on the datasource page and the api-action runner (objectstack#3843) (#2972) + - fix(actions): read objectstack#3962's single-wrapped /actions responses (#2971) + + objectui range: `a136322f8723...e651c936870e` + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 333592af68..5e8fff93e5 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "description": "Prebuilt Console SPA pinned to this @objectstack/framework release. Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui).", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/console", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index cf8f291c13..4100b2515b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,264 @@ # @objectstack/core +## 17.0.0-rc.1 + +### Minor Changes + +- 32ccb23: feat(spec,core,runtime)!: ADR-0112 batch 1 — one error-code vocabulary, SCREAMING_SNAKE, schema-enforced (#3841) + + Settles #3841 per ADR-0112: the top-level `error.code` vocabulary is + SCREAMING_SNAKE, in two tiers. + + - **`StandardErrorCode` members renamed in place** (`validation_error` → + `VALIDATION_ERROR`, all 53). Breaking for importers that branch on the old + lowercase members; the type name and member _meanings_ are unchanged. + - **New `ERROR_CODE_LEDGER`** (`@objectstack/spec/api`): service-specific codes + (`AUTH_REQUIRED`, `VALIDATION_FAILED`, `ATTACHMENT_DOWNLOAD_DENIED`, …) are + registered per owning package. `ErrorCode` = standard ∪ registered. + - **`ApiErrorSchema.code` is now `ErrorCode`**, not `z.string()` — an + unregistered code fails parse, so the envelope conformance suites assert + values, not just shape. + - **`FieldErrorSchema.code` widened to `z.string()`** (ADR-0112 D6): field-level + codes are a separate vocabulary the enum never described; #3977 owns its real + catalog. + - **Derived codes changed case on the wire**: `standardErrorCodeForHttpStatus` + now yields SCREAMING members (`permission_denied` → `PERMISSION_DENIED`, + `method_not_allowed` → `METHOD_NOT_ALLOWED`, …) — this map was #3842's + designated one-file sweep point for exactly this decision. + - **`ANONYMOUS_DENY_CODE` is `'UNAUTHENTICATED'`** (was `'unauthenticated'`) — + the promoted code on anonymous-denied requests and the REST `enforceAuth` + body change spelling with it. + + `error-catalog.mdx` and the error-handling guides are rewritten to the single + vocabulary; a spec test now locks the catalog page's headings to the enum so + they cannot drift apart again. Remaining lowercase emitters (cloud-connection, + plugin-auth envelope codes, metadata-protocol, …) are the batch-2 sweep. + +- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777) + + A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a + lower bound and was silently wrong for an upper one: the dashboard date-range + filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a + `datetime` column every row created after 00:00 of the `to` day vanished from + the result — no error, the chart renders, the numbers are just smaller. The + default configuration hit it: the filter's default field is `created_at` + (a system-injected `Field.datetime`) and 7 of the 13 presets end "today". + + The translation is operator-sensitive and half-open, applied at every + comparison emitter: + + - `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a + bare-day comparand on a `datetime` column compiles to `< next-day-midnight` + in the column's storage form; `$between [min, max]` with a bare-day max + decomposes to `>= min AND < next-day(max)`. Both the plain and the + legacy-repair (mixed-storage) column paths, both `where` spellings. + - `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day` + instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day. + - The `/analytics/sql` rendering and the dataset preview evaluator apply the + same rule, so the echoed SQL and drafted numbers reproduce execution. + + `@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`: + the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants, + `Date`s and impossible days are never widened). + + Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns + (`<= day` is already whole-day-correct there), full-ISO/`Date` comparands + (instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for + those). No authored metadata changes: a dashboard's existing + `{ $gte, $lte }` window now simply includes its final day. + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +### Patch Changes + +- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D) + + `@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS + write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte` + bound literally. On a `datetime` post-image that meant a policy of the shape + `{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**: + the write-side twin of the read-side data loss #3777 fixed, and the last of the + platform's filter backends that disagreed about what a bare day means as a + bound. + + `$lte` and a `$between` max now evaluate half-open against the next calendar + day, matching the SQL compiler, the memory and mongo drivers, and the analytics + preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep + exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a + plain `YYYY-MM-DD` value compares identically (string ordering makes the two + forms equivalent). The evaluator stays fail-closed on a null bound. + + **Where the rule now lives.** `nextUtcCalendarDay` moved from + `@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`, + whose vocabulary it interprets. `formula` cannot depend on `core`, and a second + copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one + package all six consumers already depend on, so this adds no dependency edge. + + No import changes are required: `@objectstack/core` re-exports the symbol, so + existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working. + New code should prefer `@objectstack/spec/data`. + +- 45dc446: Every in-memory fallback and dev stub now self-describes with the standard `__serviceInfo` descriptor, classified by what it actually is (#4058 step 1). + + ADR-0076 D12 gave services one way to say "I am not the real thing", but the producers never converged on it: + + - The kernel's own fallbacks (`createMemoryCache` / `Queue` / `Job` / `I18n` / `Metadata`) carried `_fallback: true` — a marker **no** consumer recognized, `readServiceSelfInfo` included — so both discovery builders reported them as fully `available`. + - `plugin-dev` marked all of its implementations with the same `_dev: true`, normalized to `status: 'stub', handlerReady: false`. That declared a working in-memory search index exactly as fake as an AI stub returning invented text. + + Both now carry `__serviceInfo`, split by a rule that holds across the whole set: + + - **`degraded`** — really does the work, with reduced capability: `cache`, `queue`, `job`, `file-storage`, `search`, `i18n`, `metadata`, `workflow`, `realtime`. Its answers are true answers; the `message` names what is missing (no persistence, no scheduling timer, no state-machine validation, …). + - **`stub`** — the answer is fabricated: `ai`, `automation`, `notification`, `data`, `auth`, `security.permissions`, `security.rls`, `security.fieldMasker`. Never to be mistaken for a capability. + + `handlerReady: false` is set independently wherever no HTTP handler serves the slot (`cache` / `queue` / `job` / `realtime`, and every `stub`). + + Discovery output changes accordingly — a kernel fallback that used to report `status: 'available'` now reports `degraded` with an explanatory message. No routing, gating, or dispatch behavior changes: every dispatcher domain still resolves services exactly as before. Consumers reading `discovery.services.*` get the truth instead of a uniform claim. + + For anything that duck-typed the old markers: `svc._fallback` / `svc._dev` → `readServiceSelfInfo(svc)` from `@objectstack/spec/api` (the legacy `_dev` key is still understood by that reader, so third-party stubs carrying it keep working). + +- 857a6cf: fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085) + + The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is + a development platform, so it has to start without one — but `os serve +objectstack.config.ts` died during boot whenever the artifact was absent: + + ``` + Loading objectstack.config.ts... + [StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT… + + ✗ Service 'manifest' is async - use await + ``` + + Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with + only its `dist/objectstack.json` moved aside), and on every freshly authored + project between `os init` and its first `os compile`. The message named neither + the missing artifact nor a fix, so it read as an internal kernel fault. + + Three separate faults, each of which alone was enough to refuse the boot: + + - **`serve` registered the config-derived `AppPlugin` before the stack's own + `plugins[]`.** Registration order _is_ the kernel's init/start order, and that + slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and + `DefaultDatasourcePlugin` (which connects the database the app seeds through). + The wrap is now **appended** to `plugins[]`, the same slot + `createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot + and artifact-boot share one plugin order. The artifact path never hit this, + which is exactly what made a plugin-**order** bug look artifact-related. + + - **`ctx.getService()` reported a never-registered service as "is async".** + `PluginLoader.getService` is an `async` method, so its return value is _always_ + a Promise and its internal "not found" rejection can never surface + synchronously — the kernel read the answer off that Promise and told every + caller to `await` a service that did not exist, while the `not found` branch + below it was unreachable. It now decides from the registry: absent ⇒ + `[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged + `Service 'x' is async - use await`. The same crash now reads + `[Kernel] Service 'manifest' not found`, which points at the layer that is + actually wrong. + + - **`MetadataPlugin` treated an absent `local-file` artifact as fatal.** + `createStandaloneStack` always points it at `dist/objectstack.json`, so a stack + with no app at all could not boot. A **missing** local artifact is now "nothing + compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so + a later `os compile` hydrates the running server. The tolerance is + ENOENT-only — a malformed or unreadable artifact stays fatal — and + `bootstrap: 'artifact-only'` (sealed runtime, where the artifact _is_ the + deployment) keeps failing loudly rather than silently serving an empty runtime. + + `[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted + at callers for whom "no artifact" is a healthy state; a present-but-unusable + artifact keeps the loud warning. + + Pinned by an e2e pair that drives the real `os serve` with **no `os compile` + anywhere**: an app defined only by `objectstack.config.ts` (asserting its object + is in the started plugin set, not merely that boot survived) and a bare + `export default {}` platform. The #4012 fixture drops the `os compile` this bug + had forced on it. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index d565ddb9ee..c3a9b093c8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Microkernel Core for ObjectStack", "type": "module", diff --git a/packages/create-objectstack/CHANGELOG.md b/packages/create-objectstack/CHANGELOG.md index 8646908b63..e27b92d4a8 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # create-objectstack +## 17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 77ec11052e..49ef82aa79 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index dccc1e8e0e..4646e98be1 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,127 @@ # @objectstack/formula +## 17.0.0-rc.1 + +### Minor Changes + +- 4965bfa: Warn on flow-node `config` keys the node type does not declare (#4045). + + `FlowNodeSchema.config` is `z.record(z.unknown())`, so a misspelled or invented + config key was accepted in total silence: `visibleIf` instead of `visibleWhen` + registered cleanly, was never read, and the only symptom was a feature that quietly + did not happen. That diagnostic vacuum is what made #3528 take three passes and two + wrong diagnoses to resolve. + + `registerFlow` now compares each node's `config` against its descriptor's + `configSchema` and warns on anything undeclared, located and with the declared set + listed: + + ``` + [flow 'lead_conversion'] node 'screen_1' (screen): unknown config key `visibleIf` + at config.fields[0].visibleIf — It is not declared by this node type's + configSchema, so nothing reads it. Declared here: name, label, type, required, + visibleWhen. + ``` + + The walk descends where the schema declares structure and **stops at free-form + keyValue maps**, whose keys are author data (`filter: { status: 'stale' }`). + Descending matters: the #3528 typo class lives _inside_ the `screen` field + repeater, so a top-level-only comparison would miss the exact mistake this exists + to catch. + + **Warn, never reject.** An undeclared key is an author typo, a key the executor + genuinely reads that its hand-written `configSchema` never declared (`notify.source` + was exactly this), or dead config. Only 4 of the 13 schema-carrying builtins have + been audited for the second population, so hard-failing would gamble on the other + nine. Tightening to an error is a later, per-key decision once this warning has + measured the real distribution. Nothing about the published `configSchema` changes, + so no consumer sees a different shape. + + `@objectstack/formula` now exports `nearestName`, the edit-distance helper already + used for unknown-field and unknown-role suggestions, so "did you mean?" + diagnostics share one threshold. It is deliberately a bonus rather than the + mechanism — `visibleIf` → `visibleWhen` is distance 4 against a threshold of 3, so + the declared set is always listed instead of only as a fallback. + + Also fixes the first real finding from the new check: `showcase_inquiry_purge`'s + `get_record` node carried `mode: 'records'`, which no executor reads, with a comment + crediting it for behaviour that `limit > 1` actually produces. + +### Patch Changes + +- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D) + + `@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS + write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte` + bound literally. On a `datetime` post-image that meant a policy of the shape + `{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**: + the write-side twin of the read-side data loss #3777 fixed, and the last of the + platform's filter backends that disagreed about what a bare day means as a + bound. + + `$lte` and a `$between` max now evaluate half-open against the next calendar + day, matching the SQL compiler, the memory and mongo drivers, and the analytics + preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep + exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a + plain `YYYY-MM-DD` value compares identically (string ordering makes the two + forms equivalent). The evaluator stays fail-closed on a null bound. + + **Where the rule now lives.** `nextUtcCalendarDay` moved from + `@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`, + whose vocabulary it interprets. `formula` cannot depend on `core`, and a second + copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one + package all six consumers already depend on, so this adds no dependency edge. + + No import changes are required: `@objectstack/core` re-exports the symbol, so + existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working. + New code should prefer `@objectstack/spec/data`. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index ebafcbde47..70c160a7a3 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index ffcb84c519..3490025e92 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/lint +## 17.0.0-rc.1 + +### Minor Changes + +- e4c61a7: Validate the expression slots a flow node's `configSchema` declares (#4027). + + A node type's designer `configSchema` and the keys its validators traverse were + two unreconciled lists. Both the engine's `registerFlow` pass and the author-time + `objectstack validate` pass hardcoded `config.condition` / `edge.condition` and + assumed every other node string was a `{var}` template — so a declared expression + property outside that hardcoded set was validated by nobody. + + That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen` + descriptor since #3304, typed `xExpression: 'expression'` (bare CEL) and offered + to authors in Studio, but no validator traversed it. An app authored the + predicate in the _other_ dialect — `'{createOpportunity} == true'` — and it passed + `tsc`, `objectstack validate` and registration in silence. Because `required` _is_ + enforced, a field the author had made conditional rendered unconditionally and + blocked Submit on an input the user was never shown: the run paused forever and no + resume was ever issued. + + Now: + + - **`FLOW_NODE_EXPRESSION_PATHS`** (`@objectstack/spec`) is the declared ledger of + expression-bearing node config paths, each recording the dialect it takes. + - **Both validators read it.** A malformed `visibleWhen` is a located, quoted + error at `registerFlow` _and_ at `objectstack validate` — `node 'screen_1' +(screen) screen field visibleWhen at config.fields[1].visibleWhen`. + - **A reconciliation ratchet** derives the expression properties from the live + descriptors and fails CI in both directions: a new `xExpression` property with + no ledger entry, or a stale entry no descriptor declares. It walks every + registered builtin, not just `screen`. + + Dialects are recorded rather than assumed because there are three, and two of them + disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace + `{var}` flow interpolation (`{…}` is correct), and the ADR-0032 §3 double-brace + text template. Only bare-CEL slots are checked — `loop.collection` and + `map.collection` are recorded as `flow-template` and deliberately left alone, + since no validator implements their dialect and checking them under either of the + other two would reject every currently-valid flow. + + `ActionDescriptor.configSchema`'s TSDoc no longer claims `registerFlow()` + validates `config` against it. It never did: `FlowNodeSchema.config` is + `z.record(z.unknown())`, so types, `required`, `enum` and unknown keys are still + unenforced. The doc now states exactly what is checked and what is designer-facing + only, so nothing relies on a guard that does not exist. + +### Patch Changes + +- 1bd2795: feat(spec,lint): the `ui` vocabularies admit what the renderers implement, and derive instead of restating (objectui#2945) + + Additions-only follow-up to the vocabulary audit + (objectstack-ai/objectui#2901, #2945). Nothing here narrows a vocabulary, so no + already-stored metadata changes meaning — three of the four `ui/` enums that had + drifted from what is actually implemented, plus the fork that drift had made + invisible. + + **`ChartTypeSchema` admits `combo`.** The taxonomy could not name the one chart + family the rest of `chart.zod.ts` is written for: `ChartSeriesSchema.type` + exists to override a series' type — its doc comment literally says _"combo + charts"_ — and `ChartSeriesSchema.yAxis` binds a series to the left or right + axis, which is only meaningful for mixed marks. objectui's renderer draws it + distinctly (mixed bar/line/area on dual axes, per-series type) and had to carry + `combo` in a local fork of this list, whose own comment claimed to mirror it. + + **`WidgetActionTypeSchema` is `ActionType`.** The two disagreed by one member, + `form`, and the disagreement was backwards: a dashboard header or widget action + button dispatches through the same `ActionRunner` that implements `form` — + objectui's `DashboardRenderer` deliberately routes everything except a raw `url` + into it, so a `flow` header action works (#3528). The narrower enum therefore + rejected at validation exactly what the shared dispatcher then executes. + Derived, so the next type the runner implements needs one edit, not two. + + **`ListChartConfigSchema.chartType` is `ChartTypeSchema.extract([...])`.** Same + five members as before — a de-duplication, not a widening. A member renamed in + the taxonomy now fails at build time instead of leaving a second list quietly + disagreeing. + + **`@objectstack/lint`'s chart-family set is derived from the taxonomy.** + `validate-widget-bindings` decides which widgets need a `chartConfig` measure + mapping from a hand-written list of families, and its omissions fail in the + worst direction: an unlisted family reads as _"not a chart"_, so a widget + missing its mapping **passes** validation. `combo` was exactly that case — + verified by pinning the old list back, where a `combo` widget with no + `chartConfig` produced zero findings. The set is now the taxonomy minus an + explicit `MEASURE_EXEMPT_CHART_TYPES` (single-value and tabular families), so a + family added to the spec is covered without editing the rule. + + Guards: `packages/spec/src/ui/vocabulary-derivation.test.ts` asserts both + derivations still hold (a restated list fails silently — it keeps validating, + just not what the other list says), and the lint suite now walks every + multi-series family in the taxonomy rather than a list of its own. + + A third ratchet already existed and did its job: `app-showcase`'s coverage test + requires a gallery widget for every distinctly-renderable `ChartType`, and it + failed the moment `combo` was admitted. The Chart Gallery dashboard now + demonstrates it — a task count as bars on the left axis, an average as a line on + the right, which is the configuration `series[].type` / `series[].yAxis` exist + for. + + `ActionType` deliberately does **not** gain `navigation`, which the audit + suggested. `ActionRunner.executeNavigation` is a strictly weaker + `executeUrl` — no `${param.X}` interpolation, no `apiBase` promotion, no + `openIn` — differing only by a `replace` option, and its one live producer is + the SDUI `element:button` `action` prop, which `ElementButtonPropsSchema` does + not model at all. Promoting the name would add a second spelling of _navigate_ + to a closed authorable vocabulary (members cannot be removed later) without + closing the gap that actually exists. Tracked separately. + + Verified: `@objectstack/spec` **6944 tests / 267 files**, `@objectstack/lint` + **544 tests / 37 files**, both green; `tsc --noEmit` clean on both. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/sdui-parser@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index d960959d6b..1c36784283 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Static, build-time validation for an ObjectStack metadata graph — dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.", "type": "module", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 150e477d15..f98a4a92a8 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,64 @@ # @objectstack/plugin-mcp-server +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index ce0a83c615..fba061a844 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack as an MCP server — exposes your app's objects (and AI tools) over the Model Context Protocol (stdio + Streamable HTTP)", "type": "module", diff --git a/packages/metadata-core/CHANGELOG.md b/packages/metadata-core/CHANGELOG.md index 8cca3e4037..72e673bb06 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,108 @@ # @objectstack/metadata-core +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 8024cfb9f6..ad2472ce36 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Metadata Repository contracts: types, canonicalization, errors, interface (ADR-0008).", "type": "module", diff --git a/packages/metadata-fs/CHANGELOG.md b/packages/metadata-fs/CHANGELOG.md index d872cd2203..585e840a15 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/metadata-fs +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [f5a4ef0] + - @objectstack/metadata-core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 45ed65d848..0397a4559f 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).", "type": "module", diff --git a/packages/metadata-protocol/CHANGELOG.md b/packages/metadata-protocol/CHANGELOG.md index fd650649b4..add0b40627 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,400 @@ # @objectstack/metadata-protocol +## 17.0.0-rc.1 + +### Major Changes + +- 77fadbf: fix(metadata-protocol,objectql)!: retire the degraded analytics shim — the `analytics` slot stays empty without service-analytics (#3891, #3878) + + The protocol assembly (`assembleMetadataProtocol`, used by both + `MetadataProtocolPlugin` and `ObjectQLPlugin`'s built-in mode) used to register + a lightweight `analytics` fallback so `POST /api/v1/analytics/query` kept + answering on installs without `@objectstack/service-analytics`. That fallback + is **removed**, and with it the facade methods that existed only to serve it: + `ObjectStackProtocolImplementation.analyticsQuery` / `getAnalyticsMeta` (the + class no longer implements `AnalyticsProtocol`). + + Why removal instead of repair (#3891): + + - **It dropped the caller's ExecutionContext at the door.** The dispatcher + passes `context.executionContext` (#2852), but the shim's `query` was + unary — aggregation reached `engine.aggregate` with no context, the security + middleware's empty-principal branch waved it through, and **no RLS or tenant + predicate was injected**. An authenticated caller got a 200 with rows RLS + would hide. + - **It ignored the contract filter.** `AnalyticsQuery`'s canonical filter field + is `where`; the shim read only a non-contract `filters` key, so a + spec-conformant filtered request silently returned a full-table aggregate. + - **Every security gate had to be built twice** (#3770 on the shim vs + #3867/#3875 on the real engine) — the "duplicates logic only, harmless" + assessment in ADR-0076 D10 did not survive contact with reality. + + `getDiscovery()` stops hardcoding analytics as an always-on kernel service — + the entry is now computed from the service registry like every other optional + service (`enabled: false, status: 'unavailable'` and **no advertised route** + when absent), which also removes the pre-#2462 discovery lie the shim was + originally invented to make true. + + **Migration.** Deployments that relied on the fallback (programmatic + `createStandaloneStack()` / `createObjectQLKernel()` embeds, hosts whose bundle + doesn't require `analytics`): install `@objectstack/service-analytics` and + mount `AnalyticsServicePlugin` — the real, context-aware engine. Without it, + `/api/v1/analytics/*` now answers **404 ROUTE_NOT_FOUND** (previously: 200 with + unscoped, unfiltered aggregates) and discovery reports + `analytics: { enabled: false, status: 'unavailable' }`. Callers of + `protocol.analyticsQuery(...)` / `protocol.getAnalyticsMeta(...)` must use the + `analytics` service (`kernel.getService('analytics')`) instead. `os serve` + default/full presets and managed environments already force the real engine and + are unaffected. + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- f4d7f1d: fix(metadata-protocol,rest): the id list is the only thing deleteMany can select on (#3897) + + `deleteManyData` built the predicate its endpoint is named after and then spread + the caller's `options` **over** it: + + ```js + return this.engine.delete(request.object, { + where: { id: { $in: request.ids } }, + ...request.options, // ← lands after `where`, so it can replace it + }); + ``` + + `request.options` is caller-supplied — `POST /data/:object/deleteMany` splatted + the whole request body into the protocol request (`{ object, ...req.body }`) — + so one body key rewrote the operation: + + ```json + { "ids": ["a"], "options": { "multi": true, "where": {} } } + ``` + + reached `engine.delete` as an unscoped bulk delete. The engine's write + middleware still composes RLS/sharing predicates onto the AST, so the blast + radius is not automatically the whole table: it is **everything the caller is + allowed to delete**. For an ordinary user with delete permission that is the + difference between the 3 records they asked for and every record they can see; + measured on a stock CRM dev deployment, that payload against one id removed all + 8 rows in the object and returned the raw driver count (`8`). The same spread + also accepted `context`, i.e. a forged principal wherever the route is reachable + without auth. + + **The id set is now authoritative, structurally.** The engine options are built + from the validated id list and nothing else — caller `options` is a + `BatchOptions` bag (`atomic` / `returnRecords` / `continueOnError` / + `validateOnly`) that carries nothing `engine.delete` consumes, so merging it + could only ever smuggle in engine keys. Ids must be scalars, so an operator + object (`{"ids":[{"$ne":null}]}`) cannot reach `where.id` either; a malformed + list is a `400 VALIDATION_FAILED` instead of a wider delete. The REST route + parses the body against `DeleteManyDataRequestSchema` first, one hop earlier — + Zod object schemas strip unknown keys, so `options.where`, top-level `where` and + a body `context` no longer survive the ingress at all. + + **The endpoint also works now.** `deleteManyData` never set `multi`, so a + correctly-formed `{"ids":[…]}` hit the engine's + `'Delete requires an ID or options.multi=true'` throw — only the requests that + triggered the override above ever completed. Deletes now go one id at a time by + primary key, the same shape `batchData`'s `delete` case uses, which closes two + gaps behind that: the bulk branch skips `cascadeDeleteRelations`, so + `deleteBehavior` (`cascade` / `set_null` / `restrict`) was not honoured for the + rows it removed; and the declared `BatchUpdateResponse` contract (per-record + `results`, `atomic`, `continueOnError`) was unimplementable from a bulk row + count. Both are delivered rather than declared. + + **Behaviour change.** The endpoint returns a `BatchUpdateResponse` + (`{ success, operation, total, succeeded, failed, results }`) where it + previously returned the driver's raw delete count — on the paths where it + returned anything at all. The caller's execution context is threaded to every + delete, so RLS/FLS now run under the caller here as they do on the single-record + route. + +- 8d5bb5a: feat(metadata): `saveMeta` persists the operator spellings the spec normalized (objectui#2945) + + `ViewFilterRuleSchema.operator` is `z.preprocess(normalizeFilterOperator, …)`, so + a stored `notEquals` / `gt` / `isNull` is folded to its canonical form during + save-time validation — and then the result was thrown away. `saveMetaItem` + persists the authored body verbatim, deliberately: `parsed.data` strips the + Studio-only auxiliary fields (`isPinned`, `isDefault`, `sortOrder`) that ride + along with an overlay document (ADR-0005 §Validation). + + The consequence is that **every save mints new legacy-alias rows.** The ~30 + entries in `VIEW_FILTER_OPERATOR_ALIASES` are documented as _"a migration bridge + [that] may be dropped in a future major"_, but there is no point at which the + last alias row is behind you, so the bridge can never be dismantled — a + migration that rewrote every existing row would be obsolete the moment the next + console personalization PUT landed. That is prerequisite 2 of the vocabulary + consolidation blocked in objectstack-ai/objectui#2945. + + `graftNormalizedOperators` grafts the normalization back on without giving up the + verbatim body. It walks the authored value and `parsed.data` in lockstep **by + structure** and copies across exactly one thing: an `operator` whose parsed value + differs from the authored one. + + - **No key list to maintain.** `ViewFilterRule[]` appears at five declared sites + today (view `filter`, `ViewTab.filter`, page `filterBy`, and two + `component.zod.ts` block props) and the structural walk covers all of them, + plus any added later. Enumerating paths would have reproduced in this file the + exact duplication #2945 exists to remove. + - **Nothing else moves.** Only an `operator` string is rewritten, and only where + both sides are strings — so a `$`-token `FilterCondition`, a different operator + vocabulary entirely, cannot be reshaped by accident. No key is added, removed, + reordered or defaulted; the unary `{field, operator}` form does not acquire a + `value` even though the schema's own output would give it one. + - **Nothing is allocated when nothing changed**, so a body already written in + canonical form is returned by identity. + + Behaviour change worth stating plainly: a `GET` after a `PUT` now returns the + canonical spelling rather than the one the author sent. That is the spelling the + spec defines, every renderer accepts it (objectstack-ai/objectui#2974, + objectstack-ai/objectui#2989 pinned all three of objectui's translation tables to + the full vocabulary), and it is the point of the change. Existing rows are not + touched — this stops the bleeding, it is not the migration. + + Verified: 11 new tests, including one that drives **every** alias the spec still + folds through the real `ViewMetadataSchema` and asserts the persisted body comes + out canonical; full `@objectstack/metadata-protocol` suite 110 tests / 18 files + green. + +### Patch Changes + +- 4f30943: Both discovery builders now compute the `metadata` service entry from the implementation that fills the slot, instead of hardcoding opposite verdicts for it (#4089). + + `metadata` sat in a "kernel-provided (always available)" block above the loop that reads `__serviceInfo`, hardcoded separately in each builder — and the two disagreed about the same slot: + + - `@objectstack/runtime`'s dispatcher declared it permanently `status: 'degraded'` with `message: 'In-memory registry; DB persistence pending'`, so a stack with `MetadataPlugin` and a real `sys_metadata` table was still reported as having no persistence. + - `@objectstack/metadata-protocol` declared the same slot permanently `status: 'available'`, so the kernel's in-memory fallback (`createMemoryMetadata`, auto-registered when no metadata plugin is present) read exactly like a persisted registry — the `__serviceInfo` marker #4058 gave it went unread here. + + Both now read the registered service's `__serviceInfo` (via `readServiceSelfInfo`) and report what it declares: + + - kernel in-memory fallback, or plugin-dev's dev registry → `status: 'degraded'` plus that implementation's own `message`, which names what is missing and what to install. + - `MetadataPlugin` (or any implementation carrying no marker) → `status: 'available'` with no message. + + `handlerReady: true` is now stated unconditionally on both sides: it answers "is `/api/v1/meta` mounted?", and that route is served by the protocol whichever implementation occupies the slot — a degraded service in it does not unmount the route. Nothing about routing, gating, or dispatch changes; consumers that treat `status` as a capability claim (AI agents, the console) simply stop being told two different things by two hosts. + +- bb192c4: Gate every dispatcher service domain on `handlerReady` instead of on slot occupancy (#4058 step 2). + + #4000 made the `/analytics` domain execute ADR-0076 D12's third conclusion ("consumers treat only `handlerReady: true` as a real capability"); every other domain still gated on "is a service registered", so a self-declared stub occupying `automation` / `notification` / `ai` / `file-storage` / `i18n` was called like a real implementation and its fabricated answer went out as a 200. Step 1 (#4082) made the two kinds of dev implementation distinguishable; this is the gate that reads the distinction. + + - The `/analytics`, `/automation`, `/notifications`, `/ai`, `/storage` and `/i18n` domains, the route-mount gate, discovery's `routes`/`features`, and the metadata-protocol builder's route advertisement now share one predicate (`isServiceServeable`): a slot whose occupant self-declares `handlerReady: false` is answered exactly as an empty slot is — the domain's existing 404, or the explicit 501 `/storage` and `/i18n` use. One predicate, so what is advertised and what is served cannot disagree. + - `handlerReady`, not `status`, is the test. An implementation that declares `degraded` defaults to `handlerReady: true` and keeps serving — which is why the in-memory `file-storage` and `i18n` implementations are unaffected. + - `discovery.services.*` stays presence-gated: a registered stub still reports `{ enabled: true, status: 'stub', handlerReady: false }` (with no `route`), which says strictly more than collapsing it to `unavailable` would. + - `/ai` improves for the stub case: an occupied-but-unserveable slot used to fall through to a 503 "AI service routes not yet initialized" and lose the `GET /ai/agents` empty-list answer the console polls for on every navigation. Both are restored. + + No change for a host whose services are real implementations. If you register your own stub under one of those six slots and relied on the dispatcher calling it, either drop the `handlerReady: false` self-declaration (declare `degraded` if it genuinely serves) or install the real service. Not gated, deliberately: `/data`, `/meta`, `/auth` and the security path — their dev stubs back the dev stack's own core loop, and gating them would 404 the dev stack itself. + +- a4a9944: fix(metadata-protocol): findData must not take its execution context from the request (#3960) + + Came out of the #3946 sweep's leftover question — whether `expand`'s "advanced + usage" (a caller-supplied `Record` whose sub-ASTs each carry an + `object`) is a cross-object read channel. **It is not**, and that needs saying + because the answer is load-bearing: `expandRelatedRecords` takes its target from + the parent schema (the expand KEY must be a real `reference` field; the sub-AST's + `object` is never read), re-enters `engine.find` so the referenced object's RLS + + FLS both run, `$and`-merges a nested `where` instead of spreading it over the id + filter, and caps depth. No change needed there. + + What the investigation did turn up is one layer down. `findData` built its engine + options as `{ ...request.query }` and then assigned `context` from + `request.context` **conditionally**: + + - `request.query` is the caller's raw bag on every ingress — the REST + `POST /data/:object/query` route passes `req.body` straight in as `query`; + - `context` sits in the known-params set, so it was not swept into the + implicit-filter bucket either — it survived the spread untouched; + - so when no server context resolved, the caller's `context` _became_ the + operation's execution context. + + Everything hangs off that value. plugin-security's middleware opens with + `if (opCtx.context?.isSystem) return next()` — the entire RLS / FLS / CRUD chain + skipped — and `__expandRead: true` collects the #2850 waiver on the object-level + CRUD gate. Neither is ever schema-stripped on the read path: + `ExecutionContextSchema.parse` runs only in `engine.createContext`, which reads + do not use. + + Route-level `enforceAuth` is what kept this unreachable: anonymous data requests + are refused unless a deployment sets `requireAuth: false`. That makes it a + fail-OPEN default rather than a live exploit — and not something the protocol + should delegate upward. `findData` now drops any inbound `context` + unconditionally before the assignment, so the execution context can only come + from `request.context`. + + Verified end-to-end at the protocol layer (a forged + `{ isSystem, userId, __expandRead }` reached `engine.find` verbatim before, is + dropped after). The anonymous HTTP reachability half is NOT verified — see #3960 + for exactly what was and was not reproduced. No caller regresses: the only + in-repo builder of these args (`rest/src/import-runner.ts` `findArgsBase`) passes + `context` at the top level, never inside `query`. + +- 627b188: fix(seed-loader): count reference fields dropped from rows that were still written + + The loader had two failure outcomes and only counted one. A record it cannot + write is counted in `errored`. But an unusable **reference value** (an object + where a natural key belongs, an array on a single-value field) is removed from + the record — never written as NULL, which would sever an existing link on + upsert replay — and the row is written **without it**. Nothing counted that. + + So a load that quietly severed N associations reported `totalErrored: 0`, and + every count-driven surface read clean. The CLI boot banner — the one seed signal + that survives `os dev`'s boot-quiet window and the default `warn` level — printed + `showcase 42 rows`, and the warn line said `0 dropped record(s)`: true, and + useless ([#3932](https://github.com/objectstack-ai/objectstack/issues/3932)). + + `SeedLoadResult.referencesDropped` and `SeedLoaderSummary.totalReferencesDropped` + now count it. It is deliberately **not** folded into `errored` — the row _was_ + written, so that would break the `inserted + updated + skipped` reconciliation + against `total`. The banner names it separately: + + ``` + ⚠ Seeds: showcase 42 ok / 3 lost links ⚠ + ``` + + Both counters are additive with a `0` default, so an existing producer or + consumer of `SeedLoaderResult` is unaffected. + +- 8d4eae7: fix(seed-loader): resolve natural-key ARRAYS for multi-value lookups + + A `multiple: true` lookup / `user` field stores an array of ids, so its seed + value is an array of natural keys (`authors: ['Alice', 'Bob']`). Reference + resolution only ever accepted a single string: the array tripped the + "expected a natural-key string but got an object. Pass the target's `name` + value as a plain string" guard — impossible advice for a field that holds + several references — and was then DROPPED from the record. The row landed with + the whole association missing and only a warn in the log + ([#3911](https://github.com/objectstack-ai/objectstack/issues/3911)). + + Every element now resolves independently (in-load records first, then the + database, then pass 2), and the field lands as an array of target ids. A lone + string is accepted as one-element shorthand for the array shape the field + stores. Deferral is all-or-nothing per field — a partially-resolved array is a + corrupt association, so pass 2 re-resolves the whole authored array — and a key + that never materializes is a reported load error naming that element, not a + silent drop. + + An array passed to a genuinely **single-value** reference field is still + rejected, now with advice an author can act on: declare the field + `multiple: true`, or pass one natural key. + + `ReferenceResolution` (`@objectstack/spec/data`) gains an optional `multiple` + flag carrying the field's array-ness into resolution; it is additive and + defaulted-absent, so existing dependency graphs are unaffected. + + **Authoring types.** `defineSeed`'s per-field value type now widens a + `multiple: true` lookup to `string | string[] | null` (a lone string stays legal + — the loader accepts it as one-element shorthand). `master_detail` is inherently + single and is not widened, and an array on a single-value lookup is still a + compile error. To make that reachable, `Field.lookup` became generic over its + config (``) so `multiple: true` survives as a + literal instead of widening to `boolean`; the return type is intersected with + `FieldInput` so its optional surface is unchanged. Type-level only — the + returned object is byte-identical at runtime. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index d85aec4acf..8c75bb71eb 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack metadata management protocol: sys_metadata CRUD, draft/publish, locks, package ownership, diagnostics (ADR-0076).", "type": "module", diff --git a/packages/metadata/CHANGELOG.md b/packages/metadata/CHANGELOG.md index 6800154162..90d3a58f3c 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,157 @@ # @objectstack/metadata +## 17.0.0-rc.1 + +### Minor Changes + +- ffb003c: **ADR-0110 — an action's identity is its `name`, and anything executable over a + governed surface must have a declaration.** + + `POST /api/v1/actions/:object/:action` resolved the DECLARATION from the URL + segment as a `name` but dispatched the HANDLER using that same segment as a + registry key. For a target-bound action (`{ name: 'complete_task', target: +'completeTask' }`) those are different strings, so the two documented callers + each worked on exactly the half the other broke: the documented curl resolved + the declaration then 404ed, while the Console's `target`-addressed call + dispatched fine and resolved no declaration — silently skipping the ADR-0066 D4 + capability gate and the ADR-0104 param contract (#3935). + + - **D1/D2** — identity is always the declarative `name`; the handler key is + derived from the resolved declaration through a rotation now shared with the + MCP `run_action` bridge (`resolveActionHandlerKeys`, `executeRegisteredAction`). + The REST route previously rotated only the object key, never the handler key. + - **D3 (breaking)** — declaration resolution is a trichotomy. A genuinely + undeclared handler is **refused (404)** with the `defineAction` to add, rather + than executed ungated with system privileges; an unreachable metadata plane is + a **503** rather than a silent ungating (`MetadataManager.loadDiagnosed` tells + a clean miss from an outage). `OS_ALLOW_UNDECLARED_ACTIONS=1` is the migration + valve — it warns on every invocation and is removed in 18. + - **D5** — `reconcileActionRegistrations` plus `ObjectQLEngine.listRegisteredActions` + power a `kernel:ready` inventory logging every registered-but-undeclared + handler (refused at dispatch) and every declared script action bound to no + handler — the ADR-0078 converse, mechanised. + - **D6** — security-gate strictness is opt-**out** (`OS_ALLOW_*`), never opt-in. + + Apps whose actions are all declared need no changes beyond gaining enforcement + of the `requiredPermissions` they already declared. + +### Patch Changes + +- 857a6cf: fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085) + + The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is + a development platform, so it has to start without one — but `os serve +objectstack.config.ts` died during boot whenever the artifact was absent: + + ``` + Loading objectstack.config.ts... + [StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT… + + ✗ Service 'manifest' is async - use await + ``` + + Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with + only its `dist/objectstack.json` moved aside), and on every freshly authored + project between `os init` and its first `os compile`. The message named neither + the missing artifact nor a fix, so it read as an internal kernel fault. + + Three separate faults, each of which alone was enough to refuse the boot: + + - **`serve` registered the config-derived `AppPlugin` before the stack's own + `plugins[]`.** Registration order _is_ the kernel's init/start order, and that + slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and + `DefaultDatasourcePlugin` (which connects the database the app seeds through). + The wrap is now **appended** to `plugins[]`, the same slot + `createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot + and artifact-boot share one plugin order. The artifact path never hit this, + which is exactly what made a plugin-**order** bug look artifact-related. + + - **`ctx.getService()` reported a never-registered service as "is async".** + `PluginLoader.getService` is an `async` method, so its return value is _always_ + a Promise and its internal "not found" rejection can never surface + synchronously — the kernel read the answer off that Promise and told every + caller to `await` a service that did not exist, while the `not found` branch + below it was unreachable. It now decides from the registry: absent ⇒ + `[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged + `Service 'x' is async - use await`. The same crash now reads + `[Kernel] Service 'manifest' not found`, which points at the layer that is + actually wrong. + + - **`MetadataPlugin` treated an absent `local-file` artifact as fatal.** + `createStandaloneStack` always points it at `dist/objectstack.json`, so a stack + with no app at all could not boot. A **missing** local artifact is now "nothing + compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so + a later `os compile` hydrates the running server. The tolerance is + ENOENT-only — a malformed or unreadable artifact stays fatal — and + `bootstrap: 'artifact-only'` (sealed runtime, where the artifact _is_ the + deployment) keeps failing loudly rather than silently serving an empty runtime. + + `[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted + at callers for whom "no artifact" is a healthy state; a present-but-unusable + artifact keeps the loud warning. + + Pinned by an e2e pair that drives the real `os serve` with **no `os compile` + anywhere**: an app defined only by `objectstack.config.ts` (asserting its object + is in the started plugin set, not merely that boot survived) and a bare + `export default {}` platform. The #4012 fixture drops the `os compile` this bug + had forced on it. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + - @objectstack/metadata-fs@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index c88185368e..a317c8160a 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Metadata loading, saving, and persistence for ObjectStack", "type": "module", diff --git a/packages/objectql/CHANGELOG.md b/packages/objectql/CHANGELOG.md index 2489b85988..07dd19c3fe 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,368 @@ # @objectstack/objectql +## 17.0.0-rc.1 + +### Major Changes + +- 2d3e255: feat!: ADR-0113 — `required` is a write contract; the column constraint becomes the explicit `storage.notNull` + + `field.required` bound three meanings to one knob (write check, `NOT NULL` DDL, + drift expectation), so tightening any invariant on a deployed object was a + destructive migration blocked by the very legacy nulls that motivated it — the + reason `criteria_json`'s mandatory-in-substance contract lived in three + imperative guards instead of one declaration. + + Split, with the **non-regression invariant** as the unifying rule — _a write + may not take a record from compliant to violating; a pre-existing violation + does not block writes that leave it in place_: + + - `required: true` = the write contract, uniformly on new and deployed objects: + insert must provide; **an update PATCHing `null` into a required field is now + rejected** (it silently passed before); omitted fields never block, so legacy + null rows rest. The column stays nullable. + - `storage: { notNull: true }` = the explicit physical constraint, owning the + DDL (`sql-driver` `createColumn`) and the destructive drift ceremony. + Orthogonal to `required` — all four combinations are legitimate, including + the engine-populated column (`storage.notNull` without `required`). + - `requiredWhen` inherits the same invariant: flipping the condition true + without providing the field is rejected (the write _creates_ the violation); + a row violating since before the rule tightened no longer locks out + unrelated edits (#3929's objection, cured). `storage.notNull` × + `requiredWhen` rejects at parse (`FieldSchema.superRefine`). + - **Pre-17 sources keep their exact meaning** via the migration-chain-only + `field-required-notnull-explicit` conversion: `os migrate meta` stamps + `storage.notNull` onto every previously-required field — writing down what + the old text already meant. The loader never infers semantics from the + physical column. + - Drift compares nullability against `storage.notNull`; a column stricter than + its declaration is `needs_confirm` (never auto-applied — dev auto-reconcile + no longer silently strips a stray `NOT NULL`), and silent when the field is + write-gated by `required`. + +- 77fadbf: fix(metadata-protocol,objectql)!: retire the degraded analytics shim — the `analytics` slot stays empty without service-analytics (#3891, #3878) + + The protocol assembly (`assembleMetadataProtocol`, used by both + `MetadataProtocolPlugin` and `ObjectQLPlugin`'s built-in mode) used to register + a lightweight `analytics` fallback so `POST /api/v1/analytics/query` kept + answering on installs without `@objectstack/service-analytics`. That fallback + is **removed**, and with it the facade methods that existed only to serve it: + `ObjectStackProtocolImplementation.analyticsQuery` / `getAnalyticsMeta` (the + class no longer implements `AnalyticsProtocol`). + + Why removal instead of repair (#3891): + + - **It dropped the caller's ExecutionContext at the door.** The dispatcher + passes `context.executionContext` (#2852), but the shim's `query` was + unary — aggregation reached `engine.aggregate` with no context, the security + middleware's empty-principal branch waved it through, and **no RLS or tenant + predicate was injected**. An authenticated caller got a 200 with rows RLS + would hide. + - **It ignored the contract filter.** `AnalyticsQuery`'s canonical filter field + is `where`; the shim read only a non-contract `filters` key, so a + spec-conformant filtered request silently returned a full-table aggregate. + - **Every security gate had to be built twice** (#3770 on the shim vs + #3867/#3875 on the real engine) — the "duplicates logic only, harmless" + assessment in ADR-0076 D10 did not survive contact with reality. + + `getDiscovery()` stops hardcoding analytics as an always-on kernel service — + the entry is now computed from the service registry like every other optional + service (`enabled: false, status: 'unavailable'` and **no advertised route** + when absent), which also removes the pre-#2462 discovery lie the shim was + originally invented to make true. + + **Migration.** Deployments that relied on the fallback (programmatic + `createStandaloneStack()` / `createObjectQLKernel()` embeds, hosts whose bundle + doesn't require `analytics`): install `@objectstack/service-analytics` and + mount `AnalyticsServicePlugin` — the real, context-aware engine. Without it, + `/api/v1/analytics/*` now answers **404 ROUTE_NOT_FOUND** (previously: 200 with + unscoped, unfiltered aggregates) and discovery reports + `analytics: { enabled: false, status: 'unavailable' }`. Callers of + `protocol.analyticsQuery(...)` / `protocol.getAnalyticsMeta(...)` must use the + `analytics` service (`kernel.getService('analytics')`) instead. `os serve` + default/full presets and managed environments already force the real engine and + are unaffected. + +### Minor Changes + +- 48fcf70: **[ADR-0110 D5] The action-governance inventory moves to the engine plugin — + AppPlugin never ran it on the platform's own dev path.** + + Dogfooding the inventory with a positive control (an injected undeclared + handler) showed the `kernel:ready` hook it hung on never fired under `os dev`: + AppPlugin is registered conditionally (`serve.ts` skips it when the host wraps + itself; the dev fast path loads apps without it), so the checklist that + justifies D3's no-opt-out refusal was never printed where an upgrade most + needs it. + + - The addressing vocabulary (`GLOBAL_ACTION_OBJECT_KEY`, + `actionHandlerObjectKeys`, `isObjectLessActionKey`, + `resolveActionHandlerKeys`) and the reconciliation move into + `@objectstack/objectql` — the engine owns the map they describe, and the + dependency direction (runtime → objectql) permits no other home. + `@objectstack/runtime` re-exports them unchanged, so dispatch, the MCP + bridge and existing importers keep reading ONE implementation. + - `ObjectQLPlugin` now runs the inventory in its existing `kernel:ready` + handler — after `resyncAuthoredActions`, so the audited registry is final — + and again on `metadata:reloaded`, fingerprint-suppressed so a reload that + changed nothing action-related logs nothing. A Studio edit that orphans or + binds a handler updates the report live; the old boot-only snapshot went + stale on the first edit. + - Verified end-to-end with a programmatic kernel: the injected orphan is + named, a clean registry is silent. The `os dev` / `os serve` consoles still + swallow ALL plugin boot logs (pre-existing, tracked separately) — on those + surfaces the inventory becomes visible once that sink is fixed. + +- ffb003c: **ADR-0110 — an action's identity is its `name`, and anything executable over a + governed surface must have a declaration.** + + `POST /api/v1/actions/:object/:action` resolved the DECLARATION from the URL + segment as a `name` but dispatched the HANDLER using that same segment as a + registry key. For a target-bound action (`{ name: 'complete_task', target: +'completeTask' }`) those are different strings, so the two documented callers + each worked on exactly the half the other broke: the documented curl resolved + the declaration then 404ed, while the Console's `target`-addressed call + dispatched fine and resolved no declaration — silently skipping the ADR-0066 D4 + capability gate and the ADR-0104 param contract (#3935). + + - **D1/D2** — identity is always the declarative `name`; the handler key is + derived from the resolved declaration through a rotation now shared with the + MCP `run_action` bridge (`resolveActionHandlerKeys`, `executeRegisteredAction`). + The REST route previously rotated only the object key, never the handler key. + - **D3 (breaking)** — declaration resolution is a trichotomy. A genuinely + undeclared handler is **refused (404)** with the `defineAction` to add, rather + than executed ungated with system privileges; an unreachable metadata plane is + a **503** rather than a silent ungating (`MetadataManager.loadDiagnosed` tells + a clean miss from an outage). `OS_ALLOW_UNDECLARED_ACTIONS=1` is the migration + valve — it warns on every invocation and is removed in 18. + - **D5** — `reconcileActionRegistrations` plus `ObjectQLEngine.listRegisteredActions` + power a `kernel:ready` inventory logging every registered-but-undeclared + handler (refused at dispatch) and every declared script action bound to no + handler — the ADR-0078 converse, mechanised. + - **D6** — security-gate strictness is opt-**out** (`OS_ALLOW_*`), never opt-in. + + Apps whose actions are all declared need no changes beyond gaining enforcement + of the `requiredPermissions` they already declared. + +- 7d7521f: feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) + + Settles the vocabulary ADR-0112 D6 deferred, per [ADR-0114](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0114-field-level-error-code-catalog.md). + + **`FieldErrorCode` — a closed, lowercase catalog.** 27 members covering what the + six emitters already emit. `FieldErrorSchema.code` tightens from `z.string()` to + this enum, so a validation body's per-field codes are validated for the first time. + `FieldValidationError.code` (objectql) and `FieldCoerceError.code` (rest) stop + being a hand-listed union and a bare `string` respectively and reference the + catalog, so the three cannot drift apart. + + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a + top-level code names the condition the _request_ hit, while a field-level code + names the _constraint_ the value violated — and constraints are declared in the + metadata's own snake_case, so `max_length` the code and `max_length: 50` the + property are the same word on purpose. + + **Zod issue codes no longer reach the wire (wire-visible).** Routes that validate + with Zod passed its vocabulary straight through, so `fields[]` spoke a different + language depending on which route served it, and `too_small` was ambiguous between + a short string, a small number and a short array. `zodIssuesToFields` now maps + using Zod's `origin`/`format`: + + | Was | Now | + | :------------------------------------------------ | :------------------------------------------------- | + | `too_small` | `min_length` / `min_value` / `min_items` | + | `too_big` | `max_length` / `max_value` / `max_items` | + | `invalid_format` | `invalid_email` / `invalid_url` / `invalid_format` | + | `invalid_value` | `invalid_option` | + | `unrecognized_keys` | `unknown_field` | + | `invalid_union`, `invalid_element`, `invalid_key` | `invalid_shape` | + + **A missing required property now reports `required`, not `invalid_type`.** Zod + spells "absent" as a type mismatch against `undefined`, so passing it through made + a form mark a _missing_ input as the wrong _type_. The two are indistinguishable on + the issue alone, so the mapper takes the parsed input as an optional argument and + walks the issue path; a caller that cannot supply it keeps `invalid_type` rather + than guessing. + + **`unknown_param` → `unknown_field`.** `ActionParamIssue.code` references the + catalog instead of its own literal union; the `param` key beside it already says + what was addressed. + + **Not changed:** `EnhancedApiErrorSchema.fieldErrors` keeps its name even though + every producer emits `fields`. Retiring an authorable key needs a tombstone plus a + migration (ADR-0104's contract guard), so it lands on its own — the property now + carries a banner saying which name the wire uses. + +- 507b92a: fix(spec,objectql,rest,runtime): field-validation messages answer in the caller's language, named by the field's label (#3957) + + The write path built every built-in validation message by concatenating the **API + field name** into a **hardcoded English** template. Those strings are what the + Console toast, the CSV-import row report, the CLI and any custom client display + verbatim, so a Chinese-locale user importing a bad row read: + + ``` + 第 1 行:penalty_amount must be ≥ 0 + ``` + + …for a field declared `label: '处罚金额'` with a full `zh-CN` bundle loaded. The + form layer localized the _same_ constraint correctly (the browser's native + `min`), so the language flipped depending on which layer caught the value. + + **Three things changed.** + + 1. **The message is rendered in the caller's locale** from a built-in catalog + (`BUILTIN_VALIDATION_MESSAGES`, `@objectstack/spec/system`) shipping `en`, + `zh-CN`, `ja-JP`, `es-ES` — the same four locales as the platform bundles. + The locale comes from `ExecutionContext.locale`, whose contract already read + "Drives message catalogs"; this is the consumer that makes that true. Both + HTTP entries (REST server, runtime dispatcher) now resolve it from the + request's `Accept-Language` / `?locale` first, falling back to the workspace + `localization.locale` — so a rejection message and the field labels around it + can no longer disagree. + + 2. **The field is named by its label, never the API name**: translation bundle + (`objects..fields..label`) → declared `label` → API name as the last + resort. `FieldValidationError.field` still carries the API name so a form can + focus the right input. + + 3. **The constraint is exposed as data**, so a client can format its own text + instead of parsing the sentence: + `{ field, code, message, label, constraint: { min: 0 } }`. This rides + ADR-0114's existing `constraint` / `value` positions on `FieldErrorSchema` + (`constraint` tightens from `unknown` to `Record`) rather + than adding a parallel payload — `label` is the only new field. The bag + carries `min`/`max`/`minLength`/`maxLength`/`actual`/`allowed`/`type`, and the + message templates interpolate from exactly those keys. + + Covered end-to-end, not only in the validator: single and batch insert, + single-id and multi-row update, ADR-0113's clear-out rejection, the object-level + rule evaluator's own built-in messages (`requiredWhen`, per-option gating, + state-machine fallbacks), and the importer's cell-coercion, required pre-check + and #3956 bound pre-check messages — all of which land in the same row report. + + **What this changes for consumers.** + + - `code` is unchanged (ADR-0114's `FieldErrorCode`) and remains the thing to + match on. Message keys are finer-grained than codes — `invalid_datetime`, + `invalid_option_value`, `required_cleared` are rendering detail and never reach + the wire — so localization never splits the client-facing vocabulary. + - `message` **text changes**: it is localized, and it names the field by label + even in English (`Budget must be ≥ 0`, not `budget must be ≥ 0`). Anything + asserting on the old English string should match `code` (and now + `constraint`) instead. + - An author-written validation-rule `message` is never touched — it is already + in the language its author chose. + - A deployment can override any built-in message with a `translation` item + defining `validation.field.` (e.g. + `validation.field.min_value: '{{label}}不得小于 {{min}} 元'`). + - The importer's reference-failure message no longer names the target object's + API name (`no sys_user matches "…"`): naming internal identifiers is the + defect being fixed, and the column plus the offending value are what an + importer can act on. + +### Patch Changes + +- c20b875: **Correct the stale premise left behind by #4012: the degraded-boot stderr copy + survives the operator's LOG LEVEL, not `os serve`'s boot-quiet window.** + + `emitDegradedBootBanner` writes the `OS_ALLOW_DRIVER_CONNECT_FAILURE` banner to + stderr in addition to `logger.warn`, and every comment and test name explaining + why cited the same reason: `os serve` swallowed all of stdout while the kernel + booted, and `Logger` routes `warn` to stdout. #4012 fixed that — the boot window + now buffers and replays `warn`-and-above — which retires the _stated_ + justification for a duplicate that is nonetheless still load-bearing: + + `Logger.write()` returns before touching a stream when the record is below + `config.level`, so at `--log-level error`, `fatal` or `silent` the banner's + `logger.warn` reaches **no** stream at all. A production host at `error` is + exactly the deployment this escape hatch exists for, and exactly where a + logger-only banner would vanish. Removing the stderr copy on the strength of + #4012 would therefore have been a regression — so this documents the reason that + is still true, in the places someone would read before deleting it: + `degraded-boot.ts`, the engine's emit site, and all three parity tests + (objectql, runtime, service-datasource), which are renamed off "which `os serve` + boot-quiet cannot swallow" to "which the operator log level cannot filter away". + + The objectql parity test now proves the claim instead of asserting around it: it + drives a **real** `ObjectLogger` at `level: 'error'` and requires the banner on + stderr _and_ nothing on stdout. Set the level to `warn` and it fails — so the + test is pinned to the level filter rather than passing for any reason. + + Also corrected in the same sweep, all comment-only, all previously overstating + what #4012 had not yet fixed: + + - the automation wiring summary (`format.ts`, `serve.ts`, its test) claimed the + boot window swallowed the engine's binding warnings. Its real justification is + stronger and unchanged: a flow that silently fails to arm emits **no** log line + at any level, so binding state has to be read off the live engine — absence of + a warning was never evidence of a bound flow. + - the seed summary (`seed-summary.ts`, `format.ts`, its test) and `AppPlugin`'s + seed-outcome note attributed the silence to the boot window; the operative + gate is that `SeedLoader`'s result logs are `info`, under the default `warn`. + + No behavior changes. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a4a9944] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [8d5bb5a] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/metadata-protocol@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 24c591a9ee..908955b9c3 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index 91677f1ca8..088e75de9e 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/observability +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 3f0435e626..88ef4ba84d 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.", "type": "module", diff --git a/packages/platform-objects/CHANGELOG.md b/packages/platform-objects/CHANGELOG.md index d3db49087c..7d211a03e1 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,56 @@ # @objectstack/platform-objects +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 603248a27c..6ab5679e97 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/driver-memory/CHANGELOG.md b/packages/plugins/driver-memory/CHANGELOG.md index d0a85df82b..0a859926a3 100644 --- a/packages/plugins/driver-memory/CHANGELOG.md +++ b/packages/plugins/driver-memory/CHANGELOG.md @@ -1,5 +1,197 @@ # @objectstack/driver-memory +## 17.0.0-rc.1 + +### Minor Changes + +- 6f98c2d: fix(driver-sql,driver-memory): an uncompilable filter now throws instead of matching everything (#3948) + + A filter the driver could not compile was **skipped**, not rejected. No predicate + was emitted and the query returned every row — the caller asked to filter and + silently received the unfiltered set. + + The reachable shape is a bare comparison triple. `['close_date','before','2024-01-01']` + arrives at a driver only when `isFilterAST()` refused it — its operator is outside + `VALID_AST_OPERATORS`, so `parseFilterAST()` never converted it and the raw array + was assigned to `where`. `driver-sql`'s loop then saw three _strings_, matched + neither `and` nor `or`, and `continue`d past all three. `driver-memory` was worse: + it cast every string to a logic keyword, opening three empty groups and returning + `{}` — a filter matching every record. + + This is reachable from ordinary authoring, not just malformed input: `before` and + `after` are canonical `VIEW_FILTER_OPERATORS` members that `VALID_AST_OPERATORS` + does not accept. Eight of the nineteen canonical view operators are in that + position, including `equals`; the others were masked only because ObjectUI's + adapter alias table happened to cover them. + + **Behaviour change.** Both drivers now throw on a filter element that is neither a + logical keyword (`and`/`or`) nor a condition array, and `driver-memory` throws on + an operator it cannot express rather than dropping the condition. The nested and + `$`-object paths already threw on the same input, so this makes the three paths + agree. A caller that was relying on the old silence was receiving wrong results; + the error names the operator and the offending filter. + + **`driver-memory` also gains seven operators it silently ignored:** `not_in`, + `is_null`, `is_not_null`, `isnull`, `isnotnull`, `is_empty`, `is_not_empty` — all + members of `VALID_AST_OPERATORS`, all previously falling through to + `default: return null`. `is_null` narrowed nothing instead of matching null rows. + Alias sets and semantics mirror `driver-sql`'s `whereNull`/`whereNotNull` arms so + the two backends accept one vocabulary. + + Migration: none for well-formed filters. If a query now throws, the filter was + never being applied — fix the operator (the message names it), or lower it to an + AST spelling. `before` → `<`, `after` → `>`, `'not in'` → `nin`. + +### Patch Changes + +- b3a2318: fix(driver-memory,driver-mongodb): a bare-day upper bound covers the whole day (#4042) + + The non-SQL half of #3777's calendar-day rule. Both drivers compiled a bare + `YYYY-MM-DD` `$lte` (and a `between` max) as-is, so on timestamp values the + window cut off at the final day's midnight — the dashboard date-range filter's + default configuration (`created_at`, 7 of 13 presets ending "today") lost the + current day, exactly as it did on SQL before #3777 was fixed. + + Both drivers now compile a bare-day upper bound half-open, sharing + `nextUtcCalendarDay` from `@objectstack/core`: + + - `driver-memory`: the Mongo-style and array `where` spellings in the mingo + lowering (`$lte`/`<=` → `$lt` next day; `$between`/`between` max the same), + the analytics cube-filter `lte`, and the analytics `dateRange` window — which + now also matches BOTH stored forms of a timestamp (ISO strings and `Date` + objects) instead of only `Date`s, since mingo compares cross-type as + never-equal. + - `driver-mongodb`: the `translateFilter` lowering, all three spellings + (`$lte`, `$between`, array `<=`/`lte`). + + Unchanged on purpose, matching the #3777 semantics table: full-ISO/`Date` + comparands keep instant semantics, and `$gte`/`$gt`/`$lt` keep their midnight + anchoring. Known remaining gap (tracked separately): values stored as BSON + `Date` (mongodb) or JS `Date` (memory `find()`) never match _string_ comparands + of any operator — a storage-form problem, not a bound-semantics one. + +- 9e8f04d: fix(driver-memory,driver-mongodb): `Field.datetime` has one storage form per driver (#4047) + + The non-SQL counterpart of ADR-0053 D-B (#3912). Both drivers let the writer + decide a datetime value's runtime type, and both compare across types by type + bracket rather than by value — so a string comparand never matched a `Date` + value, in either direction, for **every** operator including `$gte`. + + A datetime column genuinely held both forms: the drivers' own + `created_at`/`updated_at` defaults bind a `Date` (mongo) or an ISO string + (memory), while REST/JSON writes, relative-date tokens and `initialData` + fixtures supply the other. A dashboard date window therefore answered with + whichever half happened to match the comparand's type — on MongoDB, where + `created_at` is a BSON `Date` and dashboard bounds are strings, that meant + **no rows at all**, which is worse than the final-day loss #3777 fixed. + + Each driver now has one canonical form, applied on write and to every filter + comparand: + + | Driver | `datetime` | `date` | + | ---------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------- | + | `driver-mongodb` | BSON `Date` — the dialect's native instant, its `timestamptz` | `YYYY-MM-DD` text | + | `driver-memory` | canonical UTC ISO text (sorts chronologically under the string comparison mingo performs; survives JSON persistence) | `YYYY-MM-DD` text | + + Both learn their temporal fields from `syncSchema`, so an object that was never + declared is left exactly as written — the drivers do not guess types from + values. `driver-memory` additionally converges rows already in the table when + the schema arrives, which catches `initialData` fixtures and anything a + persistence adapter restored (the in-memory analogue of + `backfillCanonicalDatetimes`, and idempotent like it). + + `Field.date` deliberately stays timezone-naive text on both — converting it to + an instant would invent a midnight and re-couple it to a zone. The + calendar-day bound semantics from #3777/#4042 are unchanged and now compose + with the converged storage: the whole-day rewrite runs on the calendar string + first, and only the resulting bound is converted to the storage form. + +- 0166bd5: fix(spec,drivers): the view filter vocabulary and the AST vocabulary now agree (#3948) + + `VIEW_FILTER_OPERATORS` (`ui/view.zod.ts`) is what an author may declare on a + `ViewFilterRule`. `VALID_AST_OPERATORS` (`data/filter.zod.ts`) gates + `isFilterAST()`, which decides whether a filter is parsed into a query at all. + They disagreed on **8 of 19** members: `equals`, `not_equals`, `greater_than`, + `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `before`, `after`. + + An author could declare any of them, `ViewFilterRuleSchema` validated them, + `defineStack` accepted them — and then `isFilterAST()` refused the filter, the + protocol passed the array through unconverted, and the driver could not apply it. + Six of the eight were reachable only in theory because ObjectUI's adapter alias + table happened to translate them; the safety of the query path was resting on a + hand-written table in another repository being complete, and for `before`/`after` + it wasn't. + + **`AST_OPERATOR_MAP` is now the single source of truth.** `VALID_AST_OPERATORS` + is derived from its keys rather than restated, so an operator can no longer be + accepted by the gate without also having a lowering — the two were separate + hand-written lists that happened to agree, with nothing enforcing it. The map + gained the eight canonical view spellings plus the squashed/short forms stored + metadata carries (`notequals`, `greaterthanorequal`, `eq`, `gt`, …). + + **New export `canonicalAstOperator(op)`** folds every accepted spelling of one + comparison onto a single infix form. Both drivers now call it instead of growing + private alias lists, which is what let them accept different vocabularies. + `like`/`ilike` are deliberately not folded onto `contains`: driver-sql passes them + to SQL verbatim, so folding would silently wrap the value in `%…%`. + + Widening only — no spelling was removed, so no stored filter stops validating. + A filter that previously produced an error (after #4029) or was silently dropped + (before it) now compiles. `filter-view-operator-parity.test.ts` asserts every + `VIEW_FILTER_OPERATORS` member and every `VIEW_FILTER_OPERATOR_ALIASES` key has a + lowering that is a real `$`-operator rather than the `$${op}` fallback, so the + next operator the view layer gains fails a test instead of a query. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/driver-memory/package.json b/packages/plugins/driver-memory/package.json index 1786ea7ff0..d94b877453 100644 --- a/packages/plugins/driver-memory/package.json +++ b/packages/plugins/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/plugins/driver-mongodb/CHANGELOG.md b/packages/plugins/driver-mongodb/CHANGELOG.md index 77f2aefb09..0804ac985f 100644 --- a/packages/plugins/driver-mongodb/CHANGELOG.md +++ b/packages/plugins/driver-mongodb/CHANGELOG.md @@ -1,5 +1,124 @@ # @objectstack/driver-mongodb +## 17.0.0-rc.1 + +### Patch Changes + +- b3a2318: fix(driver-memory,driver-mongodb): a bare-day upper bound covers the whole day (#4042) + + The non-SQL half of #3777's calendar-day rule. Both drivers compiled a bare + `YYYY-MM-DD` `$lte` (and a `between` max) as-is, so on timestamp values the + window cut off at the final day's midnight — the dashboard date-range filter's + default configuration (`created_at`, 7 of 13 presets ending "today") lost the + current day, exactly as it did on SQL before #3777 was fixed. + + Both drivers now compile a bare-day upper bound half-open, sharing + `nextUtcCalendarDay` from `@objectstack/core`: + + - `driver-memory`: the Mongo-style and array `where` spellings in the mingo + lowering (`$lte`/`<=` → `$lt` next day; `$between`/`between` max the same), + the analytics cube-filter `lte`, and the analytics `dateRange` window — which + now also matches BOTH stored forms of a timestamp (ISO strings and `Date` + objects) instead of only `Date`s, since mingo compares cross-type as + never-equal. + - `driver-mongodb`: the `translateFilter` lowering, all three spellings + (`$lte`, `$between`, array `<=`/`lte`). + + Unchanged on purpose, matching the #3777 semantics table: full-ISO/`Date` + comparands keep instant semantics, and `$gte`/`$gt`/`$lt` keep their midnight + anchoring. Known remaining gap (tracked separately): values stored as BSON + `Date` (mongodb) or JS `Date` (memory `find()`) never match _string_ comparands + of any operator — a storage-form problem, not a bound-semantics one. + +- 9e8f04d: fix(driver-memory,driver-mongodb): `Field.datetime` has one storage form per driver (#4047) + + The non-SQL counterpart of ADR-0053 D-B (#3912). Both drivers let the writer + decide a datetime value's runtime type, and both compare across types by type + bracket rather than by value — so a string comparand never matched a `Date` + value, in either direction, for **every** operator including `$gte`. + + A datetime column genuinely held both forms: the drivers' own + `created_at`/`updated_at` defaults bind a `Date` (mongo) or an ISO string + (memory), while REST/JSON writes, relative-date tokens and `initialData` + fixtures supply the other. A dashboard date window therefore answered with + whichever half happened to match the comparand's type — on MongoDB, where + `created_at` is a BSON `Date` and dashboard bounds are strings, that meant + **no rows at all**, which is worse than the final-day loss #3777 fixed. + + Each driver now has one canonical form, applied on write and to every filter + comparand: + + | Driver | `datetime` | `date` | + | ---------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------- | + | `driver-mongodb` | BSON `Date` — the dialect's native instant, its `timestamptz` | `YYYY-MM-DD` text | + | `driver-memory` | canonical UTC ISO text (sorts chronologically under the string comparison mingo performs; survives JSON persistence) | `YYYY-MM-DD` text | + + Both learn their temporal fields from `syncSchema`, so an object that was never + declared is left exactly as written — the drivers do not guess types from + values. `driver-memory` additionally converges rows already in the table when + the schema arrives, which catches `initialData` fixtures and anything a + persistence adapter restored (the in-memory analogue of + `backfillCanonicalDatetimes`, and idempotent like it). + + `Field.date` deliberately stays timezone-naive text on both — converting it to + an instant would invent a midnight and re-couple it to a zone. The + calendar-day bound semantics from #3777/#4042 are unchanged and now compose + with the converged storage: the whole-day rewrite runs on the calendar string + first, and only the resulting bound is converted to the storage form. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/driver-mongodb/package.json b/packages/plugins/driver-mongodb/package.json index e60354acde..f9f93fc35d 100644 --- a/packages/plugins/driver-mongodb/package.json +++ b/packages/plugins/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/plugins/driver-sql/CHANGELOG.md b/packages/plugins/driver-sql/CHANGELOG.md index 669a079c2b..08f0cc20f9 100644 --- a/packages/plugins/driver-sql/CHANGELOG.md +++ b/packages/plugins/driver-sql/CHANGELOG.md @@ -1,5 +1,472 @@ # @objectstack/driver-sql +## 17.0.0-rc.1 + +### Major Changes + +- 2d3e255: feat!: ADR-0113 — `required` is a write contract; the column constraint becomes the explicit `storage.notNull` + + `field.required` bound three meanings to one knob (write check, `NOT NULL` DDL, + drift expectation), so tightening any invariant on a deployed object was a + destructive migration blocked by the very legacy nulls that motivated it — the + reason `criteria_json`'s mandatory-in-substance contract lived in three + imperative guards instead of one declaration. + + Split, with the **non-regression invariant** as the unifying rule — _a write + may not take a record from compliant to violating; a pre-existing violation + does not block writes that leave it in place_: + + - `required: true` = the write contract, uniformly on new and deployed objects: + insert must provide; **an update PATCHing `null` into a required field is now + rejected** (it silently passed before); omitted fields never block, so legacy + null rows rest. The column stays nullable. + - `storage: { notNull: true }` = the explicit physical constraint, owning the + DDL (`sql-driver` `createColumn`) and the destructive drift ceremony. + Orthogonal to `required` — all four combinations are legitimate, including + the engine-populated column (`storage.notNull` without `required`). + - `requiredWhen` inherits the same invariant: flipping the condition true + without providing the field is rejected (the write _creates_ the violation); + a row violating since before the rule tightened no longer locks out + unrelated edits (#3929's objection, cured). `storage.notNull` × + `requiredWhen` rejects at parse (`FieldSchema.superRefine`). + - **Pre-17 sources keep their exact meaning** via the migration-chain-only + `field-required-notnull-explicit` conversion: `os migrate meta` stamps + `storage.notNull` onto every previously-required field — writing down what + the old text already meant. The loader never infers semantics from the + physical column. + - Drift compares nullability against `storage.notNull`; a column stricter than + its declaration is `needs_confirm` (never auto-applied — dev auto-reconcile + no longer silently strips a stray `NOT NULL`), and silent when the field is + write-gated by `required`. + +### Minor Changes + +- c8124e5: fix(driver-sql): give `Field.datetime` one UTC storage form per dialect (#3912, #3942) + + Any window filter on a `Field.datetime` column returned an empty set on SQLite — + a dashboard `dateRange: last_30_days` on `created_date` read 0 while 29 matching + rows existed. + + There was never a storage _convention_, only a description of what better-sqlite3 + happened to do with a bound JS `Date`. Nothing enforced it — `formatInput` + deliberately left `datetime` untouched — so the form was decided by whichever + writer got there first: a JS `Date` landed as INTEGER epoch ms, while a REST/JSON + write (JSON has no `Date` type), a `defaultValue: 'NOW()'` slot, and the + platform's own `created_at` / `updated_at` all landed as ISO **TEXT**. One column + held both forms while the read path coerced comparands to epoch ms purely from + the _declared_ type. On SQLite's type ordering (`INTEGER < TEXT`) a two-sided + window collapsed to zero rows, and a one-sided `>=` matched every TEXT row + regardless of the bound. + + `Field.datetime` now has one canonical instant per dialect, produced by one + function applied on write **and** to every filter comparand, so the two sides of + a comparison cannot disagree about shape: + + - **SQLite** — `YYYY-MM-DDTHH:MM:SS.sssZ` text. Lexicographic order _is_ + chronological order, so range filters and `ORDER BY` read the column directly + and can use an index; `strftime` parses it, so the date-bucket expression needs + no CASE. + - **Postgres** — `timestamptz`, unchanged. The fix here is on the write and + comparand side: a zone-naive write was previously resolved against the + _server's_ timezone (measured 8 hours off on `Asia/Shanghai`), and an + un-anchored `YYYY-MM-DD` comparand meant the server's local midnight, so the + identical query over the identical instant landed a row on a different calendar + day than SQLite did. + - **MySQL** — `DATETIME(3)` instead of `TIMESTAMP`, a connection pinned to UTC on + both the mysql2 and the server layer, and a MySQL-spelled bind carrying the + same UTC wall clock. MySQL accepts neither the `T` separator nor the `Z` suffix + in a datetime literal, so datetime writes over REST had always failed outright; + `TIMESTAMP` additionally truncated milliseconds and could not store an instant + outside 1970..2038. + + Existing rows converge at schema sync. Both migrations are allowed to fail: they + log, mark nothing, and the read paths keep a repair expression, so an un-migrated + column still compares and buckets **correctly** — just unindexed. Neither can + repair instants the old timezone-ambiguous write path recorded wrongly; they + preserve what is on disk. + + Also closes #3928 (datetime `ORDER BY` mis-sorted on mixed storage) by + construction. Rationale is recorded as ADR-0053 addendum D-B1..D-B4. + + The analytics change is additive: a `coerceTemporalFilterColumn` companion to the + existing `coerceTemporalFilterValue` hook, so a raw-SQL strategy can normalise the + column side too. Absent hook → byte-identical SQL. + +- 9774b78: fix(driver-sql): `Field.time` gets a canonical storage form — `HH:MM:SS[.fff]` wall-clock text on every dialect (#3994) + + `Field.time` repeated the pre-#3912 `Field.datetime` pattern: writes were never + normalised and only reads were repaired, so one SQLite column accumulated bare + time-of-day TEXT, full-timestamp TEXT and INTEGER epoch ms side by side. + `find()` looked right; everything that compared the STORED form was wrong — + measured: a business-hours window filter silently dropped 4 of 7 rows, ORDER BY + sorted 14:30 before 08:00, a full-ISO write failed the statement outright on + both Postgres and MySQL, a bound `Date` stored a process-timezone wall clock on + pg, MySQL's bare `TIME` rounded `…00.500` up to `…01`, and a `NOW()` default + resolved against three different clocks on the three dialects. + + The #3912→#3942→#3954 construction, transplanted (ADR-0053 D-C1..D-C3): + + - One `canonicalTimeOfDay` — `HH:MM:SS`, `.fff` only when non-zero; `Date`/ + epoch/full-timestamp fold to the UTC time-of-day — applied on write + (`formatInput`), to filter comparands (`coerceFilterValue`, and thereby the + `temporalFilterValue` contract hook) and on read (`toTimeOnly`). + - SQLite: legacy columns converge at schema sync (`backfillCanonicalTimes`, + same `IS NOT`-guarded UPDATE, same log-and-swallow policy); until then the + filter paths wrap the column in the repair expression — correct, just + unindexed. `os migrate plan` lists the work as `normalize_time_storage` with + a row count. + - MySQL: new time columns are `TIME(3)`; legacy `TIME(0)` columns widen at + schema sync (`migrateMysqlTimeColumns`, plan kind `widen_time_columns`), + since zero-precision TIME _rounds_ fractional writes. + - `NOW()` defaults read the UTC clock on every dialect (Postgres previously + used the server zone, MySQL the inserting session's zone — and MySQL 8.0 + rejects a plain `CURRENT_TIMESTAMP` default on TIME entirely). + - `distinct()`/`aggregate()` present time columns exactly as `find()` does. + + `HH:MM:SS` writes round-trip byte-identically (the field-zoo `f_time` + contract); a minutes-only `HH:MM` now completes to `HH:MM:00`, and uninterpretable + values still pass through untouched. + +- 33a5ff4: `os migrate` no longer touches the database before you confirm, and refuses a + SQLite database another process is using (#3917). + + **Nothing is written before the prompt.** `plan` called itself a dry run and + `apply` gated on `[y/N]`, but both booted the full plugin set first — and boot + schema-sync issued create-table/add-column DDL (plus the artifact's inline seed + wrote rows) against the target database before either promise was kept. + `SqlDriver` gains `setDeferredDdl` / `previewDeferredSchemaWork` / + `flushDeferredSchemaDdl`: while armed, `initObjects` still registers every + in-memory map drift detection depends on but records the physical work instead + of performing it. Both commands boot with it armed, render the held-back work + as a `New (additive)` section of the plan, and `apply` performs it only after + confirmation. `os meta resync` / `os migrate files-to-references` keep the old + behaviour — they need the tables to exist. + + **Occupancy check.** A live `os dev`/`os serve` holding the same SQLite file is + the usual way a migration goes wrong: the migration is transactional and swaps + tables inside the file, but the running server keeps prepared statements and a + schema cookie the migration invalidates. `os migrate` now probes the target + before booting — `PRAGMA locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` under + `busy_timeout = 0`, which reports `SQLITE_BUSY` when another connection is + _attached_, not merely writing. (`wal_checkpoint(TRUNCATE)` only sees an active + writer, and `-wal`/`-shm` presence cannot tell a live server from a crashed one; + both are encoded as tests.) `apply` refuses with exit 1 — `error: database_busy` + under `--json` — unless the new `--force` flag is passed; `plan` warns and + continues, since it writes nothing either way. SQLite only: Postgres and MySQL + take their own server-side locks. + + `@objectstack/runtime` also exports `resolveStandaloneDatabase()`, so a caller + can resolve the database target with the same precedence the boot uses without + building the stack, and `createStandaloneStack` accepts `skipSeedData`. + +- 9e01213: fix(cli,driver-sql): `os migrate plan` lists the datetime storage convergence (#3954) + + The datetime canonicalisation (#3912/#3942) added two steps to `initObjects`' + physical path: a row-rewriting backfill on SQLite and a `TIMESTAMP` → + `DATETIME(3)` column rebuild on MySQL. Both already respected the DDL deferral, + so `plan` performed neither and `apply` performed both — the behaviour was never + wrong. The reporting was. + + `PendingSchemaWork` could only express `create_table` / `add_columns`, so an + operator saw a plan listing two added columns, confirmed it, and `apply` + additionally rewrote every row of a datetime column — or took a metadata lock to + rebuild one on a large table. The plan promises to show what apply will do. + + - `PendingSchemaWork.kind` gains `normalize_datetime_storage` and + `widen_datetime_columns`, plus an optional `rows` carrying how much data the + step touches: row-writes for the backfill, the table's size for the rebuild — + the number that decides "now" versus "in a maintenance window". + - `previewDeferredSchemaWork()` measures both without performing either, reusing + the exact predicate each migration uses (the backfill's whole `WHERE`, the + widening's own `information_schema` filter) so the plan and the apply cannot + name different sets. A probe that cannot run is swallowed to "unlisted", never + to a failed plan. + - The CLI renders them under their own heading rather than folding them into the + additive section, whose "created when you apply" framing carries an implicit + promise that the work is never data-losing. `summarizePendingSchemaWork` — the + line read just before typing `y` — never omits in-place work. + +- c53aa53: File-backed SQLite now runs `journal_mode = WAL` (#3941). + + `SqlDriver.connect()` set `auto_vacuum` and left the journal mode alone, so + every ObjectStack SQLite database ran SQLite's built-in default — a rollback + journal. That is the worst mode for the shape this platform actually has, which + is **several processes on one file**: a dev server, `os migrate`, + `os meta resync`, a test run. Measured, on the same file: + + | | rollback journal | WAL | + | :--------------------------------------------- | :------------------------------------------------- | :---------------------------------------------------------------- | + | writer while another process holds a read open | `SQLITE_BUSY` — committing needs an exclusive lock | proceeds | + | idle attached connection visible to SQL | no — a lock lasts only as long as its transaction | yes (`locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` reports busy) | + + The second row is why the `os migrate` occupancy check had to inspect file + descriptors to see a live server at all (#3940): under a rollback journal there + was nothing in the database to see. That signal stays — it names the process, + which WAL's lock probe cannot — but the SQL probe is now authoritative for + databases ObjectStack created rather than a fallback that was blind in practice. + Concurrent _writers_ still serialize; SQLite allows one at a time in any mode. + + Journal mode is a persistent property of the file, so an existing database is + converted in place on the next connect (a header change — no rows are touched) + and stays converted. Two consequences to plan for: + + - `app.db-wal` / `app.db-shm` exist beside the database while a connection is + attached, and `app.db-wal` can hold committed transactions. A clean shutdown + checkpoints them away; a naive copy of `app.db` alone while a server runs does + not. Use `sqlite3 app.db ".backup …"`. + - **WAL does not work on network filesystems** (NFS/SMB). Opt out with + `OS_DATABASE_SQLITE_JOURNAL_MODE=delete`, or per datasource with + `sqliteJournalMode: 'delete'` in the driver config (which outranks the env + var). Either form _applies_ `delete`, so it also converts a database that + already adopted WAL back — skipping would have stranded it. + + Nothing here fails a boot, and nothing is assumed: `PRAGMA journal_mode = X` + answers with the mode actually in force rather than raising on refusal, so the + reply is read back; and because a filesystem can accept WAL and then fail the + first read _through_ it, the mode is proven with a read and rolled back to + `delete` if that fails — with a warning naming the file and the escape hatch. + `synchronous` is untouched, so durability is exactly what it was. `:memory:` + databases are left alone, as is `auto_vacuum = INCREMENTAL`, which keeps + reclaiming under WAL (ADR-0057). + + `os db clean` now counts `-wal` / `-shm` as part of the database when it measures + what a `VACUUM` reclaimed, so bytes that were sitting in the log do not read as a + reclaim of zero. + + `@objectstack/driver-sqlite-wasm` deliberately stays out of WAL. Its live + database is in the WASM heap and what reaches disk is a byte image it exports, so + nothing reads the database across processes and the pragma buys it nothing — + while still being a persistent header change in the operator's file. sql.js + _accepts_ the pragma (its VFS is memory-backed), so this had to be declared + rather than discovered. + + It also now parks a `-wal` left behind by an unclean native-driver exit rather + than loading the image beside it: wasm SQLite cannot read that log, and leaving + it next to a freshly rewritten image would let a later real SQLite replay frames + that no longer belong to it. The warning names the file it parked and how to + recover what was in it. + +### Patch Changes + +- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777) + + A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a + lower bound and was silently wrong for an upper one: the dashboard date-range + filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a + `datetime` column every row created after 00:00 of the `to` day vanished from + the result — no error, the chart renders, the numbers are just smaller. The + default configuration hit it: the filter's default field is `created_at` + (a system-injected `Field.datetime`) and 7 of the 13 presets end "today". + + The translation is operator-sensitive and half-open, applied at every + comparison emitter: + + - `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a + bare-day comparand on a `datetime` column compiles to `< next-day-midnight` + in the column's storage form; `$between [min, max]` with a bare-day max + decomposes to `>= min AND < next-day(max)`. Both the plain and the + legacy-repair (mixed-storage) column paths, both `where` spellings. + - `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day` + instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day. + - The `/analytics/sql` rendering and the dataset preview evaluator apply the + same rule, so the echoed SQL and drafted numbers reproduce execution. + + `@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`: + the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants, + `Date`s and impossible days are never widened). + + Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns + (`<= day` is already whole-day-correct there), full-ISO/`Date` comparands + (instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for + those). No authored metadata changes: a dashboard's existing + `{ $gte, $lte }` window now simply includes its final day. + +- 42e3b01: fix(driver-sql): `Field.date` + `defaultValue: 'NOW()'` records the UTC calendar day on Postgres/MySQL (#4022) + + The bare `CURRENT_TIMESTAMP` default resolved the calendar day in the SERVER's + timezone on Postgres — measured: a UTC-12 server recorded yesterday; an + Asia/Shanghai server records tomorrow for every default after 16:00 UTC — and + MySQL 8.0 rejects it on a DATE column outright (MariaDB is merely permissive, + and the driver's UTC-pinned session masked the semantic half there). + `nowColumnDefault` now emits a UTC expression default on both dialects, the + #3994 D-C3 construction one type over. Defaults only govern newly created + columns; existing columns keep their legacy default, per the standing D-B3 + policy. + +- 39eb01b: fix(driver-sql): a currently-declared unique index is never legacy debt — index drift no longer ping-pongs (#3955) + + An object may declare both a tenant-scoped field-level `unique: true` and an + object-level single-column unique index on the same column: + + ```ts + email: Field.email({ unique: true }), + indexes: [{ fields: ['email'], unique: true }], + ``` + + The declared index materializes under `buildIndexName` as + `uniq__` — which is also one of the two spellings + `legacyUniqueIndexNames` looks for when hunting pre-#3696 platform-wide + uniques. The detector therefore read an index the current metadata declares + as legacy debt and proposed replacing it with the tenant composite (which + the same sync had already created). + + The resulting plan never converged: `apply` dropped the declared index, the + next `plan` reported it missing and recreated it, and the one after that + called it legacy again — an unbounded drop/create cycle on a live unique + index, every round rendered as a "safe" change. + + `legacyUniqueReplacements` now takes the object's `declaredIndexes` and + filters their normalized names out of the legacy candidate set, so an index + metadata declares today is never mistaken for debt. Genuinely legacy indexes + are still retired, including the knex-spelled `
__unique` when + only the `uniq_…` spelling is declared. + +- 6f98c2d: fix(driver-sql,driver-memory): an uncompilable filter now throws instead of matching everything (#3948) + + A filter the driver could not compile was **skipped**, not rejected. No predicate + was emitted and the query returned every row — the caller asked to filter and + silently received the unfiltered set. + + The reachable shape is a bare comparison triple. `['close_date','before','2024-01-01']` + arrives at a driver only when `isFilterAST()` refused it — its operator is outside + `VALID_AST_OPERATORS`, so `parseFilterAST()` never converted it and the raw array + was assigned to `where`. `driver-sql`'s loop then saw three _strings_, matched + neither `and` nor `or`, and `continue`d past all three. `driver-memory` was worse: + it cast every string to a logic keyword, opening three empty groups and returning + `{}` — a filter matching every record. + + This is reachable from ordinary authoring, not just malformed input: `before` and + `after` are canonical `VIEW_FILTER_OPERATORS` members that `VALID_AST_OPERATORS` + does not accept. Eight of the nineteen canonical view operators are in that + position, including `equals`; the others were masked only because ObjectUI's + adapter alias table happened to cover them. + + **Behaviour change.** Both drivers now throw on a filter element that is neither a + logical keyword (`and`/`or`) nor a condition array, and `driver-memory` throws on + an operator it cannot express rather than dropping the condition. The nested and + `$`-object paths already threw on the same input, so this makes the three paths + agree. A caller that was relying on the old silence was receiving wrong results; + the error names the operator and the offending filter. + + **`driver-memory` also gains seven operators it silently ignored:** `not_in`, + `is_null`, `is_not_null`, `isnull`, `isnotnull`, `is_empty`, `is_not_empty` — all + members of `VALID_AST_OPERATORS`, all previously falling through to + `default: return null`. `is_null` narrowed nothing instead of matching null rows. + Alias sets and semantics mirror `driver-sql`'s `whereNull`/`whereNotNull` arms so + the two backends accept one vocabulary. + + Migration: none for well-formed filters. If a query now throws, the filter was + never being applied — fix the operator (the message names it), or lower it to an + AST spelling. `before` → `<`, `after` → `>`, `'not in'` → `nin`. + +- 3fe0ff1: fix(driver-sql): `os migrate plan` no longer promises columns the apply can never create (#3978) + + `previewDeferredSchemaWork()` listed every declared field name when computing + pending `create_table` / `add_columns` work, but `createColumn` returns early + for a virtual `formula` field — no column is ever created for it. + + So a formula field showed up as pending `add_columns` that `apply` reported as + performed without doing anything, and the very next `plan` reported it again. + A freshly-applied database looked permanently un-migrated, with no invocation + able to clear the finding. On `examples/app-crm` that was 4 columns + (`crm_contact.full_name`, `crm_lead.is_closed`, `crm_opportunity.expected_revenue`, + `crm_opportunity.days_to_close`) reported forever. + + The preview now filters through `fieldHasColumn` — the same helper `createColumn` + and the column differ already answer "does this field materialize a column?" + with — so the plan and the flush cannot disagree. `multiple` fields are + unaffected: they materialize as a JSON column and are still reported. + +- 0166bd5: fix(spec,drivers): the view filter vocabulary and the AST vocabulary now agree (#3948) + + `VIEW_FILTER_OPERATORS` (`ui/view.zod.ts`) is what an author may declare on a + `ViewFilterRule`. `VALID_AST_OPERATORS` (`data/filter.zod.ts`) gates + `isFilterAST()`, which decides whether a filter is parsed into a query at all. + They disagreed on **8 of 19** members: `equals`, `not_equals`, `greater_than`, + `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `before`, `after`. + + An author could declare any of them, `ViewFilterRuleSchema` validated them, + `defineStack` accepted them — and then `isFilterAST()` refused the filter, the + protocol passed the array through unconverted, and the driver could not apply it. + Six of the eight were reachable only in theory because ObjectUI's adapter alias + table happened to translate them; the safety of the query path was resting on a + hand-written table in another repository being complete, and for `before`/`after` + it wasn't. + + **`AST_OPERATOR_MAP` is now the single source of truth.** `VALID_AST_OPERATORS` + is derived from its keys rather than restated, so an operator can no longer be + accepted by the gate without also having a lowering — the two were separate + hand-written lists that happened to agree, with nothing enforcing it. The map + gained the eight canonical view spellings plus the squashed/short forms stored + metadata carries (`notequals`, `greaterthanorequal`, `eq`, `gt`, …). + + **New export `canonicalAstOperator(op)`** folds every accepted spelling of one + comparison onto a single infix form. Both drivers now call it instead of growing + private alias lists, which is what let them accept different vocabularies. + `like`/`ilike` are deliberately not folded onto `contains`: driver-sql passes them + to SQL verbatim, so folding would silently wrap the value in `%…%`. + + Widening only — no spelling was removed, so no stored filter stops validating. + A filter that previously produced an error (after #4029) or was silently dropped + (before it) now compiles. `filter-view-operator-parity.test.ts` asserts every + `VIEW_FILTER_OPERATORS` member and every `VIEW_FILTER_OPERATOR_ALIASES` key has a + lowering that is a real `$`-operator rather than the `$${op}` fallback, so the + next operator the view layer gains fails a test instead of a query. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/driver-sql/package.json b/packages/plugins/driver-sql/package.json index 78c9f6c315..dbcaff6c22 100644 --- a/packages/plugins/driver-sql/package.json +++ b/packages/plugins/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/plugins/driver-sqlite-wasm/CHANGELOG.md b/packages/plugins/driver-sqlite-wasm/CHANGELOG.md index ef92e45276..57199a7d1e 100644 --- a/packages/plugins/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/plugins/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,160 @@ # @objectstack/driver-sqlite-wasm +## 17.0.0-rc.1 + +### Patch Changes + +- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777) + + A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a + lower bound and was silently wrong for an upper one: the dashboard date-range + filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a + `datetime` column every row created after 00:00 of the `to` day vanished from + the result — no error, the chart renders, the numbers are just smaller. The + default configuration hit it: the filter's default field is `created_at` + (a system-injected `Field.datetime`) and 7 of the 13 presets end "today". + + The translation is operator-sensitive and half-open, applied at every + comparison emitter: + + - `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a + bare-day comparand on a `datetime` column compiles to `< next-day-midnight` + in the column's storage form; `$between [min, max]` with a bare-day max + decomposes to `>= min AND < next-day(max)`. Both the plain and the + legacy-repair (mixed-storage) column paths, both `where` spellings. + - `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day` + instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day. + - The `/analytics/sql` rendering and the dataset preview evaluator apply the + same rule, so the echoed SQL and drafted numbers reproduce execution. + + `@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`: + the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants, + `Date`s and impossible days are never widened). + + Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns + (`<= day` is already whole-day-correct there), full-ISO/`Date` comparands + (instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for + those). No authored metadata changes: a dashboard's existing + `{ $gte, $lte }` window now simply includes its final day. + +- c53aa53: File-backed SQLite now runs `journal_mode = WAL` (#3941). + + `SqlDriver.connect()` set `auto_vacuum` and left the journal mode alone, so + every ObjectStack SQLite database ran SQLite's built-in default — a rollback + journal. That is the worst mode for the shape this platform actually has, which + is **several processes on one file**: a dev server, `os migrate`, + `os meta resync`, a test run. Measured, on the same file: + + | | rollback journal | WAL | + | :--------------------------------------------- | :------------------------------------------------- | :---------------------------------------------------------------- | + | writer while another process holds a read open | `SQLITE_BUSY` — committing needs an exclusive lock | proceeds | + | idle attached connection visible to SQL | no — a lock lasts only as long as its transaction | yes (`locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` reports busy) | + + The second row is why the `os migrate` occupancy check had to inspect file + descriptors to see a live server at all (#3940): under a rollback journal there + was nothing in the database to see. That signal stays — it names the process, + which WAL's lock probe cannot — but the SQL probe is now authoritative for + databases ObjectStack created rather than a fallback that was blind in practice. + Concurrent _writers_ still serialize; SQLite allows one at a time in any mode. + + Journal mode is a persistent property of the file, so an existing database is + converted in place on the next connect (a header change — no rows are touched) + and stays converted. Two consequences to plan for: + + - `app.db-wal` / `app.db-shm` exist beside the database while a connection is + attached, and `app.db-wal` can hold committed transactions. A clean shutdown + checkpoints them away; a naive copy of `app.db` alone while a server runs does + not. Use `sqlite3 app.db ".backup …"`. + - **WAL does not work on network filesystems** (NFS/SMB). Opt out with + `OS_DATABASE_SQLITE_JOURNAL_MODE=delete`, or per datasource with + `sqliteJournalMode: 'delete'` in the driver config (which outranks the env + var). Either form _applies_ `delete`, so it also converts a database that + already adopted WAL back — skipping would have stranded it. + + Nothing here fails a boot, and nothing is assumed: `PRAGMA journal_mode = X` + answers with the mode actually in force rather than raising on refusal, so the + reply is read back; and because a filesystem can accept WAL and then fail the + first read _through_ it, the mode is proven with a read and rolled back to + `delete` if that fails — with a warning naming the file and the escape hatch. + `synchronous` is untouched, so durability is exactly what it was. `:memory:` + databases are left alone, as is `auto_vacuum = INCREMENTAL`, which keeps + reclaiming under WAL (ADR-0057). + + `os db clean` now counts `-wal` / `-shm` as part of the database when it measures + what a `VACUUM` reclaimed, so bytes that were sitting in the log do not read as a + reclaim of zero. + + `@objectstack/driver-sqlite-wasm` deliberately stays out of WAL. Its live + database is in the WASM heap and what reaches disk is a byte image it exports, so + nothing reads the database across processes and the pragma buys it nothing — + while still being a persistent header change in the operator's file. sql.js + _accepts_ the pragma (its VFS is memory-backed), so this had to be declared + rather than discovered. + + It also now parks a `-wal` left behind by an unclean native-driver exit rather + than loading the image beside it: wasm SQLite cannot read that log, and leaving + it next to a freshly rewritten image would let a later real SQLite replay frames + that no longer belong to it. The warning names the file it parked and how to + recover what was in it. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [42e3b01] +- Updated dependencies [c8124e5] +- Updated dependencies [39eb01b] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [33a5ff4] +- Updated dependencies [9e01213] +- Updated dependencies [01e124d] +- Updated dependencies [3fe0ff1] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [c53aa53] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/driver-sql@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/driver-sqlite-wasm/package.json b/packages/plugins/driver-sqlite-wasm/package.json index b93b4f4ebb..51e8129e9d 100644 --- a/packages/plugins/driver-sqlite-wasm/package.json +++ b/packages/plugins/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index bf73514489..00f547b8e8 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/embedder-openai +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index e6a868276c..1cb641d467 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index e0fae322d2..cc35c77851 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/knowledge-memory +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/service-knowledge@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index c09bbd4f65..c602bd8292 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index c7a3ccc710..74e56d54f6 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/knowledge-ragflow +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/service-knowledge@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index 6a21cb392f..906ccd46c5 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 3c7abea223..72df120ad3 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,126 @@ # @objectstack/plugin-approvals +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- cd6b9f2: `decisionOutputs` entries may now be declared `required` (objectui#2955). A typed entry `{ key, label?, type?, multiple?, required?: true }` tells the runtime — not just the decision UI — that an approver must supply the value: an **approve** carrying no value, or a blank one (`''`, whitespace, `[]`, an array of blanks), is rejected with `VALIDATION_FAILED` before any write, so the audit row and the request are untouched and the run can never resume past the node with the key missing. + + That gap is what the flag closes. `decisionOutputs` exists so a decision can route the next step (`approvers: [{ type: 'expression', value: 'vars.lead_review.next_reviewers' }]`), but nothing made the approver actually answer: a skipped output resumed the run with the key absent, and the next node either faulted with `EXPRESSION_FAILED` or resolved an empty slate and stalled on `onEmptyApprovers: 'admin_rescue'` — long after the one person who could have filled it in had moved on. `onEmptyApprovers` was the only backstop, and it is a recovery mechanism, not a contract. + + **Reject never requires them.** The run leaves down the `reject` edge, where nothing reads the outputs — demanding routing data to say "no" would trap the rejection. Outputs still ride a reject when the approver filled them in. + + **No elevation bypass.** A one-click email action link and an `auto_approve` SLA escalation both fail the same way rather than advancing into a node that would resolve nobody; the escalation sweep already isolates a throwing request, so that decision stays pending and visibly overdue instead of silently breaking the run downstream. Enforcement is per decision, so on a `unanimous` / `quorum` node every approver supplies the required outputs and the finalizing decision's values are what the flow resumes with. + + `required` rides `normalizeDecisionOutputs`, so it reaches clients on `decision_output_defs` — a decision UI marks the field required and blocks locally instead of round-tripping to a 400. The console side ships in objectui#2955. + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 697a39cd38..b98de4ad36 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index ce23c14a41..095b78762d 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/plugin-audit +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 8c3e30bc27..4ae4761e5e 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index 1bce5f3d36..9666ff3483 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,157 @@ # Changelog +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- ea24593: fix(plugin-auth): the auth catch-all yields paths better-auth does not own (#4088) + + `registerAuthRoutes` mounts `rawApp.all('${basePath}/*')` over the whole auth + namespace (`/api/v1/auth` by default), and that handler was **terminal**: it + returned better-auth's response unconditionally, including the 404 better-auth + produces for a path it does not implement. Any other plugin's route under that + prefix was therefore reachable only if it happened to register **first** — Hono + runs handlers matching a path in registration order and the first to return a + Response wins. + + That put a load-bearing surface at the mercy of `kernel.use()` order. + `@objectstack/plugin-hono-server` mounts `/auth/me/permissions` and + `/auth/me/localization` from its own `kernel:ready` hook; objectui's entire + permission layer reads the former and `core`'s auth gate allow-lists the latter + as an endpoint a gated user must still reach. Register `AuthPlugin` before + `HonoServerPlugin` and all of it silently 404s. + + A 404 from better-auth now means "this path is not mine" and the catch-all yields + to whatever else matched, in either registration order. Deliberately narrow: + + - **Only 404 falls through.** 401/403 are real better-auth answers, not + disclaimers of ownership. + - **Precedence still favours the namespace owner.** better-auth wins every path + it implements; only its leftovers are up for grabs. + - **The unclaimed-path wire shape is unchanged.** When nothing downstream + answers, better-auth's own 404 is returned verbatim rather than Hono's + `404 Not Found`. + + No configuration changes and no new routes. The only behavioural difference for + an existing deployment is that a route another plugin mounts under + `/api/v1/auth/*` now answers regardless of plugin order — previously it answered + only in the lucky order. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [fccec22] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [fae74b5] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [495019b] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [be7945a] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/rest@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index b68552ee00..3015bb7438 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 95f79822d9..3ee6e35495 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,236 @@ # @objectstack/plugin-dev +## 17.0.0-rc.1 + +### Minor Changes + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +### Patch Changes + +- 45dc446: Every in-memory fallback and dev stub now self-describes with the standard `__serviceInfo` descriptor, classified by what it actually is (#4058 step 1). + + ADR-0076 D12 gave services one way to say "I am not the real thing", but the producers never converged on it: + + - The kernel's own fallbacks (`createMemoryCache` / `Queue` / `Job` / `I18n` / `Metadata`) carried `_fallback: true` — a marker **no** consumer recognized, `readServiceSelfInfo` included — so both discovery builders reported them as fully `available`. + - `plugin-dev` marked all of its implementations with the same `_dev: true`, normalized to `status: 'stub', handlerReady: false`. That declared a working in-memory search index exactly as fake as an AI stub returning invented text. + + Both now carry `__serviceInfo`, split by a rule that holds across the whole set: + + - **`degraded`** — really does the work, with reduced capability: `cache`, `queue`, `job`, `file-storage`, `search`, `i18n`, `metadata`, `workflow`, `realtime`. Its answers are true answers; the `message` names what is missing (no persistence, no scheduling timer, no state-machine validation, …). + - **`stub`** — the answer is fabricated: `ai`, `automation`, `notification`, `data`, `auth`, `security.permissions`, `security.rls`, `security.fieldMasker`. Never to be mistaken for a capability. + + `handlerReady: false` is set independently wherever no HTTP handler serves the slot (`cache` / `queue` / `job` / `realtime`, and every `stub`). + + Discovery output changes accordingly — a kernel fallback that used to report `status: 'available'` now reports `degraded` with an explanatory message. No routing, gating, or dispatch behavior changes: every dispatcher domain still resolves services exactly as before. Consumers reading `discovery.services.*` get the truth instead of a uniform claim. + + For anything that duck-typed the old markers: `svc._fallback` / `svc._dev` → `readServiceSelfInfo(svc)` from `@objectstack/spec/api` (the legacy `_dev` key is still understood by that reader, so third-party stubs carrying it keep working). + +- a3cb9c8: Retire the dev-mode `analytics` stub, and make the dispatcher gate `/analytics` on `handlerReady` rather than on service presence (#4000). + + Retiring the degraded analytics shim (#3891) made an empty `analytics` slot the honest signal: `/api/v1/analytics/*` 404s and discovery reports `unavailable`. `plugin-dev` refilled that slot with a stub, which re-created the retired shape in dev mode — the dispatcher gated on "is a service registered", so the stub was called like a real engine and its empty result came back as a 200. + + - `plugin-dev` no longer registers an `analytics` dev stub; the slot stays empty (`NO_DEV_STUB_SERVICES`). Every other dev stub is unchanged. + - The `/analytics` domain, its route-mount gate, and discovery's `routes`/`features` now share one predicate (`isAnalyticsServiceServeable`): a service that self-declares `handlerReady: false` (ADR-0076 D12 — `__serviceInfo`, or plugin-dev's legacy `_dev: true`) is treated as an empty slot. A `degraded` implementation that genuinely serves requests keeps serving; `discovery.services.analytics` still reports a registered stub as `status: 'stub'`, which says more than `unavailable` would. + + FROM → TO for dev setups that relied on the stub answering `POST /api/v1/analytics/query` with `{ rows: [], fields: [] }`: install the real engine — `@objectstack/service-analytics` runs an InMemory strategy and needs no database of its own. Nothing else changes; hosts that already install it (including `os serve`, where `analytics` is in `ALWAYS_ON_CAPABILITIES`) are unaffected. + +- 4be9d99: fix(runtime,hono,plugin-dev): retire the dispatcher's `/storage` bridge — it never spoke the storage contract (#4087) + + `POST /api/v1/storage/upload` and `GET /api/v1/storage/file/:id` were a + dispatcher-side bridge to the `file-storage` service slot, written against a + service shape that does not exist: + + - **Upload** called the contract's `upload(key, data, options?)` as + `upload(file, { request })` — the parsed file object landed in the `key` + slot and `{ request }` in `data`. That is a `TypeError` against every + implementation in the repo (`S3StorageAdapter`, `LocalStorageAdapter`, + `SwappableStorageService`, plugin-dev's in-memory one), not a + near-miss: `Buffer.from({}) → ERR_INVALID_ARG_TYPE`, or an object used as + an S3 object key / `path.join` segment. + - **Download** branched on `result.url` / `result.redirect` / `result.stream` + / `result.mimeType` while the contract's `download(key)` resolves a + `Buffer`, so every branch fell through and the route answered a + JSON-serialized Buffer. + + Both routes are removed, along with `HttpDispatcher.handleStorage()`, the + `/storage` domain registration, the dispatcher-plugin mounts and the two route + ledger rows. + + **Migration.** There is nothing to migrate off in practice — neither route + could complete a request. (They were reachable: `service-storage` mounts + `/storage/upload/presigned`, not `/storage/upload`, so nothing shadowed them. + They simply had no caller — no SDK method builds those URLs.) + `/api/v1/storage` is `@objectstack/service-storage`'s surface and always was + the working one: + + - Upload — FROM `POST /api/v1/storage/upload` TO the presigned protocol + (`POST /storage/upload/presigned` → direct `PUT` to the returned URL → + `POST /storage/upload/complete`), or `client.storage.upload(file)`, which + runs all three steps. + - Download — FROM `GET /api/v1/storage/file/:id` TO + `GET /storage/files/:fileId/url` (`client.storage.getDownloadUrl(fileId)`) + for a signed URL, or `GET /storage/files/:fileId` for a stable browser URL + that 302s to it. + + Install `@objectstack/service-storage` to get those routes; without it + `/api/v1/storage` now has no handler, which is the same answer every other + uninstalled capability gives. + + Two follow-on corrections keep `declared === enforced`: + + - `@objectstack/hono` no longer mounts `app.all('/storage/*')`. That + wildcard claimed the whole `/storage` subtree for the two dead routes, so + every other path under it — service-storage's protocol above all — got the + bridge's own 404 rather than falling through. Storage is ordinary catch-all + traffic now. + - Discovery keeps gating `routes.storage` on `isServiceServeable` — the shared + `handlerReady` predicate #4058 step 2 introduced — and plugin-dev's in-memory + implementation now self-declares `handlerReady: false`. #4058 deliberately + left that one serving because the `/storage` bridge was still there to serve + it; with the bridge retired nothing routes HTTP to that slot, so `false` is + the honest value — the position `realtime` has held since ADR-0076 D12. The + implementation keeps working for in-process callers; it is simply no longer + advertised as a reachable HTTP capability. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [ea24593] +- Updated dependencies [789ad63] +- Updated dependencies [fccec22] +- Updated dependencies [2af1988] +- Updated dependencies [b3a2318] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [fae74b5] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c8124e5] +- Updated dependencies [9e8f04d] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [43ff598] +- Updated dependencies [839982e] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [495019b] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [be7945a] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/plugin-security@17.0.0-rc.1 + - @objectstack/rest@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/plugin-auth@17.0.0-rc.1 + - @objectstack/driver-memory@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/plugin-hono-server@17.0.0-rc.1 + - @objectstack/account@17.0.0-rc.1 + - @objectstack/setup@17.0.0-rc.1 + - @objectstack/service-i18n@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 1711975068..4b85197ac9 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Development Mode Plugin for ObjectStack — auto-enables all services with in-memory implementations", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index c9babe2cdb..a677b3a95a 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,62 @@ # @objectstack/plugin-email +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 6640c7e80a..e85419318a 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index 3071d58bea..108867224c 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,183 @@ # @objectstack/plugin-hono-server +## 17.0.0-rc.1 + +### Minor Changes + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +- 43ff598: fix(plugin-hono-server): stop gating the current-user endpoints behind `registerStandardEndpoints` (#4073) + + `registerStandardEndpoints` gated two unrelated things behind one flag: + + - **Duplicate supply** — raw `POST/GET /api/v1/data/:object` (create + read + only), which `@objectstack/rest` also serves and, registering first, is what + actually answers; plus `GET /api/v1/discovery` and + `/.well-known/objectstack`, which the dispatcher and REST own and which this + surface already cedes to them (#4018). + - **Sole supply** — `GET /api/v1/auth/me/permissions`, + `/api/v1/auth/me/localization` and `/api/v1/me/apps`. Nothing else in the + platform mounts these: neither `@objectstack/rest` nor `@objectstack/runtime` + registers any `/me/*` route, the console's entire permission layer reads + `/auth/me/permissions`, the console reads `/auth/me/localization` for regional + defaults, and `core`'s auth gate allow-lists `/me/apps` + `/me/localization` + as endpoints a gated user MUST still reach to bootstrap the remediation UI. + + `os serve` gets all of it only because the flag defaults to `true` — the CLI + constructs `new HonoServerPlugin({ port })`. So `registerStandardEndpoints: +false`, whose documented job is the optional CRUD/discovery convenience surface, + silently took the console's permissions and localization down with it. + + The three current-user endpoints now register **unconditionally**, and the flag + covers the duplicate half only — what its name and docs always claimed. + + **FROM → TO.** If you set `registerStandardEndpoints: false` and worked around + the missing endpoints (proxying `/auth/me/permissions` yourself, or pinning the + flag to `true` purely to keep them), you can drop that workaround: the endpoints + are now present either way. No route is removed and no response shape changes, + so a host that left the flag at its default sees no difference. If you relied on + `false` meaning "this plugin mounts no `/api/v1` routes at all", that is no + longer true — it never was for `os serve`, which is the only host that shipped + the flag's default. + + Also removes three unreferenced `*_ENDPOINT_PRIORITY` constants; + `DISCOVERY_ENDPOINT_PRIORITY = 900` in particular implied a route-priority + mechanism that does not exist (precedence here is Hono's + first-registration-wins). + +### Patch Changes + +- 839982e: fix(plugin-hono-server): compute the standalone discovery `routes` from real registrations, and cede to the real owner (#4018) + + `registerStandardEndpoints` served a **fully static** discovery: a hardcoded + `routes` table listing `auth` / `packages` / `analytics` / `workflow` / + `automation` / `ai` / `notifications` / `i18n` / `storage` / `ui` regardless of + what the host actually mounted. A standalone Hono deployment therefore + advertised ten route families and 404'd on every one no plugin bridged — the + "advertise a route that doesn't exist" class ADR-0076 D12 exists to kill, and + the reason this surface disagreed with the two real discovery builders + (`HttpDispatcher.getDiscoveryInfo`, `metadata-protocol`'s `getDiscovery`), which + both compute per service at runtime. + + Two changes, no new discovery implementation to keep in sync: + + - **Single owner (D11 / OQ#9).** When `@objectstack/rest` or the runtime + dispatcher is on the kernel, this surface no longer registers + `${prefix}/discovery` — that plugin owns it. Both register during plugin + `start()`, i.e. before this `kernel:ready` hook, and Hono is + first-registration-wins, so they already shadowed this handler in every + composed deployment: the cede changes no served payload, it removes a third + one nobody read. `/.well-known/objectstack` is ceded to the dispatcher only + (REST never registers it), so a REST-without-dispatcher host keeps the + redirect. + + - **Computed, not hardcoded (D12).** When this surface does own `/discovery`, + `routes` is derived per request from the app's live route table: a family is + advertised iff a route is really registered at or under its base path. A + wildcard mounted _above_ the base (global `/*` middleware, `/api/v1/*`) does + not count as a mount. + + **What changes for you.** On a standalone `HonoServerPlugin` host (no REST, no + dispatcher), `GET /api/v1/discovery` now omits every family nothing mounts — + most visibly `routes.metadata`, since `/api/v1/meta` ships with + `@objectstack/rest` / the dispatcher. Clients that read a route out of + discovery and call it stop getting a 404; `@objectstack/client` falls back to + the conventional path for any omitted key, so `client.connect()` is unaffected. + Composed deployments (`os serve`, cloud) are unchanged — the dispatcher's + service-aware discovery was already the one being served. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index f2231b1f93..e252c65a3d 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-hono-server", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index 0b07e7d88d..7509f33173 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,27 @@ # @objectstack/plugin-pinyin-search +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [48fcf70] +- Updated dependencies [ffb003c] +- Updated dependencies [32ccb23] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [c20b875] +- Updated dependencies [3c628ce] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [77fadbf] +- Updated dependencies [857a6cf] + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index b6cb906bd9..1ba5ab20ba 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-pinyin-search", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-reports/CHANGELOG.md b/packages/plugins/plugin-reports/CHANGELOG.md index 7d9d64a9df..5064b5b1ab 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/plugin-reports +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 1495f3af1d..105845b838 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-reports", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 4334ee77b5..1bd9dcb10e 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,195 @@ # @objectstack/plugin-security +## 17.0.0-rc.1 + +### Minor Changes + +- 1ea6bce: feat(sharing): hierarchy managers may manage shares within their write DEPTH (ADR-0111 D1 DEPTH) + + `canManageShares` gains its named DEPTH extension: a caller whose effective + WRITE scope on the object is a hierarchy scope (`unit` / `unit_and_below` / + `own_and_reports`) may now manage shares on a record whose owner falls within + that scope's owner set — the same set the write filter and `canEdit` already + honour, resolved by the enterprise `hierarchy-scope-resolver`. This lets a + manager grant/revoke/list shares on a subordinate's record, matching + Salesforce (roles above the owner) and Dataverse (the `Share` privilege's BU + depth), without expanding the MVP owner + Modify-All authority. + + - New `ISecurityService.resolveWriteScope(object, context)` — the effective + write scope, resolved by the same evaluator the CRUD middleware uses; fails + closed to `own`. Mirrored on the sharing plugin's structural probe. + - The gate honours only the three hierarchy scopes. `org` from the probe is + deliberately ignored: it means both a genuine Modify-All holder (already + granted via `hasWriteBypass`) AND the fail-OPEN "no permission set mentions + this object" default, so honouring it here would reopen the hole + `hasWriteBypass` was chosen to avoid. + - Fails closed with no security service or no enterprise resolver — the open + edition stays owner + Modify-All, exactly as before. + +- c1dcacd: fix(sharing)!: the share-management surface gains the authorization layer it never had (ADR-0111 P0, #3902) + + Record sharing shipped as a data layer with no authorization of its own: every + `/data/:object/:id/shares` and `/sharing/rules` route authenticated the caller + and then ran the service under `SYSTEM_CTX` — any signed-in user could revoke + anyone's share, enumerate who-can-see-what, write self-grants, and define / + evaluate org-wide sharing rules. ADR-0111's P0 rulings land here: + + - **D1/D2** — `ISharingService.canManageShares(object, recordId, context)`: + system, the record's owner, or a holder of Modify All Data (probed via the + new fail-closed `ISecurityService.hasWriteBypass`). Enforced in the SERVICE, + so every caller is covered; without plugin-security it fails closed to + owner-only. + - **D4** — `revoke` is symmetric with grant, validates the share belongs to the + URL's record (`NOT_FOUND` on mismatch), and refuses non-`manual` rows + (`CONFLICT` — a rule-materialised grant would be resurrected by the next + reconcile). + - **D5** — `listShares` is management-gated (invisible record → `NOT_FOUND`, + visible-but-not-manager → `PERMISSION_DENIED`), and the open + `/data/sys_record_share` read surface is self-scoped: non-admin callers see + only rows naming them as recipient or grantor. + - **D6** — the whole `/sharing/rules` surface (list/create/get/delete/evaluate) + requires the new **`manage_sharing`** capability (D9; seeded into + `admin_full_access`, `manage_platform_settings` honoured as the legacy + equivalent), enforced in `SharingRuleService`. + - **D7** — no inert grants: `recipientType` is narrowed to `user` (the only + type any gate enforces), grants on objects the sharing gates never consult + (public model, no `owner_id`, bypass, `controlled_by_parent`) fail with + `SHARING_NOT_ENABLED` (422), and the manual upsert keys on + `(object, record, recipient, source)` so manual and rule rows coexist. + + **Breaking** for callers that relied on the missing gate: unauthorized share + management now fails with 403/404/409/422 instead of silently succeeding, and + `ISharingService.revoke` gained an optional `scope` parameter. The verb + boundary (edit ≠ delete, ADR-0111 D3) is NOT in this change — it lands as the + separate P1. + +- ad303ed: fix(sharing)!: an edit-level share no longer grants delete (ADR-0111 D3, the verb boundary) + + `update` and `delete` shared one `canEdit` gate, and `canEdit` accepts an + `edit`-level share — so one "edit" grant silently conferred delete, the + opposite error from the retired `full` level. A share widens _which rows_ a + principal reaches, never _which verbs_ they may use (Salesforce Read/Write + cannot delete; Dataverse `Delete` is a distinct privilege; Odoo splits + `write`/`unlink`). + + - `ISharingService.canDelete(object, recordId, context)` — ownership (widened + by write DEPTH) or the `modifyAllRecords` super-user bypass ONLY; an `edit` + or legacy `full` share does not confer it. `canEdit` is unchanged (the + update gate, share included). + - `SharingService.buildWriteFilter` takes a `verb` parameter: a bulk + `delete({multi:true})` scopes to the owner/DEPTH set alone (no share + widening), while a bulk `update` keeps it. + - The sharing middleware routes `delete` through `canDelete` and logs a + specific fail-closed reason on denial (ADR-0111 D10). + - `/security/explain` consults `canDelete` for a `delete` operation, so the + record-level explanation matches enforcement. + + **Breaking**: a caller who could delete a record _only_ through an edit-level + share (and holds object-level delete CRUD) can no longer delete it — delete now + requires ownership, write depth, or Modify All Data. No new delete access level + is introduced; a future per-record delete grant would be a capability mask + AND-ed with object CRUD, not a fourth share level. + +### Patch Changes + +- 94a0bbc: fix(security)!: a disabled RLS policy no longer grants — found by re-verifying the ledger's security subset (#3896 follow-up) + + **The fix.** `RowLevelSecurityPolicySchema.enabled` promises, verbatim: _"Disabled + policies are not evaluated."_ Nothing read it — not the collection site, not the + projection round-trip, not the compiler. Because applicable policies OR-combine + (any match allows access), a policy an admin switched off **kept contributing its + grant**: disabling a too-permissive policy silently changed nothing. That is the + #3896 shape — a documented security control whose real behaviour is wider than + its contract — one layer up, on RLS instead of sharing rules. + + `getApplicablePolicies` now excludes `enabled === false` before any matching, at + the single choke point both the find path and the analytics path flow through — + the same place, and the same ADR-0049 enforce-or-remove resolution, as the + formerly-unenforced `positions` domain. Exact `=== false` on purpose: the schema + defaults `enabled` to true and projection rows may omit the key, so absent stays + active. Four tests pin both directions. Access-narrowing only: no policy grants + MORE after this change, and nothing in-repo authors `enabled: false`. + + **The audit that found it.** All 44 entries of the liveness ledger's security + subset (`permission` 33, `position` 4, `object` sharing/access 7) were + call-graph-closed by hand and stamped `verifiedAt: 2026-07-30` — the subset's + first-ever re-verification (previously 4 dated entries repo-wide, and the last + sweep that cited preview renderers went 10-for-13 wrong). Beyond `enabled`: + + - `rowLevelSecurity.priority` → **dead + authorWarn**. Not merely unimplemented: + policies OR-combine (the schema's own describe says most-permissive-wins), so + the promised "conflict resolution" semantics cannot exist. A REMOVE candidate + per the #3715/#3950 precedent while the v17 breaking window is open. + - `rowLevelSecurity.label` / `description` / `tags` → dead (benign display — + no consumer in either repo; deliberately not authorWarn'd). + - `tabPermissions` was UNDERSTATED: the note said only `'hidden'` is read, but + hono's rank merge reads all four visibility values across resolved sets, and + the `me-apps-and-everyone-baseline` dogfood test exercises it. Evidence + upgraded; noted as a proof-binding candidate. + - `allowExport` re-verified TRUE against the suspicion that it was + projection-only: the export route carries its own caller-level 403 gate + (`enforceExportPermission`), fail-closed when the security service cannot + answer, separate from the object-level 405. + - `allowTransfer/Restore/Purge` notes re-confirmed accurate (M2 operations still + unshipped; the RBAC gates are pre-mapped fail-closed). + - `object.ownership` evidence had rotted (line drift) — refreshed; six other + object-level security entries re-cited and stamped. + + No other runtime behaviour changes. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 20b5089b1b..a4d6f067c6 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index 354b9d946b..4a23b7c300 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,330 @@ # @objectstack/plugin-sharing +## 17.0.0-rc.1 + +### Minor Changes + +- 1ea6bce: feat(sharing): hierarchy managers may manage shares within their write DEPTH (ADR-0111 D1 DEPTH) + + `canManageShares` gains its named DEPTH extension: a caller whose effective + WRITE scope on the object is a hierarchy scope (`unit` / `unit_and_below` / + `own_and_reports`) may now manage shares on a record whose owner falls within + that scope's owner set — the same set the write filter and `canEdit` already + honour, resolved by the enterprise `hierarchy-scope-resolver`. This lets a + manager grant/revoke/list shares on a subordinate's record, matching + Salesforce (roles above the owner) and Dataverse (the `Share` privilege's BU + depth), without expanding the MVP owner + Modify-All authority. + + - New `ISecurityService.resolveWriteScope(object, context)` — the effective + write scope, resolved by the same evaluator the CRUD middleware uses; fails + closed to `own`. Mirrored on the sharing plugin's structural probe. + - The gate honours only the three hierarchy scopes. `org` from the probe is + deliberately ignored: it means both a genuine Modify-All holder (already + granted via `hasWriteBypass`) AND the fail-OPEN "no permission set mentions + this object" default, so honouring it here would reopen the hole + `hasWriteBypass` was chosen to avoid. + - Fails closed with no security service or no enterprise resolver — the open + edition stays owner + Modify-All, exactly as before. + +- e5e8b10: feat(sharing): a record's share-manager may revoke any share-link on that record (ADR-0111 D8) + + `ShareLinkService.revokeLink` was creator-or-system only, so a record's owner or + a Modify-All admin could not kill a link someone else minted on their record — + their record's exposure, but not their link to revoke. Revoke authority now + also admits a record **share-manager**, probed via the sharing service's + late-bound `canManageShares` (owner / `modifyAllRecords`). The probe fails + closed: a deployment without it (or a throwing probe) keeps the pre-D8 + creator-only behaviour. Mint authority is unchanged and now documented as the + D8 decision it always enforced — the object's `publicSharing` opt-in AND the + caller's visibility of the record. + +- c1dcacd: fix(sharing)!: the share-management surface gains the authorization layer it never had (ADR-0111 P0, #3902) + + Record sharing shipped as a data layer with no authorization of its own: every + `/data/:object/:id/shares` and `/sharing/rules` route authenticated the caller + and then ran the service under `SYSTEM_CTX` — any signed-in user could revoke + anyone's share, enumerate who-can-see-what, write self-grants, and define / + evaluate org-wide sharing rules. ADR-0111's P0 rulings land here: + + - **D1/D2** — `ISharingService.canManageShares(object, recordId, context)`: + system, the record's owner, or a holder of Modify All Data (probed via the + new fail-closed `ISecurityService.hasWriteBypass`). Enforced in the SERVICE, + so every caller is covered; without plugin-security it fails closed to + owner-only. + - **D4** — `revoke` is symmetric with grant, validates the share belongs to the + URL's record (`NOT_FOUND` on mismatch), and refuses non-`manual` rows + (`CONFLICT` — a rule-materialised grant would be resurrected by the next + reconcile). + - **D5** — `listShares` is management-gated (invisible record → `NOT_FOUND`, + visible-but-not-manager → `PERMISSION_DENIED`), and the open + `/data/sys_record_share` read surface is self-scoped: non-admin callers see + only rows naming them as recipient or grantor. + - **D6** — the whole `/sharing/rules` surface (list/create/get/delete/evaluate) + requires the new **`manage_sharing`** capability (D9; seeded into + `admin_full_access`, `manage_platform_settings` honoured as the legacy + equivalent), enforced in `SharingRuleService`. + - **D7** — no inert grants: `recipientType` is narrowed to `user` (the only + type any gate enforces), grants on objects the sharing gates never consult + (public model, no `owner_id`, bypass, `controlled_by_parent`) fail with + `SHARING_NOT_ENABLED` (422), and the manual upsert keys on + `(object, record, recipient, source)` so manual and rule rows coexist. + + **Breaking** for callers that relied on the missing gate: unauthorized share + management now fails with 403/404/409/422 instead of silently succeeding, and + `ISharingService.revoke` gained an optional `scope` parameter. The verb + boundary (edit ≠ delete, ADR-0111 D3) is NOT in this change — it lands as the + separate P1. + +- ad303ed: fix(sharing)!: an edit-level share no longer grants delete (ADR-0111 D3, the verb boundary) + + `update` and `delete` shared one `canEdit` gate, and `canEdit` accepts an + `edit`-level share — so one "edit" grant silently conferred delete, the + opposite error from the retired `full` level. A share widens _which rows_ a + principal reaches, never _which verbs_ they may use (Salesforce Read/Write + cannot delete; Dataverse `Delete` is a distinct privilege; Odoo splits + `write`/`unlink`). + + - `ISharingService.canDelete(object, recordId, context)` — ownership (widened + by write DEPTH) or the `modifyAllRecords` super-user bypass ONLY; an `edit` + or legacy `full` share does not confer it. `canEdit` is unchanged (the + update gate, share included). + - `SharingService.buildWriteFilter` takes a `verb` parameter: a bulk + `delete({multi:true})` scopes to the owner/DEPTH set alone (no share + widening), while a bulk `update` keeps it. + - The sharing middleware routes `delete` through `canDelete` and logs a + specific fail-closed reason on denial (ADR-0111 D10). + - `/security/explain` consults `canDelete` for a `delete` operation, so the + record-level explanation matches enforcement. + + **Breaking**: a caller who could delete a record _only_ through an edit-level + share (and holds object-level delete CRUD) can no longer delete it — delete now + requires ownership, write depth, or Modify All Data. No new delete access level + is introduced; a future per-record delete grant would be a capability mask + AND-ed with object CRUD, not a fourth share level. + +- ccd9397: fix(security)!: a sharing rule with no criteria now shares NOTHING instead of every record (#3896) + + `SharingRuleSchema` has always required `condition`, and its doc is explicit + that a predicate the compiler cannot lower is _"skipped and logged — never + seeded as a permissive match-all (ADR-0049)"_. The declared/seed path honoured + that. The two other ways to create a rule did not: + + - **`POST {basePath}/sharing/rules`** plucks its body field-by-field into + `SharingRuleService.defineRule`, which validated `name` / `label` / `object` / + `recipientType` / `recipientId` — and not `criteria`. A missing, `null`, or + **misspelled** key (`criterias`) was stored as `criteria_json: null`, answered + `201` with no warning, and evaluated as + `find(object, { filter: {}, context: SYSTEM_CTX })`: every record of the + object, up to 5000, granted to the recipient. Triggering it took a typo, not + an attacker. + - **Authoring a rule in Setup** is a direct `sys_sharing_rule` insert, which + never reaches `defineRule` at all. + + Empty criteria is now rejected everywhere a rule can be written, and — because + rules created before this gate are already in the table — the evaluator refuses + to act on one regardless of how it got there. + + - **`defineRule` rejects a match-all criteria** with + `VALIDATION_FAILED: criteria is required …`, alongside its other required + fields. Covers the REST endpoint, programmatic callers, and the seeder. + Rejected shapes: missing / `null` / `''` / `{}` / `[]` / `{ $and: [] }` / + unparsable JSON (e.g. a CEL source typed into the Criteria box). + - **The evaluator matches nothing** for such a rule and logs why, so a row + stored before this release under-shares instead of over-sharing: the next + reconcile _revokes_ the grants it had materialised. Both evaluation paths are + covered — the bulk `evaluateRule` and the per-record write-hook path. + - **`bindRuleCriteriaGuard`** fails `sys_sharing_rule` inserts with no + criteria as a field-level `VALIDATION_FAILED` (a 400 naming `criteria_json`), + so the Setup path reports the problem instead of saving an inert rule + (ADR-0078). Updates are checked only when the patch supplies + `criteria_json` — switching an over-broad legacy rule off must not require + inventing a criteria for it first. + - **The seed bootstrap's "empty condition = match-all" branch is gone**: a + missing or empty `condition` is now skipped and logged like any other + non-lowerable one. + - `POST {basePath}/sharing/rules` also accepts `criteria_json` as an alias for + `criteria`, matching the snake_case aliases the endpoint already takes for + `object_name` / `recipient_type` / `access_level`. + + **Migration.** There is no "share every record" sharing rule, and there never + usefully was one — the shape existed only as a failure mode. A rule that + relied on it must state its predicate (`criteria: { stage: 'won' }`), or, if + the object really should be readable by everyone, use the object's + organization-wide default (`sharingModel`) instead. Rules already stored with + a null `criteria_json` need no data migration: they stop granting on the next + evaluation and their existing grants are revoked. + +### Patch Changes + +- 7df7c64: feat(sharing): `sys_sharing_rule.criteria_json` is declaratively required (ADR-0113 P2) + + The field the ADR was written for: `required: true` as the write contract — + insert must provide, update may not null out, legacy null rows rest, an admin + can still `active: false` an over-broad legacy rule. Deliberately NO + `storage.notNull`: deployed tenants' legacy nulls are the case the split + exists for. The Setup form's required marker and client validation now derive + from the declaration. + + Not breaking: a rule without criteria was already rejected by the #3929 hook + guard; the guard narrows to the non-null match-all shapes `required` cannot + express ('{}', vacuous $and/$or, unparsable JSON), `defineRule` keeps the API + seam, and the evaluator stays fail-closed (ADR-0049). + +- 71af9f5: fix(sharing): the criteria-less-rule warn is once per rule per process, plus one boot aggregate (#3929 follow-up) + + Pre-dedup the fail-closed evaluator warned on EVERY pass — per evaluation and + per reconciled write — so one legacy criteria-less rule could dominate a + deployment's log. Enforcement is unchanged (such a rule still matches + nothing and its grants are revoked on reconcile); the warn now fires once + per rule per process, and the boot backfill emits a single operator-facing + aggregate (count + rule names + the fix: repair the criteria or set + active: false). + +- 4580597: fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983) + + The fifth and final drifting route module. Unlike the four in #3843, this one was + not found by reading — `scripts/check-route-envelope.mjs` surfaced it the moment + that scan went repo-wide, which is the whole argument for a repo-wide guard over + per-package copies. It also turned out to be the one where the drift had actually + **broken shipped SDK methods**, not merely mis-shaped a body. + + ## Two SDK methods did not work on this surface + + Three of these routes are `disposition: 'sdk'` in `runtime/src/route-ledger.ts`, + and `ObjectStackClient.unwrapResponse` decides a body is an envelope by finding a + boolean `success`. With no flag it hands back the body verbatim: + + | method | documented / typed as | actually returned | + | --------------------- | ------------------------------ | ------------------------------------------- | + | `shareLinks.create()` | "the link row (incl. `token`)" | `{ link: … }` — so `.token` was `undefined` | + | `shareLinks.list()` | `Promise` | `{ links: [] }` — so `.map()` threw | + + `packages/client/src/admin-surfaces.test.ts` mocks all three as + `{ success: true, data: }`. The SDK was written and tested against the + **dispatcher's** shape and only ever worked there. + + ## This is a convergence, not a redesign + + `runtime/src/domains/share-links.ts` serves the same five paths, and for cloud's + per-environment kernels it is the _designed primary_ surface + (`registerShareLinkRoutes: false`). It has always answered in the declared + envelope. The plugin now answers identically: + + | route | was | now | + | -------------------------------- | -------------------------------- | --------------------------------------- | + | `POST /share-links` | `{ link }` | `{ success: true, data: link }` | + | `GET /share-links` | `{ links }` | `{ success: true, data: link[] }` | + | `DELETE /share-links/:idOrToken` | `{ ok: true }` | `{ success: true, data: { ok: true } }` | + | `GET /:token/resolve` | `{ record, link, redactFields }` | `{ success: true, data: { … } }` | + | `GET /:token/messages` | `{ data: rows }` | `{ success: true, data: rows }` | + | errors | `{ error: { code, message } }` | `{ success: false, error: { … } }` | + + `data` carries each payload **directly** — `data: links`, not `data: { links }`. + That is what makes `unwrapResponse` return the same value on both surfaces, and + it is what the SDK already expected. + + ## Breaking: raw `fetch` callers add one hop + + SDK callers get the fix for free (two of them go from broken to working). Direct + body readers add `.data`: + + ```diff + - const { links } = await (await fetch('/api/v1/share-links')).json(); + + const { data: links } = await (await fetch('/api/v1/share-links')).json(); + ``` + + `{ ok: true }` on revoke survives, but as the payload rather than as the body: at + the top level it was a second word for `success`, which #3689 retired from + storage; under `data` it is what the dispatcher already returned. + + The `error` half was already nested `{ code, message }` — #3675's changeset cited + this module as the good example of that — so only the `success` flag is new there. + All eleven codes were already SCREAMING_SNAKE and registered, so ADR-0112 needs + nothing. + + ## Consumers + + Swept, and the result is smaller than #3983 assumed. The framework has **zero** + consumers of these routes. In objectui, `ShareDialog` was already dual-shape + tolerant on all three routes it calls (`body.links ?? body.data`, + `created.link ?? created.data`, and revoke never reads the body) — it needs no + change, and it carried that tolerance precisely _because_ both shapes existed in + the fleet. + + `SharedRecordPage` did need one fix, and it is the kind a shape-swap would have + missed: it renamed the wire's `redactFields` to `redactedFields` only on the + _bare_ branch, so on the already-enveloped dispatcher path the "fields are hidden + by the owner" notice never rendered. Converting this surface would have spread + that to every share page. Fixed in objectui#2980, which merges first. + + ## Guard + + **7 conformant / 0 ratcheted / 1 exempt**, from 6 / 1 / 1. The ratchet mechanism + stays for the next module that needs it. + + `privateOk` also got narrowed to what its own doc always claimed — a literal `ok` + at the **top** of a body, where it competes with `success`. The same literal + inside `data` is payload, which is what a conformant revoke returns. Four + self-test assertions pin both readings. + +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 01b89e7715..38fc0caef1 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index 00c4787a12..9626779ebb 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,146 @@ # @objectstack/plugin-webhooks +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- a225ef5: fix(runtime,webhooks): the path object wins on /data/:object/query, and the webhook envelope owns its keys (#3946) + + Follow-up sweep for the shape behind #3897 and #3933 — a trusted, server-derived + value written into an object literal with a caller-controlled bag spread OVER + it. Both of those were in the same block of REST code, so the pattern was swept + across all 1313 non-test TypeScript files in `packages/`. Nine candidate sites; + one real, one worth hardening, seven verified clean (recorded in #3946 so the + next sweep does not re-litigate them). + + **`POST /data/:object/query` (runtime dispatcher).** The `/data` domain built + `{ object: objectName, ...body }`, so `{"object":"other", …}` in the body moved + the read to a different object than the URL named. + + This is NOT an authorization bypass, and the tests pin why: `callData` gates + API exposure on `params.object`, so the gate followed the body and agreed with + the read — an object hidden by `apiEnabled: false` was refused either way. What + broke is that the URL stopped describing the operation (audit trails, logs, and + anything keyed on the request path saw object A while object B was read), and + that one endpoint spoke a second dialect of the contract the REST side had just + standardised on: the path object wins. The other handlers in that file never had + the problem — they nest caller data (`data: body`, `query: normalized`) instead + of splatting it, and the GET-by-id branch already allowlists its query params + against exactly this pollution. + + **Webhook delivery envelope.** `auto-enqueuer` built + `{ object, recordId, action, timestamp, ...payload }`, letting an event payload + rewrite the envelope a subscriber receives. Behaviour-neutral for the engine's + own publishers — `data.record.*` payloads are `{ recordId, after, changes }` + with record fields nested under `after`, so none of those four keys collide + today — but the shape was wrong, and the `payload.id` fallback right above it + suggests publishers that flatten record fields do exist. Envelope keys are + written last now. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/service-messaging@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index b48290dcdc..8544611e0e 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.", "type": "module", diff --git a/packages/qa/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md index 05a06c9bb0..3bbe8a85d9 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/dogfood +## 0.0.40-rc.1 + +### Patch Changes + +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [e5e8b10] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [3abd233] +- Updated dependencies [ea24593] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [7df7c64] +- Updated dependencies [a225ef5] +- Updated dependencies [c8124e5] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [71af9f5] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [4580597] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/plugin-sharing@17.0.0-rc.1 + - @objectstack/plugin-security@17.0.0-rc.1 + - @objectstack/plugin-auth@17.0.0-rc.1 + - @objectstack/plugin-webhooks@17.0.0-rc.1 + - @objectstack/service-messaging@17.0.0-rc.1 + - @objectstack/service-analytics@17.0.0-rc.1 + - @objectstack/verify@17.0.0-rc.1 + - @objectstack/example-showcase@0.3.14-rc.1 + - @objectstack/example-crm@4.0.92-rc.1 + - @objectstack/connector-mcp@17.0.0-rc.1 + - @objectstack/connector-openapi@17.0.0-rc.1 + - @objectstack/connector-rest@17.0.0-rc.1 + - @objectstack/mcp@17.0.0-rc.1 + - @objectstack/plugin-audit@17.0.0-rc.1 + - @objectstack/service-storage@17.0.0-rc.1 + ## 0.0.40-rc.0 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 39c5f930f9..45c7efcc9e 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.40-rc.0", + "version": "0.0.40-rc.1", "private": true, "license": "Apache-2.0", "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", diff --git a/packages/qa/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md index 23b012dd21..5aa7ec5433 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/downstream-contract +## 0.0.38-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 0.0.38-rc.0 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index 7802949431..439517ccf7 100644 --- a/packages/qa/downstream-contract/package.json +++ b/packages/qa/downstream-contract/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/downstream-contract", - "version": "0.0.38-rc.0", + "version": "0.0.38-rc.1", "description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).", "license": "Apache-2.0", "private": true, diff --git a/packages/qa/http-conformance/CHANGELOG.md b/packages/qa/http-conformance/CHANGELOG.md index 71035687cb..92da00d0d4 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,17 @@ # @objectstack/http-conformance +## 0.0.6-rc.1 + +### Patch Changes + +- Updated dependencies [32ccb23] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [3c628ce] +- Updated dependencies [45dc446] +- Updated dependencies [857a6cf] + - @objectstack/core@17.0.0-rc.1 + ## 0.0.6-rc.0 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 5898f53ffb..02f0592b75 100644 --- a/packages/qa/http-conformance/package.json +++ b/packages/qa/http-conformance/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/http-conformance", - "version": "0.0.6-rc.0", + "version": "0.0.6-rc.1", "private": true, "license": "Apache-2.0", "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index 310d5499cd..906644e189 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,776 @@ # @objectstack/rest +## 17.0.0-rc.1 + +### Major Changes + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +### Minor Changes + +- c1dcacd: fix(sharing)!: the share-management surface gains the authorization layer it never had (ADR-0111 P0, #3902) + + Record sharing shipped as a data layer with no authorization of its own: every + `/data/:object/:id/shares` and `/sharing/rules` route authenticated the caller + and then ran the service under `SYSTEM_CTX` — any signed-in user could revoke + anyone's share, enumerate who-can-see-what, write self-grants, and define / + evaluate org-wide sharing rules. ADR-0111's P0 rulings land here: + + - **D1/D2** — `ISharingService.canManageShares(object, recordId, context)`: + system, the record's owner, or a holder of Modify All Data (probed via the + new fail-closed `ISecurityService.hasWriteBypass`). Enforced in the SERVICE, + so every caller is covered; without plugin-security it fails closed to + owner-only. + - **D4** — `revoke` is symmetric with grant, validates the share belongs to the + URL's record (`NOT_FOUND` on mismatch), and refuses non-`manual` rows + (`CONFLICT` — a rule-materialised grant would be resurrected by the next + reconcile). + - **D5** — `listShares` is management-gated (invisible record → `NOT_FOUND`, + visible-but-not-manager → `PERMISSION_DENIED`), and the open + `/data/sys_record_share` read surface is self-scoped: non-admin callers see + only rows naming them as recipient or grantor. + - **D6** — the whole `/sharing/rules` surface (list/create/get/delete/evaluate) + requires the new **`manage_sharing`** capability (D9; seeded into + `admin_full_access`, `manage_platform_settings` honoured as the legacy + equivalent), enforced in `SharingRuleService`. + - **D7** — no inert grants: `recipientType` is narrowed to `user` (the only + type any gate enforces), grants on objects the sharing gates never consult + (public model, no `owner_id`, bypass, `controlled_by_parent`) fail with + `SHARING_NOT_ENABLED` (422), and the manual upsert keys on + `(object, record, recipient, source)` so manual and rule rows coexist. + + **Breaking** for callers that relied on the missing gate: unauthorized share + management now fails with 403/404/409/422 instead of silently succeeding, and + `ISharingService.revoke` gained an optional `scope` parameter. The verb + boundary (edit ≠ delete, ADR-0111 D3) is NOT in this change — it lands as the + separate P1. + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- 7d7521f: feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) + + Settles the vocabulary ADR-0112 D6 deferred, per [ADR-0114](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0114-field-level-error-code-catalog.md). + + **`FieldErrorCode` — a closed, lowercase catalog.** 27 members covering what the + six emitters already emit. `FieldErrorSchema.code` tightens from `z.string()` to + this enum, so a validation body's per-field codes are validated for the first time. + `FieldValidationError.code` (objectql) and `FieldCoerceError.code` (rest) stop + being a hand-listed union and a bare `string` respectively and reference the + catalog, so the three cannot drift apart. + + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a + top-level code names the condition the _request_ hit, while a field-level code + names the _constraint_ the value violated — and constraints are declared in the + metadata's own snake_case, so `max_length` the code and `max_length: 50` the + property are the same word on purpose. + + **Zod issue codes no longer reach the wire (wire-visible).** Routes that validate + with Zod passed its vocabulary straight through, so `fields[]` spoke a different + language depending on which route served it, and `too_small` was ambiguous between + a short string, a small number and a short array. `zodIssuesToFields` now maps + using Zod's `origin`/`format`: + + | Was | Now | + | :------------------------------------------------ | :------------------------------------------------- | + | `too_small` | `min_length` / `min_value` / `min_items` | + | `too_big` | `max_length` / `max_value` / `max_items` | + | `invalid_format` | `invalid_email` / `invalid_url` / `invalid_format` | + | `invalid_value` | `invalid_option` | + | `unrecognized_keys` | `unknown_field` | + | `invalid_union`, `invalid_element`, `invalid_key` | `invalid_shape` | + + **A missing required property now reports `required`, not `invalid_type`.** Zod + spells "absent" as a type mismatch against `undefined`, so passing it through made + a form mark a _missing_ input as the wrong _type_. The two are indistinguishable on + the issue alone, so the mapper takes the parsed input as an optional argument and + walks the issue path; a caller that cannot supply it keeps `invalid_type` rather + than guessing. + + **`unknown_param` → `unknown_field`.** `ActionParamIssue.code` references the + catalog instead of its own literal union; the `param` key beside it already says + what was addressed. + + **Not changed:** `EnhancedApiErrorSchema.fieldErrors` keeps its name even though + every producer emits `fields`. Retiring an authorable key needs a tombstone plus a + migration (ADR-0104's contract guard), so it lands on its own — the property now + carries a banner saying which name the wire uses. + +- 789ad63: fix(spec,rest): the batch-size cap is enforced now, and each bulk endpoint has one Zod source (#3939) + + `max 200` was declared in four places and enforced in one. + + `batch.zod.ts` put `.min(1).max(200)` on `BatchUpdateRequestSchema`, + `UpdateManyRequestSchema` and `DeleteManyRequestSchema`, and the docs repeated + it — but no per-object bulk route validated against those schemas, so + `createMany` / `updateMany` / `deleteMany` / `/data/:object/batch` all accepted + an unbounded list. The only route that capped anything was the cross-object + `/batch`, and it checked the _configured_ `maxBatchSize` rather than the + hardcoded 200 — so even the one enforcement point disagreed with the schema. + + That stopped being cosmetic with #3897, which made `deleteMany` delete per id by + primary key (so `deleteBehavior` cascades run and every row gets its own + result). A 10k-id body is now 10k sequential engine round-trips inside a single + request, where before it was one statement that mostly failed anyway. + + **The cap moved to the routes, and the schemas gave it up.** Batch size is + deployment policy — `RestServerConfig.batch.maxBatchSize`, 1..1000, default 200 + — so a hardcoded bound in the spec could only ever be a second, wrong answer + (a deployment raising the limit to 500 would still have been refused at 200). + All five bulk routes now call one `enforceBatchSize` helper with the configured + value and answer with one envelope: + + ```json + { + "error": "Batch too large: 500 records (max 200)", + "code": "BATCH_TOO_LARGE", + "count": 500, + "max": 200, + "object": "account" + } + ``` + + The cross-object route is included: it used to answer with a bare `error` string + and no `code` for a client to key on. + + **One Zod source per bulk endpoint (Prime Directive #7).** Each of these + endpoints had _two_ schemas, and they had already drifted into disagreeing about + more than counts: `UpdateManyRequestSchema` described its rows with + `BatchRecordSchema`, whose `id` and `data` are optional because the generic + `/batch` route serves create (no id) and delete (no data) through the same + shape — so the declared contract accepted `{}` rows that `updateManyData`, which + reads `record.id` and `record.data` unconditionally, could never process. The + enforced shape lived in the _other_ copy, in `protocol.zod.ts`. + + The wire body is now the single source (`UpdateManyRequestSchema` / + `DeleteManyRequestSchema`, with the new `UpdateManyRecordSchema` for a row), and + the protocol schemas are that plus the `object` the route takes from the URL + path (#3933) — `UpdateManyRequestSchema.extend({ object })`. The derivation runs + that direction because `protocol.zod` already imports `batch.zod`; the reverse + would be a cycle. + + **Behaviour changes.** + + - A bulk request over the configured cap is `400 BATCH_TOO_LARGE` instead of + being executed. Deployments that were quietly relying on unbounded batches + should raise `batch.maxBatchSize` (up to 1000) rather than discover the cap in + production. + - `.min(1)` is gone with `.max(200)`: an empty batch is a no-op returning + `total: 0`, which is what these routes already did, rather than a validation + error the schema claimed but nothing raised. + - `UpdateManyRequest` now types (and validates) `records` as + `{ id: string; data: Record }[]`. Callers already had to send + that — the route has validated the strict shape since #3933 — but the declared + type was looser. + - New export: `UpdateManyRecordSchema` / `UpdateManyRecord`. + +- fccec22: fix(rest): bulk writes bind to the object in the path, not the one in the body (#3933) + + `POST /data/:object/updateMany` spread the request body over the value it had + just taken from the URL: + + ```js + const result = await p.updateManyData!({ + object: req.params.object, // trusted, written first + ...req.body, // …and spread over it + ... + }); + ``` + + The gate on the line above reads the PATH object — `enforceApiAccess` starts + with `const objectName = req?.params?.object` — so `enable.apiEnabled` / + `enable.apiMethods` (ADR-0049 / #1889) was enforced on the object in the URL + while the object named in the body got written. Measured on a stock CRM dev + deployment: `POST /data/crm_account/updateMany` with + `{"object":"crm_contact", "records":[…]}` returned `succeeded: 1` and changed + the `crm_contact` row. Point the URL at any exposed object, name a hidden one in + the body, and the gate clears the wrong object every time. + + This is not a row-authorization bypass — the engine middleware still evaluates + RLS/FLS against the object actually written, and `assertObjectRegistered` (#3770) + still resolves it. What it defeats is the object-level exposure policy, the layer + ADR-0049 exists to make enforceable rather than advisory. + + The path object is now written LAST, after the body, so the object the gate + cleared is the object that gets written — a property of the code rather than of + the caller declining to send that key. The body is parsed against + `UpdateManyDataRequestSchema` first, which (Zod strips unknown keys) also stops a + body `context` from becoming the execution context on a deployment where none + resolves — `requireAuth: false` plus an anonymous caller, the one case where the + trailing `...(context ? { context } : {})` has nothing to overwrite it with. + + `deleteMany` gets the same ordering: #3897 moved it behind a schema parse, but + fed that parse `{ object: req.params.object, ...req.body }` — still body-wins. + `createMany` (`records: req.body || []`) and `batch` (`request: req.body`) never + splatted the body at the top level and are unaffected. + + **Behaviour change.** A malformed `updateMany` body is now `400 +VALIDATION_FAILED` naming the offending path, instead of reaching the protocol + and failing further in. A body `object` key is ignored rather than honoured. + +- f4d7f1d: fix(metadata-protocol,rest): the id list is the only thing deleteMany can select on (#3897) + + `deleteManyData` built the predicate its endpoint is named after and then spread + the caller's `options` **over** it: + + ```js + return this.engine.delete(request.object, { + where: { id: { $in: request.ids } }, + ...request.options, // ← lands after `where`, so it can replace it + }); + ``` + + `request.options` is caller-supplied — `POST /data/:object/deleteMany` splatted + the whole request body into the protocol request (`{ object, ...req.body }`) — + so one body key rewrote the operation: + + ```json + { "ids": ["a"], "options": { "multi": true, "where": {} } } + ``` + + reached `engine.delete` as an unscoped bulk delete. The engine's write + middleware still composes RLS/sharing predicates onto the AST, so the blast + radius is not automatically the whole table: it is **everything the caller is + allowed to delete**. For an ordinary user with delete permission that is the + difference between the 3 records they asked for and every record they can see; + measured on a stock CRM dev deployment, that payload against one id removed all + 8 rows in the object and returned the raw driver count (`8`). The same spread + also accepted `context`, i.e. a forged principal wherever the route is reachable + without auth. + + **The id set is now authoritative, structurally.** The engine options are built + from the validated id list and nothing else — caller `options` is a + `BatchOptions` bag (`atomic` / `returnRecords` / `continueOnError` / + `validateOnly`) that carries nothing `engine.delete` consumes, so merging it + could only ever smuggle in engine keys. Ids must be scalars, so an operator + object (`{"ids":[{"$ne":null}]}`) cannot reach `where.id` either; a malformed + list is a `400 VALIDATION_FAILED` instead of a wider delete. The REST route + parses the body against `DeleteManyDataRequestSchema` first, one hop earlier — + Zod object schemas strip unknown keys, so `options.where`, top-level `where` and + a body `context` no longer survive the ingress at all. + + **The endpoint also works now.** `deleteManyData` never set `multi`, so a + correctly-formed `{"ids":[…]}` hit the engine's + `'Delete requires an ID or options.multi=true'` throw — only the requests that + triggered the override above ever completed. Deletes now go one id at a time by + primary key, the same shape `batchData`'s `delete` case uses, which closes two + gaps behind that: the bulk branch skips `cascadeDeleteRelations`, so + `deleteBehavior` (`cascade` / `set_null` / `restrict`) was not honoured for the + rows it removed; and the declared `BatchUpdateResponse` contract (per-record + `results`, `atomic`, `continueOnError`) was unimplementable from a bulk row + count. Both are delivered rather than declared. + + **Behaviour change.** The endpoint returns a `BatchUpdateResponse` + (`{ success, operation, total, succeeded, failed, results }`) where it + previously returned the driver's raw delete count — on the paths where it + returned anything at all. The caller's execution context is threaded to every + delete, so RLS/FLS now run under the caller here as they do on the single-record + route. + +- 507b92a: fix(spec,objectql,rest,runtime): field-validation messages answer in the caller's language, named by the field's label (#3957) + + The write path built every built-in validation message by concatenating the **API + field name** into a **hardcoded English** template. Those strings are what the + Console toast, the CSV-import row report, the CLI and any custom client display + verbatim, so a Chinese-locale user importing a bad row read: + + ``` + 第 1 行:penalty_amount must be ≥ 0 + ``` + + …for a field declared `label: '处罚金额'` with a full `zh-CN` bundle loaded. The + form layer localized the _same_ constraint correctly (the browser's native + `min`), so the language flipped depending on which layer caught the value. + + **Three things changed.** + + 1. **The message is rendered in the caller's locale** from a built-in catalog + (`BUILTIN_VALIDATION_MESSAGES`, `@objectstack/spec/system`) shipping `en`, + `zh-CN`, `ja-JP`, `es-ES` — the same four locales as the platform bundles. + The locale comes from `ExecutionContext.locale`, whose contract already read + "Drives message catalogs"; this is the consumer that makes that true. Both + HTTP entries (REST server, runtime dispatcher) now resolve it from the + request's `Accept-Language` / `?locale` first, falling back to the workspace + `localization.locale` — so a rejection message and the field labels around it + can no longer disagree. + + 2. **The field is named by its label, never the API name**: translation bundle + (`objects..fields..label`) → declared `label` → API name as the last + resort. `FieldValidationError.field` still carries the API name so a form can + focus the right input. + + 3. **The constraint is exposed as data**, so a client can format its own text + instead of parsing the sentence: + `{ field, code, message, label, constraint: { min: 0 } }`. This rides + ADR-0114's existing `constraint` / `value` positions on `FieldErrorSchema` + (`constraint` tightens from `unknown` to `Record`) rather + than adding a parallel payload — `label` is the only new field. The bag + carries `min`/`max`/`minLength`/`maxLength`/`actual`/`allowed`/`type`, and the + message templates interpolate from exactly those keys. + + Covered end-to-end, not only in the validator: single and batch insert, + single-id and multi-row update, ADR-0113's clear-out rejection, the object-level + rule evaluator's own built-in messages (`requiredWhen`, per-option gating, + state-machine fallbacks), and the importer's cell-coercion, required pre-check + and #3956 bound pre-check messages — all of which land in the same row report. + + **What this changes for consumers.** + + - `code` is unchanged (ADR-0114's `FieldErrorCode`) and remains the thing to + match on. Message keys are finer-grained than codes — `invalid_datetime`, + `invalid_option_value`, `required_cleared` are rendering detail and never reach + the wire — so localization never splits the client-facing vocabulary. + - `message` **text changes**: it is localized, and it names the field by label + even in English (`Budget must be ≥ 0`, not `budget must be ≥ 0`). Anything + asserting on the old English string should match `code` (and now + `constraint`) instead. + - An author-written validation-rule `message` is never touched — it is already + in the language its author chose. + - A deployment can override any built-in message with a `translation` item + defining `validation.field.` (e.g. + `validation.field.min_value: '{{label}}不得小于 {{min}} 元'`). + - The importer's reference-failure message no longer names the target object's + API name (`no sys_user matches "…"`): naming internal identifiers is the + defect being fixed, and the column plus the offending value are what an + importer can act on. + +- be7945a: feat(rest): `audience: 'public'` publishes a book anonymously on a secure-by-default deployment (#3963) + + `book.audience: 'public'` was a declared per-book capability that in practice + required the deployment to open its **entire** data plane. The `/meta` umbrella + gate refused every anonymous caller unless `api.requireAuth` was `false`, so a + `public` book was only ever reachable inside a globally-public deployment — the + audience model was _re-narrowing_ what that flag had already opened, not granting + anything of its own. ADR-0046 §6.7 recorded exactly that as ground truth ("the + gate is the optional global `requireAuth` … not the handler"). + + The exemption is now derived from the declaration, the same shape ADR-0056 + Option A chose for public form submission (`publicFormGrant`): the umbrella gate + admits an anonymous **GET** of the book/doc read surface, and the §6.7 audience + gate inside the handler is what authorizes it. + + Narrow in three independent ways: + + 1. **Only when no execution context resolved.** An authenticated caller still + goes through `enforceAuth` unchanged, so the ADR-0069 auth-policy gate + (expired password, enforced MFA) keeps governing a gated session's book reads. + 2. **Only GET, only book/doc.** `GET /meta/:type`, `GET /meta/:type/:name` (type + `book` or `doc`, either spelling — #3984) and `GET /meta/book/:name/tree`. + Every other type stays 401 for anonymous, writes stay 401, and `GET /meta` + itself stays 401. The predicate keys on the REGISTERED route path plus the + normalized `:type`, so a route added later cannot fall into it by accident. + 3. **Reachability, not authorization.** `audienceAllows` admits `'public'` only; + `org` and `{ permissionSet }` books require `caller.authenticated` and + unresolvable holdings fail closed, so an anonymous read of a gated book is + still `401`. + + A deployment can now publish a public manual with `requireAuth: true` — which is + the prerequisite for retiring that flag entirely (#3963 step 2). ADR-0046 §6.7 + carries an amendment recording the new gate; its SEO and tenant-from-host + reasoning is unchanged, having never depended on the flag. + +### Patch Changes + +- fae74b5: fix(rest): give the bare 501 error exits a machine `code` (#4067) + + Most REST error exits already carry a typed `code` (`VALIDATION_FAILED`, + `BATCH_NOT_ATOMIC`, `BATCH_TOO_LARGE`, `PERMISSION_DENIED`), and the clone / + search 501s already answer `{ error, code: 'NOT_IMPLEMENTED' }`. Four 501 exits + still returned a bare `{ error: '' }` with no code, so a client could + only key on the prose: + + - the cross-object transactional batch route (`POST {basePath}/batch`) when the + runtime has no `transaction()` — the last untyped exit on that route, whose + siblings (`BATCH_NOT_ATOMIC`, `VALIDATION_FAILED`, the `enforceBatchSize` + `BATCH_TOO_LARGE`) were already typed by the #3897 / #3933 / #3939 line; + - the two `saveMetaItem`-unsupported exits; + - the UI-view-resolution-unsupported exit. + + Each now carries `code: 'NOT_IMPLEMENTED'`, matching the clone / search 501s. + Additive only — the `error` message is unchanged and no status changes — so + existing clients are unaffected; new ones can branch on the code. + +- 99b4392: Advertise `mcp` in `/discovery` only when it is actually serveable (#4024). + + Both discovery producers gated the `/mcp` route on `isMcpServerEnabled()` alone. + The stated justification was a lockstep — `os serve` auto-loads plugin-mcp from + the same flag, so on that path advertised did imply mounted. But the lockstep is + a property of the CLI, not of the dispatcher: `@objectstack/rest` has no + `@objectstack/mcp` dependency, mounts no `/mcp` route and performs no auto-load, + so a host that embedded it without plugin-mcp advertised `/mcp` in `/discovery` + and then answered 501 on it — the `declared ≠ enforced` failure #3369 forbids, + and a broken contract for third-party clients that read `/discovery` to decide + what exists. + + Both producers now require the flag AND a serveable MCP service. The runtime + dispatcher gates on the handler's own predicate (`typeof +mcp.handleHttpRequest === 'function'`), so a wrong-shaped service can't + over-promise either. `@objectstack/rest` probes via the per-request kernel or the + single-env `serviceExistsProvider`; when it genuinely cannot probe it keeps the + prior flag-only answer rather than hiding a working endpoint (fail-open, + ADR-0057 D10). The `os serve` / `os dev` path is unchanged — it loads the plugin, + so the service resolves and `/mcp` is still advertised. + + Also exercises the `mcp: false` seam in `route-parity.integration.test.ts`, which + had existed unused since the file was written: `bootServe()` was only ever called + with no args or `{ notification: false }`. The one capability whose advertisement + was not service-presence gated was also the one whose absence was never tested. + +- 495019b: fix(rest): the /meta per-type gates are enforced on both spellings of the type segment (#3984) + + Every per-type filter on `GET /meta/:type` and `GET /meta/:type/:name` compared + `req.params.type` to a literal SINGULAR name, while the protocol's `getMetaItems` + normalizes singular↔plural and serves either. Prime Directive #3 makes plural the + canonical REST spelling, so the form a client is most likely to use — + `/api/v1/meta/books` — reached the handler with every gate skipped. + + Three of those gates are authorization: + + - **ADR-0046 §6.7 book / doc audience** (three sites: the list, the single-item + read, and the doc effective-audience union). `GET /meta/books` returned a + `{ permissionSet }`-gated book — an _Admin Guide_ — to a caller who does not + hold the set, and `GET /meta/books/admin_guide` answered `200` where the + singular spelling answers `401`. On a publicly-served deployment the same skip + handed an `org` book to an anonymous reader. + - **App RBAC filter** — hides privileged apps (Studio, Setup) and gated nav + entries from callers without the grants. `GET /meta/apps` skipped it. + - **Dashboard `requiresService` gate** (ADR-0057 D10). `GET /meta/dashboards` + skipped it. + + The remaining spelling-sensitive branches are behavioural rather than + authorization — doc i18n locale collapse, and the list-response `content` strip — + and were inconsistent between the two spellings for the same reason. + + Each handler now normalizes the type ONCE (`RestServer.metaTypeSingular`, backed + by the same `PLURAL_TO_SINGULAR` table the protocol uses) and every gate keys on + that value, so the two spellings of one route can no longer diverge. Found while + scoping #3963. + +- 0931185: fix(rest,service-settings,service-datasource)!: four more route modules emit the declared envelope, and the guard is now shared (#3843) + + #3675 and #3689 moved `service-storage` and `service-i18n` onto the declared + response envelope (`BaseResponseSchema` + `ApiErrorSchema`). Each scoped itself + to one service, and neither asked whether the same drift existed elsewhere. It + did — in four more modules, and in two of them it was the _older_ shape, the one + #3675 had already declared wrong: + + | Module | before | now | + | ------------------------------------- | -------------------------------------------------------------- | ------------- | + | `service-settings/settings-routes.ts` | nested `error`, no `success` on any of 5 bodies | full envelope | + | `service-datasource/admin-routes.ts` | `{ error: '' }`, `message` a **sibling** | full envelope | + | `rest/external-datasource-routes.ts` | `{ error: '' }` + a private `ok` | full envelope | + | `rest/package-routes.ts` | 3 of 16 bodies had `success`, 2 failures had no `error` at all | full envelope | + + ## Breaking: where to read things now + + **Success payloads move under `data`.** The keys are unchanged — only their + depth. `unwrapResponse` in `ObjectStackClient` returns `body.data` when the flag + is present, so every SDK method (`packages.list()`, `datasources.external.*`) + resolves to exactly the object it always did. Raw `fetch` callers must add one + hop: + + ``` + GET /api/v1/datasources body.datasources → body.data.datasources + GET /api/v1/datasources/drivers body.drivers → body.data.drivers + GET /api/v1/datasources/:name body.datasource → body.data.datasource + GET /api/v1/packages body.packages → body.data.packages + GET /api/v1/packages/:id body.package → body.data.package + GET /api/settings body.manifests → body.data.manifests + GET /api/settings/:ns body.manifest/.values → body.data.manifest/.values + POST /…/external/validate body.ok, body.results → body.data.ok, body.data.results + ``` + + `SettingsNamespacePayloadSchema` and friends still describe those payloads + exactly; they now describe the envelope's `data` rather than the whole body. + + **Error bodies stop being a string.** `{ error: 'datasource_admin_error', +message }` → `{ success: false, error: { code: 'datasource_admin_error', +message } }`. Read `body.error.message`, not `body.message`; read + `body.error.code`, not `body.error`. This is the asymmetry #3675 opened on: a + caller reading `body.error.message` previously got the real message from the + dispatcher and `undefined` from these routes. + + **Two failures that never said why now do.** `DELETE /api/v1/packages/:id` + answered a bare `{ success: false }` and a bare + `{ success: false, failed, cleanups }`. They are now `PACKAGE_DELETE_FAILED` and + `PACKAGE_DELETE_PARTIAL`, with the per-item `failed` / `cleanups` arrays under + `error.details`. + + **Codes follow ADR-0112.** #3841 settled the vocabulary while this was in review: + `error.code` is SCREAMING_SNAKE and `ApiErrorSchema.code` is now the closed + `ErrorCode` union, so an unregistered code fails schema parse. Generic conditions + reuse the STANDARD catalog rather than becoming registered synonyms of it, per the + ledger's own guidance: + + ``` + datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard) + external_service_unavailable → SERVICE_UNAVAILABLE (standard) + not_found / PACKAGE_NOT_FOUND → RESOURCE_NOT_FOUND (standard) + PUBLISH_FIELDS_MISSING → MISSING_REQUIRED_FIELD (standard) + INTERNAL → INTERNAL_ERROR (standard) + datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered) + external_import_error → EXTERNAL_IMPORT_ERROR (registered) + PUBLISH_MANIFEST_INVALID → PACKAGE_MANIFEST_INVALID (registered) + PUBLISH_FAILED → PACKAGE_PUBLISH_FAILED (registered) + PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED / SETTINGS_ACTION_FAILED (registered) + ``` + + Which service is unavailable is carried by `message`. The seven registered codes are + added to `ERROR_CODE_LEDGER` under their owning packages — including a new + `@objectstack/service-datasource` entry. + + **`POST /external/validate` keeps its `ok`.** Unlike the `{ ok: true, key }` + #3689 retired from storage — a private second word for `success` — this `ok` is a + computed verdict over the federated objects (`results.every(r => r.ok)`). The + request can succeed while the verdict is false, so the two flags are not the same + field; `ok` moves inside `data` rather than being dropped. + + Consumers were taught both shapes first, so the two repos are not coupled by + merge order: objectui's `packages` readers were already tolerant + (`payload?.data ?? payload`), and its datasource page plus the generic + `type: 'api'` action runner now unwrap the envelope and read `error.message` + (the latter previously toasted `[object Object]` for any nested error). + + ## The guard is shared now, not copied + + `scripts/check-route-envelope.mjs` + `pnpm check:route-envelope`, wired into + `lint.yml` alongside the nine sibling `check:*` guards. Its load-bearing assertion + is structural rather than per-route: **it counts the response write sites per + module.** When every body goes through the `sendOk` / `sendError` pair that count + is fixed at two and does not grow with the route list — so a _future_ route that + hand-rolls a body fails the guard. That is the coverage a driven-body test can + never give, since it can only drive the routes that existed the day it was + written. + + This existed three times already as an open-coded regex block (storage error, + storage success, i18n error). Lifting it did more than deduplicate: a per-package + scan **structurally cannot notice a module nobody thought to convert**, and going + repo-wide found two the moment it ran — neither is in #3843's hand-written survey: + + - `plugin-sharing/share-link-routes.ts` — the fifth drifting module. No body + carries `success`, and one answers `{ ok: true }`, the private second word #3689 + retired from storage. Filed as #3983 and pinned by the guard; converting it is + breaking for share-link consumers and needs its own sweep. + - `metadata/routes/hmr-routes.ts` — declared **exempt** with a reason (dev-only + SSE endpoint, not on the SDK surface), not skipped. Three states, deliberately — + conformant / ratcheted / exempt — because that is the honest classification + ADR-0049 asks for. A route module the scan finds but the table does not declare + is an **error**, never a default: applying `2 / 1 / 1` to an unknown module would + let a new one pass by coincidence. + + It also drops the regex for the TypeScript AST, fixing two real bugs the copies + had. They stripped comments with `String.replace`, whose line-comment pattern also + ate `//` inside string literals and truncated the rest of that line — response + writes included. And `.json(` does not mean "write a response": `hmr-routes.ts` + calls `c.req.json()` twice to READ a request body, which a textual count reports as + two unenveloped responses. Comments and literals are not AST tokens, and + request-vs-response is a property of the callee, so both disappear. The script + carries a `--self-test` pinning each case — the nine sibling guards have none, but + both of these bugs survived a review of the regex version. + + **The i18n ratchet, stated rather than hidden.** `i18n-service-plugin.ts` is + declared at `responses: 5, ok: 4, err: 1` with a ratchet pointing at #3973. Its + error half _is_ consolidated (#3675), but each of its four read routes builds + `{ success: true, data }` inline. Those bodies are correct — that is not envelope + drift — but an unconsolidated builder is a weaker guard: a fifth read route could + get the shape wrong and only a driven test would notice. The numbers pin today's + structure exactly (a new inline body fails) and drop to the conformant `2 / 1 / 1` + when #3973 lands. + +- ccd9397: fix(security)!: a sharing rule with no criteria now shares NOTHING instead of every record (#3896) + + `SharingRuleSchema` has always required `condition`, and its doc is explicit + that a predicate the compiler cannot lower is _"skipped and logged — never + seeded as a permissive match-all (ADR-0049)"_. The declared/seed path honoured + that. The two other ways to create a rule did not: + + - **`POST {basePath}/sharing/rules`** plucks its body field-by-field into + `SharingRuleService.defineRule`, which validated `name` / `label` / `object` / + `recipientType` / `recipientId` — and not `criteria`. A missing, `null`, or + **misspelled** key (`criterias`) was stored as `criteria_json: null`, answered + `201` with no warning, and evaluated as + `find(object, { filter: {}, context: SYSTEM_CTX })`: every record of the + object, up to 5000, granted to the recipient. Triggering it took a typo, not + an attacker. + - **Authoring a rule in Setup** is a direct `sys_sharing_rule` insert, which + never reaches `defineRule` at all. + + Empty criteria is now rejected everywhere a rule can be written, and — because + rules created before this gate are already in the table — the evaluator refuses + to act on one regardless of how it got there. + + - **`defineRule` rejects a match-all criteria** with + `VALIDATION_FAILED: criteria is required …`, alongside its other required + fields. Covers the REST endpoint, programmatic callers, and the seeder. + Rejected shapes: missing / `null` / `''` / `{}` / `[]` / `{ $and: [] }` / + unparsable JSON (e.g. a CEL source typed into the Criteria box). + - **The evaluator matches nothing** for such a rule and logs why, so a row + stored before this release under-shares instead of over-sharing: the next + reconcile _revokes_ the grants it had materialised. Both evaluation paths are + covered — the bulk `evaluateRule` and the per-record write-hook path. + - **`bindRuleCriteriaGuard`** fails `sys_sharing_rule` inserts with no + criteria as a field-level `VALIDATION_FAILED` (a 400 naming `criteria_json`), + so the Setup path reports the problem instead of saving an inert rule + (ADR-0078). Updates are checked only when the patch supplies + `criteria_json` — switching an over-broad legacy rule off must not require + inventing a criteria for it first. + - **The seed bootstrap's "empty condition = match-all" branch is gone**: a + missing or empty `condition` is now skipped and logged like any other + non-lowerable one. + - `POST {basePath}/sharing/rules` also accepts `criteria_json` as an alias for + `criteria`, matching the snake_case aliases the endpoint already takes for + `object_name` / `recipient_type` / `access_level`. + + **Migration.** There is no "share every record" sharing rule, and there never + usefully was one — the shape existed only as a failure mode. A rule that + relied on it must state its predicate (`criteria: { stage: 'won' }`), or, if + the object really should be readable by everyone, use the object's + organization-wide default (`sharingModel`) instead. Rules already stored with + a null `criteria_json` need no data migration: they stop granting on the next + evaluation and their existing grants are revoked. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + - @objectstack/service-package@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 9100b10546..10a4823468 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack REST API Server - automatic REST endpoint generation from protocol", "type": "module", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index bdbf443b98..a0c31d6d0c 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,1425 @@ # @objectstack/runtime +## 17.0.0-rc.1 + +### Major Changes + +- 195ad76: fix(actions)!: failures speak HTTP — business rejections are 400, success is a single wrap (#3962) + + **BREAKING (raw-HTTP callers of `POST /api/v1/actions/...` only).** The + 200-with-inner-envelope wire was never a designed contract: no ADR or doc ever + specified it, it originated as the route's catch block reusing + `deps.success()`, and `/actions` was the only route of 12 that double-wrapped. + #3962 classifies it as a bug. Five defects traced back to that one extra layer + (the console's green toast on failed actions, `redirectUrl` never firing, a + marketplace install reported as installed when it failed, the client-envelope + divergence #3927 papered over, and crashes invisible to monitoring). + + The contract now, identical to `/data`: + + | Outcome | HTTP | Body | + | :------------------------------------------------------------- | :-------------------: | :-------------------------------------------------------------------- | + | Ran, returned | **200** | `{success: true, data: }` — single wrap | + | Ran, rejected (business rule / validation) | **400** | `{success: false, error: {message, code, details: {code?, fields?}}}` | + | Never dispatched (unknown / denied / wrong type / unavailable) | 404 / 403 / 400 / 503 | unchanged (#3930/#3951) | + | Crashed (`TypeError`, driver class, sandbox timeout) | **500** | unchanged (#3951) | + + A validation rejection carries `details.code: 'VALIDATION_FAILED'` and + `details.fields[]` — the exact payload #3937 fought for, now on the same wire + shape `/data` has always used, which `@objectstack/client` normalizes to + `err.code` / `err.fields` (#3927). A rejected flow is a 400 with + `details.code: 'FLOW_FAILED'`. The crash-vs-rejection discriminator (#3951, + error `name`) now selects 400 vs 500. + + `client.actions.invoke` / `invokeGlobal` still never throw: they fold every + failure status into `{success: false, error}`, read the single wrap on + success, and keep a NARROW legacy heuristic so a current SDK talking to a + pre-#3962 server still folds the old double-wrapped 200s correctly. + + **Migration for raw-HTTP third parties:** branch on the HTTP status — a + non-2xx is the failure, `error.message` / `error.details` carry the detail; on + a 200, `data` is the handler's return value directly (one level less than + before). Callers using `@objectstack/client` need no change. + +- ffb003c: **ADR-0110 — an action's identity is its `name`, and anything executable over a + governed surface must have a declaration.** + + `POST /api/v1/actions/:object/:action` resolved the DECLARATION from the URL + segment as a `name` but dispatched the HANDLER using that same segment as a + registry key. For a target-bound action (`{ name: 'complete_task', target: +'completeTask' }`) those are different strings, so the two documented callers + each worked on exactly the half the other broke: the documented curl resolved + the declaration then 404ed, while the Console's `target`-addressed call + dispatched fine and resolved no declaration — silently skipping the ADR-0066 D4 + capability gate and the ADR-0104 param contract (#3935). + + - **D1/D2** — identity is always the declarative `name`; the handler key is + derived from the resolved declaration through a rotation now shared with the + MCP `run_action` bridge (`resolveActionHandlerKeys`, `executeRegisteredAction`). + The REST route previously rotated only the object key, never the handler key. + - **D3 (breaking)** — declaration resolution is a trichotomy. A genuinely + undeclared handler is **refused (404)** with the `defineAction` to add, rather + than executed ungated with system privileges; an unreachable metadata plane is + a **503** rather than a silent ungating (`MetadataManager.loadDiagnosed` tells + a clean miss from an outage). `OS_ALLOW_UNDECLARED_ACTIONS=1` is the migration + valve — it warns on every invocation and is removed in 18. + - **D5** — `reconcileActionRegistrations` plus `ObjectQLEngine.listRegisteredActions` + power a `kernel:ready` inventory logging every registered-but-undeclared + handler (refused at dispatch) and every declared script action bound to no + handler — the ADR-0078 converse, mechanised. + - **D6** — security-gate strictness is opt-**out** (`OS_ALLOW_*`), never opt-in. + + Apps whose actions are all declared need no changes beyond gaining enforcement + of the `requiredPermissions` they already declared. + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +- 347f460: **[ADR-0110 D3, revised] The undeclared-action refusal has no opt-out — + `OS_ALLOW_UNDECLARED_ACTIONS` is removed before 17 ships.** + + D3 as accepted refused an undeclared handler but shipped + `OS_ALLOW_UNDECLARED_ACTIONS=1` as a migration valve that ran it anyway, + "slated for removal in 18". Removed now, for two reasons: + + - **It contradicts the ruling it accompanies.** A flag that executes an + ungoverned, system-elevated handler _is_ the fail-open D3 closes. ADR-0049's + trichotomy has no "enforced unless a flag says otherwise" state. + - **It had no observed users.** A reconciliation sweep across the platform + packages, every example and every plugin found the only `engine.registerAction` + call sites are `app-todo`'s eight, all declared. The valve would have shipped + a documented way to reopen the gate for a population nobody has ever seen. + + What it was buying is covered without it: the app still boots, every declared + action still works, D5's boot inventory names each offender at startup, and the + 404 names the `defineAction` to add. Migration costs a code change rather than + an env var — the correct price for reopening an authorization gate. + + Setting the retired variable has no effect; a regression test pins that, so a + stale deployment script fails loudly rather than silently re-opening the gate. + +### Minor Changes + +- 6e141bc: fix(actions): an action that CRASHED is a 500, not a 200 reporting success:false (#3913 follow-up) + + #3937 settled that a failed action reports in the payload at HTTP 200 — "an + action that fails is a normal outcome, not a transport error". That is a + statement about the action **rejecting**: a business rule saying no. The same + exit was also covering a third case it never argued for. + + A `TypeError` in a handler, a driver blowing up, a sandbox timeout — those are + not outcomes the action chose to report, they are the server failing to produce + one. Serving them as 200 hid **every handler crash** from the layers that exist + to catch server faults: gateway error rates, retry and circuit-breaker policy, + APM auto-capture, alerting, `fetch().ok`. For a platform whose main extension + surface is customer-authored script bodies, "customer action bodies are + throwing" had no signal short of body-parsing at every hop. + + Those are **500** now, through the same `errorFromThrown` exit every other + domain catch has used since #3925 — which also means a driver dump finally goes + through the internal-error-leak sanitiser (#3867) instead of reaching the client + verbatim in a 200 body. + + **Nothing #3937 put in the payload moves.** A rejection and a crash are told + apart by the error's NAME, the signal `@objectstack/rest` already uses on this + exact distinction ("non-default names (`TypeError: …`) […] signal a genuine + script bug rather than a deliberately thrown business rule"): + + | Thrown | Verdict | Wire | + | :------------------------------------------------------------------ | :------------------------ | :------------ | + | `new Error(msg)` — a registered handler rejecting | rejection | 200 + payload | + | `SandboxError` with `innerMessage` — a body's deliberate throw | rejection | 200 + payload | + | Anything carrying `code` / `fields`, or a `ValidationError` by name | rejection | 200 + payload | + | A throw with no `name` at all | _not confidently a fault_ | 200 + payload | + | `TypeError` / `ReferenceError` / `SqliteError` / a driver's class | crash | **500** | + | `SandboxError` with no `innerMessage` — timeout, capability denial | crash | **500** | + + Deliberately the narrow direction: only what is _certainly_ a fault moves, and + everything uncertain keeps the 200 it has today. + + One related fix in the same exit: an error carrying its own `status` / + `statusCode` (a plugin's `FORBIDDEN` with `status: 403`) is now served with it + rather than buried in a 200 payload — that status was the one thing the thrower + was unambiguous about. Record `ValidationError`s deliberately carry no + `.status`, so #3937's cases never reach that branch. + + Documented in `api/error-catalog.mdx` (new **Action Errors** section with the + full status table and the two-check pattern a raw `fetch` caller needs) and + `ui/actions.mdx`. + +- c2bbd97: fix(actions): reach global actions at their real registration key, and 404 an action that never dispatched (#3913) + + **1 — the registration key and the lookup key disagreed.** Both writers + register an objectName-less action under the literal `'global'`: `AppPlugin` + (`action.object || 'global'`) and `ObjectQLPlugin.actionObjectKey`. The REST + route's fallback probed `'*'`, and `engine.executeAction` is an exact-string + `Map` lookup with no wildcard semantics — so the probe could only ever miss: + + ``` + Action 'log_call' on object '*' not found + ``` + + `POST /api/v1/actions/global/log_call` worked by **accident** (the path segment + happened to spell the registration key); `POST /api/v1/actions//log_call` never + worked at all, and neither did falling back from an object-scoped route to a + global handler. `'global'` is now the canonical key + (`GLOBAL_ACTION_OBJECT_KEY`), the probe order is + `[, 'global', '*']` for both the REST route and the MCP + `run_action` bridge (`actionHandlerObjectKeys` — one list, two surfaces), and a + single-segment path (`/actions//:action`) routes at `'global'` instead of + 400-ing. A handler registered directly under `'*'` still resolves; the doc + comments that called `'global'` a "wildcard" are corrected at every site. + + **2 — "no such action" was reported as a success.** The not-found exit called + `deps.success(...)`, which always emits `{status: 200, body: {success: true, +data}}`, so a request naming an action that does not exist came back as: + + ```json + { + "success": true, + "data": { + "success": false, + "error": "Action 'log_call' on object '*' not found" + } + } + ``` + + Every caller that did not hand-unwrap the INNER envelope read the outer + `success: true` and reported a success that never happened — including the + shipped console, which showed a green toast (fixed on that side in + objectui#2963). Nothing **dispatched** there, so it is a **404** now, joining + the answers this route already gives a status: 403 denied, 400 wrong action + type, 503 unavailable. The miss also names the **routed** object rather than + whichever probe ran last (the old fallback said `on object '*'`, an object the + caller never asked for). + + A handler that **ran and rejected** is unchanged: HTTP 200 with + `data: {success: false, error, code?, fields?}`. That is a business outcome, + not a transport error, and #3937 pins it. The line is "did a handler run" — + below it the payload, above it the status. + + `client.actions.invoke` / `invokeGlobal` still do **not** throw. `client.fetch` + throws on every non-2xx, so `invoke` now catches and folds a dispatch failure + into the same `{ success, data?, error? }` result with `error` as a plain + string — otherwise the routes that just gained a status would have started + propagating exceptions into callers that only ever checked `result.success`. + +- 32ccb23: feat(spec,core,runtime)!: ADR-0112 batch 1 — one error-code vocabulary, SCREAMING_SNAKE, schema-enforced (#3841) + + Settles #3841 per ADR-0112: the top-level `error.code` vocabulary is + SCREAMING_SNAKE, in two tiers. + + - **`StandardErrorCode` members renamed in place** (`validation_error` → + `VALIDATION_ERROR`, all 53). Breaking for importers that branch on the old + lowercase members; the type name and member _meanings_ are unchanged. + - **New `ERROR_CODE_LEDGER`** (`@objectstack/spec/api`): service-specific codes + (`AUTH_REQUIRED`, `VALIDATION_FAILED`, `ATTACHMENT_DOWNLOAD_DENIED`, …) are + registered per owning package. `ErrorCode` = standard ∪ registered. + - **`ApiErrorSchema.code` is now `ErrorCode`**, not `z.string()` — an + unregistered code fails parse, so the envelope conformance suites assert + values, not just shape. + - **`FieldErrorSchema.code` widened to `z.string()`** (ADR-0112 D6): field-level + codes are a separate vocabulary the enum never described; #3977 owns its real + catalog. + - **Derived codes changed case on the wire**: `standardErrorCodeForHttpStatus` + now yields SCREAMING members (`permission_denied` → `PERMISSION_DENIED`, + `method_not_allowed` → `METHOD_NOT_ALLOWED`, …) — this map was #3842's + designated one-file sweep point for exactly this decision. + - **`ANONYMOUS_DENY_CODE` is `'UNAUTHENTICATED'`** (was `'unauthenticated'`) — + the promoted code on anonymous-denied requests and the REST `enforceAuth` + body change spelling with it. + + `error-catalog.mdx` and the error-handling guides are rewritten to the single + vocabulary; a spec test now locks the catalog page's headings to the enum so + they cannot drift apart again. Remaining lowercase emitters (cloud-connection, + plugin-auth envelope codes, metadata-protocol, …) are the batch-2 sweep. + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- 0f12193: feat(runtime): mount /analytics routes only when the capability exists (#3891 follow-through, ADR-0076 D11) + + `createDispatcherPlugin` used to mount `POST /analytics/query`, + `GET /analytics/meta` and `POST /analytics/sql` on the `IHttpServer` + unconditionally — so a deployment without `@objectstack/service-analytics` + still had the routes in its table: a `PUT` answered `405` with + `Allow: POST`, advertising a method on an API that wasn't there (the `POST` + itself answered 404 since #3989). + + The mounts are now capability-conditional. Plugin `start()` runs after the + kernel's Phase-1 init, so service presence is authoritative: + + - **single-kernel mode, `analytics` not registered** — the three routes are + NOT mounted; every method on `/api/v1/analytics/*` answers the adapter's + shared not-found contract (`404 { "error": "Not found" }`), and a boot log + names the fix (`Install @objectstack/service-analytics`); + - **single-kernel mode, `analytics` registered** — unchanged; + - **multi-tenant host** (a `kernel-resolver` is wired) — mounted + unconditionally, because mounts are host-global while the analytics service + lives in each per-project kernel: capability presence is a per-request + question, answered by the analytics domain's existing `handled:false` → 404 + (new public `HttpDispatcher.isMultiTenantHost()` exposes the mode). + + With this, the `/analytics` API surface exists exactly when the capability is + installed — completing the #3891 arc: #3989 emptied the slot (no more + unscoped-aggregate shim), #4010 made the body contract strict at the entry, + and this change removes the last wire-level residue of the uninstalled API. + +- 9b6fe7c: fix(spec,runtime)!: `AnalyticsQueryRequest` is the bare `AnalyticsQuery`; the dispatcher validates `/analytics` bodies at the entry (#3878) + + **Spec.** `AnalyticsQueryRequestSchema` used to describe a + `{ cube, query: {...}, format }` ENVELOPE — the dialect of the retired degraded + analytics shim (#3891), which the real engine never understood: an envelope + body inferred a column-less cube and died as an SQL syntax error + (`SELECT FROM …`) instead of a shape error. The schema now describes what the + engine and every real caller actually use — the **bare `AnalyticsQuery`**: + + ``` + FROM { "cube": "orders", "query": { "measures": ["count"] }, "format": "json" } + TO { "cube": "orders", "measures": ["count"], "dimensions": [...], "where": {...} } + ``` + + `cube` + `measures` are required at the top level; `dimensions` / `where` / + `timeDimensions` / `order` / `limit` / `offset` / `timezone` sit beside them. + The schema is `.strict()`; `query` and `format` are tombstoned (`retiredKey`) + so both `tsc` and the parse answer with this exact migration. `format` was + never implemented (every response is the JSON envelope) — for CSV/XLSX use the + export surface. The removal is registered as two step-17 semantic migrations + (`analytics-query-request-envelope-retired`, + `analytics-query-request-format-retired`) — it is an HTTP-wire change with no + stored metadata to rewrite. + + **Runtime.** `POST /api/v1/analytics/query` and `/analytics/sql` now validate + the body against that schema AT THE ENTRY and answer + **400 `VALIDATION_FAILED`** with per-field details — including the envelope + prescription above, and a bespoke hint that `filters` is not a contract field + (the filter field is `where`, the same canonical FilterCondition `find()` + takes). Previously a malformed body reached the engine and failed as a 500 SQL + syntax error, or had its off-contract filter key silently ignored. A valid + body is forwarded to the analytics service byte-identical (validation only — + parsing would inject the schema's `timezone: 'UTC'` default and override + org-timezone resolution). An uninstalled analytics capability still answers + 404 before any body inspection (#3891). + +- c9d254a: feat(datasource,runtime): kernel teardown disconnects through the one datasource path — and never closes an adopted pool (#3993) + + After the #3826 connect convergence, ADR-0062 D5's "owns connect/disconnect" + was half-true: nothing disconnected the `default` (or a declared datasource's + pool) on graceful shutdown. `DriverPlugin` never had teardown, `ObjectQLPlugin` + teardown never touched drivers, and the kernel's actual teardown phase is + `destroy()` — the Plugin contract has no `stop()`, so stray `stop` methods were + never called by anything. + + The disconnect half now mirrors the connect half: + + - **`DatasourceConnectionService.disconnect(name, { asDefault })`** resolves + the default under its NATURAL name (the same #3826 rule that makes + `drivers.get('default')` impossible — the old lookup could never have found + it), and honours a new ownership discriminator recorded at connect time. + - **`disconnectAll()`** closes exactly the pools THIS service opened — + `'connected'` states only. `already-registered` drivers belong to whoever + registered them (an `onEnable` bridge, the default's idempotent replay) and + are never touched. + - **`DatasourceDriverHandle.ownership: 'factory' | 'host'`** is the + discriminator. `createPrebuiltDriverFactory` stamps its handles `'host'`: + an ADOPTED instance's pool outlives the kernel (the cloud control-plane + driver doubles as every environment kernel's proxy base; per-environment + drivers are registry-cached across kernel rebuilds), so kernel teardown — + including a cloud LRU eviction's `kernel.shutdown()` — clears the retained + verdict but NEVER closes the pool. Factory-built instances disconnect as + before there was a before. + - **`DefaultDatasourcePlugin.destroy()`** and + **`DatasourceAdminServicePlugin.destroy()`** wire the sweep at the kernel's + real teardown phase, best-effort (a failed disconnect never masks shutdown). + + A welcome side effect: a file-backed `sqlite-wasm` default with + `persist: 'on-disconnect'` now actually flushes on graceful shutdown. + + Also flips ADR-0062's status to reflect the completed convergence (#3992): + D1 is fully implemented across both repos since cloud#915; the remaining + `DriverPlugin` uses are documented named-auxiliary/escape-hatch cases, and the + degraded-boot parity guard stays with its role shifted to "the escape hatches + must not drift". + +- c3bcb42: feat(runtime,datasource): the default-datasource connect seam accepts a host driver factory — adopt pre-built instances without forking the verdict (#3826) + + ADR-0062 D1's open-core convergence (#3869/#3886) left one structural question + open: a host whose `default` needs a driver the shared factory cannot build — + the cloud distribution's `turso`, or an instance pooled BEYOND one kernel (the + cloud control-plane driver doubles as the proxy base of every environment + kernel; per-environment drivers are cached across kernel rebuilds) — had only + two options, both bad: stay on the legacy pre-built `DriverPlugin` path, whose + connect verdict lives in `ObjectQLEngine.init()` (the second implementation + #3826 exists to retire), or fork the connect orchestration. Either re-opens the + #3741 → #3758 drift this whole line of work is about. + + Two additive pieces close it: + + - **`DefaultDatasourcePlugin` accepts an injected `IDatasourceDriverFactory`** + (defaults to the shared open-core factory, byte-for-byte unchanged when + omitted). The factory only changes what `create()` returns — the policy-free + init connect, `bootCritical` fail-fast, `OS_ALLOW_DRIVER_CONNECT_FAILURE` + escape hatch, and the start() replay into retained admin state are identical + either way, and the new tests pin that (an adopted instance that cannot + connect takes the exact same verdict). + - **`createPrebuiltDriverFactory(driver, { driverId?, fallback? })`** in + `@objectstack/service-datasource` — the "adopt an existing driver" seam the + first #3826 pass found missing, landed AS a factory so it composes into the + one connect path instead of becoming a second entry point. `create()` returns + the SAME instance every call: construction, pooling, and reuse stay host + concerns; only the verdict converges. Not for the common case — a `default` + expressible as `{ driver, config }` should stay a plain definition. + + The `@objectstack/verify` dogfood harness now boots through + `DefaultDatasourcePlugin` (declared `sqlite-wasm` definition) instead of a + pre-built `DriverPlugin` — so the dogfood gate exercises the same declared + -default connect path `objectstack dev`/`serve` use, which is the §Risk + mitigation ADR-0062 promised ("behind the dogfood gate") and did not yet have. + The degraded-boot parity guard stays: `ObjectQLEngine.init()`'s verdict is + still live for the boot re-verification, `DriverPlugin` escape-hatch drivers, + and the cloud compositions until they converge onto this seam. + +- 03d26f7: fix(runtime,spec)!: the dispatcher's `error.code` is the semantic string it always declared; the HTTP status moves to `httpStatus` (#3842) + + `HttpDispatcher.error()` took the HTTP status as its `code` argument and wrote it + straight into the field `ApiErrorSchema` reserves for a semantic string, so + `error.code` came back as `400`/`403`/`503` — a number, duplicating the response + status and occupying the one slot a caller is meant to branch on. The real code + then had to go somewhere else, and did, three somewhere-elses: `details.code` + (auth gate, permission denial, anonymous deny), `details.type` + (project-membership gate), and `error.type` (`routeNotFound`). Four sites, three + parking spots, because the declared one was full. + + **FROM → TO on the wire.** A dispatcher error body + + ```json + { + "success": false, + "error": { + "message": "…", + "code": 403, + "details": { "code": "PERMISSION_DENIED" } + } + } + ``` + + is now + + ```json + { + "success": false, + "error": { "code": "PERMISSION_DENIED", "message": "…", "httpStatus": 403 } + } + ``` + + | Reading | Was | Now | + | ------------- | ---------------------------------------------------------- | ------------------------------------------------- | + | semantic code | `error.details.code` / `error.details.type` / `error.type` | `error.code` | + | HTTP status | `error.code` | `error.httpStatus` (or the response status) | + | context | `error.details` (with the code mixed in) | `error.details` (context only, absent when empty) | + + **One-line fix for a direct reader:** replace `body.error.details?.code ?? +body.error.type` with `body.error.code`, and `body.error.code` with + `body.error.httpStatus`. **SDK callers need no change** — `ObjectStackClient` + already normalised this (`err.code` semantic, `err.httpStatus` numeric) and still + reads the old shape, so a client newer than its server is unaffected. + + Every code already on the wire moves **verbatim** — `PERMISSION_DENIED`, + `ROUTE_NOT_FOUND`, `PASSWORD_EXPIRED`, `PROJECT_MEMBERSHIP_REQUIRED`, + `VALIDATION_FAILED`, `unauthenticated`. This change moves a field; it does not + rename anything. Reconciling the repo's two code vocabularies is #3841, and this + leaves it exactly one map and one enum to sweep instead of four parking spots. + + A branch with no code of its own is served one derived from the status, via the + single declared map `HttpStatusErrorCodeMap` / `standardErrorCodeForHttpStatus` + in `@objectstack/spec/api` (`403` → `permission_denied`, `503` → + `service_unavailable`, …). Derivation is necessary because `ApiErrorSchema.code` + is required; drawing it from `StandardErrorCode` keeps a derived code a + catalogued one rather than an invented string. + + **Spec changes:** + + - `ApiErrorSchema` gains optional `httpStatus: number` — the precedent is + `EnhancedApiErrorSchema.httpStatus`. Additive. + - `StandardErrorCode` gains `method_not_allowed` and `precondition_required`, + the two statuses the runtime returns that the enum could not name. Additive. + - **Breaking — `DispatcherErrorCode`** was `'404' | '405' | '501' | '503'` (string + spellings of HTTP statuses, for matching against the numeric `error.code`). It + is now `'ROUTE_NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'NOT_IMPLEMENTED' | +'SERVICE_UNAVAILABLE'` — the same four members the removed `error.type` enum + declared, moved verbatim. FROM `DispatcherErrorCode.parse('404')` TO + `DispatcherErrorCode.parse('ROUTE_NOT_FOUND')`; to match a status, read + `error.httpStatus`. TypeScript flags every call site. + - **Breaking — `DispatcherErrorResponseSchema`**: `error.code` is `z.string()` + (was `z.number().int()`), `error.type` is **removed** (folded into `code`), and + `error.httpStatus` / `error.details` are declared. This schema is what + legitimised the deviation — it declared the opposite of `ApiErrorSchema` for + the same field. FROM `{ code: 404, type: 'ROUTE_NOT_FOUND' }` TO + `{ code: 'ROUTE_NOT_FOUND', httpStatus: 404 }`. + + **Also aligned, because they are the same wire surface:** `dispatcher-plugin`'s + `errorResponseBase` (the THROWN-error exit) and its inline 404, and the MCP 405. + `errorResponseBase` previously discarded a thrown error's `.code` outright — it + had nowhere to put it — so the two exits of one surface disagreed about what a + caller would see; they now agree. Every body on this surface is built by one + helper (`packages/runtime/src/error-envelope.ts`), guarded in both directions by + `error-envelope.conformance.test.ts`: each branch driven and parsed against the + schema imported from `packages/spec`, plus a source scan so a new branch cannot + quietly reintroduce a numeric `code` or a `type`-as-code sibling. + + This deletes the #3687 pin in `http-dispatcher.test.ts`, which asked to be + deleted rather than updated once the dispatcher was fixed. + +- 33a5ff4: `os migrate` no longer touches the database before you confirm, and refuses a + SQLite database another process is using (#3917). + + **Nothing is written before the prompt.** `plan` called itself a dry run and + `apply` gated on `[y/N]`, but both booted the full plugin set first — and boot + schema-sync issued create-table/add-column DDL (plus the artifact's inline seed + wrote rows) against the target database before either promise was kept. + `SqlDriver` gains `setDeferredDdl` / `previewDeferredSchemaWork` / + `flushDeferredSchemaDdl`: while armed, `initObjects` still registers every + in-memory map drift detection depends on but records the physical work instead + of performing it. Both commands boot with it armed, render the held-back work + as a `New (additive)` section of the plan, and `apply` performs it only after + confirmation. `os meta resync` / `os migrate files-to-references` keep the old + behaviour — they need the tables to exist. + + **Occupancy check.** A live `os dev`/`os serve` holding the same SQLite file is + the usual way a migration goes wrong: the migration is transactional and swaps + tables inside the file, but the running server keeps prepared statements and a + schema cookie the migration invalidates. `os migrate` now probes the target + before booting — `PRAGMA locking_mode = EXCLUSIVE` + `BEGIN IMMEDIATE` under + `busy_timeout = 0`, which reports `SQLITE_BUSY` when another connection is + _attached_, not merely writing. (`wal_checkpoint(TRUNCATE)` only sees an active + writer, and `-wal`/`-shm` presence cannot tell a live server from a crashed one; + both are encoded as tests.) `apply` refuses with exit 1 — `error: database_busy` + under `--json` — unless the new `--force` flag is passed; `plan` warns and + continues, since it writes nothing either way. SQLite only: Postgres and MySQL + take their own server-side locks. + + `@objectstack/runtime` also exports `resolveStandaloneDatabase()`, so a caller + can resolve the database target with the same precedence the boot uses without + building the stack, and `createStandaloneStack` accepts `skipSeedData`. + +- 3ba8d77: fix(actions): dispatch on the declared action `type` over REST — flow actions are no longer MCP-only (#3915) + + `POST /api/v1/actions/:object/:action` had **no action-type branching at all**. + Whatever an action declared, the route went straight to `ql.executeAction` — the + script-handler registry — while the MCP `run_action` bridge had implemented the + `flow` branch since #2849. The spec is unambiguous that every non-`script` type + dispatches on `target` (`packages/spec/src/ui/action.zod.ts`), so a REST/SDK + caller who followed it and invoked a `type: 'flow'` action got + + ``` + Action '' on object '*' not found + ``` + + and had to know, out of band, to call `POST /api/v1/automation/:target/trigger` + itself. Worse for the Studio-authored case: `resyncAuthoredActions` deliberately + registers **no** handler for a flow-typed action ("no body (target/flow/url + action)"), so there was never anything for the registry to find. + + The two headless surfaces now share one dispatch: + + - **`flow`** → `automation.execute(action.target, …)` via the new + `dispatchFlowAction`, which the MCP path now calls too. The caller's identity + (`userId` / `positions` / `permissions` / `tenantId`) is forwarded, so a + `runAs: 'user'` flow enforces RLS as the invoker instead of falling into the + user-less UNSCOPED path (ADR-0049). A flow action on a kernel with no + automation service reports **503**, not a `{ success: false }` body. + - **`script`** → the handler registry, unchanged. An action with no resolvable + declaration is handler-only by definition and keeps that path. + - **`url` / `modal` / `form` / `api`** → **400** naming the type and the + prescription (for `api`, the `target` endpoint to call directly) instead of a + registry miss that reads like the action does not exist. + + The route also resolves **standalone declarations** now — `defineAction` + artifacts in the ObjectQL registry and Studio-authored `action` metadata rows, + neither of which appears inside any object's `actions[]`. They were invisible + to this route before, which is why a flow-typed one could not be dispatched — + and, separately, why its `requiredPermissions` were declared-but-unenforced on + REST while MCP honoured them. The ADR-0066 D4 gate still runs **before** the + type check, so an unauthorized caller learns nothing about how an action + dispatches. + + **Migration:** a caller invoking a `url`/`modal`/`form`/`api` action through + this endpoint used to receive `{ success: false, error: "Action '' on object +'*' not found" }` (HTTP 200) and now receives a 400 that says what to call + instead. No spec-faithful action changes behavior. + +- 4be9d99: fix(runtime,hono,plugin-dev): retire the dispatcher's `/storage` bridge — it never spoke the storage contract (#4087) + + `POST /api/v1/storage/upload` and `GET /api/v1/storage/file/:id` were a + dispatcher-side bridge to the `file-storage` service slot, written against a + service shape that does not exist: + + - **Upload** called the contract's `upload(key, data, options?)` as + `upload(file, { request })` — the parsed file object landed in the `key` + slot and `{ request }` in `data`. That is a `TypeError` against every + implementation in the repo (`S3StorageAdapter`, `LocalStorageAdapter`, + `SwappableStorageService`, plugin-dev's in-memory one), not a + near-miss: `Buffer.from({}) → ERR_INVALID_ARG_TYPE`, or an object used as + an S3 object key / `path.join` segment. + - **Download** branched on `result.url` / `result.redirect` / `result.stream` + / `result.mimeType` while the contract's `download(key)` resolves a + `Buffer`, so every branch fell through and the route answered a + JSON-serialized Buffer. + + Both routes are removed, along with `HttpDispatcher.handleStorage()`, the + `/storage` domain registration, the dispatcher-plugin mounts and the two route + ledger rows. + + **Migration.** There is nothing to migrate off in practice — neither route + could complete a request. (They were reachable: `service-storage` mounts + `/storage/upload/presigned`, not `/storage/upload`, so nothing shadowed them. + They simply had no caller — no SDK method builds those URLs.) + `/api/v1/storage` is `@objectstack/service-storage`'s surface and always was + the working one: + + - Upload — FROM `POST /api/v1/storage/upload` TO the presigned protocol + (`POST /storage/upload/presigned` → direct `PUT` to the returned URL → + `POST /storage/upload/complete`), or `client.storage.upload(file)`, which + runs all three steps. + - Download — FROM `GET /api/v1/storage/file/:id` TO + `GET /storage/files/:fileId/url` (`client.storage.getDownloadUrl(fileId)`) + for a signed URL, or `GET /storage/files/:fileId` for a stable browser URL + that 302s to it. + + Install `@objectstack/service-storage` to get those routes; without it + `/api/v1/storage` now has no handler, which is the same answer every other + uninstalled capability gives. + + Two follow-on corrections keep `declared === enforced`: + + - `@objectstack/hono` no longer mounts `app.all('/storage/*')`. That + wildcard claimed the whole `/storage` subtree for the two dead routes, so + every other path under it — service-storage's protocol above all — got the + bridge's own 404 rather than falling through. Storage is ordinary catch-all + traffic now. + - Discovery keeps gating `routes.storage` on `isServiceServeable` — the shared + `handlerReady` predicate #4058 step 2 introduced — and plugin-dev's in-memory + implementation now self-declares `handlerReady: false`. #4058 deliberately + left that one serving because the `/storage` bridge was still there to serve + it; with the bridge retired nothing routes HTTP to that slot, so `false` is + the honest value — the position `realtime` has held since ADR-0076 D12. The + implementation keeps working for in-process callers; it is simply no longer + advertised as a reachable HTTP capability. + +### Patch Changes + +- bc35e00: fix(runtime): action bodies execute under a real execution context — every owner-scoped write no longer dies FORBIDDEN + + An action body's `ctx.api` was never bound. The sandbox's `buildSandboxApi` + walked its whole fallback chain — no `actionCtx.api`, and the raw `ObjectQL` + engine has no `.object()` (that lives on `ScopedContext`, reachable only via + `engine.createContext()`, which the action path never called) — and landed on a + repo facade that proxied every call to the engine with **no `context`**. + `ctx.engine` had the identical hole. + + Context-less is not "trusted", it is **identity-less**, and identity-less is + strictly worse than either coherent posture: plugin-sharing's write gate + short-circuits on `!context.userId` (no user to own the record) and its bypass + needs `context.isSystem` (never set). So a `type: 'script'` action whose body + called `ctx.api.object('crm_case').update(...)` failed with + `FORBIDDEN: insufficient privileges to update crm_case` — **as the built-in + admin** — while the `[action-audit]` line on the same request announced + RLS-bypassing TRUSTED execution. Objects with a `public` sharing model, no + owner field, or a bypass listing passed the gate early, so only _some_ actions + broke and the defect read as object-dependent flakiness. + + Both dispatch paths (REST `/actions/:object/:action` and MCP `run_action`) now + bind `ctx.api` to `engine.createContext(...)` and thread the same envelope + through `ctx.engine`, matching what hook bodies already get from the engine's + `buildHookApi`. The envelope is the caller's `ExecutionContext` elevated with + `isSystem: true` — the posture the action surface already documents and gates + for at invoke time (the ADR-0066 D4 capability gate and the `ai.exposed` gate + are what admit a body to trusted execution). The caller's fields are spread + first, so a body's writes stay attributable (`userId` stamps + `created_by`/`updated_by`), org-scoped (`tenantId` stamps the org column and + drives driver-level tenant isolation), and joined to an open transaction — + rather than the unattributable, org-less rows a bare `{ isSystem: true }` would + write. + + No authoring change is required: `ctx.api.object(name)` inside a `body` now + does what the docs always said it does. Bodies that worked before (public / + owner-less objects) are unaffected apart from their writes now being correctly + attributed and org-stamped. + +- 48fcf70: **[ADR-0110 D5] The action-governance inventory moves to the engine plugin — + AppPlugin never ran it on the platform's own dev path.** + + Dogfooding the inventory with a positive control (an injected undeclared + handler) showed the `kernel:ready` hook it hung on never fired under `os dev`: + AppPlugin is registered conditionally (`serve.ts` skips it when the host wraps + itself; the dev fast path loads apps without it), so the checklist that + justifies D3's no-opt-out refusal was never printed where an upgrade most + needs it. + + - The addressing vocabulary (`GLOBAL_ACTION_OBJECT_KEY`, + `actionHandlerObjectKeys`, `isObjectLessActionKey`, + `resolveActionHandlerKeys`) and the reconciliation move into + `@objectstack/objectql` — the engine owns the map they describe, and the + dependency direction (runtime → objectql) permits no other home. + `@objectstack/runtime` re-exports them unchanged, so dispatch, the MCP + bridge and existing importers keep reading ONE implementation. + - `ObjectQLPlugin` now runs the inventory in its existing `kernel:ready` + handler — after `resyncAuthoredActions`, so the audited registry is final — + and again on `metadata:reloaded`, fingerprint-suppressed so a reload that + changed nothing action-related logs nothing. A Studio edit that orphans or + binds a handler updates the report live; the old boot-only snapshot went + stale on the first edit. + - Verified end-to-end with a programmatic kernel: the injected orphan is + named, a clean registry is silent. The `os dev` / `os serve` consoles still + swallow ALL plugin boot logs (pre-existing, tracked separately) — on those + surfaces the inventory becomes visible once that sink is fixed. + +- 0c90ece: fix(actions): the object-less `POST /actions//:action` shape is actually reachable over HTTP (#3913 follow-up) + + #3913 taught `handleActionsRequest` to route a single-segment path — the + object-less shape `POST /api/v1/actions//:action` — at the canonical `'global'` + key. That code was correct and unit-tested, and **unreachable**: the dispatcher + mounts its routes explicitly, `:object` does not match an empty path segment, + and no registration covered the `//` form. Over real HTTP the request fell + through to Hono's `notFound` and answered a bare `{error: 'Not found'}` with the + actions domain never running — so the exact URL #3913 was filed against still + did not dispatch. + + The tests could not catch it because they call `dispatcher.handleActions()` / + `dispatcher.dispatch()` directly, bypassing the route table. This is the same + class of bug `dispatcher-plugin.routes.test.ts` was created for after `/mcp` and + `/keys` shipped the same way; the guard now covers the action routes too. + + Found by dogfooding the running showcase app, not by the suite. + + `POST /api/v1/actions//:action` now answers identically to + `POST /api/v1/actions/global/:action` — same envelope, same `'global'` key. The + object-scoped registrations are untouched and unshadowed (Hono matches the + literal `//` without competing with `:object/:action`). + +- a225ef5: fix(runtime,webhooks): the path object wins on /data/:object/query, and the webhook envelope owns its keys (#3946) + + Follow-up sweep for the shape behind #3897 and #3933 — a trusted, server-derived + value written into an object literal with a caller-controlled bag spread OVER + it. Both of those were in the same block of REST code, so the pattern was swept + across all 1313 non-test TypeScript files in `packages/`. Nine candidate sites; + one real, one worth hardening, seven verified clean (recorded in #3946 so the + next sweep does not re-litigate them). + + **`POST /data/:object/query` (runtime dispatcher).** The `/data` domain built + `{ object: objectName, ...body }`, so `{"object":"other", …}` in the body moved + the read to a different object than the URL named. + + This is NOT an authorization bypass, and the tests pin why: `callData` gates + API exposure on `params.object`, so the gate followed the body and agreed with + the read — an object hidden by `apiEnabled: false` was refused either way. What + broke is that the URL stopped describing the operation (audit trails, logs, and + anything keyed on the request path saw object A while object B was read), and + that one endpoint spoke a second dialect of the contract the REST side had just + standardised on: the path object wins. The other handlers in that file never had + the problem — they nest caller data (`data: body`, `query: normalized`) instead + of splatting it, and the GET-by-id branch already allowlists its query params + against exactly this pollution. + + **Webhook delivery envelope.** `auto-enqueuer` built + `{ object, recordId, action, timestamp, ...payload }`, letting an event payload + rewrite the envelope a subscriber receives. Behaviour-neutral for the engine's + own publishers — `data.record.*` payloads are `{ recordId, after, changes }` + with record fields nested under `after`, so none of those four keys collide + today — but the shape was wrong, and the `payload.id` fallback right above it + suggests publishers that flatten record fields do exist. Envelope keys are + written last now. + +- c20b875: **Correct the stale premise left behind by #4012: the degraded-boot stderr copy + survives the operator's LOG LEVEL, not `os serve`'s boot-quiet window.** + + `emitDegradedBootBanner` writes the `OS_ALLOW_DRIVER_CONNECT_FAILURE` banner to + stderr in addition to `logger.warn`, and every comment and test name explaining + why cited the same reason: `os serve` swallowed all of stdout while the kernel + booted, and `Logger` routes `warn` to stdout. #4012 fixed that — the boot window + now buffers and replays `warn`-and-above — which retires the _stated_ + justification for a duplicate that is nonetheless still load-bearing: + + `Logger.write()` returns before touching a stream when the record is below + `config.level`, so at `--log-level error`, `fatal` or `silent` the banner's + `logger.warn` reaches **no** stream at all. A production host at `error` is + exactly the deployment this escape hatch exists for, and exactly where a + logger-only banner would vanish. Removing the stderr copy on the strength of + #4012 would therefore have been a regression — so this documents the reason that + is still true, in the places someone would read before deleting it: + `degraded-boot.ts`, the engine's emit site, and all three parity tests + (objectql, runtime, service-datasource), which are renamed off "which `os serve` + boot-quiet cannot swallow" to "which the operator log level cannot filter away". + + The objectql parity test now proves the claim instead of asserting around it: it + drives a **real** `ObjectLogger` at `level: 'error'` and requires the banner on + stderr _and_ nothing on stdout. Set the level to `warn` and it fails — so the + test is pinned to the level filter rather than passing for any reason. + + Also corrected in the same sweep, all comment-only, all previously overstating + what #4012 had not yet fixed: + + - the automation wiring summary (`format.ts`, `serve.ts`, its test) claimed the + boot window swallowed the engine's binding warnings. Its real justification is + stronger and unchanged: a flow that silently fails to arm emits **no** log line + at any level, so binding state has to be read off the live engine — absence of + a warning was never evidence of a bound flow. + - the seed summary (`seed-summary.ts`, `format.ts`, its test) and `AppPlugin`'s + seed-outcome note attributed the silence to the boot window; the operative + gate is that `SeedLoader`'s result logs are `info`, under the default `warn`. + + No behavior changes. + +- 4f30943: Both discovery builders now compute the `metadata` service entry from the implementation that fills the slot, instead of hardcoding opposite verdicts for it (#4089). + + `metadata` sat in a "kernel-provided (always available)" block above the loop that reads `__serviceInfo`, hardcoded separately in each builder — and the two disagreed about the same slot: + + - `@objectstack/runtime`'s dispatcher declared it permanently `status: 'degraded'` with `message: 'In-memory registry; DB persistence pending'`, so a stack with `MetadataPlugin` and a real `sys_metadata` table was still reported as having no persistence. + - `@objectstack/metadata-protocol` declared the same slot permanently `status: 'available'`, so the kernel's in-memory fallback (`createMemoryMetadata`, auto-registered when no metadata plugin is present) read exactly like a persisted registry — the `__serviceInfo` marker #4058 gave it went unread here. + + Both now read the registered service's `__serviceInfo` (via `readServiceSelfInfo`) and report what it declares: + + - kernel in-memory fallback, or plugin-dev's dev registry → `status: 'degraded'` plus that implementation's own `message`, which names what is missing and what to install. + - `MetadataPlugin` (or any implementation carrying no marker) → `status: 'available'` with no message. + + `handlerReady: true` is now stated unconditionally on both sides: it answers "is `/api/v1/meta` mounted?", and that route is served by the protocol whichever implementation occupies the slot — a degraded service in it does not unmount the route. Nothing about routing, gating, or dispatch changes; consumers that treat `status` as a capability claim (AI agents, the console) simply stop being told two different things by two hosts. + +- bb192c4: Gate every dispatcher service domain on `handlerReady` instead of on slot occupancy (#4058 step 2). + + #4000 made the `/analytics` domain execute ADR-0076 D12's third conclusion ("consumers treat only `handlerReady: true` as a real capability"); every other domain still gated on "is a service registered", so a self-declared stub occupying `automation` / `notification` / `ai` / `file-storage` / `i18n` was called like a real implementation and its fabricated answer went out as a 200. Step 1 (#4082) made the two kinds of dev implementation distinguishable; this is the gate that reads the distinction. + + - The `/analytics`, `/automation`, `/notifications`, `/ai`, `/storage` and `/i18n` domains, the route-mount gate, discovery's `routes`/`features`, and the metadata-protocol builder's route advertisement now share one predicate (`isServiceServeable`): a slot whose occupant self-declares `handlerReady: false` is answered exactly as an empty slot is — the domain's existing 404, or the explicit 501 `/storage` and `/i18n` use. One predicate, so what is advertised and what is served cannot disagree. + - `handlerReady`, not `status`, is the test. An implementation that declares `degraded` defaults to `handlerReady: true` and keeps serving — which is why the in-memory `file-storage` and `i18n` implementations are unaffected. + - `discovery.services.*` stays presence-gated: a registered stub still reports `{ enabled: true, status: 'stub', handlerReady: false }` (with no `route`), which says strictly more than collapsing it to `unavailable` would. + - `/ai` improves for the stub case: an occupied-but-unserveable slot used to fall through to a 503 "AI service routes not yet initialized" and lose the `GET /ai/agents` empty-list answer the console polls for on every navigation. Both are restored. + + No change for a host whose services are real implementations. If you register your own stub under one of those six slots and relied on the dispatcher calling it, either drop the `handlerReady: false` self-declaration (declare `degraded` if it genuinely serves) or install the real service. Not gated, deliberately: `/data`, `/meta`, `/auth` and the security path — their dev stubs back the dev stack's own core loop, and gating them would 404 the dev stack itself. + +- 98e7cc7: fix(runtime): dispatcher error exits serve VALIDATION_FAILED as 400 with `fields[]` (#3918) + + `ValidationError` — what objectql's record and rule validators throw — carries + `.code = 'VALIDATION_FAILED'` and `.fields[]`, one entry per offending field. It + deliberately carries no `.status`, no `.statusCode`, and no `.issues`: it is a + plain domain error, and deciding it means "400" is the HTTP boundary's job. + `@objectstack/rest` has always done that (`mapDataError` → 400 with `fields[]`). + The runtime dispatcher's two error exits did not, because each read exactly the + properties this error lacks: + + - **`HttpDispatcher.errorFromThrown`** (the RETURNED-error path — `/meta` save, + `/packages` publish, …) fell back to the caller's `fallbackStatus` for want of + a `.status`, and built its structured `details` from `.issues` alone, so + `fields[]` was dropped. + - **`dispatcher-plugin`'s `errorResponseBase`** (the THROWN-error path — every + route the plugin mounts: `/analytics`, `/packages`, `/i18n`, `/storage`, + `/automation`, `/auth`, `/notifications`, `/mcp`, …) took the same 500 + fallback, and its body was only `{message, code}`. Landing on 5xx then dragged + the message through the #3867 leak sanitiser, so a user typing a bad email + address got back a **500 "Internal server error"** — no status a client could + act on, no message worth showing, and nothing to attach to the input. + + Both exits now recognise the shape and answer the way rest-server does: **status + 400**, with the error's `fields[]` passed through verbatim in `details` + alongside `code: 'VALIDATION_FAILED'`. Any surface the dispatcher serves can + therefore highlight the specific field the user got wrong, the way a form served + by `/data` already could. + + Matched by duck-typing on `code === 'VALIDATION_FAILED' || name === +'ValidationError'` — the same both-ways predicate `mapDataError` uses — so a + hook or service that throws `{ code: 'VALIDATION_FAILED', fields }` by hand is + served identically, and the runtime takes no dependency on objectql. An explicit + `.status` / `.statusCode` on the error still wins: 400 is supplied only as the + fallback that was previously 500. Non-validation errors are untouched — same + status, same message sanitising, same `details`, and `errorResponseBase` still + emits the exact two-key body it always did. + +- 4cf7c61: fix(runtime): route every domain `catch` through `errorFromThrown` so status and `fields[]` survive (#3918 follow-up) + + #3867 taught `dispatcher-plugin`'s `errorResponseBase` to read an error's + `status` (not just `statusCode`), and #3918 taught + `HttpDispatcher.errorFromThrown` the `VALIDATION_FAILED` shape. Both fixes were + invisible to a whole tier of handlers underneath them: the domain modules each + caught their own errors and called `deps.error(e.message, e.statusCode || 500)` + directly, bypassing `errorFromThrown` entirely — 13 call sites, 9 of them in + `/packages` alone. + + The consequence on `/packages`, `/meta/_drafts`, `/ui`, `/security` and the + `/mcp` transport: + + - **A deliberate status was downgraded to 500.** Every protocol-layer domain + error in this codebase carries its HTTP status as `status`, not `statusCode` + (`OBJECT_NOT_FOUND`, `RECORD_NOT_FOUND`, `CLONE_DISABLED`, plugin-sharing's + `FORBIDDEN`, …) — the exact read #3867 fixed one tier up. So a 404 these + routes meant to return arrived as a 500, and the message was dragged through + the 5xx leak sanitiser on the way out. + - **A `ValidationError` still lost its `fields[]`** and its 400, re-opening + #3918 on precisely the routes it was filed against. + + Every one of those catches now calls `deps.errorFromThrown(e, …)`, so both + fixes finally reach the routes that need them. Deliberate per-route fallbacks + are preserved rather than flattened to 500: the `/meta` save fallback keeps + **501** (that branch is reached only when the protocol has no `saveMetaItem`, + so "unsupported" is the honest default) and the `/meta` two-part lookup keeps + **404** — but a validation failure on either now answers 400 with its fields + instead of being swallowed by the fallback. + + `domains/keys.ts` is deliberately **not** converted: it discards the underlying + error on purpose, because the message could echo row contents. Its literal + `'Failed to create API key'` is the correct answer there and stays. + + No behaviour change for errors that already carried `statusCode` — that read is + preserved, only widened. + +- 385c4b0: fix(actions): seed a flow action's params with the row id, like the trigger route does (#3915 follow-up) + + #3915 gave the REST `/actions/:object/:action` route its flow dispatch and + documented it as "equivalent to `POST /api/v1/automation/:target/trigger`, + without having to know the flow name". A real run showed that claim did not + hold: the params bag carried the subject record's fields — so `id` — but never + `recordId`. The CRM's own `crm_convert_lead` action declares + `recordIdParam: 'recordId'` and its flow reads `{recordId}`, so invoking it + through the actions endpoint reached the automation engine and then died at its + first node: + + ``` + Flow 'crm_convert_lead_wizard' failed: Node 'get_lead' failed: get_record: + refusing to run — 1 filter condition(s) resolved to nothing … `{recordId}` (at id) + ``` + + while the identical run through `/automation/crm_convert_lead_wizard/trigger` + paused normally on its first screen. Only a live invocation surfaced it — the + unit tests mock `automation.execute`, so they pinned the call shape without + noticing the bag was missing the key flows actually read. + + `dispatchFlowAction` now seeds the row id under the same keys + `domains/automation.ts` seeds for the trigger route — `recordId` and the + `Id` camelCase alias — plus the action's own declared + `recordIdParam` (sourced from `recordIdField`, default `id`) when it names a + third key. Explicit action params still win over every seed, and the seeding + applies to the MCP `run_action` path too, which shared the same gap. A declared + `recordIdParam` that no dispatcher honoured was the `declared ≠ enforced` shape + in miniature. + +- 45dc446: Every in-memory fallback and dev stub now self-describes with the standard `__serviceInfo` descriptor, classified by what it actually is (#4058 step 1). + + ADR-0076 D12 gave services one way to say "I am not the real thing", but the producers never converged on it: + + - The kernel's own fallbacks (`createMemoryCache` / `Queue` / `Job` / `I18n` / `Metadata`) carried `_fallback: true` — a marker **no** consumer recognized, `readServiceSelfInfo` included — so both discovery builders reported them as fully `available`. + - `plugin-dev` marked all of its implementations with the same `_dev: true`, normalized to `status: 'stub', handlerReady: false`. That declared a working in-memory search index exactly as fake as an AI stub returning invented text. + + Both now carry `__serviceInfo`, split by a rule that holds across the whole set: + + - **`degraded`** — really does the work, with reduced capability: `cache`, `queue`, `job`, `file-storage`, `search`, `i18n`, `metadata`, `workflow`, `realtime`. Its answers are true answers; the `message` names what is missing (no persistence, no scheduling timer, no state-machine validation, …). + - **`stub`** — the answer is fabricated: `ai`, `automation`, `notification`, `data`, `auth`, `security.permissions`, `security.rls`, `security.fieldMasker`. Never to be mistaken for a capability. + + `handlerReady: false` is set independently wherever no HTTP handler serves the slot (`cache` / `queue` / `job` / `realtime`, and every `stub`). + + Discovery output changes accordingly — a kernel fallback that used to report `status: 'available'` now reports `degraded` with an explanatory message. No routing, gating, or dispatch behavior changes: every dispatcher domain still resolves services exactly as before. Consumers reading `discovery.services.*` get the truth instead of a uniform claim. + + For anything that duck-typed the old markers: `svc._fallback` / `svc._dev` → `readServiceSelfInfo(svc)` from `@objectstack/spec/api` (the legacy `_dev` key is still understood by that reader, so third-party stubs carrying it keep working). + +- 507b92a: fix(spec,objectql,rest,runtime): field-validation messages answer in the caller's language, named by the field's label (#3957) + + The write path built every built-in validation message by concatenating the **API + field name** into a **hardcoded English** template. Those strings are what the + Console toast, the CSV-import row report, the CLI and any custom client display + verbatim, so a Chinese-locale user importing a bad row read: + + ``` + 第 1 行:penalty_amount must be ≥ 0 + ``` + + …for a field declared `label: '处罚金额'` with a full `zh-CN` bundle loaded. The + form layer localized the _same_ constraint correctly (the browser's native + `min`), so the language flipped depending on which layer caught the value. + + **Three things changed.** + + 1. **The message is rendered in the caller's locale** from a built-in catalog + (`BUILTIN_VALIDATION_MESSAGES`, `@objectstack/spec/system`) shipping `en`, + `zh-CN`, `ja-JP`, `es-ES` — the same four locales as the platform bundles. + The locale comes from `ExecutionContext.locale`, whose contract already read + "Drives message catalogs"; this is the consumer that makes that true. Both + HTTP entries (REST server, runtime dispatcher) now resolve it from the + request's `Accept-Language` / `?locale` first, falling back to the workspace + `localization.locale` — so a rejection message and the field labels around it + can no longer disagree. + + 2. **The field is named by its label, never the API name**: translation bundle + (`objects..fields..label`) → declared `label` → API name as the last + resort. `FieldValidationError.field` still carries the API name so a form can + focus the right input. + + 3. **The constraint is exposed as data**, so a client can format its own text + instead of parsing the sentence: + `{ field, code, message, label, constraint: { min: 0 } }`. This rides + ADR-0114's existing `constraint` / `value` positions on `FieldErrorSchema` + (`constraint` tightens from `unknown` to `Record`) rather + than adding a parallel payload — `label` is the only new field. The bag + carries `min`/`max`/`minLength`/`maxLength`/`actual`/`allowed`/`type`, and the + message templates interpolate from exactly those keys. + + Covered end-to-end, not only in the validator: single and batch insert, + single-id and multi-row update, ADR-0113's clear-out rejection, the object-level + rule evaluator's own built-in messages (`requiredWhen`, per-option gating, + state-machine fallbacks), and the importer's cell-coercion, required pre-check + and #3956 bound pre-check messages — all of which land in the same row report. + + **What this changes for consumers.** + + - `code` is unchanged (ADR-0114's `FieldErrorCode`) and remains the thing to + match on. Message keys are finer-grained than codes — `invalid_datetime`, + `invalid_option_value`, `required_cleared` are rendering detail and never reach + the wire — so localization never splits the client-facing vocabulary. + - `message` **text changes**: it is localized, and it names the field by label + even in English (`Budget must be ≥ 0`, not `budget must be ≥ 0`). Anything + asserting on the old English string should match `code` (and now + `constraint`) instead. + - An author-written validation-rule `message` is never touched — it is already + in the language its author chose. + - A deployment can override any built-in message with a `translation` item + defining `validation.field.` (e.g. + `validation.field.min_value: '{{label}}不得小于 {{min}} 元'`). + - The importer's reference-failure message no longer names the target object's + API name (`no sys_user matches "…"`): naming internal identifiers is the + defect being fixed, and the column plus the offending value are what an + importer can act on. + +- 99b4392: Advertise `mcp` in `/discovery` only when it is actually serveable (#4024). + + Both discovery producers gated the `/mcp` route on `isMcpServerEnabled()` alone. + The stated justification was a lockstep — `os serve` auto-loads plugin-mcp from + the same flag, so on that path advertised did imply mounted. But the lockstep is + a property of the CLI, not of the dispatcher: `@objectstack/rest` has no + `@objectstack/mcp` dependency, mounts no `/mcp` route and performs no auto-load, + so a host that embedded it without plugin-mcp advertised `/mcp` in `/discovery` + and then answered 501 on it — the `declared ≠ enforced` failure #3369 forbids, + and a broken contract for third-party clients that read `/discovery` to decide + what exists. + + Both producers now require the flag AND a serveable MCP service. The runtime + dispatcher gates on the handler's own predicate (`typeof +mcp.handleHttpRequest === 'function'`), so a wrong-shaped service can't + over-promise either. `@objectstack/rest` probes via the per-request kernel or the + single-env `serviceExistsProvider`; when it genuinely cannot probe it keeps the + prior flag-only answer rather than hiding a working endpoint (fail-open, + ADR-0057 D10). The `os serve` / `os dev` path is unchanged — it loads the plugin, + so the service resolves and `/mcp` is still advertised. + + Also exercises the `mcp: false` seam in `route-parity.integration.test.ts`, which + had existed unused since the file was written: `bootServe()` was only ever called + with no args or `{ notification: false }`. The one capability whose advertisement + was not service-presence gated was also the one whose absence was never tested. + +- 39eb01b: fix(runtime,cli,types): `os migrate` and the dev runtime now share one `__search` companion schema view (#3955) + + On a zh-locale deployment the dev runtime provisions the hidden `__search` + pinyin companion column (ADR-0098) on every eligible object, but the + `os migrate plan`/`apply` boot went through `createStandaloneStack`, which + never derived the locale-gated pinyin decision from the compiled artifact. + Its metadata therefore lacked every companion column, and `migrate plan` + reported each live `__search` column of a dev-created database as a + destructive orphan — with `--allow-destructive` as the printed remediation, + which would have dropped live feature columns. + + - `@objectstack/types`: new `collectConfiguredLocales(i18n)` and + `stampSearchPinyinEnabled(i18n)` — the single resolve-and-stamp helper for + `OS_SEARCH_PINYIN_ENABLED`. An explicit env value still wins; only a + positive locale-derived decision is stamped. + - `@objectstack/runtime`: `createStandaloneStack` stamps the decision from + the artifact's `i18n` before any plugin constructs a `SchemaRegistry`, and + surfaces `i18n` on its result like `requires`/`objects`/`manifest`. + - `@objectstack/cli`: the `serve`/`dev` boot now stamps through the same + shared helper (behaviour unchanged), so create/serve and plan/apply cannot + compute different schema views of the same source tree. + + A fresh CLI-created database is now also born with the same `__search` + columns the dev runtime would provision, instead of acquiring them on the + next dev boot. + +- a3cb9c8: Retire the dev-mode `analytics` stub, and make the dispatcher gate `/analytics` on `handlerReady` rather than on service presence (#4000). + + Retiring the degraded analytics shim (#3891) made an empty `analytics` slot the honest signal: `/api/v1/analytics/*` 404s and discovery reports `unavailable`. `plugin-dev` refilled that slot with a stub, which re-created the retired shape in dev mode — the dispatcher gated on "is a service registered", so the stub was called like a real engine and its empty result came back as a 200. + + - `plugin-dev` no longer registers an `analytics` dev stub; the slot stays empty (`NO_DEV_STUB_SERVICES`). Every other dev stub is unchanged. + - The `/analytics` domain, its route-mount gate, and discovery's `routes`/`features` now share one predicate (`isAnalyticsServiceServeable`): a service that self-declares `handlerReady: false` (ADR-0076 D12 — `__serviceInfo`, or plugin-dev's legacy `_dev: true`) is treated as an empty slot. A `degraded` implementation that genuinely serves requests keeps serving; `discovery.services.analytics` still reports a registered stub as `status: 'stub'`, which says more than `unavailable` would. + + FROM → TO for dev setups that relied on the stub answering `POST /api/v1/analytics/query` with `{ rows: [], fields: [] }`: install the real engine — `@objectstack/service-analytics` runs an InMemory strategy and needs no database of its own. Nothing else changes; hosts that already install it (including `os serve`, where `analytics` is in `ALWAYS_ON_CAPABILITIES`) are unaffected. + +- 1d5dc46: fix(runtime): carry `code` / `fields[]` across the sandbox boundary so form actions can anchor validation errors (#3918 follow-up) + + Found by dogfooding the merged #3918 chain against a running app. Submitting a + record that fails validation through a form **action** came back as: + + ``` + HTTP 200 + { "success": true, "data": { "success": false, + "error": "ValidationError: issued_on is required" } } + ``` + + No status a client could branch on, no code, no `fields[]`. The chain's + dispatcher fixes could not help: the field list was already gone before any + dispatcher exit ran. It was lost at the QuickJS boundary, twice — + + 1. **host → VM.** `vm.newError({ name, message })` dropped every other property, + so a body reaching a record `ValidationError` through + `ctx.api.object(x).update(...)` saw bare prose. + 2. **VM → host.** The wrapper's reject handler flattened the error to the string + `: ` before the host ever saw it. + + Both hops now carry an explicit **allowlist** — `code` and `fields` — alongside + the message, and `SandboxError` exposes them as `.code` / `.fields`. The + allowlist is a security boundary, not a style choice: host errors routinely hang + driver state, connection details or whole record payloads off themselves, and + anything crossing INTO the VM is readable by untrusted sandboxed code. Copying + the error's own enumerable keys would leak all of it. + + `/actions` then surfaces them, so a form can highlight the offending input: + + ``` + HTTP 200 + { "success": true, "data": { "success": false, + "error": "ValidationError: issued_on is required", + "code": "VALIDATION_FAILED", + "fields": [ { "field": "issued_on", "code": "required", … } ] } } + ``` + + **The `/actions` wire contract is deliberately unchanged.** The status stays + 200 and `success: false` remains the failure signal: that route has always + reported business failure in the payload (an action that "fails" is a normal + outcome, not a transport error) and every caller branches on `data.success`. + Making it a 4xx would be a break in exchange for a strictly additive fix, so the + fix is additive — `code` and `fields` are simply omitted when absent, and a + caller that ignores them sees exactly what it saw before. + + Message channels are byte-identical: `SandboxError.message` keeps the + ` '' threw:` debug wrapper for server logs and `.innerMessage` stays + the plain business text a toast shows. The structured payload rides alongside + them, never instead of them. + + Also adds `dispatcher-validation-error.real.test.ts`, which pins both dispatcher + exits against the **real** objectql `ValidationError` rather than a hand-built + fixture — including its deliberate absence of `.status`, the assumption the + whole #3918 fix rests on. The existing fixture-based tests restate that contract; + these check it, so a future change to the class fails a test instead of quietly + regressing production. + +- 627b188: fix(seed-loader): count reference fields dropped from rows that were still written + + The loader had two failure outcomes and only counted one. A record it cannot + write is counted in `errored`. But an unusable **reference value** (an object + where a natural key belongs, an array on a single-value field) is removed from + the record — never written as NULL, which would sever an existing link on + upsert replay — and the row is written **without it**. Nothing counted that. + + So a load that quietly severed N associations reported `totalErrored: 0`, and + every count-driven surface read clean. The CLI boot banner — the one seed signal + that survives `os dev`'s boot-quiet window and the default `warn` level — printed + `showcase 42 rows`, and the warn line said `0 dropped record(s)`: true, and + useless ([#3932](https://github.com/objectstack-ai/objectstack/issues/3932)). + + `SeedLoadResult.referencesDropped` and `SeedLoaderSummary.totalReferencesDropped` + now count it. It is deliberately **not** folded into `errored` — the row _was_ + written, so that would break the `inserted + updated + skipped` reconciliation + against `total`. The banner names it separately: + + ``` + ⚠ Seeds: showcase 42 ok / 3 lost links ⚠ + ``` + + Both counters are additive with a `0` default, so an existing producer or + consumer of `SeedLoaderResult` is unaffected. + +- 857a6cf: fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085) + + The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is + a development platform, so it has to start without one — but `os serve +objectstack.config.ts` died during boot whenever the artifact was absent: + + ``` + Loading objectstack.config.ts... + [StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT… + + ✗ Service 'manifest' is async - use await + ``` + + Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with + only its `dist/objectstack.json` moved aside), and on every freshly authored + project between `os init` and its first `os compile`. The message named neither + the missing artifact nor a fix, so it read as an internal kernel fault. + + Three separate faults, each of which alone was enough to refuse the boot: + + - **`serve` registered the config-derived `AppPlugin` before the stack's own + `plugins[]`.** Registration order _is_ the kernel's init/start order, and that + slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and + `DefaultDatasourcePlugin` (which connects the database the app seeds through). + The wrap is now **appended** to `plugins[]`, the same slot + `createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot + and artifact-boot share one plugin order. The artifact path never hit this, + which is exactly what made a plugin-**order** bug look artifact-related. + + - **`ctx.getService()` reported a never-registered service as "is async".** + `PluginLoader.getService` is an `async` method, so its return value is _always_ + a Promise and its internal "not found" rejection can never surface + synchronously — the kernel read the answer off that Promise and told every + caller to `await` a service that did not exist, while the `not found` branch + below it was unreachable. It now decides from the registry: absent ⇒ + `[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged + `Service 'x' is async - use await`. The same crash now reads + `[Kernel] Service 'manifest' not found`, which points at the layer that is + actually wrong. + + - **`MetadataPlugin` treated an absent `local-file` artifact as fatal.** + `createStandaloneStack` always points it at `dist/objectstack.json`, so a stack + with no app at all could not boot. A **missing** local artifact is now "nothing + compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so + a later `os compile` hydrates the running server. The tolerance is + ENOENT-only — a malformed or unreadable artifact stays fatal — and + `bootstrap: 'artifact-only'` (sealed runtime, where the artifact _is_ the + deployment) keeps failing loudly rather than silently serving an empty runtime. + + `[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted + at callers for whom "no artifact" is a healthy state; a present-but-unusable + artifact keeps the loud warning. + + Pinned by an e2e pair that drives the real `os serve` with **no `os compile` + anywhere**: an app defined only by `objectstack.config.ts` (asserting its object + is in the started plugin set, not merely that boot survived) and a bare + `export default {}` platform. The #4012 fixture drops the `os compile` this bug + had forced on it. + +- de6daa5: fix(runtime)!: the /share-links dispatcher domain stops emitting a duplicate `link`/`links` beside `data` (#4038) + + The producer-side other half of #3983. That PR moved the sharing plugin's routes + onto the declared envelope; this removes the compatibility shim the dispatcher + twin had been carrying _because_ that surface answered bare. + + Create and list answered with the payload under **two** keys: + + ```ts + { success: true, data: link, link } // POST /share-links + { success: true, data: links, links } // GET /share-links + ``` + + The duplicate existed so readers predating the envelope kept working — which is + why objectui's `ShareDialog` reads `body.links ?? body.data`. Once #3983 made both + surfaces answer `data`, that first branch had no producer left, and the duplicate + had no reader in **any** repo: + + - **framework** — no consumer of these routes at all + - **objectui** — `ShareDialog` already falls through to `body.data` + - **cloud** — swept: it only _registers_ `SharingServicePlugin` into per-environment + kernels with `registerShareLinkRoutes: false` so this dispatcher serves the paths. + It never calls them and never reads a body. That sweep is what #4038 was waiting + on, and it came back clean. + + ## Shape + + | route | was | now | + | ------------------- | --------------------------------- | -------------------------- | + | `POST /share-links` | `{ success, data: link, link }` | `{ success, data: link }` | + | `GET /share-links` | `{ success, data: links, links }` | `{ success, data: links }` | + + `data` is unchanged in both — only the duplicate key is gone. Anything reading + `body.data`, or going through `ObjectStackClient.unwrapResponse`, sees no + difference. A raw reader of the top-level `body.link` / `body.links` must move to + `body.data`. + + The list route now routes through `deps.success(...)` like the domain's other + three. Create stays hand-built, because `deps.success` hardcodes status 200 and + this route is a **201** — the same reason `/keys` hand-builds its own 201, and the + same shape it uses. + + ## Guard + + `scripts/check-route-envelope.mjs` does not and cannot cover this file: it scans + route modules that write via `res.json(...)`, while dispatcher domains return + `{ status, body }` for a central sender. So the drift was invisible to it by + construction. Three tests in `domain-handler-registry.test.ts` cover it instead — + two per-route, plus a general one asserting no success body carries a top-level + key outside `success` / `data` / `meta`. Restoring the duplicates fails all three. + +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [ea24593] +- Updated dependencies [789ad63] +- Updated dependencies [fccec22] +- Updated dependencies [2af1988] +- Updated dependencies [b3a2318] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [fae74b5] +- Updated dependencies [c9d254a] +- Updated dependencies [42e3b01] +- Updated dependencies [c8124e5] +- Updated dependencies [9e8f04d] +- Updated dependencies [39eb01b] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [6f98c2d] +- Updated dependencies [a4a9944] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [974c6d4] +- Updated dependencies [495019b] +- Updated dependencies [33a5ff4] +- Updated dependencies [9e01213] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [3fe0ff1] +- Updated dependencies [be7945a] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [8d5bb5a] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [c53aa53] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/metadata@17.0.0-rc.1 + - @objectstack/plugin-security@17.0.0-rc.1 + - @objectstack/rest@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/plugin-auth@17.0.0-rc.1 + - @objectstack/metadata-protocol@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + - @objectstack/driver-sql@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + - @objectstack/driver-memory@17.0.0-rc.1 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.1 + - @objectstack/service-datasource@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + - @objectstack/service-cluster@17.0.0-rc.1 + - @objectstack/service-i18n@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 743d04332e..9c9c223c8f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack Core Runtime & Query Engine", "type": "module", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 52a9f19282..28f579e7e0 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.0.0-rc.1 + ## 17.0.0-rc.0 ## 16.1.0 diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index c46dbb71c7..55727d7270 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index e13a4d9e06..0c4751e1f1 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,253 @@ # Changelog — @objectstack/service-analytics +## 17.0.0-rc.1 + +### Patch Changes + +- 3abd233: fix(analytics): project a `timeDimensions` bucket into the result rows and fields (#4033) + + An analytics query that buckets by `timeDimensions` alone grouped correctly — + the echoed SQL read `date_trunc('month', due_date) AS "due_date"` — but the row + mapper and `buildFieldMeta` both enumerated `query.dimensions` only, so the + bucket never reached the caller: rows carried just the measures and `fields` + never mentioned the dimension. A trend chart got N values and no x-axis. The + same query written with `dimensions: ['due_date']` was unaffected, which is why + it went unnoticed. + + Grouping, row mapping and field metadata now derive the projected set from one + `projectedDimensions()` helper — `dimensions` plus every _granular_ + `timeDimensions` entry not already among them. A `timeDimensions` entry without + a granularity contributes only its `dateRange` predicate and stays out of the + projection, so no phantom column is declared. + +- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777) + + A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a + lower bound and was silently wrong for an upper one: the dashboard date-range + filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a + `datetime` column every row created after 00:00 of the `to` day vanished from + the result — no error, the chart renders, the numbers are just smaller. The + default configuration hit it: the filter's default field is `created_at` + (a system-injected `Field.datetime`) and 7 of the 13 presets end "today". + + The translation is operator-sensitive and half-open, applied at every + comparison emitter: + + - `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a + bare-day comparand on a `datetime` column compiles to `< next-day-midnight` + in the column's storage form; `$between [min, max]` with a bare-day max + decomposes to `>= min AND < next-day(max)`. Both the plain and the + legacy-repair (mixed-storage) column paths, both `where` spellings. + - `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day` + instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day. + - The `/analytics/sql` rendering and the dataset preview evaluator apply the + same rule, so the echoed SQL and drafted numbers reproduce execution. + + `@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`: + the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants, + `Date`s and impossible days are never widened). + + Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns + (`<= day` is already whole-day-correct there), full-ISO/`Date` comparands + (instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for + those). No authored metadata changes: a dashboard's existing + `{ $gte, $lte }` window now simply includes its final day. + +- c8124e5: fix(driver-sql): give `Field.datetime` one UTC storage form per dialect (#3912, #3942) + + Any window filter on a `Field.datetime` column returned an empty set on SQLite — + a dashboard `dateRange: last_30_days` on `created_date` read 0 while 29 matching + rows existed. + + There was never a storage _convention_, only a description of what better-sqlite3 + happened to do with a bound JS `Date`. Nothing enforced it — `formatInput` + deliberately left `datetime` untouched — so the form was decided by whichever + writer got there first: a JS `Date` landed as INTEGER epoch ms, while a REST/JSON + write (JSON has no `Date` type), a `defaultValue: 'NOW()'` slot, and the + platform's own `created_at` / `updated_at` all landed as ISO **TEXT**. One column + held both forms while the read path coerced comparands to epoch ms purely from + the _declared_ type. On SQLite's type ordering (`INTEGER < TEXT`) a two-sided + window collapsed to zero rows, and a one-sided `>=` matched every TEXT row + regardless of the bound. + + `Field.datetime` now has one canonical instant per dialect, produced by one + function applied on write **and** to every filter comparand, so the two sides of + a comparison cannot disagree about shape: + + - **SQLite** — `YYYY-MM-DDTHH:MM:SS.sssZ` text. Lexicographic order _is_ + chronological order, so range filters and `ORDER BY` read the column directly + and can use an index; `strftime` parses it, so the date-bucket expression needs + no CASE. + - **Postgres** — `timestamptz`, unchanged. The fix here is on the write and + comparand side: a zone-naive write was previously resolved against the + _server's_ timezone (measured 8 hours off on `Asia/Shanghai`), and an + un-anchored `YYYY-MM-DD` comparand meant the server's local midnight, so the + identical query over the identical instant landed a row on a different calendar + day than SQLite did. + - **MySQL** — `DATETIME(3)` instead of `TIMESTAMP`, a connection pinned to UTC on + both the mysql2 and the server layer, and a MySQL-spelled bind carrying the + same UTC wall clock. MySQL accepts neither the `T` separator nor the `Z` suffix + in a datetime literal, so datetime writes over REST had always failed outright; + `TIMESTAMP` additionally truncated milliseconds and could not store an instant + outside 1970..2038. + + Existing rows converge at schema sync. Both migrations are allowed to fail: they + log, mark nothing, and the read paths keep a repair expression, so an un-migrated + column still compares and buckets **correctly** — just unindexed. Neither can + repair instants the old timezone-ambiguous write path recorded wrongly; they + preserve what is on disk. + + Also closes #3928 (datetime `ORDER BY` mis-sorted on mixed storage) by + construction. Rationale is recorded as ADR-0053 addendum D-B1..D-B4. + + The analytics change is additive: a `coerceTemporalFilterColumn` companion to the + existing `coerceTemporalFilterValue` hook, so a raw-SQL strategy can normalise the + column side too. Absent hook → byte-identical SQL. + +- f752ee3: feat(analytics): order the time axis by default, and give reports a sort declaration (#3916) + + A matrix report with a date dimension across rendered its columns in arbitrary + order — `2026-07-01, 2026-07-05, …, 2026-07-02`. Declaring `dateGranularity` on + the dataset dimension made the bucket keys _sortable_ (`2026-07`, `2026-Q3`) + without making anything _sort_ them, and the report author had no way to ask: + `DatasetSelection.order` existed on the wire, but `ReportSchema` had no ordering + field at all (dashboard widgets had their own `options.sortBy` channel; reports + did not). Nothing in the chain supplied an order either — `resolveOrdering` + returned `undefined` unless the selection carried one explicitly, the ObjectQL + aggregate path has no ordering grammar so its buckets came back in Map-insertion + order, and the pivot builds its column headers in row-arrival order. + + - **A selected time dimension is now chronological by default.** When a + selection states no `order` (and no `limit`, whose own fallback already + ordered by every dimension), each selected dimension the cube types as `time` + defaults to ASCENDING, in selection order. Bucket keys are minted sort-stable + precisely so this works — `2026-07` sorts after `2026-06`, `2026-Q3` after + `2026-Q1`. This lands on both strategy paths: a real `ORDER BY` where native + SQL serves the query, and the executor's post-pass where a date-bucketed query + is handed to the ObjectQL path. Null / empty buckets stay last, as everywhere + else. Deliberately narrow: only time dimensions get a default, so grids with + nothing wrong with them are not reordered. + - **Reports can declare an ordering.** `ReportSchema.order` (and + `blocks[].order` for a `joined` report) is a list of `{ by, direction }` sort + keys, most significant first — an array, not a `Record`, because key order is + the contract and JSON object key order should not have to be. `by` must name a + dimension the report groups by (`rows` / `columns`) or a measure it displays + (`values`); anything else fails at authoring time rather than becoming an + ordering that silently does nothing. Duplicate keys are rejected. A `joined` + report orders per block — declaring `order` on the container is an error. + `reportSelectionOrder()` lowers the list into the `DatasetSelection.order` a + renderer posts, and returns `undefined` for an empty list so the runtime's own + defaults still apply. + + An explicit `order` still wins outright — the chronological default is a + default, not a policy, so "newest month first" is one declaration away. + + `report.order` ships as `planned` + `authorWarn` in the liveness ledger: the + framework half is complete and live (schema, lowering helper, executor), but + objectui's `DatasetReportRenderer` does not yet carry `report.order` into the + selection it posts. The default time-axis ordering needs no renderer change and + is live now. + +- b3a3d83: feat(spec): a shared temporal conformance matrix, and the `$between` gap it found (ADR-0053 D-A3, #4081) + + `@objectstack/spec/data` gains `TEMPORAL_ROWS` and `TEMPORAL_CASES` — the + single set of temporal filter cases every backend is checked against, the twin + of the existing `FILTER_LOGIC_CASES`. Five backends consume it and assert **row + results**: `driver-sql` (and, through the live-dialect CI job, real Postgres and + MySQL), `driver-memory`, `driver-mongodb` (real MongoDB), the analytics preview + evaluator, and `formula`'s RLS write-side `check`. + + This is the regression backstop ADR-0053 D-A3 has asked for since 2026-06 and + the last of its decisions to be actioned. Four separate incidents — #3650, + #3773, #3777, #4047 — were each found by a human by accident, and each left a + suite proving only its own issue against its own fixture. Nothing held the + backends to one standard, so the fifth divergence had nowhere to fail. + + **`service-analytics` — a real fix the matrix found on its first run.** The + draft-preview evaluator had no `$between` case, so it fell through to its + permissive `default` and matched **every** row: a drafted dashboard carrying a + range filter charted the entire dataset, then changed its numbers at publish — + the exact continuity the preview exists to provide. It now evaluates + `$between`, sharing the upper-bound helper with `$lte` so the whole-day + calendar-day rule (#3777) applies to a range's max as well. + + Also recorded (ADR-0053 D-A3.1): `$gt` with a bare-day comparand on a + `datetime` column cannot agree between typed and type-blind backends, and the + gap is irreducible without field types. It is asserted in the shared matrix on + `date` only, with the `datetime` cell left to the typed drivers' own suites, + rather than papered over. + +- 35accbf: feat(spec): promote the temporal storage hooks onto the IDataDriver contract (ADR-0053 D-A2) + + `temporalFilterValue` and `temporalFilterColumnSql` — the pair that closed + #3912's storage-form drift — were duck-typed: analytics probed + `typeof driver.x === 'function'` against a locally-invented interface, and + nothing at the type level said a driver must implement both or neither. The + lesson of #3912 is precisely that coercing the comparand without normalising + the column reintroduces half the bug, so a driver implementing one hook alone + would silently regress. + + Both are now optional members of `IDataDriver` + (`@objectstack/spec/contracts`), documented as a pair with "absent = identity" + semantics for drivers whose storage form is the wire form (memory, mongo). + `SqlDriver implements IDataDriver`, so its signatures are compile-checked from + here on; analytics derives its driver seam by `Pick`-ing the contract instead + of a local duck type. Runtime `typeof` guards remain — that is the correct way + to consume an optional contract member — but the shape they guard now has one + authoritative definition. + + No runtime behaviour change. ADR-0053 D-A2 is recorded as resolved. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 87bb7149dc..4f16ffee8c 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Analytics Service for ObjectStack — implements IAnalyticsService with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory)", "type": "module", diff --git a/packages/services/service-automation/CHANGELOG.md b/packages/services/service-automation/CHANGELOG.md index 011b0919fb..4a4ba92def 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,277 @@ # @objectstack/service-automation +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- a47ac06: feat(spec,automation): graduate the seven flow-node config key aliases into the conversion layer — the `readAliasedConfig` shim retires with them (#3796) + + `FlowNodeSchema.config` is an unconstrained record, so the executors were the + only statement of which config key is canonical — and seven deprecated aliases + lived there as tolerance the spec never declared: one behind the + `readAliasedConfig` deprecation shim (warned, ledgered), six as open-coded + `??` fallbacks (no warning, no ledger, no retirement path). All seven now + graduate into the ADR-0087 D2 conversion layer as protocol-17 **live-window** + entries: a stored flow authored with an alias is rewritten to the canonical + key at load — `defineStack` / `validate` / `lint` and the + `AutomationEngine.registerFlow` rehydration seam alike — with a structured + `ConversionNotice` per rewrite, and the executors read the canonical keys + only. The shim (`service-automation/src/builtin/config-aliases.ts`) is empty + and deleted. + + FROM → TO (per node type; conversion entry in parentheses): + + - `get_record`/`create_record`/`update_record`/`delete_record`: + `config.object` → `config.objectName` (`flow-node-crud-object-alias`) + - `notify`: `config.to` → `config.recipients`, `config.subject` → + `config.title`, `config.body` → `config.message`, `config.url` → + `config.actionUrl` (`flow-node-notify-config-aliases`) + - `script`: `config.functionName` → `config.function`, `config.input` → + `config.inputs` (`flow-node-script-config-aliases`) + + One-line fix: rename the key in your flow source — values are unchanged; `os +migrate meta --from 16` rewrites all seven mechanically. Until then nothing + breaks: the protocol-17 loader accepts and converts the old shape (window + retires in 18). + + `actionUrl` (not `url`) is the deliberate canonical of its pair, resolving a + contradiction where the notify descriptor documented `url` as canonical while + the executor, tests, and examples preferred `actionUrl`: the whole downstream + chain already uses that name (`sys_notification.action_url`, the + channel-dispatch contract, the REST notification read model), and `url` + elsewhere in the platform means "HTTP endpoint to call" (`http` node, + webhooks) — a different concept from this in-app click-through target. The + executor precedence already put `actionUrl` first, so the choice is + behaviour-preserving; the `notify` descriptor's `configSchema` now documents + `actionUrl`. + + Callers that hand a node config **directly** to an executor (bypassing + `registerFlow`) no longer get alias resolution — build the config with the + canonical keys. + +- e4c61a7: Validate the expression slots a flow node's `configSchema` declares (#4027). + + A node type's designer `configSchema` and the keys its validators traverse were + two unreconciled lists. Both the engine's `registerFlow` pass and the author-time + `objectstack validate` pass hardcoded `config.condition` / `edge.condition` and + assumed every other node string was a `{var}` template — so a declared expression + property outside that hardcoded set was validated by nobody. + + That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen` + descriptor since #3304, typed `xExpression: 'expression'` (bare CEL) and offered + to authors in Studio, but no validator traversed it. An app authored the + predicate in the _other_ dialect — `'{createOpportunity} == true'` — and it passed + `tsc`, `objectstack validate` and registration in silence. Because `required` _is_ + enforced, a field the author had made conditional rendered unconditionally and + blocked Submit on an input the user was never shown: the run paused forever and no + resume was ever issued. + + Now: + + - **`FLOW_NODE_EXPRESSION_PATHS`** (`@objectstack/spec`) is the declared ledger of + expression-bearing node config paths, each recording the dialect it takes. + - **Both validators read it.** A malformed `visibleWhen` is a located, quoted + error at `registerFlow` _and_ at `objectstack validate` — `node 'screen_1' +(screen) screen field visibleWhen at config.fields[1].visibleWhen`. + - **A reconciliation ratchet** derives the expression properties from the live + descriptors and fails CI in both directions: a new `xExpression` property with + no ledger entry, or a stale entry no descriptor declares. It walks every + registered builtin, not just `screen`. + + Dialects are recorded rather than assumed because there are three, and two of them + disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace + `{var}` flow interpolation (`{…}` is correct), and the ADR-0032 §3 double-brace + text template. Only bare-CEL slots are checked — `loop.collection` and + `map.collection` are recorded as `flow-template` and deliberately left alone, + since no validator implements their dialect and checking them under either of the + other two would reject every currently-valid flow. + + `ActionDescriptor.configSchema`'s TSDoc no longer claims `registerFlow()` + validates `config` against it. It never did: `FlowNodeSchema.config` is + `z.record(z.unknown())`, so types, `required`, `enum` and unknown keys are still + unenforced. The doc now states exactly what is checked and what is designer-facing + only, so nothing relies on a guard that does not exist. + +- 4965bfa: Warn on flow-node `config` keys the node type does not declare (#4045). + + `FlowNodeSchema.config` is `z.record(z.unknown())`, so a misspelled or invented + config key was accepted in total silence: `visibleIf` instead of `visibleWhen` + registered cleanly, was never read, and the only symptom was a feature that quietly + did not happen. That diagnostic vacuum is what made #3528 take three passes and two + wrong diagnoses to resolve. + + `registerFlow` now compares each node's `config` against its descriptor's + `configSchema` and warns on anything undeclared, located and with the declared set + listed: + + ``` + [flow 'lead_conversion'] node 'screen_1' (screen): unknown config key `visibleIf` + at config.fields[0].visibleIf — It is not declared by this node type's + configSchema, so nothing reads it. Declared here: name, label, type, required, + visibleWhen. + ``` + + The walk descends where the schema declares structure and **stops at free-form + keyValue maps**, whose keys are author data (`filter: { status: 'stale' }`). + Descending matters: the #3528 typo class lives _inside_ the `screen` field + repeater, so a top-level-only comparison would miss the exact mistake this exists + to catch. + + **Warn, never reject.** An undeclared key is an author typo, a key the executor + genuinely reads that its hand-written `configSchema` never declared (`notify.source` + was exactly this), or dead config. Only 4 of the 13 schema-carrying builtins have + been audited for the second population, so hard-failing would gamble on the other + nine. Tightening to an error is a later, per-key decision once this warning has + measured the real distribution. Nothing about the published `configSchema` changes, + so no consumer sees a different shape. + + `@objectstack/formula` now exports `nearestName`, the edit-distance helper already + used for unknown-field and unknown-role suggestions, so "did you mean?" + diagnostics share one threshold. It is deliberately a bonus rather than the + mechanism — `visibleIf` → `visibleWhen` is distance 4 against a threshold of 3, so + the declared set is always listed instead of only as a fallback. + + Also fixes the first real finding from the new check: `showcase_inquiry_purge`'s + `get_record` node carried `mode: 'records'`, which no executor reads, with a comment + crediting it for behaviour that `limit > 1` actually produces. + +### Patch Changes + +- 01e124d: Graduate `notify`'s nested `source: { object, id }` into the conversion layer (#4045). + + The `notify` executor tolerated a second spelling of its click-through target with + a bare consumer-side fallback: + + ```ts + const object = toStr(interpolate(cfg.sourceObject ?? src?.object, …)); + ``` + + Its own doc comment named `sourceObject`/`sourceId` **canonical** (they mirror the + `sys_notification.source_object`/`source_id` columns), so the nested form was an + alias tolerated by exactly the mechanism Prime Directive #12 calls debt — and the + one alias on this executor that #3796 missed when it moved `to`/`subject`/`body`/ + `url` into `flow-node-notify-config-aliases`. + + It now graduates the same way `filters` → `filter` and `object` → `objectName` + did: the conversion lifts it onto the canonical pair at load — including the + `AutomationEngine.registerFlow` rehydration seam — and the executor's fallback is + deleted, so no consumer-side dialect tolerance survives and the alias is declared, + tested and retirable on schedule (it rides the existing entry's window, retiring + at 18). + + Unlike the four renames this is a **1→2 destructuring**, which the pair mechanism + cannot express, so it is a small custom transform. It mirrors the `??` precedence + exactly: a canonical key already present wins and its nested counterpart is left + shadowed, matching how a shadowed alias is treated elsewhere. `source` is dropped + once at least one part is lifted; a `source` that is not an object, or carries + neither key, is left untouched rather than silently deleted. + + No behaviour change for authors — both spellings keep working, and a + half-specified target is still dropped rather than emitting a dead deep-link. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/formula@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 2238c19a55..a27e0416da 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Automation Service for ObjectStack — implements IAutomationService with plugin-based DAG flow execution engine", "type": "module", diff --git a/packages/services/service-cache/CHANGELOG.md b/packages/services/service-cache/CHANGELOG.md index c62d5be0a0..8a7a1d8941 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-cache +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index f7c1b0897d..1b4e27e697 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters", "type": "module", diff --git a/packages/services/service-cluster-redis/CHANGELOG.md b/packages/services/service-cluster-redis/CHANGELOG.md index 3ab92bbd4f..bb120944c0 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,56 @@ # @objectstack/service-cluster-redis +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/service-cluster@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index d232d2b61e..18dabc193c 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster-redis", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Redis cluster driver for ObjectStack — implements IPubSub/ILock/IKV/ICounter against Redis using ioredis.", "type": "module", diff --git a/packages/services/service-cluster/CHANGELOG.md b/packages/services/service-cluster/CHANGELOG.md index cb56bed688..d2309b72c1 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/service-cluster +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 0e7ab190f5..6cff8122ac 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.", "type": "module", diff --git a/packages/services/service-datasource/CHANGELOG.md b/packages/services/service-datasource/CHANGELOG.md index 52feccd8f8..7e6f11f81d 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,321 @@ # @objectstack/service-external-datasource +## 17.0.0-rc.1 + +### Minor Changes + +- c9d254a: feat(datasource,runtime): kernel teardown disconnects through the one datasource path — and never closes an adopted pool (#3993) + + After the #3826 connect convergence, ADR-0062 D5's "owns connect/disconnect" + was half-true: nothing disconnected the `default` (or a declared datasource's + pool) on graceful shutdown. `DriverPlugin` never had teardown, `ObjectQLPlugin` + teardown never touched drivers, and the kernel's actual teardown phase is + `destroy()` — the Plugin contract has no `stop()`, so stray `stop` methods were + never called by anything. + + The disconnect half now mirrors the connect half: + + - **`DatasourceConnectionService.disconnect(name, { asDefault })`** resolves + the default under its NATURAL name (the same #3826 rule that makes + `drivers.get('default')` impossible — the old lookup could never have found + it), and honours a new ownership discriminator recorded at connect time. + - **`disconnectAll()`** closes exactly the pools THIS service opened — + `'connected'` states only. `already-registered` drivers belong to whoever + registered them (an `onEnable` bridge, the default's idempotent replay) and + are never touched. + - **`DatasourceDriverHandle.ownership: 'factory' | 'host'`** is the + discriminator. `createPrebuiltDriverFactory` stamps its handles `'host'`: + an ADOPTED instance's pool outlives the kernel (the cloud control-plane + driver doubles as every environment kernel's proxy base; per-environment + drivers are registry-cached across kernel rebuilds), so kernel teardown — + including a cloud LRU eviction's `kernel.shutdown()` — clears the retained + verdict but NEVER closes the pool. Factory-built instances disconnect as + before there was a before. + - **`DefaultDatasourcePlugin.destroy()`** and + **`DatasourceAdminServicePlugin.destroy()`** wire the sweep at the kernel's + real teardown phase, best-effort (a failed disconnect never masks shutdown). + + A welcome side effect: a file-backed `sqlite-wasm` default with + `persist: 'on-disconnect'` now actually flushes on graceful shutdown. + + Also flips ADR-0062's status to reflect the completed convergence (#3992): + D1 is fully implemented across both repos since cloud#915; the remaining + `DriverPlugin` uses are documented named-auxiliary/escape-hatch cases, and the + degraded-boot parity guard stays with its role shifted to "the escape hatches + must not drift". + +- c3bcb42: feat(runtime,datasource): the default-datasource connect seam accepts a host driver factory — adopt pre-built instances without forking the verdict (#3826) + + ADR-0062 D1's open-core convergence (#3869/#3886) left one structural question + open: a host whose `default` needs a driver the shared factory cannot build — + the cloud distribution's `turso`, or an instance pooled BEYOND one kernel (the + cloud control-plane driver doubles as the proxy base of every environment + kernel; per-environment drivers are cached across kernel rebuilds) — had only + two options, both bad: stay on the legacy pre-built `DriverPlugin` path, whose + connect verdict lives in `ObjectQLEngine.init()` (the second implementation + #3826 exists to retire), or fork the connect orchestration. Either re-opens the + #3741 → #3758 drift this whole line of work is about. + + Two additive pieces close it: + + - **`DefaultDatasourcePlugin` accepts an injected `IDatasourceDriverFactory`** + (defaults to the shared open-core factory, byte-for-byte unchanged when + omitted). The factory only changes what `create()` returns — the policy-free + init connect, `bootCritical` fail-fast, `OS_ALLOW_DRIVER_CONNECT_FAILURE` + escape hatch, and the start() replay into retained admin state are identical + either way, and the new tests pin that (an adopted instance that cannot + connect takes the exact same verdict). + - **`createPrebuiltDriverFactory(driver, { driverId?, fallback? })`** in + `@objectstack/service-datasource` — the "adopt an existing driver" seam the + first #3826 pass found missing, landed AS a factory so it composes into the + one connect path instead of becoming a second entry point. `create()` returns + the SAME instance every call: construction, pooling, and reuse stay host + concerns; only the verdict converges. Not for the common case — a `default` + expressible as `{ driver, config }` should stay a plain definition. + + The `@objectstack/verify` dogfood harness now boots through + `DefaultDatasourcePlugin` (declared `sqlite-wasm` definition) instead of a + pre-built `DriverPlugin` — so the dogfood gate exercises the same declared + -default connect path `objectstack dev`/`serve` use, which is the §Risk + mitigation ADR-0062 promised ("behind the dogfood gate") and did not yet have. + The degraded-boot parity guard stays: `ObjectQLEngine.init()`'s verdict is + still live for the boot re-verification, `DriverPlugin` escape-hatch drivers, + and the cloud compositions until they converge onto this seam. + +### Patch Changes + +- 974c6d4: fix(datasource): a `memory` datasource is ephemeral again, and each pool gets its own store (#4083) + + The shared driver factory built `new InMemoryDriver()` for `driver: 'memory'` with + no config, so the pool inherited that driver's own `persistence: 'auto'` default — + in Node, a file adapter at the **relative, process-global** path + `.objectstack/data/memory-driver.json`. Two consequences, neither intended: + + - **It was not ephemeral.** The pool flushed its whole store into the server's + working directory (on an unref'd 2s autosave timer, and again at teardown) and + reloaded it on the next boot. That is the opposite of what the driver id + promises the operator who asks for it — `OS_DATABASE_DRIVER=memory` is + documented as _ephemeral, not real SQL_ — and it means a "throwaway" datasource + left state in the deploy directory. + - **Every memory pool in a process shared one destination.** The default path + carries no per-datasource component, so two `driver: 'memory'` datasources + loaded and saved the same file: each saw the other's tables, and the last + teardown to flush clobbered the other's rows. + + Both were visible as an intermittent test failure. The ADR-0062 D1 federated-read + acceptance seeds 2 rows into an auto-connected external memory datasource and + reads them back; it returned 2 rows on a clean checkout and 2×N on the Nth run in + the same tree — passing in CI (always run #1, always a fresh checkout) and + failing locally for anyone who ran it twice. Whether a given run leaked depended + on the autosave timer, which is what made it look flaky rather than wrong. + + - The factory now builds the memory pool with **`persistence: false` by default**. + - It also **honors the datasource's own `config`**, which was previously dropped + entirely: `initialData` and `strictMode` never reached the driver. + - When an author _does_ opt into persistence (`config.persistence`), the default + destination is **scoped to the datasource** — + `.objectstack/data/memory-.json` / `objectstack:memory-db:` — so + pools stay independent. An explicit `path`/`key`, or a custom `adapter`, is + left exactly as written. + - The dev-only sqlite step-down's last-resort in-memory driver + (`resolveSqliteDriver`, #2229) is built the same way, making its own + "not persistent" contract true. + + `InMemoryDriver`'s documented defaults are unchanged — constructing one directly + still auto-detects persistence. Only the datasource-scoped pools this factory + builds changed. + + **Migration.** A deployment relying on `driver: 'memory'` state surviving a + restart was relying on a bug, and should declare it: set + `config: { persistence: 'file' }` on the datasource (now written to a + per-datasource file), or use a real driver — `sqlite`/`sqlite-wasm` give durable + storage with real SQL. Existing `.objectstack/data/memory-driver.json` files are + no longer read; delete them. + +- 0931185: fix(rest,service-settings,service-datasource)!: four more route modules emit the declared envelope, and the guard is now shared (#3843) + + #3675 and #3689 moved `service-storage` and `service-i18n` onto the declared + response envelope (`BaseResponseSchema` + `ApiErrorSchema`). Each scoped itself + to one service, and neither asked whether the same drift existed elsewhere. It + did — in four more modules, and in two of them it was the _older_ shape, the one + #3675 had already declared wrong: + + | Module | before | now | + | ------------------------------------- | -------------------------------------------------------------- | ------------- | + | `service-settings/settings-routes.ts` | nested `error`, no `success` on any of 5 bodies | full envelope | + | `service-datasource/admin-routes.ts` | `{ error: '' }`, `message` a **sibling** | full envelope | + | `rest/external-datasource-routes.ts` | `{ error: '' }` + a private `ok` | full envelope | + | `rest/package-routes.ts` | 3 of 16 bodies had `success`, 2 failures had no `error` at all | full envelope | + + ## Breaking: where to read things now + + **Success payloads move under `data`.** The keys are unchanged — only their + depth. `unwrapResponse` in `ObjectStackClient` returns `body.data` when the flag + is present, so every SDK method (`packages.list()`, `datasources.external.*`) + resolves to exactly the object it always did. Raw `fetch` callers must add one + hop: + + ``` + GET /api/v1/datasources body.datasources → body.data.datasources + GET /api/v1/datasources/drivers body.drivers → body.data.drivers + GET /api/v1/datasources/:name body.datasource → body.data.datasource + GET /api/v1/packages body.packages → body.data.packages + GET /api/v1/packages/:id body.package → body.data.package + GET /api/settings body.manifests → body.data.manifests + GET /api/settings/:ns body.manifest/.values → body.data.manifest/.values + POST /…/external/validate body.ok, body.results → body.data.ok, body.data.results + ``` + + `SettingsNamespacePayloadSchema` and friends still describe those payloads + exactly; they now describe the envelope's `data` rather than the whole body. + + **Error bodies stop being a string.** `{ error: 'datasource_admin_error', +message }` → `{ success: false, error: { code: 'datasource_admin_error', +message } }`. Read `body.error.message`, not `body.message`; read + `body.error.code`, not `body.error`. This is the asymmetry #3675 opened on: a + caller reading `body.error.message` previously got the real message from the + dispatcher and `undefined` from these routes. + + **Two failures that never said why now do.** `DELETE /api/v1/packages/:id` + answered a bare `{ success: false }` and a bare + `{ success: false, failed, cleanups }`. They are now `PACKAGE_DELETE_FAILED` and + `PACKAGE_DELETE_PARTIAL`, with the per-item `failed` / `cleanups` arrays under + `error.details`. + + **Codes follow ADR-0112.** #3841 settled the vocabulary while this was in review: + `error.code` is SCREAMING_SNAKE and `ApiErrorSchema.code` is now the closed + `ErrorCode` union, so an unregistered code fails schema parse. Generic conditions + reuse the STANDARD catalog rather than becoming registered synonyms of it, per the + ledger's own guidance: + + ``` + datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard) + external_service_unavailable → SERVICE_UNAVAILABLE (standard) + not_found / PACKAGE_NOT_FOUND → RESOURCE_NOT_FOUND (standard) + PUBLISH_FIELDS_MISSING → MISSING_REQUIRED_FIELD (standard) + INTERNAL → INTERNAL_ERROR (standard) + datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered) + external_import_error → EXTERNAL_IMPORT_ERROR (registered) + PUBLISH_MANIFEST_INVALID → PACKAGE_MANIFEST_INVALID (registered) + PUBLISH_FAILED → PACKAGE_PUBLISH_FAILED (registered) + PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED / SETTINGS_ACTION_FAILED (registered) + ``` + + Which service is unavailable is carried by `message`. The seven registered codes are + added to `ERROR_CODE_LEDGER` under their owning packages — including a new + `@objectstack/service-datasource` entry. + + **`POST /external/validate` keeps its `ok`.** Unlike the `{ ok: true, key }` + #3689 retired from storage — a private second word for `success` — this `ok` is a + computed verdict over the federated objects (`results.every(r => r.ok)`). The + request can succeed while the verdict is false, so the two flags are not the same + field; `ok` moves inside `data` rather than being dropped. + + Consumers were taught both shapes first, so the two repos are not coupled by + merge order: objectui's `packages` readers were already tolerant + (`payload?.data ?? payload`), and its datasource page plus the generic + `type: 'api'` action runner now unwrap the envelope and read `error.message` + (the latter previously toasted `[object Object]` for any nested error). + + ## The guard is shared now, not copied + + `scripts/check-route-envelope.mjs` + `pnpm check:route-envelope`, wired into + `lint.yml` alongside the nine sibling `check:*` guards. Its load-bearing assertion + is structural rather than per-route: **it counts the response write sites per + module.** When every body goes through the `sendOk` / `sendError` pair that count + is fixed at two and does not grow with the route list — so a _future_ route that + hand-rolls a body fails the guard. That is the coverage a driven-body test can + never give, since it can only drive the routes that existed the day it was + written. + + This existed three times already as an open-coded regex block (storage error, + storage success, i18n error). Lifting it did more than deduplicate: a per-package + scan **structurally cannot notice a module nobody thought to convert**, and going + repo-wide found two the moment it ran — neither is in #3843's hand-written survey: + + - `plugin-sharing/share-link-routes.ts` — the fifth drifting module. No body + carries `success`, and one answers `{ ok: true }`, the private second word #3689 + retired from storage. Filed as #3983 and pinned by the guard; converting it is + breaking for share-link consumers and needs its own sweep. + - `metadata/routes/hmr-routes.ts` — declared **exempt** with a reason (dev-only + SSE endpoint, not on the SDK surface), not skipped. Three states, deliberately — + conformant / ratcheted / exempt — because that is the honest classification + ADR-0049 asks for. A route module the scan finds but the table does not declare + is an **error**, never a default: applying `2 / 1 / 1` to an unknown module would + let a new one pass by coincidence. + + It also drops the regex for the TypeScript AST, fixing two real bugs the copies + had. They stripped comments with `String.replace`, whose line-comment pattern also + ate `//` inside string literals and truncated the rest of that line — response + writes included. And `.json(` does not mean "write a response": `hmr-routes.ts` + calls `c.req.json()` twice to READ a request body, which a textual count reports as + two unenveloped responses. Comments and literals are not AST tokens, and + request-vs-response is a property of the callee, so both disappear. The script + carries a `--self-test` pinning each case — the nine sibling guards have none, but + both of these bugs survived a review of the regex version. + + **The i18n ratchet, stated rather than hidden.** `i18n-service-plugin.ts` is + declared at `responses: 5, ok: 4, err: 1` with a ratchet pointing at #3973. Its + error half _is_ consolidated (#3675), but each of its four read routes builds + `{ success: true, data }` inline. Those bodies are correct — that is not envelope + drift — but an unconsolidated builder is a weaker guard: a fifth read route could + get the shape wrong and only a driven test would notice. The numbers pin today's + structure exactly (a new inline body fails) and drop to the conformant `2 / 1 / 1` + when #3973 lands. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 4c14b5dba3..d1b576b0b6 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.", "type": "module", diff --git a/packages/services/service-i18n/CHANGELOG.md b/packages/services/service-i18n/CHANGELOG.md index 133a814cb9..b59580e361 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/service-i18n +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 4cf7da2f8c..a7715bbaf5 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading", "type": "module", diff --git a/packages/services/service-job/CHANGELOG.md b/packages/services/service-job/CHANGELOG.md index 18170f74f9..1a46319970 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-job +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 8fb351b87f..129fc018ae 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling", "type": "module", diff --git a/packages/services/service-knowledge/CHANGELOG.md b/packages/services/service-knowledge/CHANGELOG.md index 55b18f4d5b..39eeb163cd 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/service-knowledge +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 2b938d5ed8..8a23ad0394 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index e090680d0b..599ff467b7 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/service-messaging +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index 2bdfd1a83f..7edfe19ac9 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Messaging Service for ObjectStack — outbound notification dispatch (ADR-0012). Ships the MessagingChannel registry, emit() fan-out, and the always-on inbox channel; other channels (email/webhook/push/IM) plug in.", "type": "module", diff --git a/packages/services/service-package/CHANGELOG.md b/packages/services/service-package/CHANGELOG.md index b060e87641..b65930e385 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-package +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/metadata-core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 613fd5a1fa..0b22249a26 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index 618e19e5f2..96c1002b3e 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-queue +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index f57d297f5e..eede710795 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters", "type": "module", diff --git a/packages/services/service-realtime/CHANGELOG.md b/packages/services/service-realtime/CHANGELOG.md index ac40905976..50f1817aee 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-realtime +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 7cb7b65c0a..13f0cbd938 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub", "type": "module", diff --git a/packages/services/service-settings/CHANGELOG.md b/packages/services/service-settings/CHANGELOG.md index a1aa770621..de5bbba683 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,194 @@ # @objectstack/service-settings +## 17.0.0-rc.1 + +### Patch Changes + +- 0931185: fix(rest,service-settings,service-datasource)!: four more route modules emit the declared envelope, and the guard is now shared (#3843) + + #3675 and #3689 moved `service-storage` and `service-i18n` onto the declared + response envelope (`BaseResponseSchema` + `ApiErrorSchema`). Each scoped itself + to one service, and neither asked whether the same drift existed elsewhere. It + did — in four more modules, and in two of them it was the _older_ shape, the one + #3675 had already declared wrong: + + | Module | before | now | + | ------------------------------------- | -------------------------------------------------------------- | ------------- | + | `service-settings/settings-routes.ts` | nested `error`, no `success` on any of 5 bodies | full envelope | + | `service-datasource/admin-routes.ts` | `{ error: '' }`, `message` a **sibling** | full envelope | + | `rest/external-datasource-routes.ts` | `{ error: '' }` + a private `ok` | full envelope | + | `rest/package-routes.ts` | 3 of 16 bodies had `success`, 2 failures had no `error` at all | full envelope | + + ## Breaking: where to read things now + + **Success payloads move under `data`.** The keys are unchanged — only their + depth. `unwrapResponse` in `ObjectStackClient` returns `body.data` when the flag + is present, so every SDK method (`packages.list()`, `datasources.external.*`) + resolves to exactly the object it always did. Raw `fetch` callers must add one + hop: + + ``` + GET /api/v1/datasources body.datasources → body.data.datasources + GET /api/v1/datasources/drivers body.drivers → body.data.drivers + GET /api/v1/datasources/:name body.datasource → body.data.datasource + GET /api/v1/packages body.packages → body.data.packages + GET /api/v1/packages/:id body.package → body.data.package + GET /api/settings body.manifests → body.data.manifests + GET /api/settings/:ns body.manifest/.values → body.data.manifest/.values + POST /…/external/validate body.ok, body.results → body.data.ok, body.data.results + ``` + + `SettingsNamespacePayloadSchema` and friends still describe those payloads + exactly; they now describe the envelope's `data` rather than the whole body. + + **Error bodies stop being a string.** `{ error: 'datasource_admin_error', +message }` → `{ success: false, error: { code: 'datasource_admin_error', +message } }`. Read `body.error.message`, not `body.message`; read + `body.error.code`, not `body.error`. This is the asymmetry #3675 opened on: a + caller reading `body.error.message` previously got the real message from the + dispatcher and `undefined` from these routes. + + **Two failures that never said why now do.** `DELETE /api/v1/packages/:id` + answered a bare `{ success: false }` and a bare + `{ success: false, failed, cleanups }`. They are now `PACKAGE_DELETE_FAILED` and + `PACKAGE_DELETE_PARTIAL`, with the per-item `failed` / `cleanups` arrays under + `error.details`. + + **Codes follow ADR-0112.** #3841 settled the vocabulary while this was in review: + `error.code` is SCREAMING_SNAKE and `ApiErrorSchema.code` is now the closed + `ErrorCode` union, so an unregistered code fails schema parse. Generic conditions + reuse the STANDARD catalog rather than becoming registered synonyms of it, per the + ledger's own guidance: + + ``` + datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard) + external_service_unavailable → SERVICE_UNAVAILABLE (standard) + not_found / PACKAGE_NOT_FOUND → RESOURCE_NOT_FOUND (standard) + PUBLISH_FIELDS_MISSING → MISSING_REQUIRED_FIELD (standard) + INTERNAL → INTERNAL_ERROR (standard) + datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered) + external_import_error → EXTERNAL_IMPORT_ERROR (registered) + PUBLISH_MANIFEST_INVALID → PACKAGE_MANIFEST_INVALID (registered) + PUBLISH_FAILED → PACKAGE_PUBLISH_FAILED (registered) + PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED / SETTINGS_ACTION_FAILED (registered) + ``` + + Which service is unavailable is carried by `message`. The seven registered codes are + added to `ERROR_CODE_LEDGER` under their owning packages — including a new + `@objectstack/service-datasource` entry. + + **`POST /external/validate` keeps its `ok`.** Unlike the `{ ok: true, key }` + #3689 retired from storage — a private second word for `success` — this `ok` is a + computed verdict over the federated objects (`results.every(r => r.ok)`). The + request can succeed while the verdict is false, so the two flags are not the same + field; `ok` moves inside `data` rather than being dropped. + + Consumers were taught both shapes first, so the two repos are not coupled by + merge order: objectui's `packages` readers were already tolerant + (`payload?.data ?? payload`), and its datasource page plus the generic + `type: 'api'` action runner now unwrap the envelope and read `error.message` + (the latter previously toasted `[object Object]` for any nested error). + + ## The guard is shared now, not copied + + `scripts/check-route-envelope.mjs` + `pnpm check:route-envelope`, wired into + `lint.yml` alongside the nine sibling `check:*` guards. Its load-bearing assertion + is structural rather than per-route: **it counts the response write sites per + module.** When every body goes through the `sendOk` / `sendError` pair that count + is fixed at two and does not grow with the route list — so a _future_ route that + hand-rolls a body fails the guard. That is the coverage a driven-body test can + never give, since it can only drive the routes that existed the day it was + written. + + This existed three times already as an open-coded regex block (storage error, + storage success, i18n error). Lifting it did more than deduplicate: a per-package + scan **structurally cannot notice a module nobody thought to convert**, and going + repo-wide found two the moment it ran — neither is in #3843's hand-written survey: + + - `plugin-sharing/share-link-routes.ts` — the fifth drifting module. No body + carries `success`, and one answers `{ ok: true }`, the private second word #3689 + retired from storage. Filed as #3983 and pinned by the guard; converting it is + breaking for share-link consumers and needs its own sweep. + - `metadata/routes/hmr-routes.ts` — declared **exempt** with a reason (dev-only + SSE endpoint, not on the SDK surface), not skipped. Three states, deliberately — + conformant / ratcheted / exempt — because that is the honest classification + ADR-0049 asks for. A route module the scan finds but the table does not declare + is an **error**, never a default: applying `2 / 1 / 1` to an unknown module would + let a new one pass by coincidence. + + It also drops the regex for the TypeScript AST, fixing two real bugs the copies + had. They stripped comments with `String.replace`, whose line-comment pattern also + ate `//` inside string literals and truncated the rest of that line — response + writes included. And `.json(` does not mean "write a response": `hmr-routes.ts` + calls `c.req.json()` twice to READ a request body, which a textual count reports as + two unenveloped responses. Comments and literals are not AST tokens, and + request-vs-response is a property of the callee, so both disappear. The script + carries a `--self-test` pinning each case — the nine sibling guards have none, but + both of these bugs survived a review of the regex version. + + **The i18n ratchet, stated rather than hidden.** `i18n-service-plugin.ts` is + declared at `responses: 5, ok: 4, err: 1` with a ratchet pointing at #3973. Its + error half _is_ consolidated (#3675), but each of its four read routes builds + `{ success: true, data }` inline. Those bodies are correct — that is not envelope + drift — but an unconsolidated builder is a weaker guard: a fifth read route could + get the shape wrong and only a driven test would notice. The numbers pin today's + structure exactly (a new inline body fails) and drop to the conformant `2 / 1 / 1` + when #3973 lands. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [c20b875] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/types@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 3f4f40d516..c818f439f1 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Settings service for ObjectStack — manifest registry + K/V resolver (OS_* env > Tenant > User > Default) + REST routes. See ADR-0007.", "type": "module", diff --git a/packages/services/service-sms/CHANGELOG.md b/packages/services/service-sms/CHANGELOG.md index 3abfd38342..3b88a2ef53 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/service-sms +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index e09e9ac4b2..1995aba283 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 7ca22779d7..f342c62d40 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,61 @@ # @objectstack/service-storage +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/observability@17.0.0-rc.1 + - @objectstack/platform-objects@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index d39bb1af57..9d30dd1823 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 645ef16fa4..8227c8471e 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,1782 @@ # @objectstack/spec +## 17.0.0-rc.1 + +### Major Changes + +- 2d3e255: feat!: ADR-0113 — `required` is a write contract; the column constraint becomes the explicit `storage.notNull` + + `field.required` bound three meanings to one knob (write check, `NOT NULL` DDL, + drift expectation), so tightening any invariant on a deployed object was a + destructive migration blocked by the very legacy nulls that motivated it — the + reason `criteria_json`'s mandatory-in-substance contract lived in three + imperative guards instead of one declaration. + + Split, with the **non-regression invariant** as the unifying rule — _a write + may not take a record from compliant to violating; a pre-existing violation + does not block writes that leave it in place_: + + - `required: true` = the write contract, uniformly on new and deployed objects: + insert must provide; **an update PATCHing `null` into a required field is now + rejected** (it silently passed before); omitted fields never block, so legacy + null rows rest. The column stays nullable. + - `storage: { notNull: true }` = the explicit physical constraint, owning the + DDL (`sql-driver` `createColumn`) and the destructive drift ceremony. + Orthogonal to `required` — all four combinations are legitimate, including + the engine-populated column (`storage.notNull` without `required`). + - `requiredWhen` inherits the same invariant: flipping the condition true + without providing the field is rejected (the write _creates_ the violation); + a row violating since before the rule tightened no longer locks out + unrelated edits (#3929's objection, cured). `storage.notNull` × + `requiredWhen` rejects at parse (`FieldSchema.superRefine`). + - **Pre-17 sources keep their exact meaning** via the migration-chain-only + `field-required-notnull-explicit` conversion: `os migrate meta` stamps + `storage.notNull` onto every previously-required field — writing down what + the old text already meant. The loader never infers semantics from the + physical column. + - Drift compares nullability against `storage.notNull`; a column stricter than + its declaration is `needs_confirm` (never auto-applied — dev auto-reconcile + no longer silently strips a stray `NOT NULL`), and silent when the field is + write-gated by `required`. + +- 5dc4d02: feat(spec)!: `EnhancedApiError.fieldErrors` → `fields`, tombstoned (ADR-0114 D4, #3977) + + Completes ADR-0114 D4, which the field-level catalog PR (#4035) decided but left + unexecuted because retiring an authorable key needs its own sequence. + + **FROM → TO:** `EnhancedApiError.fieldErrors` → `EnhancedApiError.fields`. The array + and its element shape are unchanged — only the property name. + + The wire has always carried `fields`: the validators, import coercion, + `validation-failure.ts`, `@objectstack/client` and the console's field-error + extractor all say `fields`. `fieldErrors` was declared and emitted by nobody, so + anyone reading `error.fieldErrors` was reading a field no server sent — ADR-0078's + silently-inert declaration, sitting on the error envelope. + + **The old key is tombstoned, not deleted.** `EnhancedApiErrorSchema` is not + `.strict()`, so a plain removal would let a producer still writing `fieldErrors` + parse clean and lose the per-field detail — a validation failure that mentions no + field. Writing it now fails with the rename prescription instead + (`retiredKey()`, ADR-0104). + + **Migration:** read `error.fields`. There is nothing to run: this is a response + envelope, so no stack, example or template carries the key and `os migrate meta` + has no source to rewrite. The change is recorded as a semantic chain entry + (`enhanced-api-error-field-errors-renamed`) with its reason and acceptance + criterion, which is what reaches the generated upgrade guide and the + `spec_changes` MCP tool. + +- 9b6fe7c: fix(spec,runtime)!: `AnalyticsQueryRequest` is the bare `AnalyticsQuery`; the dispatcher validates `/analytics` bodies at the entry (#3878) + + **Spec.** `AnalyticsQueryRequestSchema` used to describe a + `{ cube, query: {...}, format }` ENVELOPE — the dialect of the retired degraded + analytics shim (#3891), which the real engine never understood: an envelope + body inferred a column-less cube and died as an SQL syntax error + (`SELECT FROM …`) instead of a shape error. The schema now describes what the + engine and every real caller actually use — the **bare `AnalyticsQuery`**: + + ``` + FROM { "cube": "orders", "query": { "measures": ["count"] }, "format": "json" } + TO { "cube": "orders", "measures": ["count"], "dimensions": [...], "where": {...} } + ``` + + `cube` + `measures` are required at the top level; `dimensions` / `where` / + `timeDimensions` / `order` / `limit` / `offset` / `timezone` sit beside them. + The schema is `.strict()`; `query` and `format` are tombstoned (`retiredKey`) + so both `tsc` and the parse answer with this exact migration. `format` was + never implemented (every response is the JSON envelope) — for CSV/XLSX use the + export surface. The removal is registered as two step-17 semantic migrations + (`analytics-query-request-envelope-retired`, + `analytics-query-request-format-retired`) — it is an HTTP-wire change with no + stored metadata to rewrite. + + **Runtime.** `POST /api/v1/analytics/query` and `/analytics/sql` now validate + the body against that schema AT THE ENTRY and answer + **400 `VALIDATION_FAILED`** with per-field details — including the envelope + prescription above, and a bespoke hint that `filters` is not a contract field + (the filter field is `where`, the same canonical FilterCondition `find()` + takes). Previously a malformed body reached the engine and failed as a 500 SQL + syntax error, or had its off-contract filter key silently ignored. A valid + body is forwarded to the analytics service byte-identical (validation only — + parsing would inject the schema's `timezone: 'UTC'` default and override + org-timezone resolution). An uninstalled analytics capability still answers + 404 before any body inspection (#3891). + +- 12a19a8: refactor(spec)!: the #3896 close-out sweep — fourteen inert authoring keys leave the surface + + The enforce-or-remove worklist across the remaining metadata types, each key + tombstoned at its schema with the prescription (`retiredKey`) and stripped by + a protocol-17 conversion (`os migrate meta` rewrites sources): + + - **action** `shortcut` / `bulkEnabled` — no keydown path ever dispatched a + shortcut; the multi-select toolbar reads the view's `bulkActions`. + - **flow** `active` / `template`, node `outputSchema`, errorHandling + `fallbackNodeId` — `active: false` never stopped a flow (`status` is the + enforced lifecycle; the default even read as disabled while the engine + treated unset as enabled); faults route via per-node fault edges. + - **view** list `responsive` / `performance`, form `defaultSort` / `aria` — + no renderer read any of them. List `aria`/`data` stay live, and **form + `data` survived the sweep**: the removal attempt broke the build — + `defineForm` writes `data.provider='schema'` onto every metadata form — + which re-verified the entry; its ledger verdict is corrected instead. + - **dashboard** `aria` / `performance`, widget `performance` (+ the orphaned + `PerformanceConfigSchema`) — no renderer applied them; virtual scrolling is + the live top-level `virtualScroll`. + - **agent** `knowledge` (+ `AIKnowledgeSchema`) — declaring sources/indexes + never scoped retrieval: `search_knowledge` takes `sourceIds` from the LLM's + tool-call arguments. The protocol-17 `topics`→`sources` rename is absorbed + into the removal pre-release. + - **skill** `triggerPhrases` — phrases were never matched; activation is + `triggerConditions` ∩ the agent's `skills[]` allowlist. + + Docs-shaped annotation fields (`hook.label`/`description`, `flow.description`) + are deliberately KEPT and so noted in the ledger — they document intent for + the next reader and are exempt from enforce-or-remove. The stale report + `aria`/`performance` ledger entries (schema already clean) are deleted as + hygiene. + +- 3c628ce: feat(auth)!: retire the `api.requireAuth` opt-out — anonymous access to object data is always denied (#3963) + + `api.requireAuth: false` let a deployment open its ENTIRE data plane with one + config key. It is removed. Auth is a kernel concern, not a deployment posture: + anonymous callers are denied on every HTTP surface that reaches object data, + unconditionally. + + Every surface that legitimately serves a session-less caller already derives its + own narrow authorization from a DECLARATION, so none of them needed the global + switch: + + - control plane (`/auth/*`, `/health`, `/ready`, `/discovery`, ADR-0069 + remediation) — the auth-gate allowlist; + - public form submission — `publicFormGrant` (ADR-0056 Option A); + - share links — the capability token, validated then read as SYSTEM; + - a `book.audience: 'public'` read — the ADR-0046 §6.7 audience gate (#3995); + - MCP — an OAuth token or API key. + + **Breaking changes.** + + - `api.requireAuth` is a retired key. It is tombstoned (`retiredKey`) in both + `RestApiConfigSchema` and the stack `api` block, so authoring it now fails with + a fix-it message rather than being silently stripped (the ADR-0104 / #3733 + quiet-failure this whole line of work has been closing). `os migrate meta` + drops it via the protocol-18 conversion `stack-api-require-auth-removed`. + - `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth` + input; it denies any anonymous, non-system caller outside the control-plane + allowlist. + - A stack that mounts **no auth at all** now FAILS AT BOOT when it would serve a + data API (`objectstack serve`, plugin-dev), instead of getting an explicit + fail-open. Enable auth (the `auth` tier or AuthPlugin), or run without the data + API. There is no anonymous-data carve-out any more — publishing a public + surface is done by declaration (see above). + + **Migration.** Delete `api.requireAuth` from the stack config (or run + `os migrate meta`). If you were serving data publicly with `requireAuth: false`, + replace it with the declaration that fits: a public form view, a share link, or + `book.audience: 'public'`. If you have an auth-less stack that intentionally + served data, it must now mount auth or stop serving the data API. + +- 62f8017: refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up) + + `DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and + `PluginDiscoverySourceSchema` declared a plugin security control set — + `defaultSandbox`, `requireIntegrity`, `allowedSources`, and discovery's + `requireApproval` ("require admin approval before loading discovered plugins"). + + **None of it was ever wired to anything.** The three schemas were an island: not + composed into any parent schema, not read by any runtime, referenced only by + their own round-trip tests. They were nonetheless published into `json-schema/` + and the authorable key surface, where an author — very often an AI (ADR-0033) — + would read them as capabilities this platform has. A reader of the spec could + reasonably conclude ObjectStack sandboxes dynamically loaded plugins. It does + not. + + That is the ADR-0049 false-compliance shape, and the precedent for a + SAFETY-shaped instance is to remove rather than mark dead: + `tool.requiresConfirmation` was pruned in #3715 because it was "unenforced on + every path, so it was false compliance, not merely dead". This is the same case, + one layer up. + + Found while building the empty-state gate (#3945): `allowedSources` documented + `[]` as admitting every source, and checking who enforced that turned up nobody. + + **No `retiredKey()` tombstones, deliberately.** A tombstone earns its keep by + making a removal audible at a parse the author actually reaches — and nothing + parses these schemas, so the prescription could never be delivered. The + silent-strip that the key-vanish guard exists to prevent was already these keys' + permanent condition: writing one has always been a no-op, because no parent + schema ever accepted them. The guard's baseline entries in + `json-schema.manifest.json` and `authorable-surface.json` are therefore dropped + in this PR as the deliberate removal both files document as the legitimate path, + rather than tombstoning 15 keys nobody could have successfully authored. + + **Breaking, in the narrow sense.** `packages/spec/src/kernel/index.ts` + re-exports this module with `export *`, and `./kernel` is a published subpath, so + `DynamicLoadingConfig`, `PluginDiscoveryConfig`, `PluginDiscoverySource` and + their schemas were importable as types. Nothing in this repo imported them. + Marked `major` because removing a public export is breaking regardless of use; + in practice it folds into the unreleased 17.0.0. + + The rest of `plugin-runtime.zod.ts` is untouched — including + `ActivationEventSchema`, the one export in the file with real consumers. Note + that the remainder (`DynamicLoadRequest`, `DynamicUnloadRequest`, + `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`) also has no + runtime consumer today; it is left in place because those are operation + contracts, not security promises, and the enforce-or-remove call on them is a + design decision rather than a correction. + + **Rebuilding this surface is a design job, not a schema job**: write the runtime + first, then declare only what it enforces. + +- ec796d5: feat(spec)!: retire `BatchOptions.validateOnly` — a dry-run flag that was never implemented (#4052) + + `BatchOptions.validateOnly` promised a dry-run — "validate records without + persisting changes" — but no batch surface ever read it. `updateManyData`, + `deleteManyData` and `batchData` all persist regardless, so a caller sending + `options.validateOnly: true` to PREVIEW a mutation got it executed. That is the + dangerous direction of "declared ≠ enforced": a flag lying about a data-safety + guarantee, not merely an inert no-op. + + There is no dry-run today. Rather than back-fill an implementation to match a + promise nothing kept — a real no-commit batch has its own design space (cascade + and constraint semantics under rollback, a response contract that reports each + row's would-succeed verdict) — the key is retired so it can be reintroduced + deliberately when there is a real need. + + **Breaking change.** + + - `BatchOptions.validateOnly` is a retired key. It is tombstoned (`retiredKey`) + in `BatchOptionsSchema`, so authoring it now fails with a fix-it prescription + rather than being silently stripped (the ADR-0104 / #3733 quiet-failure class). + The `BatchOptions` type's `validateOnly` becomes `never`. + - The retirement is HTTP-only (the key never appeared in stored stack metadata), + so it is recorded as a semantic migration on the protocol-18 chain step + (`batch-options-validate-only-retired`) — a TODO for API callers, not a stack + conversion. + + **Migration.** Stop sending `options.validateOnly` on `/batch`, `/updateMany` + and `/deleteMany`. It never previewed anything; removing it changes no behaviour. + If you need to validate a batch without writing, follow #4052 so a real + no-commit preview can be designed. + + Also fixes a dangling documentation reference: the `/createMany` route + registration named `requestSchema: 'CreateManyRequestSchema'`, a schema no + module ever exported — pointed at the real `CreateManyDataRequestSchema`. + +- 3ca34c1: refactor(spec)!: retire three orphan operator vocabularies (objectui#2945 Track A) + + An audit of every comparison/aggregation vocabulary the spec ships + (objectstack-ai/objectui#2901) found the operator vocabularies had multiplied + past what any code consults. Three had **no importer at all** — not in this + repo, not in objectui, not in cloud — and each contradicted the vocabulary that + is actually enforced. Removed rather than reconciled: a second name for one + concept is how they drifted apart in the first place. + + **`AggregationFunctionEnum`** (`shared/enums.zod.ts`). Its own doc comment + claimed it was _"used across query, data-engine, analytics, field"_. It was used + by nothing. `AggregationFunction` (`data/query.zod.ts`) is the vocabulary the + query engine, `service-analytics`' dataset compiler and the native-SQL strategy + all gate on — and the two disagreed: this one carried + `percentile`/`median`/`stddev`/`variance`, that one carries + `array_agg`/`string_agg`. It also exported a _type_ named `AggregationFunction` + while `data/query.zod.ts` exports a _value_ of that name, so the two occupied + the same identifier in different declaration spaces with different members. + + **`FilterOperator`** + `EventFilterCondition` + `EventFilterSchema` + (`api/websocket.zod.ts`), reached from `EventSubscriptionSchema.filters`. No + runtime ever evaluated an event filter — `matchesSubscription` matches on object + name and event type only (`contracts/realtime-service.ts`) — and the + subscription shape the transports actually carry is the separate, deliberately + unvalidated `filters: z.unknown()` on `SubscriptionEventSchema` + (`api/realtime.zod.ts`). So this was a _second_ modelling of event filtering + that described a capability no code provided: a subscriber who set `filters` + received every event regardless. + + The `filters` **key stays**, now typed `z.unknown()` with the same + NOT-YET-ENFORCED marker as its `api/realtime.zod.ts` counterpart. Retiring an + object key requires a tombstone plus a conversion (ADR-0104), which is the right + rule and the wrong trade here — there is no author to migrate for a shape nothing + validated, and Track A is meant to carry no migration. The two subscription + surfaces now describe event filtering identically, and neither implies an + enforcement that does not exist. Whichever grows real filtering should lower onto + `AST_OPERATOR_MAP` rather than reintroduce a vocabulary of its own. + + **`ODataFilterOperatorSchema`** (`api/odata.zod.ts`). Nothing parses an OData + `$filter` against it — `$filter` is carried as an opaque string on + `ODataQuerySchema` and as the `odata` adapter template in + `query-adapter.zod.ts` — and an enum mixing operators with `(`/`)` could not + validate an expression anyway, since it describes tokens, not a grammar. A real + implementation needs a parser, and that parser should lower onto + `AST_OPERATOR_MAP` like every other entry point. + + **Breaking, in the narrowest sense.** All three were reachable as public + exports (`@objectstack/spec/shared` and `@objectstack/spec/api`), so this is a + `major`. No consumer exists to break: verified by grep across framework + `packages/` + `apps/`, objectui, and cloud. Nothing is _narrowed_ — no accepted + value stops being accepted, so no already-stored metadata or in-flight payload + changes meaning. That is what made this the one track of objectui#2945 that was + safe to start; narrowing `VALID_AST_OPERATORS` or retiring a + `VIEW_FILTER_OPERATORS` alias is not, and remains blocked on #3948. + + The generated artefacts move with the deletions, as the ratchets require: + `json-schema.manifest.json` drops the five unpublished schemas, + `authorable-surface.json` the seven keys of the two deleted objects, + `api-surface.json` the eight exports, and the three reference-doc pages are + regenerated. + + Verified: full `@objectstack/spec` suite **6917 tests across 266 files**, plus + `tsc --noEmit`, `check:docs`, `check:api-surface`, `check:authorable-surface` and + `check:skill-docs`, all clean. + +- d6bfb3d: refactor(spec)!: remove the RLS-policy `priority` key — it promised conflict resolution that cannot exist (#3896 audit) + + `RowLevelSecurityPolicySchema.priority` was documented as _"Policy priority for + conflict resolution"_. The 2026-07-30 security-subset liveness re-verification + found that **nothing ever read it** — and, stronger, that nothing ever could: + applicable policies **OR-combine** (any match allows access, most permissive + wins — the schema's own describe said so), so there is never a conflict to + order and evaluation order cannot change an outcome. A semantically-void knob + on a security policy is worse than dead: an author — very often an AI + (ADR-0033) — reads it as a precedence lever and reasons about policy + interactions that do not exist. + + Removed per the `tool.requiresConfirmation` (#3715) / `DynamicLoadingConfig` + (#3950) precedent, inside the v17 breaking window: + + - **Tombstoned, not silently stripped** (`retiredKey`, #3855 pattern): an + authored `priority` fails `tsc` (the input type is `never`) and rejects at + parse with the prescription itself — _"policies OR-combine (most permissive + wins), so there is no conflict to order. Delete the key — policy outcomes are + unchanged."_ + - **ADR-0087 D2 conversion + D3 chain step** (`permission-rls-priority-removed`): + `os migrate meta` deletes the key from authored sources mechanically — a pure + lossless delete, no semantic residue. spec-changes.json and the protocol + upgrade guide carry the entry. + - The policy factory helpers (`ownerPolicy`, `tenantPolicy`, …), the showcase + example's permission sets, and `content/docs/permissions/rls.mdx` no longer + author it; the docs table's `enabled` row now states the (since-enforced) + contract instead. + - Liveness ledger entry updated to record the removal; the tombstone and entry + age out ~two majors from now. + + Dropping the key changes **no policy outcome anywhere** — that impossibility of + effect is the entire reason for the removal. + +- eb95d97: refactor(spec)!: remove the four inert tool authoring keys — two of them promised safety they never delivered (#3896 close-out) + + `tool.category`, `tool.permissions`, `tool.active` and `tool.builtIn` were + authorable and inert: none is part of `AIToolDefinition`, and no execution path + read them. The liveness ledger had already corrected all four to dead+authorWarn + (#3686); this finishes the enforce-or-remove disposition inside the v17 window, + following the `requiresConfirmation` precedent (#3715) — because two of the four + were misleading in the dangerous direction: + + - **`permissions`** promised a capability gate on tool invocation. Nothing + enforced it — a tool "requiring" capabilities ran for everyone. The real gates + are `action.requiredPermissions` (ADR-0066) and permission sets on the objects + the tool touches. + - **`active: false`** read as "withdrawn". It withdrew nothing: `ToolRegistry.getAll()` + returns everything, the tool kept reaching the LLM tool set, and + `POST /ai/tools/:name/execute` kept running it — unlike `agent.active` / + `skill.active`, which are enforced. To withdraw a tool, remove it from the + skills/agents that reference it. + + The retirement kit: + + - The `.strict()` ToolSchema rejects each retired key with its own prescription + (`TOOL_RETIRED_KEY_GUIDANCE`, the #3715 pattern) — no silent strip. + - **ADR-0087 D2 conversion + D3 chain step** (`tool-inert-authoring-keys-removed`): + `os migrate meta` deletes the keys mechanically; a pure lossless delete, since + they never had any effect to lose. + - `ToolCategorySchema` / `ToolCategory` are removed with the key they typed + (zero consumers; `action.zod.ts` deliberately keeps its own inline vocabulary). + - The Studio tool form drops its inputs for the retired keys — a form input for + an unenforced gate is the UI half of false compliance, the same + "advertising the failure mode" shape objectui#2962 removed from the + sharing-criteria builder. + - Ledger entries deleted per the #3715 precedent; baselines + (`authorable-surface.json`, `json-schema.manifest.json`) updated deliberately; + reference docs and the v17 release notes regenerated/extended. + + No runtime behaviour changes — that impossibility is the reason for the removal. + +- 4d7bebf: feat(spec)!: reject unknown keys on RLS policies, sharing rules, and positions (#4001 step 2) + + Second click of the unknown-key strictness ratchet (first: flow + permission, + #4071), extending `.strict()` + the `strictUnknownKeyError` fixable-error + factory to the remaining small security-class authoring surfaces, per + `docs/audits/2026-07-unknown-key-strictness-ledger.md`: + + - **`security/rls.zod.ts`** — `RowLevelSecurityPolicySchema` is `.strict()`. + A silently dropped key on an RLS policy meant a row-level restriction the + author wrote was never compiled into the filter. The runtime shapes + (`RLSUserContextSchema`, `RLSEvaluationResultSchema`) stay tolerant. The + retired `priority` key keeps its existing tombstone. + - **`security/sharing.zod.ts`** — the sharing-rule surface is `.strict()` + (base + criteria extension + the `sharedWith` recipient shape). A silently + dropped key meant a share the author intended was never materialised. + - **`identity/position.zod.ts`** — `PositionSchema` is `.strict()`, and gains + the author-facing `protection` block plus the ADR-0010 runtime protection + envelope (`_lock`, `_packageId`, `_provenance`, …) — closing the sibling + gap the #4071 ledger flagged: `applyProtection` stamps every registered + metadata type, and position was the last one whose schema could not + represent the stamp. + + **Migration.** Any key these schemas now reject was previously stripped and + had **no runtime effect** — removing or renaming it never changes behavior. + The error carries the fix; FROM → TO mappings baked in include: + + - RLS policy: `roles`/`role` → `positions` (ADR-0090 D3 rename), + `withCheck` → `check` (the PostgreSQL spelling), `condition`/`filter`/`where` + → `using`. `priority` stays a tombstone (#3896: OR-combined policies have no + precedence to order — delete the key). + - Sharing rule: `criteria` → `condition` (the persisted row spells the + compiled predicate `criteria_json`; the authored key is the CEL + `condition`), `access`/`level` → `accessLevel`, + `recipient`/`shareWith`/`sharedTo` → `sharedWith`, `enabled` → `active`; + recipient `id`/`target` → `value`. `ownedBy` carries the removed + owner-type-rule prescription (only `criteria` rules are authorable). + - Position: `title` → `label`; `permissionSets` / `users` are runtime + bindings (`sys_position_permission_set` / `sys_user_position`), never + authored on the position; `parent` is rejected with the flatness rule + (ADR-0090 D3 — hierarchy is the business-unit tree, not a position tree). + +- 821ac7a: feat(spec)!: reject unknown keys on the approval authoring schemas (#4001 step 3) + + Third click of the unknown-key strictness ratchet (flow + permission in + #4071, RLS / sharing / position in #4099). Approval is a v17-new authoring + surface — tightened while young, before stored volume exists: + + - **`automation/approval.zod.ts`** — `ApprovalNodeConfigSchema`, + `ApprovalNodeApproverSchema`, `ApprovalEscalationSchema`, and + `DecisionOutputDefSchema` are `.strict()` with fixable errors. An approval + gate that quietly ignores half its config is the worst instance of the + ADR-0078 trap — the request routes, but not the way the author declared. + - The published JSON schema (`getApprovalNodeConfigJsonSchema`) now carries + `additionalProperties: false` into the Studio property form AND + `registerFlow()`'s per-node config validation (#4027/#4040), so an unknown + key inside an approval node's `config` is rejected at registration too. + + **Migration.** Any key now rejected was previously stripped and had no + runtime effect — removing or renaming it never changes behavior. Mappings + baked into the errors include the ADR-0019 re-home map for process-era + concepts: `steps` → successive approval NODES on the canvas, `entryCriteria` + → the condition on the entering edge, `onApprove` / `onReject` → the nodes + wired to the `approve` / `reject` out-edges, `rejectionBehavior` → a declared + back-edge (ADR-0044) with `maxRevisions`. Plus spelling aliases: + `mode` / `approvalMode` → `behavior`, `quorum` → `minApprovals`, + `statusField` → `approvalStatusField`, `org` → `organization`, + `expandAs` → `resolveAs`, `timeout` / `hours` / `sla` → `timeoutHours`, + `to` / `target` → `escalateTo`, `name` → `key`, `widget` → `type`. + +- 8f81731: feat(spec)!: reject unknown keys on the flow and permission authoring schemas (#4001 Tier-A) + + Zod's default `.strip` silently discarded any key these schemas did not + declare — the instance kept parsing, so a mis-spelled or wrong-layer key + shipped as metadata that quietly ignored the author's config (#3405's + action-param `reference`, #1535's object-level `workflows`). #3746 tightened + one schema; this extends the same treatment to the two highest-risk + authorable surfaces, per the ADR-0054 ratchet and the + `docs/audits/2026-07-unknown-key-strictness-ledger.md` triage: + + - **`security/permission.zod.ts`** — `PermissionSetSchema`, + `ObjectPermissionSchema`, `FieldPermissionSchema`, `AdminScopeSchema` are + now `.strict()`. A silently dropped key on the capability container meant + the author believed a grant or restriction was in place that the runtime + never saw. `EffectiveObjectPermissionSchema` (response-side) explicitly + `.strip()`s back and stays wire-tolerant. + - **`automation/flow.zod.ts`** — `FlowSchema`, `FlowNodeSchema`, + `FlowEdgeSchema`, `FlowVariableSchema` are now `.strict()`. A node's + `config` record stays **open**: it is per-node-type, owned by the + registered executor's `configSchema` (#4027/#4040) and the ADR-0087 + conversion layer. + - **`shared/suggestions.zod.ts`** — new `strictUnknownKeyError` factory (the + #3746 hand-rolled map, generalized): every rejection names the offending + key(s) and, where recognisable, the canonical spelling or a retired-key + tombstone. `ui/action.zod.ts` re-homes onto it with byte-identical messages. + - **`PermissionSetSchema` gains `description`, `protection` and the ADR-0010 + runtime protection envelope (`_lock`, `_packageId`, `_provenance`, …).** The + strict gate's own catches: all of these are written by real code — the + built-in default sets author `description` and the Setup projection reads + it; `applyProtection` stamps the envelope on every metadata type and + `getMetaItemLayered` → `saveMetaItem` round-trips it — but the schema could + not represent them, so they were silently stripped at every parse (ADR-0078 + §3 inverse drift). Every sibling registered metadata type already spread + `MetadataProtectionFields`; permission was the outlier. + + **Migration.** Any key these schemas now reject was previously stripped and + therefore had **no runtime effect** — removing or renaming it never changes + the behavior of a working app; validation simply stops lying about it. The + error message carries the fix; the FROM → TO mappings baked into it include: + + - Permission set: `objectPermissions`→`objects`, `fieldPermissions`/`fls`→`fields`, + `tabs`→`tabPermissions`, `rls`/`policies`→`rowLevelSecurity`; + `read`/`edit`/`export`/…→`allowRead`/`allowEdit`/`allowExport`/…; + `readable`/`editable` vocabulary for FLS (`hidden` → declare `readable: false`). + Retired keys carry tombstones: `contextVariables` (ADR-0105 D11 — use a + registered `rlsMembership` resolver or an inline literal), `isProfile` + (ADR-0090 D2 — use `isDefault`). + - Flow: `steps`→`nodes`, `connections`/`transitions`/`links`→`edges`, + `trigger`/`triggerType`→`type`, `title`→`label`; edge `from`/`to`→`source`/`target`, + `guard`/`when`/`expression`→`condition`; a top-level `object`/`objectName`/`schedule` + belongs on the START node's `config` (`{ objectName, triggerType, condition, +schedule }`), not on the flow. + +### Minor Changes + +- 1ea6bce: feat(sharing): hierarchy managers may manage shares within their write DEPTH (ADR-0111 D1 DEPTH) + + `canManageShares` gains its named DEPTH extension: a caller whose effective + WRITE scope on the object is a hierarchy scope (`unit` / `unit_and_below` / + `own_and_reports`) may now manage shares on a record whose owner falls within + that scope's owner set — the same set the write filter and `canEdit` already + honour, resolved by the enterprise `hierarchy-scope-resolver`. This lets a + manager grant/revoke/list shares on a subordinate's record, matching + Salesforce (roles above the owner) and Dataverse (the `Share` privilege's BU + depth), without expanding the MVP owner + Modify-All authority. + + - New `ISecurityService.resolveWriteScope(object, context)` — the effective + write scope, resolved by the same evaluator the CRUD middleware uses; fails + closed to `own`. Mirrored on the sharing plugin's structural probe. + - The gate honours only the three hierarchy scopes. `org` from the probe is + deliberately ignored: it means both a genuine Modify-All holder (already + granted via `hasWriteBypass`) AND the fail-OPEN "no permission set mentions + this object" default, so honouring it here would reopen the hole + `hasWriteBypass` was chosen to avoid. + - Fails closed with no security service or no enterprise resolver — the open + edition stays owner + Modify-All, exactly as before. + +- c1dcacd: fix(sharing)!: the share-management surface gains the authorization layer it never had (ADR-0111 P0, #3902) + + Record sharing shipped as a data layer with no authorization of its own: every + `/data/:object/:id/shares` and `/sharing/rules` route authenticated the caller + and then ran the service under `SYSTEM_CTX` — any signed-in user could revoke + anyone's share, enumerate who-can-see-what, write self-grants, and define / + evaluate org-wide sharing rules. ADR-0111's P0 rulings land here: + + - **D1/D2** — `ISharingService.canManageShares(object, recordId, context)`: + system, the record's owner, or a holder of Modify All Data (probed via the + new fail-closed `ISecurityService.hasWriteBypass`). Enforced in the SERVICE, + so every caller is covered; without plugin-security it fails closed to + owner-only. + - **D4** — `revoke` is symmetric with grant, validates the share belongs to the + URL's record (`NOT_FOUND` on mismatch), and refuses non-`manual` rows + (`CONFLICT` — a rule-materialised grant would be resurrected by the next + reconcile). + - **D5** — `listShares` is management-gated (invisible record → `NOT_FOUND`, + visible-but-not-manager → `PERMISSION_DENIED`), and the open + `/data/sys_record_share` read surface is self-scoped: non-admin callers see + only rows naming them as recipient or grantor. + - **D6** — the whole `/sharing/rules` surface (list/create/get/delete/evaluate) + requires the new **`manage_sharing`** capability (D9; seeded into + `admin_full_access`, `manage_platform_settings` honoured as the legacy + equivalent), enforced in `SharingRuleService`. + - **D7** — no inert grants: `recipientType` is narrowed to `user` (the only + type any gate enforces), grants on objects the sharing gates never consult + (public model, no `owner_id`, bypass, `controlled_by_parent`) fail with + `SHARING_NOT_ENABLED` (422), and the manual upsert keys on + `(object, record, recipient, source)` so manual and rule rows coexist. + + **Breaking** for callers that relied on the missing gate: unauthorized share + management now fails with 403/404/409/422 instead of silently succeeding, and + `ISharingService.revoke` gained an optional `scope` parameter. The verb + boundary (edit ≠ delete, ADR-0111 D3) is NOT in this change — it lands as the + separate P1. + +- ad303ed: fix(sharing)!: an edit-level share no longer grants delete (ADR-0111 D3, the verb boundary) + + `update` and `delete` shared one `canEdit` gate, and `canEdit` accepts an + `edit`-level share — so one "edit" grant silently conferred delete, the + opposite error from the retired `full` level. A share widens _which rows_ a + principal reaches, never _which verbs_ they may use (Salesforce Read/Write + cannot delete; Dataverse `Delete` is a distinct privilege; Odoo splits + `write`/`unlink`). + + - `ISharingService.canDelete(object, recordId, context)` — ownership (widened + by write DEPTH) or the `modifyAllRecords` super-user bypass ONLY; an `edit` + or legacy `full` share does not confer it. `canEdit` is unchanged (the + update gate, share included). + - `SharingService.buildWriteFilter` takes a `verb` parameter: a bulk + `delete({multi:true})` scopes to the owner/DEPTH set alone (no share + widening), while a bulk `update` keeps it. + - The sharing middleware routes `delete` through `canDelete` and logs a + specific fail-closed reason on denial (ADR-0111 D10). + - `/security/explain` consults `canDelete` for a `delete` operation, so the + record-level explanation matches enforcement. + + **Breaking**: a caller who could delete a record _only_ through an edit-level + share (and holds object-level delete CRUD) can no longer delete it — delete now + requires ownership, write depth, or Modify All Data. No new delete access level + is introduced; a future per-record delete grant would be a capability mask + AND-ed with object CRUD, not a fourth share level. + +- 32ccb23: feat(spec,core,runtime)!: ADR-0112 batch 1 — one error-code vocabulary, SCREAMING_SNAKE, schema-enforced (#3841) + + Settles #3841 per ADR-0112: the top-level `error.code` vocabulary is + SCREAMING_SNAKE, in two tiers. + + - **`StandardErrorCode` members renamed in place** (`validation_error` → + `VALIDATION_ERROR`, all 53). Breaking for importers that branch on the old + lowercase members; the type name and member _meanings_ are unchanged. + - **New `ERROR_CODE_LEDGER`** (`@objectstack/spec/api`): service-specific codes + (`AUTH_REQUIRED`, `VALIDATION_FAILED`, `ATTACHMENT_DOWNLOAD_DENIED`, …) are + registered per owning package. `ErrorCode` = standard ∪ registered. + - **`ApiErrorSchema.code` is now `ErrorCode`**, not `z.string()` — an + unregistered code fails parse, so the envelope conformance suites assert + values, not just shape. + - **`FieldErrorSchema.code` widened to `z.string()`** (ADR-0112 D6): field-level + codes are a separate vocabulary the enum never described; #3977 owns its real + catalog. + - **Derived codes changed case on the wire**: `standardErrorCodeForHttpStatus` + now yields SCREAMING members (`permission_denied` → `PERMISSION_DENIED`, + `method_not_allowed` → `METHOD_NOT_ALLOWED`, …) — this map was #3842's + designated one-file sweep point for exactly this decision. + - **`ANONYMOUS_DENY_CODE` is `'UNAUTHENTICATED'`** (was `'unauthenticated'`) — + the promoted code on anonymous-denied requests and the REST `enforceAuth` + body change spelling with it. + + `error-catalog.mdx` and the error-handling guides are rewritten to the single + vocabulary; a spec test now locks the catalog page's headings to the enum so + they cannot drift apart again. Remaining lowercase emitters (cloud-connection, + plugin-auth envelope codes, metadata-protocol, …) are the batch-2 sweep. + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +- 7d7521f: feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) + + Settles the vocabulary ADR-0112 D6 deferred, per [ADR-0114](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0114-field-level-error-code-catalog.md). + + **`FieldErrorCode` — a closed, lowercase catalog.** 27 members covering what the + six emitters already emit. `FieldErrorSchema.code` tightens from `z.string()` to + this enum, so a validation body's per-field codes are validated for the first time. + `FieldValidationError.code` (objectql) and `FieldCoerceError.code` (rest) stop + being a hand-listed union and a bare `string` respectively and reference the + catalog, so the three cannot drift apart. + + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a + top-level code names the condition the _request_ hit, while a field-level code + names the _constraint_ the value violated — and constraints are declared in the + metadata's own snake_case, so `max_length` the code and `max_length: 50` the + property are the same word on purpose. + + **Zod issue codes no longer reach the wire (wire-visible).** Routes that validate + with Zod passed its vocabulary straight through, so `fields[]` spoke a different + language depending on which route served it, and `too_small` was ambiguous between + a short string, a small number and a short array. `zodIssuesToFields` now maps + using Zod's `origin`/`format`: + + | Was | Now | + | :------------------------------------------------ | :------------------------------------------------- | + | `too_small` | `min_length` / `min_value` / `min_items` | + | `too_big` | `max_length` / `max_value` / `max_items` | + | `invalid_format` | `invalid_email` / `invalid_url` / `invalid_format` | + | `invalid_value` | `invalid_option` | + | `unrecognized_keys` | `unknown_field` | + | `invalid_union`, `invalid_element`, `invalid_key` | `invalid_shape` | + + **A missing required property now reports `required`, not `invalid_type`.** Zod + spells "absent" as a type mismatch against `undefined`, so passing it through made + a form mark a _missing_ input as the wrong _type_. The two are indistinguishable on + the issue alone, so the mapper takes the parsed input as an optional argument and + walks the issue path; a caller that cannot supply it keeps `invalid_type` rather + than guessing. + + **`unknown_param` → `unknown_field`.** `ActionParamIssue.code` references the + catalog instead of its own literal union; the `param` key beside it already says + what was addressed. + + **Not changed:** `EnhancedApiErrorSchema.fieldErrors` keeps its name even though + every producer emits `fields`. Retiring an authorable key needs a tombstone plus a + migration (ADR-0104's contract guard), so it lands on its own — the property now + carries a banner saying which name the wire uses. + +- 789ad63: fix(spec,rest): the batch-size cap is enforced now, and each bulk endpoint has one Zod source (#3939) + + `max 200` was declared in four places and enforced in one. + + `batch.zod.ts` put `.min(1).max(200)` on `BatchUpdateRequestSchema`, + `UpdateManyRequestSchema` and `DeleteManyRequestSchema`, and the docs repeated + it — but no per-object bulk route validated against those schemas, so + `createMany` / `updateMany` / `deleteMany` / `/data/:object/batch` all accepted + an unbounded list. The only route that capped anything was the cross-object + `/batch`, and it checked the _configured_ `maxBatchSize` rather than the + hardcoded 200 — so even the one enforcement point disagreed with the schema. + + That stopped being cosmetic with #3897, which made `deleteMany` delete per id by + primary key (so `deleteBehavior` cascades run and every row gets its own + result). A 10k-id body is now 10k sequential engine round-trips inside a single + request, where before it was one statement that mostly failed anyway. + + **The cap moved to the routes, and the schemas gave it up.** Batch size is + deployment policy — `RestServerConfig.batch.maxBatchSize`, 1..1000, default 200 + — so a hardcoded bound in the spec could only ever be a second, wrong answer + (a deployment raising the limit to 500 would still have been refused at 200). + All five bulk routes now call one `enforceBatchSize` helper with the configured + value and answer with one envelope: + + ```json + { + "error": "Batch too large: 500 records (max 200)", + "code": "BATCH_TOO_LARGE", + "count": 500, + "max": 200, + "object": "account" + } + ``` + + The cross-object route is included: it used to answer with a bare `error` string + and no `code` for a client to key on. + + **One Zod source per bulk endpoint (Prime Directive #7).** Each of these + endpoints had _two_ schemas, and they had already drifted into disagreeing about + more than counts: `UpdateManyRequestSchema` described its rows with + `BatchRecordSchema`, whose `id` and `data` are optional because the generic + `/batch` route serves create (no id) and delete (no data) through the same + shape — so the declared contract accepted `{}` rows that `updateManyData`, which + reads `record.id` and `record.data` unconditionally, could never process. The + enforced shape lived in the _other_ copy, in `protocol.zod.ts`. + + The wire body is now the single source (`UpdateManyRequestSchema` / + `DeleteManyRequestSchema`, with the new `UpdateManyRecordSchema` for a row), and + the protocol schemas are that plus the `object` the route takes from the URL + path (#3933) — `UpdateManyRequestSchema.extend({ object })`. The derivation runs + that direction because `protocol.zod` already imports `batch.zod`; the reverse + would be a cycle. + + **Behaviour changes.** + + - A bulk request over the configured cap is `400 BATCH_TOO_LARGE` instead of + being executed. Deployments that were quietly relying on unbounded batches + should raise `batch.maxBatchSize` (up to 1000) rather than discover the cap in + production. + - `.min(1)` is gone with `.max(200)`: an empty batch is a no-op returning + `total: 0`, which is what these routes already did, rather than a validation + error the schema claimed but nothing raised. + - `UpdateManyRequest` now types (and validates) `records` as + `{ id: string; data: Record }[]`. Callers already had to send + that — the route has validated the strict shape since #3933 — but the declared + type was looser. + - New export: `UpdateManyRecordSchema` / `UpdateManyRecord`. + +- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D) + + `@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS + write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte` + bound literally. On a `datetime` post-image that meant a policy of the shape + `{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**: + the write-side twin of the read-side data loss #3777 fixed, and the last of the + platform's filter backends that disagreed about what a bare day means as a + bound. + + `$lte` and a `$between` max now evaluate half-open against the next calendar + day, matching the SQL compiler, the memory and mongo drivers, and the analytics + preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep + exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a + plain `YYYY-MM-DD` value compares identically (string ordering makes the two + forms equivalent). The evaluator stays fail-closed on a null bound. + + **Where the rule now lives.** `nextUtcCalendarDay` moved from + `@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`, + whose vocabulary it interprets. `formula` cannot depend on `core`, and a second + copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one + package all six consumers already depend on, so this adds no dependency edge. + + No import changes are required: `@objectstack/core` re-exports the symbol, so + existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working. + New code should prefer `@objectstack/spec/data`. + +- 03d26f7: fix(runtime,spec)!: the dispatcher's `error.code` is the semantic string it always declared; the HTTP status moves to `httpStatus` (#3842) + + `HttpDispatcher.error()` took the HTTP status as its `code` argument and wrote it + straight into the field `ApiErrorSchema` reserves for a semantic string, so + `error.code` came back as `400`/`403`/`503` — a number, duplicating the response + status and occupying the one slot a caller is meant to branch on. The real code + then had to go somewhere else, and did, three somewhere-elses: `details.code` + (auth gate, permission denial, anonymous deny), `details.type` + (project-membership gate), and `error.type` (`routeNotFound`). Four sites, three + parking spots, because the declared one was full. + + **FROM → TO on the wire.** A dispatcher error body + + ```json + { + "success": false, + "error": { + "message": "…", + "code": 403, + "details": { "code": "PERMISSION_DENIED" } + } + } + ``` + + is now + + ```json + { + "success": false, + "error": { "code": "PERMISSION_DENIED", "message": "…", "httpStatus": 403 } + } + ``` + + | Reading | Was | Now | + | ------------- | ---------------------------------------------------------- | ------------------------------------------------- | + | semantic code | `error.details.code` / `error.details.type` / `error.type` | `error.code` | + | HTTP status | `error.code` | `error.httpStatus` (or the response status) | + | context | `error.details` (with the code mixed in) | `error.details` (context only, absent when empty) | + + **One-line fix for a direct reader:** replace `body.error.details?.code ?? +body.error.type` with `body.error.code`, and `body.error.code` with + `body.error.httpStatus`. **SDK callers need no change** — `ObjectStackClient` + already normalised this (`err.code` semantic, `err.httpStatus` numeric) and still + reads the old shape, so a client newer than its server is unaffected. + + Every code already on the wire moves **verbatim** — `PERMISSION_DENIED`, + `ROUTE_NOT_FOUND`, `PASSWORD_EXPIRED`, `PROJECT_MEMBERSHIP_REQUIRED`, + `VALIDATION_FAILED`, `unauthenticated`. This change moves a field; it does not + rename anything. Reconciling the repo's two code vocabularies is #3841, and this + leaves it exactly one map and one enum to sweep instead of four parking spots. + + A branch with no code of its own is served one derived from the status, via the + single declared map `HttpStatusErrorCodeMap` / `standardErrorCodeForHttpStatus` + in `@objectstack/spec/api` (`403` → `permission_denied`, `503` → + `service_unavailable`, …). Derivation is necessary because `ApiErrorSchema.code` + is required; drawing it from `StandardErrorCode` keeps a derived code a + catalogued one rather than an invented string. + + **Spec changes:** + + - `ApiErrorSchema` gains optional `httpStatus: number` — the precedent is + `EnhancedApiErrorSchema.httpStatus`. Additive. + - `StandardErrorCode` gains `method_not_allowed` and `precondition_required`, + the two statuses the runtime returns that the enum could not name. Additive. + - **Breaking — `DispatcherErrorCode`** was `'404' | '405' | '501' | '503'` (string + spellings of HTTP statuses, for matching against the numeric `error.code`). It + is now `'ROUTE_NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'NOT_IMPLEMENTED' | +'SERVICE_UNAVAILABLE'` — the same four members the removed `error.type` enum + declared, moved verbatim. FROM `DispatcherErrorCode.parse('404')` TO + `DispatcherErrorCode.parse('ROUTE_NOT_FOUND')`; to match a status, read + `error.httpStatus`. TypeScript flags every call site. + - **Breaking — `DispatcherErrorResponseSchema`**: `error.code` is `z.string()` + (was `z.number().int()`), `error.type` is **removed** (folded into `code`), and + `error.httpStatus` / `error.details` are declared. This schema is what + legitimised the deviation — it declared the opposite of `ApiErrorSchema` for + the same field. FROM `{ code: 404, type: 'ROUTE_NOT_FOUND' }` TO + `{ code: 'ROUTE_NOT_FOUND', httpStatus: 404 }`. + + **Also aligned, because they are the same wire surface:** `dispatcher-plugin`'s + `errorResponseBase` (the THROWN-error exit) and its inline 404, and the MCP 405. + `errorResponseBase` previously discarded a thrown error's `.code` outright — it + had nowhere to put it — so the two exits of one surface disagreed about what a + caller would see; they now agree. Every body on this surface is built by one + helper (`packages/runtime/src/error-envelope.ts`), guarded in both directions by + `error-envelope.conformance.test.ts`: each branch driven and parsed against the + schema imported from `packages/spec`, plus a source scan so a new branch cannot + quietly reintroduce a numeric `code` or a `type`-as-code sibling. + + This deletes the #3687 pin in `http-dispatcher.test.ts`, which asked to be + deleted rather than updated once the dispatcher was fixed. + +- dc530b4: **`envelopeViolations` — the conformance check `BaseResponseSchema` cannot express.** + + Every conformance suite from the #3843 line leads with + `BaseResponseSchema.safeParse(body)`, under a comment claiming it is "the contract + itself, imported — not a restatement of it". That overclaimed, and the gap is + demonstrable: + + ```ts + BaseResponseSchema.safeParse({ success: true }); // passes + BaseResponseSchema.safeParse({ success: true, data: link, link }); // passes + ``` + + The schema declares no `data` — each response type adds its own via + `.extend({ data })` — and a plain `z.object` strips unknown keys rather than + rejecting them. So it catches the one drift it was added for (a missing or + non-boolean `success`, the flag `unwrapResponse` keys on) and nothing else. The + second body above is exactly the duplicate-payload drift `/share-links` shipped + until #4038 / #4049 removed it, and `safeParse` passed it the whole time. + + `envelopeViolations(body)` returns every departure from the declared envelope as + readable reasons, empty when conformant: + + - `success` must be a **boolean** + - a success body must carry `data` (`undefined` only — `null`, `[]`, `{}`, `0` + and `''` are payloads, not absences) + - a failure body must carry `error` with a string `code` and `message` — the + nested form, not the pre-#3675 bare string + - no top-level key outside `success` / `data` / `error` / `meta`, which is the + general form of the duplicate-payload drift + + It deliberately does **not** check the shape of `data`: that is each route's own + payload schema, and conflating the two is what let `SettingsNamespacePayload` + describe a whole body before #3843 and only `data` after it. + + The ten conformance suites now assert it beside `safeParse`, and their comments + say what each of the two actually proves. Reintroducing the `/share-links` + duplicate key is caught by the new assertion and still passes the old one — which + is the demonstration that the pairing is the point. + + Placed in `contract.zod.ts` beside the schema it completes, alongside the other + plain predicates `spec/api` already exports (`standardErrorCodeForHttpStatus`, + `readServiceSelfInfo`). No new package. + +- a47ac06: feat(spec,automation): graduate the seven flow-node config key aliases into the conversion layer — the `readAliasedConfig` shim retires with them (#3796) + + `FlowNodeSchema.config` is an unconstrained record, so the executors were the + only statement of which config key is canonical — and seven deprecated aliases + lived there as tolerance the spec never declared: one behind the + `readAliasedConfig` deprecation shim (warned, ledgered), six as open-coded + `??` fallbacks (no warning, no ledger, no retirement path). All seven now + graduate into the ADR-0087 D2 conversion layer as protocol-17 **live-window** + entries: a stored flow authored with an alias is rewritten to the canonical + key at load — `defineStack` / `validate` / `lint` and the + `AutomationEngine.registerFlow` rehydration seam alike — with a structured + `ConversionNotice` per rewrite, and the executors read the canonical keys + only. The shim (`service-automation/src/builtin/config-aliases.ts`) is empty + and deleted. + + FROM → TO (per node type; conversion entry in parentheses): + + - `get_record`/`create_record`/`update_record`/`delete_record`: + `config.object` → `config.objectName` (`flow-node-crud-object-alias`) + - `notify`: `config.to` → `config.recipients`, `config.subject` → + `config.title`, `config.body` → `config.message`, `config.url` → + `config.actionUrl` (`flow-node-notify-config-aliases`) + - `script`: `config.functionName` → `config.function`, `config.input` → + `config.inputs` (`flow-node-script-config-aliases`) + + One-line fix: rename the key in your flow source — values are unchanged; `os +migrate meta --from 16` rewrites all seven mechanically. Until then nothing + breaks: the protocol-17 loader accepts and converts the old shape (window + retires in 18). + + `actionUrl` (not `url`) is the deliberate canonical of its pair, resolving a + contradiction where the notify descriptor documented `url` as canonical while + the executor, tests, and examples preferred `actionUrl`: the whole downstream + chain already uses that name (`sys_notification.action_url`, the + channel-dispatch contract, the REST notification read model), and `url` + elsewhere in the platform means "HTTP endpoint to call" (`http` node, + webhooks) — a different concept from this in-app click-through target. The + executor precedence already put `actionUrl` first, so the choice is + behaviour-preserving; the `notify` descriptor's `configSchema` now documents + `actionUrl`. + + Callers that hand a node config **directly** to an executor (bypassing + `registerFlow`) no longer get alias resolution — build the config with the + canonical keys. + +- e4c61a7: Validate the expression slots a flow node's `configSchema` declares (#4027). + + A node type's designer `configSchema` and the keys its validators traverse were + two unreconciled lists. Both the engine's `registerFlow` pass and the author-time + `objectstack validate` pass hardcoded `config.condition` / `edge.condition` and + assumed every other node string was a `{var}` template — so a declared expression + property outside that hardcoded set was validated by nobody. + + That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen` + descriptor since #3304, typed `xExpression: 'expression'` (bare CEL) and offered + to authors in Studio, but no validator traversed it. An app authored the + predicate in the _other_ dialect — `'{createOpportunity} == true'` — and it passed + `tsc`, `objectstack validate` and registration in silence. Because `required` _is_ + enforced, a field the author had made conditional rendered unconditionally and + blocked Submit on an input the user was never shown: the run paused forever and no + resume was ever issued. + + Now: + + - **`FLOW_NODE_EXPRESSION_PATHS`** (`@objectstack/spec`) is the declared ledger of + expression-bearing node config paths, each recording the dialect it takes. + - **Both validators read it.** A malformed `visibleWhen` is a located, quoted + error at `registerFlow` _and_ at `objectstack validate` — `node 'screen_1' +(screen) screen field visibleWhen at config.fields[1].visibleWhen`. + - **A reconciliation ratchet** derives the expression properties from the live + descriptors and fails CI in both directions: a new `xExpression` property with + no ledger entry, or a stale entry no descriptor declares. It walks every + registered builtin, not just `screen`. + + Dialects are recorded rather than assumed because there are three, and two of them + disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace + `{var}` flow interpolation (`{…}` is correct), and the ADR-0032 §3 double-brace + text template. Only bare-CEL slots are checked — `loop.collection` and + `map.collection` are recorded as `flow-template` and deliberately left alone, + since no validator implements their dialect and checking them under either of the + other two would reject every currently-valid flow. + + `ActionDescriptor.configSchema`'s TSDoc no longer claims `registerFlow()` + validates `config` against it. It never did: `FlowNodeSchema.config` is + `z.record(z.unknown())`, so types, `required`, `enum` and unknown keys are still + unenforced. The doc now states exactly what is checked and what is designer-facing + only, so nothing relies on a guard that does not exist. + +- 507b92a: fix(spec,objectql,rest,runtime): field-validation messages answer in the caller's language, named by the field's label (#3957) + + The write path built every built-in validation message by concatenating the **API + field name** into a **hardcoded English** template. Those strings are what the + Console toast, the CSV-import row report, the CLI and any custom client display + verbatim, so a Chinese-locale user importing a bad row read: + + ``` + 第 1 行:penalty_amount must be ≥ 0 + ``` + + …for a field declared `label: '处罚金额'` with a full `zh-CN` bundle loaded. The + form layer localized the _same_ constraint correctly (the browser's native + `min`), so the language flipped depending on which layer caught the value. + + **Three things changed.** + + 1. **The message is rendered in the caller's locale** from a built-in catalog + (`BUILTIN_VALIDATION_MESSAGES`, `@objectstack/spec/system`) shipping `en`, + `zh-CN`, `ja-JP`, `es-ES` — the same four locales as the platform bundles. + The locale comes from `ExecutionContext.locale`, whose contract already read + "Drives message catalogs"; this is the consumer that makes that true. Both + HTTP entries (REST server, runtime dispatcher) now resolve it from the + request's `Accept-Language` / `?locale` first, falling back to the workspace + `localization.locale` — so a rejection message and the field labels around it + can no longer disagree. + + 2. **The field is named by its label, never the API name**: translation bundle + (`objects..fields..label`) → declared `label` → API name as the last + resort. `FieldValidationError.field` still carries the API name so a form can + focus the right input. + + 3. **The constraint is exposed as data**, so a client can format its own text + instead of parsing the sentence: + `{ field, code, message, label, constraint: { min: 0 } }`. This rides + ADR-0114's existing `constraint` / `value` positions on `FieldErrorSchema` + (`constraint` tightens from `unknown` to `Record`) rather + than adding a parallel payload — `label` is the only new field. The bag + carries `min`/`max`/`minLength`/`maxLength`/`actual`/`allowed`/`type`, and the + message templates interpolate from exactly those keys. + + Covered end-to-end, not only in the validator: single and batch insert, + single-id and multi-row update, ADR-0113's clear-out rejection, the object-level + rule evaluator's own built-in messages (`requiredWhen`, per-option gating, + state-machine fallbacks), and the importer's cell-coercion, required pre-check + and #3956 bound pre-check messages — all of which land in the same row report. + + **What this changes for consumers.** + + - `code` is unchanged (ADR-0114's `FieldErrorCode`) and remains the thing to + match on. Message keys are finer-grained than codes — `invalid_datetime`, + `invalid_option_value`, `required_cleared` are rendering detail and never reach + the wire — so localization never splits the client-facing vocabulary. + - `message` **text changes**: it is localized, and it names the field by label + even in English (`Budget must be ≥ 0`, not `budget must be ≥ 0`). Anything + asserting on the old English string should match `code` (and now + `constraint`) instead. + - An author-written validation-rule `message` is never touched — it is already + in the language its author chose. + - A deployment can override any built-in message with a `translation` item + defining `validation.field.` (e.g. + `validation.field.min_value: '{{label}}不得小于 {{min}} 元'`). + - The importer's reference-failure message no longer names the target object's + API name (`no sys_user matches "…"`): naming internal identifiers is the + defect being fixed, and the column plus the offending value are what an + importer can act on. + +- cd6b9f2: `decisionOutputs` entries may now be declared `required` (objectui#2955). A typed entry `{ key, label?, type?, multiple?, required?: true }` tells the runtime — not just the decision UI — that an approver must supply the value: an **approve** carrying no value, or a blank one (`''`, whitespace, `[]`, an array of blanks), is rejected with `VALIDATION_FAILED` before any write, so the audit row and the request are untouched and the run can never resume past the node with the key missing. + + That gap is what the flag closes. `decisionOutputs` exists so a decision can route the next step (`approvers: [{ type: 'expression', value: 'vars.lead_review.next_reviewers' }]`), but nothing made the approver actually answer: a skipped output resumed the run with the key absent, and the next node either faulted with `EXPRESSION_FAILED` or resolved an empty slate and stalled on `onEmptyApprovers: 'admin_rescue'` — long after the one person who could have filled it in had moved on. `onEmptyApprovers` was the only backstop, and it is a recovery mechanism, not a contract. + + **Reject never requires them.** The run leaves down the `reject` edge, where nothing reads the outputs — demanding routing data to say "no" would trap the rejection. Outputs still ride a reject when the approver filled them in. + + **No elevation bypass.** A one-click email action link and an `auto_approve` SLA escalation both fail the same way rather than advancing into a node that would resolve nobody; the escalation sweep already isolates a throwing request, so that decision stays pending and visibly overdue instead of silently breaking the run downstream. Enforcement is per decision, so on a `unanimous` / `quorum` node every approver supplies the required outputs and the finalizing decision's values are what the flow resumes with. + + `required` rides `normalizeDecisionOutputs`, so it reaches clients on `decision_output_defs` — a decision UI marks the field required and blocks locally instead of round-tripping to a 400. The console side ships in objectui#2955. + +- b3a3d83: feat(spec): a shared temporal conformance matrix, and the `$between` gap it found (ADR-0053 D-A3, #4081) + + `@objectstack/spec/data` gains `TEMPORAL_ROWS` and `TEMPORAL_CASES` — the + single set of temporal filter cases every backend is checked against, the twin + of the existing `FILTER_LOGIC_CASES`. Five backends consume it and assert **row + results**: `driver-sql` (and, through the live-dialect CI job, real Postgres and + MySQL), `driver-memory`, `driver-mongodb` (real MongoDB), the analytics preview + evaluator, and `formula`'s RLS write-side `check`. + + This is the regression backstop ADR-0053 D-A3 has asked for since 2026-06 and + the last of its decisions to be actioned. Four separate incidents — #3650, + #3773, #3777, #4047 — were each found by a human by accident, and each left a + suite proving only its own issue against its own fixture. Nothing held the + backends to one standard, so the fifth divergence had nowhere to fail. + + **`service-analytics` — a real fix the matrix found on its first run.** The + draft-preview evaluator had no `$between` case, so it fell through to its + permissive `default` and matched **every** row: a drafted dashboard carrying a + range filter charted the entire dataset, then changed its numbers at publish — + the exact continuity the preview exists to provide. It now evaluates + `$between`, sharing the upper-bound helper with `$lte` so the whole-day + calendar-day rule (#3777) applies to a range's max as well. + + Also recorded (ADR-0053 D-A3.1): `$gt` with a bare-day comparand on a + `datetime` column cannot agree between typed and type-blind backends, and the + gap is irreducible without field types. It is asserted in the shared matrix on + `date` only, with the `datetime` cell left to the typed drivers' own suites, + rather than papered over. + +- 35accbf: feat(spec): promote the temporal storage hooks onto the IDataDriver contract (ADR-0053 D-A2) + + `temporalFilterValue` and `temporalFilterColumnSql` — the pair that closed + #3912's storage-form drift — were duck-typed: analytics probed + `typeof driver.x === 'function'` against a locally-invented interface, and + nothing at the type level said a driver must implement both or neither. The + lesson of #3912 is precisely that coercing the comparand without normalising + the column reintroduces half the bug, so a driver implementing one hook alone + would silently regress. + + Both are now optional members of `IDataDriver` + (`@objectstack/spec/contracts`), documented as a pair with "absent = identity" + semantics for drivers whose storage form is the wire form (memory, mongo). + `SqlDriver implements IDataDriver`, so its signatures are compile-checked from + here on; analytics derives its driver seam by `Pick`-ing the contract instead + of a local duck type. Runtime `typeof` guards remain — that is the correct way + to consume an optional contract member — but the shape they guard now has one + authoritative definition. + + No runtime behaviour change. ADR-0053 D-A2 is recorded as resolved. + +- 1bd2795: feat(spec,lint): the `ui` vocabularies admit what the renderers implement, and derive instead of restating (objectui#2945) + + Additions-only follow-up to the vocabulary audit + (objectstack-ai/objectui#2901, #2945). Nothing here narrows a vocabulary, so no + already-stored metadata changes meaning — three of the four `ui/` enums that had + drifted from what is actually implemented, plus the fork that drift had made + invisible. + + **`ChartTypeSchema` admits `combo`.** The taxonomy could not name the one chart + family the rest of `chart.zod.ts` is written for: `ChartSeriesSchema.type` + exists to override a series' type — its doc comment literally says _"combo + charts"_ — and `ChartSeriesSchema.yAxis` binds a series to the left or right + axis, which is only meaningful for mixed marks. objectui's renderer draws it + distinctly (mixed bar/line/area on dual axes, per-series type) and had to carry + `combo` in a local fork of this list, whose own comment claimed to mirror it. + + **`WidgetActionTypeSchema` is `ActionType`.** The two disagreed by one member, + `form`, and the disagreement was backwards: a dashboard header or widget action + button dispatches through the same `ActionRunner` that implements `form` — + objectui's `DashboardRenderer` deliberately routes everything except a raw `url` + into it, so a `flow` header action works (#3528). The narrower enum therefore + rejected at validation exactly what the shared dispatcher then executes. + Derived, so the next type the runner implements needs one edit, not two. + + **`ListChartConfigSchema.chartType` is `ChartTypeSchema.extract([...])`.** Same + five members as before — a de-duplication, not a widening. A member renamed in + the taxonomy now fails at build time instead of leaving a second list quietly + disagreeing. + + **`@objectstack/lint`'s chart-family set is derived from the taxonomy.** + `validate-widget-bindings` decides which widgets need a `chartConfig` measure + mapping from a hand-written list of families, and its omissions fail in the + worst direction: an unlisted family reads as _"not a chart"_, so a widget + missing its mapping **passes** validation. `combo` was exactly that case — + verified by pinning the old list back, where a `combo` widget with no + `chartConfig` produced zero findings. The set is now the taxonomy minus an + explicit `MEASURE_EXEMPT_CHART_TYPES` (single-value and tabular families), so a + family added to the spec is covered without editing the rule. + + Guards: `packages/spec/src/ui/vocabulary-derivation.test.ts` asserts both + derivations still hold (a restated list fails silently — it keeps validating, + just not what the other list says), and the lint suite now walks every + multi-series family in the taxonomy rather than a list of its own. + + A third ratchet already existed and did its job: `app-showcase`'s coverage test + requires a gallery widget for every distinctly-renderable `ChartType`, and it + failed the moment `combo` was admitted. The Chart Gallery dashboard now + demonstrates it — a task count as bars on the left axis, an average as a line on + the right, which is the configuration `series[].type` / `series[].yAxis` exist + for. + + `ActionType` deliberately does **not** gain `navigation`, which the audit + suggested. `ActionRunner.executeNavigation` is a strictly weaker + `executeUrl` — no `${param.X}` interpolation, no `apiBase` promotion, no + `openIn` — differing only by a `replace` option, and its one live producer is + the SDUI `element:button` `action` prop, which `ElementButtonPropsSchema` does + not model at all. Promoting the name would add a second spelling of _navigate_ + to a closed authorable vocabulary (members cannot be removed later) without + closing the gap that actually exists. Tracked separately. + + Verified: `@objectstack/spec` **6944 tests / 267 files**, `@objectstack/lint` + **544 tests / 37 files**, both green; `tsc --noEmit` clean on both. + +- 0166bd5: fix(spec,drivers): the view filter vocabulary and the AST vocabulary now agree (#3948) + + `VIEW_FILTER_OPERATORS` (`ui/view.zod.ts`) is what an author may declare on a + `ViewFilterRule`. `VALID_AST_OPERATORS` (`data/filter.zod.ts`) gates + `isFilterAST()`, which decides whether a filter is parsed into a query at all. + They disagreed on **8 of 19** members: `equals`, `not_equals`, `greater_than`, + `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `before`, `after`. + + An author could declare any of them, `ViewFilterRuleSchema` validated them, + `defineStack` accepted them — and then `isFilterAST()` refused the filter, the + protocol passed the array through unconverted, and the driver could not apply it. + Six of the eight were reachable only in theory because ObjectUI's adapter alias + table happened to translate them; the safety of the query path was resting on a + hand-written table in another repository being complete, and for `before`/`after` + it wasn't. + + **`AST_OPERATOR_MAP` is now the single source of truth.** `VALID_AST_OPERATORS` + is derived from its keys rather than restated, so an operator can no longer be + accepted by the gate without also having a lowering — the two were separate + hand-written lists that happened to agree, with nothing enforcing it. The map + gained the eight canonical view spellings plus the squashed/short forms stored + metadata carries (`notequals`, `greaterthanorequal`, `eq`, `gt`, …). + + **New export `canonicalAstOperator(op)`** folds every accepted spelling of one + comparison onto a single infix form. Both drivers now call it instead of growing + private alias lists, which is what let them accept different vocabularies. + `like`/`ilike` are deliberately not folded onto `contains`: driver-sql passes them + to SQL verbatim, so folding would silently wrap the value in `%…%`. + + Widening only — no spelling was removed, so no stored filter stops validating. + A filter that previously produced an error (after #4029) or was silently dropped + (before it) now compiles. `filter-view-operator-parity.test.ts` asserts every + `VIEW_FILTER_OPERATORS` member and every `VIEW_FILTER_OPERATOR_ALIASES` key has a + lowering that is a real `$`-operator rather than the `$${op}` fallback, so the + next operator the view layer gains fails a test instead of a query. + +### Patch Changes + +- 06772eb: docs(spec): state what the action `requiredPermissions` server gate does NOT cover (#3923) + + `ActionSchema.requiredPermissions` documented itself as a dual-surface gate whose + server half is "the source of truth": declare once, get a 403 on the server and a + hidden button in the UI. An app author reasonably read that as "any action I + declare this on is enforced somewhere", and it isn't. + + Server enforcement lives on the PLATFORM ACTION ROUTE — `POST +/api/v1/actions//` and the MCP/AI path — which is where `type: +'script' | 'flow' | 'modal'` actions execute. A `type: 'api'` action pointed at a + self-authored endpoint is fetched by the browser directly; that request never + reaches the platform, so nothing checks the declaration server-side and the + endpoint has to re-check the capability itself. The doc comment, the `describe()` + string (which is what surfaces in generated schema docs and editor tooltips), and + ADR-0066 D4 now say so. + + Behaviour is unchanged — this is the contract being honest about its edges. The + UI half's gaps were separate and are fixed in objectui. + +- c8124e5: fix(driver-sql): give `Field.datetime` one UTC storage form per dialect (#3912, #3942) + + Any window filter on a `Field.datetime` column returned an empty set on SQLite — + a dashboard `dateRange: last_30_days` on `created_date` read 0 while 29 matching + rows existed. + + There was never a storage _convention_, only a description of what better-sqlite3 + happened to do with a bound JS `Date`. Nothing enforced it — `formatInput` + deliberately left `datetime` untouched — so the form was decided by whichever + writer got there first: a JS `Date` landed as INTEGER epoch ms, while a REST/JSON + write (JSON has no `Date` type), a `defaultValue: 'NOW()'` slot, and the + platform's own `created_at` / `updated_at` all landed as ISO **TEXT**. One column + held both forms while the read path coerced comparands to epoch ms purely from + the _declared_ type. On SQLite's type ordering (`INTEGER < TEXT`) a two-sided + window collapsed to zero rows, and a one-sided `>=` matched every TEXT row + regardless of the bound. + + `Field.datetime` now has one canonical instant per dialect, produced by one + function applied on write **and** to every filter comparand, so the two sides of + a comparison cannot disagree about shape: + + - **SQLite** — `YYYY-MM-DDTHH:MM:SS.sssZ` text. Lexicographic order _is_ + chronological order, so range filters and `ORDER BY` read the column directly + and can use an index; `strftime` parses it, so the date-bucket expression needs + no CASE. + - **Postgres** — `timestamptz`, unchanged. The fix here is on the write and + comparand side: a zone-naive write was previously resolved against the + _server's_ timezone (measured 8 hours off on `Asia/Shanghai`), and an + un-anchored `YYYY-MM-DD` comparand meant the server's local midnight, so the + identical query over the identical instant landed a row on a different calendar + day than SQLite did. + - **MySQL** — `DATETIME(3)` instead of `TIMESTAMP`, a connection pinned to UTC on + both the mysql2 and the server layer, and a MySQL-spelled bind carrying the + same UTC wall clock. MySQL accepts neither the `T` separator nor the `Z` suffix + in a datetime literal, so datetime writes over REST had always failed outright; + `TIMESTAMP` additionally truncated milliseconds and could not store an instant + outside 1970..2038. + + Existing rows converge at schema sync. Both migrations are allowed to fail: they + log, mark nothing, and the read paths keep a repair expression, so an un-migrated + column still compares and buckets **correctly** — just unindexed. Neither can + repair instants the old timezone-ambiguous write path recorded wrongly; they + preserve what is on disk. + + Also closes #3928 (datetime `ORDER BY` mis-sorted on mixed storage) by + construction. Rationale is recorded as ADR-0053 addendum D-B1..D-B4. + + The analytics change is additive: a `coerceTemporalFilterColumn` companion to the + existing `coerceTemporalFilterValue` hook, so a raw-SQL strategy can normalise the + column side too. Absent hook → byte-identical SQL. + +- 7cb922e: chore(spec): the empty-state gate now scans platform-object definitions, where #3896 actually happened + + #3945 added a gate requiring any _"empty = permissive"_ statement in the spec to + be classified on purpose. It scanned `packages/spec/src/**/*.zod.ts` — and that + scope had a hole big enough to miss the bug it was built for. + + The sentence that shipped #3896, _"leave empty to share every record"_, was the + `description` of `sys_sharing_rule.criteria_json`, which lives in + **`plugin-sharing`**. The gate could not see its own crime scene. + + **Now scans `**/\*.object.ts`anywhere under`packages/`** — plugins, +`platform-objects`, `metadata-core`, and the `create-objectstack` templates + (a starter file is the highest-leverage thing a model copies from). 214 → 290 + files. + + **It immediately found a real one.** `sys_user_permission_set.organization_id` + declares _"NULL = applies in every org context"_: a user↔permission-set grant with + no org scope applies everywhere. That is deliberate and load-bearing rather than + an oversight — ADR-0095 D3 / ADR-0068 D2 derive the `platform_admin` posture from + an **unscoped** `admin_full_access` grant specifically, and an org-scoped grant of + the same set must not confer it. So the empty state is not merely wider, it is the + distinguishing input to the highest privilege in the system. Registered `open` + with that rationale and both enforcement sites cited, which is the point: the + answer now lives somewhere other than a maintainer's memory. + + Three fixes the new surface forced, each a case of the gate being wrong in a way + that mattered: + + - **Repudiated prose no longer fires.** #3929's own comment on `criteria_json` + reads _Deliberately NOT "leave empty to share everything"_ — the gate flagged the + sentence recording why the gate exists. Negation is now handled for the + imperative form as well as the token form (`deny-all`), and the escape is + deliberately narrow: the negator must be attached to the phrase, not merely + present in the line, because a false negative here is a missed over-share. + - **The owning property is found by nesting, not by a name list.** A field's prose + sits in a nested key, so the first attempt answered `description` for every + platform-object hit; skipping doc slots then answered `required`, the sibling + above it. What separates a field from its own config is indentation, so the + resolver now takes the nearest key at a shallower indent. + - **The property-search window is per-surface.** A platform-object `description:` + can sit 15+ lines below its field name; a `.zod.ts` statement sits beside its + property. Widening globally would let `.zod.ts` narrative be mis-attributed to a + distant property — turning a correct non-failing note into a wrong failure — so + `.object.ts` gets a wider window and the schema surface keeps its tight one. + + Also makes evidence resolution honest: entries are now parsed with the liveness + ledger's own `checkEvidence`, so prose around the paths works, several paths can + be cited, and another repo's path (`objectui: …`) is recorded without being + resolved here. The README already promised evidence resolved "like the ledger's"; + a single raw-path `existsSync` quietly did not. + + 6 new unit tests (32 total). No runtime behaviour changes. + +- 1d22114: chore(spec): classify what a restriction-shaped property's EMPTY value means (#3896 follow-up) + + #3929 fixed one field. `sys_sharing_rule.criteria_json` was optional, and its + absence evaluated to `find(object, { filter: {} })` — every record of the object, + granted to the recipient. The field description said so out loud: _"leave empty + to share every record."_ + + That sentence is the part worth generalising. For a platform whose premise is + that agents author metadata, **a field description is not documentation about the + contract, it is the contract the next author reads** — and omission is the + commonest authoring error a model makes, precisely because it produces no error. + When omission also lands on the widest grant, the likeliest mistake is the most + dangerous outcome, silently. + + Sweeping the spec surface found the same syntactic shape — an optional list or + predicate that "restricts" something — carrying opposite meanings when empty: + + | Property | Empty means | + | ------------------------------- | ----------------------------------------------- | + | `object.apiMethods` | `undefined` = unrestricted, **`[]` = deny-all** | + | `plugin-runtime.allowedSources` | _"empty = all allowed"_ | + | sharing `condition` | nothing is shared (#3929) | + + Nothing marked which was which. A maintainer knows by memory; a model cannot. + + **New gate — `pnpm --filter @objectstack/spec check:empty-state`**, wired into the + existing Spec Liveness Check workflow. It scans `packages/spec/src/**/*.zod.ts` + for statements declaring an empty state to be permissive and requires each to be + classified in `scripts/liveness/empty-state-registry.mts` as `scope` (selects a + range of work — empty = all is fine), `closed` (an access gate whose empty state + denies — the required posture for new gates), `open` (default-open on purpose, + mandatory rationale), or `output` (a computed projection, not authorable). + `closed` / `open` must cite where the posture is enforced, and the path is + resolved against the checkout so a pointer that rots is reported. 20 statements + across 214 schema files are now classified; adding an unclassified one fails CI. + + Detection matches the **statement**, not field names — names would be a guess, + and the liveness README is blunt about where a guessy check ends up ("a + permanently-noisy check is a check nobody reads"). It ignores negated tokens, so + the ⚠ `object.zod.ts` prints to warn that an empty whitelist is DENY-ALL is not + flagged as if it were permissive. Statements that resolve to no property are + narrative and reported as non-failing notes. + + **One behavioural correction.** `DynamicLoadingConfig.allowedSources` — a + supply-chain gate — documented `[]` as admitting every source. It now states the + `apiMethods` three-state: `undefined` = any source, `[]` = **deny-all**, a subset + = exactly those types. The empty ARRAY is closed; only ABSENCE is open. Collapsing + the two is what makes an allow-list _vacuous_, where the value an author reaches + by mistake is also the widest grant. + + The field has **no runtime consumer** — the whole `DynamicLoadingConfig` block + (`requireIntegrity`, `defaultSandbox`, `allowedSources`) is declared and + unenforced, the ADR-0049 false-compliance shape, and is not addressed here. That + is exactly why the wording mattered: an unimplemented property's description is + the specification whoever implements it will build to. It now carries an + `[EXPERIMENTAL — not enforced]` marker so authors are not misled meanwhile. + + Also registers the `sharing-rule-criteria-required` dogfood proof added by #3929, + which was declaring a `@proof:` tag the registry did not know about (unbound, for + the same reason as `showcase-bu-hierarchy-sharing`: sharing rules are authored at + stack level, so there is no governed per-type ledger entry to ratchet). + + No runtime behaviour changes. + +- 9774b78: fix(driver-sql): `Field.time` gets a canonical storage form — `HH:MM:SS[.fff]` wall-clock text on every dialect (#3994) + + `Field.time` repeated the pre-#3912 `Field.datetime` pattern: writes were never + normalised and only reads were repaired, so one SQLite column accumulated bare + time-of-day TEXT, full-timestamp TEXT and INTEGER epoch ms side by side. + `find()` looked right; everything that compared the STORED form was wrong — + measured: a business-hours window filter silently dropped 4 of 7 rows, ORDER BY + sorted 14:30 before 08:00, a full-ISO write failed the statement outright on + both Postgres and MySQL, a bound `Date` stored a process-timezone wall clock on + pg, MySQL's bare `TIME` rounded `…00.500` up to `…01`, and a `NOW()` default + resolved against three different clocks on the three dialects. + + The #3912→#3942→#3954 construction, transplanted (ADR-0053 D-C1..D-C3): + + - One `canonicalTimeOfDay` — `HH:MM:SS`, `.fff` only when non-zero; `Date`/ + epoch/full-timestamp fold to the UTC time-of-day — applied on write + (`formatInput`), to filter comparands (`coerceFilterValue`, and thereby the + `temporalFilterValue` contract hook) and on read (`toTimeOnly`). + - SQLite: legacy columns converge at schema sync (`backfillCanonicalTimes`, + same `IS NOT`-guarded UPDATE, same log-and-swallow policy); until then the + filter paths wrap the column in the repair expression — correct, just + unindexed. `os migrate plan` lists the work as `normalize_time_storage` with + a row count. + - MySQL: new time columns are `TIME(3)`; legacy `TIME(0)` columns widen at + schema sync (`migrateMysqlTimeColumns`, plan kind `widen_time_columns`), + since zero-precision TIME _rounds_ fractional writes. + - `NOW()` defaults read the UTC clock on every dialect (Postgres previously + used the server zone, MySQL the inserting session's zone — and MySQL 8.0 + rejects a plain `CURRENT_TIMESTAMP` default on TIME entirely). + - `distinct()`/`aggregate()` present time columns exactly as `find()` does. + + `HH:MM:SS` writes round-trip byte-identically (the field-zoo `f_time` + contract); a minutes-only `HH:MM` now completes to `HH:MM:00`, and uninterpretable + values still pass through untouched. + +- 01e124d: Graduate `notify`'s nested `source: { object, id }` into the conversion layer (#4045). + + The `notify` executor tolerated a second spelling of its click-through target with + a bare consumer-side fallback: + + ```ts + const object = toStr(interpolate(cfg.sourceObject ?? src?.object, …)); + ``` + + Its own doc comment named `sourceObject`/`sourceId` **canonical** (they mirror the + `sys_notification.source_object`/`source_id` columns), so the nested form was an + alias tolerated by exactly the mechanism Prime Directive #12 calls debt — and the + one alias on this executor that #3796 missed when it moved `to`/`subject`/`body`/ + `url` into `flow-node-notify-config-aliases`. + + It now graduates the same way `filters` → `filter` and `object` → `objectName` + did: the conversion lifts it onto the canonical pair at load — including the + `AutomationEngine.registerFlow` rehydration seam — and the executor's fallback is + deleted, so no consumer-side dialect tolerance survives and the alias is declared, + tested and retirable on schedule (it rides the existing entry's window, retiring + at 18). + + Unlike the four renames this is a **1→2 destructuring**, which the pair mechanism + cannot express, so it is a small custom transform. It mirrors the `??` precedence + exactly: a canonical key already present wins and its nested counterpart is left + shadowed, matching how a shadowed alias is treated elsewhere. `source` is dropped + once at least one part is lifted; a `source` that is not an object, or carries + neither key, is left untouched rather than silently deleted. + + No behaviour change for authors — both spellings keep working, and a + half-specified target is still dropped rather than emitting a dead deep-link. + +- a831df1: chore(liveness): `report.order` is live — objectui now lowers it onto the selection (#3916) + + `ReportSchema.order` shipped as `planned` + `authorWarn`: the framework half was + complete (schema, `reportSelectionOrder`, executor), but objectui's + `DatasetReportRenderer` built the selection it posted and never carried the + declaration into it, so an authored ordering reached no query. Marking it `live` + then would have been the exact failure the gate exists to catch. + + objectui#2964 landed that wiring — `useDatasetRows`, the single fetch choke point + behind every report path, now carries the lowered ordering across all four call + sites (grouped table, embedded chart, matrix cross-tab, each joined block), with + the ordering in the refetch signature and scoped per sub-selection so the + chart's narrower x/y query cannot post a key it never selected. + + So the ledger entry flips to `live`, gains the framework evidence paths, and + drops `authorWarn` / `authorHint` — an authored `order` now does what it says, + and the advisory that it did not is no longer true. + + No behaviour change in this repo; the ledger is the deliverable. + +- f752ee3: feat(analytics): order the time axis by default, and give reports a sort declaration (#3916) + + A matrix report with a date dimension across rendered its columns in arbitrary + order — `2026-07-01, 2026-07-05, …, 2026-07-02`. Declaring `dateGranularity` on + the dataset dimension made the bucket keys _sortable_ (`2026-07`, `2026-Q3`) + without making anything _sort_ them, and the report author had no way to ask: + `DatasetSelection.order` existed on the wire, but `ReportSchema` had no ordering + field at all (dashboard widgets had their own `options.sortBy` channel; reports + did not). Nothing in the chain supplied an order either — `resolveOrdering` + returned `undefined` unless the selection carried one explicitly, the ObjectQL + aggregate path has no ordering grammar so its buckets came back in Map-insertion + order, and the pivot builds its column headers in row-arrival order. + + - **A selected time dimension is now chronological by default.** When a + selection states no `order` (and no `limit`, whose own fallback already + ordered by every dimension), each selected dimension the cube types as `time` + defaults to ASCENDING, in selection order. Bucket keys are minted sort-stable + precisely so this works — `2026-07` sorts after `2026-06`, `2026-Q3` after + `2026-Q1`. This lands on both strategy paths: a real `ORDER BY` where native + SQL serves the query, and the executor's post-pass where a date-bucketed query + is handed to the ObjectQL path. Null / empty buckets stay last, as everywhere + else. Deliberately narrow: only time dimensions get a default, so grids with + nothing wrong with them are not reordered. + - **Reports can declare an ordering.** `ReportSchema.order` (and + `blocks[].order` for a `joined` report) is a list of `{ by, direction }` sort + keys, most significant first — an array, not a `Record`, because key order is + the contract and JSON object key order should not have to be. `by` must name a + dimension the report groups by (`rows` / `columns`) or a measure it displays + (`values`); anything else fails at authoring time rather than becoming an + ordering that silently does nothing. Duplicate keys are rejected. A `joined` + report orders per block — declaring `order` on the container is an error. + `reportSelectionOrder()` lowers the list into the `DatasetSelection.order` a + renderer posts, and returns `undefined` for an empty list so the runtime's own + defaults still apply. + + An explicit `order` still wins outright — the chronological default is a + default, not a policy, so "newest month first" is one declaration away. + + `report.order` ships as `planned` + `authorWarn` in the liveness ledger: the + framework half is complete and live (schema, lowering helper, executor), but + objectui's `DatasetReportRenderer` does not yet carry `report.order` into the + selection it posts. The default time-axis ordering needs no renderer change and + is live now. + +- 94a0bbc: fix(security)!: a disabled RLS policy no longer grants — found by re-verifying the ledger's security subset (#3896 follow-up) + + **The fix.** `RowLevelSecurityPolicySchema.enabled` promises, verbatim: _"Disabled + policies are not evaluated."_ Nothing read it — not the collection site, not the + projection round-trip, not the compiler. Because applicable policies OR-combine + (any match allows access), a policy an admin switched off **kept contributing its + grant**: disabling a too-permissive policy silently changed nothing. That is the + #3896 shape — a documented security control whose real behaviour is wider than + its contract — one layer up, on RLS instead of sharing rules. + + `getApplicablePolicies` now excludes `enabled === false` before any matching, at + the single choke point both the find path and the analytics path flow through — + the same place, and the same ADR-0049 enforce-or-remove resolution, as the + formerly-unenforced `positions` domain. Exact `=== false` on purpose: the schema + defaults `enabled` to true and projection rows may omit the key, so absent stays + active. Four tests pin both directions. Access-narrowing only: no policy grants + MORE after this change, and nothing in-repo authors `enabled: false`. + + **The audit that found it.** All 44 entries of the liveness ledger's security + subset (`permission` 33, `position` 4, `object` sharing/access 7) were + call-graph-closed by hand and stamped `verifiedAt: 2026-07-30` — the subset's + first-ever re-verification (previously 4 dated entries repo-wide, and the last + sweep that cited preview renderers went 10-for-13 wrong). Beyond `enabled`: + + - `rowLevelSecurity.priority` → **dead + authorWarn**. Not merely unimplemented: + policies OR-combine (the schema's own describe says most-permissive-wins), so + the promised "conflict resolution" semantics cannot exist. A REMOVE candidate + per the #3715/#3950 precedent while the v17 breaking window is open. + - `rowLevelSecurity.label` / `description` / `tags` → dead (benign display — + no consumer in either repo; deliberately not authorWarn'd). + - `tabPermissions` was UNDERSTATED: the note said only `'hidden'` is read, but + hono's rank merge reads all four visibility values across resolved sets, and + the `me-apps-and-everyone-baseline` dogfood test exercises it. Evidence + upgraded; noted as a proof-binding candidate. + - `allowExport` re-verified TRUE against the suspicion that it was + projection-only: the export route carries its own caller-level 403 gate + (`enforceExportPermission`), fail-closed when the security service cannot + answer, separate from the object-level 405. + - `allowTransfer/Restore/Purge` notes re-confirmed accurate (M2 operations still + unshipped; the RBAC gates are pre-mapped fail-closed). + - `object.ownership` evidence had rotted (line drift) — refreshed; six other + object-level security entries re-cited and stamped. + + No other runtime behaviour changes. + +- 627b188: fix(seed-loader): count reference fields dropped from rows that were still written + + The loader had two failure outcomes and only counted one. A record it cannot + write is counted in `errored`. But an unusable **reference value** (an object + where a natural key belongs, an array on a single-value field) is removed from + the record — never written as NULL, which would sever an existing link on + upsert replay — and the row is written **without it**. Nothing counted that. + + So a load that quietly severed N associations reported `totalErrored: 0`, and + every count-driven surface read clean. The CLI boot banner — the one seed signal + that survives `os dev`'s boot-quiet window and the default `warn` level — printed + `showcase 42 rows`, and the warn line said `0 dropped record(s)`: true, and + useless ([#3932](https://github.com/objectstack-ai/objectstack/issues/3932)). + + `SeedLoadResult.referencesDropped` and `SeedLoaderSummary.totalReferencesDropped` + now count it. It is deliberately **not** folded into `errored` — the row _was_ + written, so that would break the `inserted + updated + skipped` reconciliation + against `total`. The banner names it separately: + + ``` + ⚠ Seeds: showcase 42 ok / 3 lost links ⚠ + ``` + + Both counters are additive with a `0` default, so an existing producer or + consumer of `SeedLoaderResult` is unaffected. + +- 8d4eae7: fix(seed-loader): resolve natural-key ARRAYS for multi-value lookups + + A `multiple: true` lookup / `user` field stores an array of ids, so its seed + value is an array of natural keys (`authors: ['Alice', 'Bob']`). Reference + resolution only ever accepted a single string: the array tripped the + "expected a natural-key string but got an object. Pass the target's `name` + value as a plain string" guard — impossible advice for a field that holds + several references — and was then DROPPED from the record. The row landed with + the whole association missing and only a warn in the log + ([#3911](https://github.com/objectstack-ai/objectstack/issues/3911)). + + Every element now resolves independently (in-load records first, then the + database, then pass 2), and the field lands as an array of target ids. A lone + string is accepted as one-element shorthand for the array shape the field + stores. Deferral is all-or-nothing per field — a partially-resolved array is a + corrupt association, so pass 2 re-resolves the whole authored array — and a key + that never materializes is a reported load error naming that element, not a + silent drop. + + An array passed to a genuinely **single-value** reference field is still + rejected, now with advice an author can act on: declare the field + `multiple: true`, or pass one natural key. + + `ReferenceResolution` (`@objectstack/spec/data`) gains an optional `multiple` + flag carrying the field's array-ness into resolution; it is additive and + defaulted-absent, so existing dependency graphs are unaffected. + + **Authoring types.** `defineSeed`'s per-field value type now widens a + `multiple: true` lookup to `string | string[] | null` (a lone string stays legal + — the loader accepts it as one-element shorthand). `master_detail` is inherently + single and is not widened, and an array on a single-value lookup is still a + compile error. To make that reachable, `Field.lookup` became generic over its + config (``) so `multiple: true` survives as a + literal instead of widening to `boolean`; the return type is intersected with + `FieldInput` so its optional surface is unchanged. Type-level only — the + returned object is byte-identical at runtime. + +- ccd9397: fix(security)!: a sharing rule with no criteria now shares NOTHING instead of every record (#3896) + + `SharingRuleSchema` has always required `condition`, and its doc is explicit + that a predicate the compiler cannot lower is _"skipped and logged — never + seeded as a permissive match-all (ADR-0049)"_. The declared/seed path honoured + that. The two other ways to create a rule did not: + + - **`POST {basePath}/sharing/rules`** plucks its body field-by-field into + `SharingRuleService.defineRule`, which validated `name` / `label` / `object` / + `recipientType` / `recipientId` — and not `criteria`. A missing, `null`, or + **misspelled** key (`criterias`) was stored as `criteria_json: null`, answered + `201` with no warning, and evaluated as + `find(object, { filter: {}, context: SYSTEM_CTX })`: every record of the + object, up to 5000, granted to the recipient. Triggering it took a typo, not + an attacker. + - **Authoring a rule in Setup** is a direct `sys_sharing_rule` insert, which + never reaches `defineRule` at all. + + Empty criteria is now rejected everywhere a rule can be written, and — because + rules created before this gate are already in the table — the evaluator refuses + to act on one regardless of how it got there. + + - **`defineRule` rejects a match-all criteria** with + `VALIDATION_FAILED: criteria is required …`, alongside its other required + fields. Covers the REST endpoint, programmatic callers, and the seeder. + Rejected shapes: missing / `null` / `''` / `{}` / `[]` / `{ $and: [] }` / + unparsable JSON (e.g. a CEL source typed into the Criteria box). + - **The evaluator matches nothing** for such a rule and logs why, so a row + stored before this release under-shares instead of over-sharing: the next + reconcile _revokes_ the grants it had materialised. Both evaluation paths are + covered — the bulk `evaluateRule` and the per-record write-hook path. + - **`bindRuleCriteriaGuard`** fails `sys_sharing_rule` inserts with no + criteria as a field-level `VALIDATION_FAILED` (a 400 naming `criteria_json`), + so the Setup path reports the problem instead of saving an inert rule + (ADR-0078). Updates are checked only when the patch supplies + `criteria_json` — switching an over-broad legacy rule off must not require + inventing a criteria for it first. + - **The seed bootstrap's "empty condition = match-all" branch is gone**: a + missing or empty `condition` is now skipped and logged like any other + non-lowerable one. + - `POST {basePath}/sharing/rules` also accepts `criteria_json` as an alias for + `criteria`, matching the snake_case aliases the endpoint already takes for + `object_name` / `recipient_type` / `access_level`. + + **Migration.** There is no "share every record" sharing rule, and there never + usefully was one — the shape existed only as a failure mode. A rule that + relied on it must state its predicate (`criteria: { stage: 'won' }`), or, if + the object really should be readable by everyone, use the object's + organization-wide default (`sharingModel`) instead. Rules already stored with + a null `criteria_json` need no data migration: they stop granting on the next + evaluation and their existing grants are revoked. + +- 0a2f233: fix(spec): regenerate the ADR-0087 change manifest the RC version bump left stale + + The v17 RC bump moved `PROTOCOL_VERSION` to `17.0.0` but did not re-run + `gen:spec-changes` / `gen:upgrade-guide`, so the published `spec-changes.json` + still declared `protocolVersion: 16.0.0` with no protocol-17 entries — while the + protocol-17 conversions (`execute`→`target`, `conditionalRequired`→`requiredWhen`, + `knowledge.topics`→`sources`, `agent.tools` removal, sharing `full`→`edit`) were + already registered and applied at load. Anything projecting the manifest (the + generated upgrade guide, the `spec_changes` MCP tool) reported a 16-era chain on + a 17 protocol. + + `check:spec-changes` caught it, but only on the first PR to touch a + generated-artifacts path after the bump — this regenerates both projections and + turns the gate green again. `docs/protocol-upgrade-guide.md` now carries the + Protocol 16 → 17 section. + ## 17.0.0-rc.0 ### Major Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index 01c4507899..cdc3627d0f 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index b68cf7b2ff..3537e24d55 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/trigger-api +## 17.0.0-rc.1 + +### Minor Changes + +- f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) + + Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed + the set; this batch moves the emitters that still spoke lowercase `snake_case` + onto it. + + **Wire-visible change.** Error codes on these surfaces change spelling. Generic + conditions collapse onto the standard catalog rather than keeping a synonym: + `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` → + `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` → + `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` → + `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes + registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`, + `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept: + `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`, + `service-messaging`, `service-automation`, `trigger-api`. + + Branch on `error.code` values rather than pattern-matching their case: the + console's fix for the same rename (objectui#2977) reads codes case-insensitively + for exactly this reason, and that is the pattern to copy in your own consumers if + you support servers on both sides of the change. + + **Four routes stop putting a code in the message slot.** The webhook redeliver + route, the API-trigger webhook, and two `rest` routes answered + `{ success: false, error: '', message }` — the code occupying `error`, the + declared object envelope nowhere. They now emit `error: { code, message }`, and + three API-trigger branches gained a message they never had. Clients reading + `body.error` as a string on those routes must read `body.error.code`. + + **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a): + `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and + two `RetryStrategy` types. The connector-side pair is renamed; importers of the + `integration` subpath update the name. Side effect: the api-side `ErrorCategory` + and `RetryStrategy` now appear in the generated API reference at all — the name + collision had been silently dropping them. + + **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client + registration route put better-auth's arbitrary `body.error` string straight into + `error.code`. The code is now ours and the upstream discriminator moved to + `details.upstreamError`. + + **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values + (ADR-0112 D6b): it is persisted audit history, and the same column holds + non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a + 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's + `--json` output contract. + + A `check:error-code-casing` CI guard now fails on a new lowercase literal in a + code position, since the ledger's casing rule can only police codes that someone + registers. + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 705c5fd3dc..b07a0777c1 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 269fae377b..1aa239e350 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/plugin-trigger-record-change +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index cbe317c688..8539dcb88d 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-record-change", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 36d45394df..45f00e6191 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/plugin-trigger-schedule +## 17.0.0-rc.1 + +### Patch Changes + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 122b7c2606..3952d121a4 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack — auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "main": "dist/index.js", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 4ae5411c08..b2ccafb7bc 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/types +## 17.0.0-rc.1 + +### Patch Changes + +- c20b875: **Correct the stale premise left behind by #4012: the degraded-boot stderr copy + survives the operator's LOG LEVEL, not `os serve`'s boot-quiet window.** + + `emitDegradedBootBanner` writes the `OS_ALLOW_DRIVER_CONNECT_FAILURE` banner to + stderr in addition to `logger.warn`, and every comment and test name explaining + why cited the same reason: `os serve` swallowed all of stdout while the kernel + booted, and `Logger` routes `warn` to stdout. #4012 fixed that — the boot window + now buffers and replays `warn`-and-above — which retires the _stated_ + justification for a duplicate that is nonetheless still load-bearing: + + `Logger.write()` returns before touching a stream when the record is below + `config.level`, so at `--log-level error`, `fatal` or `silent` the banner's + `logger.warn` reaches **no** stream at all. A production host at `error` is + exactly the deployment this escape hatch exists for, and exactly where a + logger-only banner would vanish. Removing the stderr copy on the strength of + #4012 would therefore have been a regression — so this documents the reason that + is still true, in the places someone would read before deleting it: + `degraded-boot.ts`, the engine's emit site, and all three parity tests + (objectql, runtime, service-datasource), which are renamed off "which `os serve` + boot-quiet cannot swallow" to "which the operator log level cannot filter away". + + The objectql parity test now proves the claim instead of asserting around it: it + drives a **real** `ObjectLogger` at `level: 'error'` and requires the banner on + stderr _and_ nothing on stdout. Set the level to `warn` and it fails — so the + test is pinned to the level filter rather than passing for any reason. + + Also corrected in the same sweep, all comment-only, all previously overstating + what #4012 had not yet fixed: + + - the automation wiring summary (`format.ts`, `serve.ts`, its test) claimed the + boot window swallowed the engine's binding warnings. Its real justification is + stronger and unchanged: a flow that silently fails to arm emits **no** log line + at any level, so binding state has to be read off the live engine — absence of + a warning was never evidence of a bound flow. + - the seed summary (`seed-summary.ts`, `format.ts`, its test) and `AppPlugin`'s + seed-outcome note attributed the silence to the boot window; the operative + gate is that `SeedLoader`'s result logs are `info`, under the default `warn`. + + No behavior changes. + +- 39eb01b: fix(runtime,cli,types): `os migrate` and the dev runtime now share one `__search` companion schema view (#3955) + + On a zh-locale deployment the dev runtime provisions the hidden `__search` + pinyin companion column (ADR-0098) on every eligible object, but the + `os migrate plan`/`apply` boot went through `createStandaloneStack`, which + never derived the locale-gated pinyin decision from the compiled artifact. + Its metadata therefore lacked every companion column, and `migrate plan` + reported each live `__search` column of a dev-created database as a + destructive orphan — with `--allow-destructive` as the printed remediation, + which would have dropped live feature columns. + + - `@objectstack/types`: new `collectConfiguredLocales(i18n)` and + `stampSearchPinyinEnabled(i18n)` — the single resolve-and-stamp helper for + `OS_SEARCH_PINYIN_ENABLED`. An explicit env value still wins; only a + positive locale-derived decision is stamped. + - `@objectstack/runtime`: `createStandaloneStack` stamps the decision from + the artifact's `i18n` before any plugin constructs a `SchemaRegistry`, and + surfaces `i18n` on its result like `requires`/`objects`/`manifest`. + - `@objectstack/cli`: the `serve`/`dev` boot now stamps through the same + shared helper (behaviour unchanged), so create/serve and plan/apply cannot + compute different schema views of the same source tree. + + A fresh CLI-created database is now also born with the same `__search` + columns the dev runtime would provision, instead of acquiring them on the + next dev boot. + +- Updated dependencies [06772eb] +- Updated dependencies [1ea6bce] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [9b6fe7c] +- Updated dependencies [789ad63] +- Updated dependencies [2af1988] +- Updated dependencies [12a19a8] +- Updated dependencies [c8124e5] +- Updated dependencies [03d26f7] +- Updated dependencies [3c628ce] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [507b92a] +- Updated dependencies [01e124d] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [ec796d5] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [0166bd5] + - @objectstack/spec@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index eaf148b248..fe2a8872d5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index ab536a1377..18fccd31cb 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,151 @@ # @objectstack/verify +## 17.0.0-rc.1 + +### Patch Changes + +- c3bcb42: feat(runtime,datasource): the default-datasource connect seam accepts a host driver factory — adopt pre-built instances without forking the verdict (#3826) + + ADR-0062 D1's open-core convergence (#3869/#3886) left one structural question + open: a host whose `default` needs a driver the shared factory cannot build — + the cloud distribution's `turso`, or an instance pooled BEYOND one kernel (the + cloud control-plane driver doubles as the proxy base of every environment + kernel; per-environment drivers are cached across kernel rebuilds) — had only + two options, both bad: stay on the legacy pre-built `DriverPlugin` path, whose + connect verdict lives in `ObjectQLEngine.init()` (the second implementation + #3826 exists to retire), or fork the connect orchestration. Either re-opens the + #3741 → #3758 drift this whole line of work is about. + + Two additive pieces close it: + + - **`DefaultDatasourcePlugin` accepts an injected `IDatasourceDriverFactory`** + (defaults to the shared open-core factory, byte-for-byte unchanged when + omitted). The factory only changes what `create()` returns — the policy-free + init connect, `bootCritical` fail-fast, `OS_ALLOW_DRIVER_CONNECT_FAILURE` + escape hatch, and the start() replay into retained admin state are identical + either way, and the new tests pin that (an adopted instance that cannot + connect takes the exact same verdict). + - **`createPrebuiltDriverFactory(driver, { driverId?, fallback? })`** in + `@objectstack/service-datasource` — the "adopt an existing driver" seam the + first #3826 pass found missing, landed AS a factory so it composes into the + one connect path instead of becoming a second entry point. `create()` returns + the SAME instance every call: construction, pooling, and reuse stay host + concerns; only the verdict converges. Not for the common case — a `default` + expressible as `{ driver, config }` should stay a plain definition. + + The `@objectstack/verify` dogfood harness now boots through + `DefaultDatasourcePlugin` (declared `sqlite-wasm` definition) instead of a + pre-built `DriverPlugin` — so the dogfood gate exercises the same declared + -default connect path `objectstack dev`/`serve` use, which is the §Risk + mitigation ADR-0062 promised ("behind the dogfood gate") and did not yet have. + The degraded-boot parity guard stays: `ObjectQLEngine.init()`'s verdict is + still live for the boot re-verification, `DriverPlugin` escape-hatch drivers, + and the cloud compositions until they converge onto this seam. + +- Updated dependencies [bc35e00] +- Updated dependencies [6e141bc] +- Updated dependencies [48fcf70] +- Updated dependencies [06772eb] +- Updated dependencies [0c90ece] +- Updated dependencies [195ad76] +- Updated dependencies [c2bbd97] +- Updated dependencies [ffb003c] +- Updated dependencies [1ea6bce] +- Updated dependencies [e5e8b10] +- Updated dependencies [c1dcacd] +- Updated dependencies [ad303ed] +- Updated dependencies [32ccb23] +- Updated dependencies [f5a4ef0] +- Updated dependencies [2d3e255] +- Updated dependencies [7d7521f] +- Updated dependencies [5dc4d02] +- Updated dependencies [0f12193] +- Updated dependencies [9b6fe7c] +- Updated dependencies [3abd233] +- Updated dependencies [ea24593] +- Updated dependencies [789ad63] +- Updated dependencies [fccec22] +- Updated dependencies [2af1988] +- Updated dependencies [0af50a3] +- Updated dependencies [12a19a8] +- Updated dependencies [7df7c64] +- Updated dependencies [fae74b5] +- Updated dependencies [a225ef5] +- Updated dependencies [c9d254a] +- Updated dependencies [c8124e5] +- Updated dependencies [c3bcb42] +- Updated dependencies [c20b875] +- Updated dependencies [f4d7f1d] +- Updated dependencies [4f30943] +- Updated dependencies [03d26f7] +- Updated dependencies [bb192c4] +- Updated dependencies [98e7cc7] +- Updated dependencies [4cf7c61] +- Updated dependencies [3c628ce] +- Updated dependencies [347f460] +- Updated dependencies [7cb922e] +- Updated dependencies [1d22114] +- Updated dependencies [dc530b4] +- Updated dependencies [9774b78] +- Updated dependencies [385c4b0] +- Updated dependencies [a47ac06] +- Updated dependencies [e4c61a7] +- Updated dependencies [45dc446] +- Updated dependencies [43ff598] +- Updated dependencies [839982e] +- Updated dependencies [71af9f5] +- Updated dependencies [507b92a] +- Updated dependencies [99b4392] +- Updated dependencies [974c6d4] +- Updated dependencies [495019b] +- Updated dependencies [33a5ff4] +- Updated dependencies [39eb01b] +- Updated dependencies [01e124d] +- Updated dependencies [be7945a] +- Updated dependencies [62f8017] +- Updated dependencies [a831df1] +- Updated dependencies [f752ee3] +- Updated dependencies [cd6b9f2] +- Updated dependencies [3ba8d77] +- Updated dependencies [ec796d5] +- Updated dependencies [77fadbf] +- Updated dependencies [a3cb9c8] +- Updated dependencies [4be9d99] +- Updated dependencies [3ca34c1] +- Updated dependencies [94a0bbc] +- Updated dependencies [d6bfb3d] +- Updated dependencies [0931185] +- Updated dependencies [1d5dc46] +- Updated dependencies [627b188] +- Updated dependencies [8d4eae7] +- Updated dependencies [857a6cf] +- Updated dependencies [4580597] +- Updated dependencies [de6daa5] +- Updated dependencies [ccd9397] +- Updated dependencies [0a2f233] +- Updated dependencies [b3a3d83] +- Updated dependencies [35accbf] +- Updated dependencies [eb95d97] +- Updated dependencies [1bd2795] +- Updated dependencies [4d7bebf] +- Updated dependencies [821ac7a] +- Updated dependencies [8f81731] +- Updated dependencies [4965bfa] +- Updated dependencies [0166bd5] + - @objectstack/runtime@17.0.0-rc.1 + - @objectstack/objectql@17.0.0-rc.1 + - @objectstack/spec@17.0.0-rc.1 + - @objectstack/plugin-sharing@17.0.0-rc.1 + - @objectstack/plugin-security@17.0.0-rc.1 + - @objectstack/rest@17.0.0-rc.1 + - @objectstack/core@17.0.0-rc.1 + - @objectstack/plugin-auth@17.0.0-rc.1 + - @objectstack/service-automation@17.0.0-rc.1 + - @objectstack/service-analytics@17.0.0-rc.1 + - @objectstack/service-datasource@17.0.0-rc.1 + - @objectstack/plugin-hono-server@17.0.0-rc.1 + - @objectstack/service-settings@17.0.0-rc.1 + ## 17.0.0-rc.0 ### Minor Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 5effc7e6b4..aa8306fae4 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module", diff --git a/packages/vscode-objectstack/CHANGELOG.md b/packages/vscode-objectstack/CHANGELOG.md index 99f22fb719..8b1006fc53 100644 --- a/packages/vscode-objectstack/CHANGELOG.md +++ b/packages/vscode-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # objectstack-vscode +## 17.0.0-rc.1 + ## 17.0.0-rc.0 ## 16.1.0 diff --git a/packages/vscode-objectstack/package.json b/packages/vscode-objectstack/package.json index 6eb35662e7..5990ce95fe 100644 --- a/packages/vscode-objectstack/package.json +++ b/packages/vscode-objectstack/package.json @@ -2,7 +2,7 @@ "name": "objectstack-vscode", "displayName": "ObjectStack", "description": "ObjectStack Protocol — Autocomplete, validation, and inline diagnostics for .object.ts, .view.ts, and objectstack.config.ts files", - "version": "17.0.0-rc.0", + "version": "17.0.0-rc.1", "publisher": "objectstack", "license": "Apache-2.0", "repository": {