diff --git a/.changeset/pre.json b/.changeset/pre.json index 1e94c0e538..586cd57ae1 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -80,5 +80,229 @@ "@objectstack/verify": "16.1.0", "objectstack-vscode": "16.1.0" }, - "changesets": [] + "changesets": [ + "action-execute-target-precedence", + "action-param-inline-lookup-reference", + "action-param-strict-unknown-keys", + "adr-0104-d1-media-strict-per-deployment", + "adr-0104-d1-value-shape-contract", + "adr-0104-d2-typed-action-handlers", + "adr-0104-d3-wave1-file-value-shape", + "adr-0104-d3w2-pr1-upload-complete-fileid", + "adr-0104-d3w2-pr2-file-read-resolution", + "adr-0104-d3w2-pr3-file-ownership", + "adr-0104-d3w2-pr4-governed-download", + "adr-0104-d3w2-pr5a-write-cutover", + "adr-0104-d3w2-pr6-backfill", + "adr-0104-deployment-migration-gate", + "adr-0104-design-doc-only", + "adr-0105-d8-delegated-admin-org-role", + "adr-0105-d8-issuance-resolves-issuer-grants", + "adr-0105-group-posture-entitlement", + "adr-0105-group-tenancy-phase-0-1", + "adr-0105-group-tenancy-posture", + "adr-0106-metadata-fls-masking", + "agent-knowledge-alias-and-experimental-markers", + "ai-wildcard-to-zero", + "analytics-client-dispatcher-alignment", + "analytics-effective-granularity", + "analytics-execute-aggregate-execution-context", + "analytics-label-read-scope", + "analytics-objectql-read-scope", + "analytics-order-by-display-label", + "analytics-read-scope-bridge-order", + "analytics-widget-query-options", + "api-exposure-failopen-observability", + "api-methods-derivation-contract", + "apimethod-enum-shrink", + "apimethods-batch-conformance-ratchet", + "app-metadata-reference-integrity-assessment", + "app-org-roles-storable", + "approval-action-hierarchy", + "approval-actions-translation-bundle", + "approval-approver-value-bindings", + "approval-attachment-descriptors", + "approval-dead-run-ordering-invariant", + "approval-dead-run-record-lock", + "approval-empty-position-admin-override", + "approval-lock-schedule-run-provenance", + "approval-participant-visibility", + "approval-pending-approver-groups", + "approvals-payload-labels", + "approver-live-record-3447", + "array-form-triggertype-not-silent", + "auth-org-roles-self-derived", + "auth-route-ledger", + "auth-validationerror-4xx-mapping", + "authz-ledger-flow-runas", + "automation-client-resume-screen-flow", + "better-auth-1-7-0-rc-2-and-prod-dep-batch", + "better-auth-team-member-count", + "chartconfig-trim-zoom-clickaction", + "ci-cache-tier1-optimizations", + "ci-performance-optimization", + "client-actions-surface", + "client-keys-sharelinks-security", + "client-meta-automation-descriptors", + "client-packages-lifecycle", + "client-url-conformance-capstone", + "close-approvals-and-record-shares-gaps", + "close-sharing-rules-explain-search-gaps", + "close-the-eight-reports-rest-gaps", + "close-the-final-nine-rest-gaps", + "close-the-nine-metadata-rest-gaps", + "console-09c6a177bb4a", + "console-2cb8d78e24ad", + "control-plane-guard-crossref", + "decision-outputs-surface-3447", + "delegable-scope-read-surface", + "deprecate-kernel-assignment-notifications", + "docs-audience-first-ia", + "dogfood-gate-cancelled-not-failure", + "dogfood-shared-boot", + "drop-dead-env-template-flag", + "drop-dead-list-templates", + "dropped-fields-bulk-graphql-client", + "enforce-user-level-export-axis", + "export-axis-opt-in", + "export-empty-result-header", + "expression-approvers-3447-p2", + "field-conditional-required-fold", + "field-readonly-doc-preserveaudit", + "file-access-delegate", + "filter-context-tokens-gate", + "fix-stale-scaffolder-changeset-refs", + "fix-unmounted-local-file-url", + "flow-error-object-serialization", + "flow-lookup-expand", + "flow-template-lint-and-hydrate-guards", + "flow-trigger-unknown-event-lint", + "formview-buttons-defaults-live", + "govern-report-dashboard-liveness", + "govern-sys-member-writes", + "govern-webhook-liveness", + "group-union-driver-scope", + "historical-import-audit-docs", + "i18n-bundle-drift-sweep", + "i18n-coverage-ratchet", + "i18n-extract-check-flag", + "i18n-gate-declared-labels", + "i18n-sso-scim-userposition-importjob-coverage", + "ihttpserver-contract-codify", + "import-historical-audit", + "import-historical-fsm", + "import-sanitize-row-errors", + "import-undo-preserveaudit", + "index-drift-migrate-plan", + "invitation-accepted-host-seam", + "isLikelyEmail-no-control-char", + "job-retry-timeout-3494", + "lazy-deps-dist-probe-timeout", + "lint-flag-record-change-trap", + "list-column-prefix-summary-object", + "liveness-ledger-ai-scope-honesty", + "liveness-ten-preview-claims", + "manifest-bridge-arm-on-project-kernels", + "marketplace-objects-bridge-metadata-service", + "marketplace-rehydrate-seed-heal", + "metadata-unresolvable-posture-fail-closed", + "mongodb-single-tenant-boot-guard", + "naming-drift-recheck", + "nav-access-lint", + "notifications-redos-fix", + "objectchart-aggregate-result-columns", + "objectchart-contract-back-to-spec-shape", + "objectql-crossobj-capability", + "objectql-crossobj-fail-closed", + "objectql-driver-connect-failfast", + "objectql-strategy-daterange", + "osv-batch-2026-07-dep-bumps", + "page-field-and-chart-binding-lint", + "page-header-i18n-3589", + "plugin-page-i18n-drift-guard", + "preserveaudit-test-and-docs", + "previous-null-on-create-leg", + "prune-aspirational-config-3494", + "prune-dead-audit-config-cluster", + "prune-dead-capabilities-descriptor", + "prune-field-prune-orphan-schemas", + "prune-orphan-featureflag-schema", + "prune-portal-schema-3464", + "prune-report-aria-performance", + "prune-report-column-grouping-schemas", + "prune-skill-permissions", + "purge-webhook-delivery-i18n-and-bundle-ownership-guards", + "rbac-objects-bulk-primitive", + "readme-fde-audience", + "readonly-flow-write-json-warning", + "readonly-flow-write-lint", + "ready-probe-driver-health", + "reconcile-packages-post-and-ui-view-dialect", + "record-after-write-trigger", + "record-change-hydrate-formula-fields", + "reference-integrity-object-and-action-names", + "regenerate-ui-action-reference-doc", + "reject-body-on-non-script-action", + "release-hotcrm-gate-premode", + "remove-dead-client-surfaces", + "remove-dead-sdk-surface", + "remove-enable-trash-mru", + "remove-graphql-surface", + "report-chart-dataset-describe", + "rest-env-resolution-kernel-resolver-seam", + "rest-patch-data-dropped-fields", + "rest-route-ledger-audit-guard", + "retire-default-dispatcher-routes", + "route-audit-tranche-3-service-mounts", + "route-ledger-audit-guard", + "runtime-action-execution-module", + "runtime-actions-mcp-extraction", + "runtime-auth-ai-extraction", + "runtime-automation-extraction", + "runtime-domain-body-extraction", + "runtime-domain-extraction-batch3", + "runtime-domain-handler-registry", + "runtime-meta-data-extraction", + "runtime-packages-extraction", + "runtime-share-links-extraction", + "scim-provider-key-and-sso-scim-parity", + "scoped-invitation-placement", + "security-get-readable-fields", + "security-props-liveness-recheck", + "security-service-contract", + "seed-datasets-multitenant-replay-union", + "seed-insert-replay-lint", + "seed-loader-composite-external-id", + "seed-loader-engine-schema-fallback", + "seed-state-machine-lint", + "seed-summary-banner", + "seed-summary-marketplace", + "seed-writes-exempt-state-machine", + "service-error-envelope-conformance", + "sharing-rule-recipient-reconcile", + "showcase-action-disabled-specimen", + "showcase-nav-affordance-specimen", + "sqlite-datetime-date-bucket", + "startup-log-noise-cleanup", + "step2-metadata-protocol-plugin", + "step2-prc-single-source", + "storage-download-filename", + "sys-view-definition-default-open", + "tool-requires-confirmation-not-enforced", + "two-factor-lockout-and-object-translations", + "two-factor-lockout-extension", + "two-factor-lockout-follows-settings", + "typed-decision-outputs-3447", + "unique-tenant-scoped-materialization", + "update-record-dropped-field-warnings", + "url-field-accepts-relative-urls", + "user-level-export-axis", + "v17-dissolve-protocol-alias", + "v17-rc-anchor", + "verify-multitenant-requests-isolated-posture", + "webhook-authoring-surface-bridge", + "webhook-liveness-ledger-flip", + "webhooks-drop-dead-delivery-i18n", + "withdraw-adr-0107-drop-writes-proposal" + ] } diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 7457e58abb..b6ecbbd31b 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,119 @@ # @objectstack/example-crm +## 4.0.92-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + ## 4.0.91 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index 0b85ea68ea..64d06256b9 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.91", + "version": "4.0.92-rc.0", "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 feea567b4d..987db900d6 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,129 @@ # @objectstack/example-showcase +## 0.3.14-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [402f534] +- Updated dependencies [1c8bf4f] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/driver-sql@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/cloud-connection@17.0.0-rc.0 + - @objectstack/connector-mcp@17.0.0-rc.0 + - @objectstack/connector-openapi@17.0.0-rc.0 + - @objectstack/connector-rest@17.0.0-rc.0 + - @objectstack/connector-slack@17.0.0-rc.0 + - @objectstack/service-datasource@17.0.0-rc.0 + ## 0.3.13 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 4a7d789dfa..2e3c3f9bdd 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.13", + "version": "0.3.14-rc.0", "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 727245607b..a020e53445 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/example-todo +## 4.0.92-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [984396b] +- Updated dependencies [0cdb57a] +- Updated dependencies [9f060e5] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [1b717e5] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [094fa34] +- Updated dependencies [5e55739] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [4e9e184] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [a137bbc] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/client@17.0.0-rc.0 + - @objectstack/mcp@17.0.0-rc.0 + - @objectstack/metadata@17.0.0-rc.0 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.0 + - @objectstack/knowledge-memory@17.0.0-rc.0 + - @objectstack/service-knowledge@17.0.0-rc.0 + ## 4.0.91 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 5b93fa3a8c..c11fbeac9a 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.91", + "version": "4.0.92-rc.0", "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 faaa209fb8..531ba63f62 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,104 @@ # @objectstack/example-embed-objectql +## 0.0.32-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/driver-memory@17.0.0-rc.0 + ## 0.0.31 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 1879006109..cc02a45c00 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.31", + "version": "0.0.32-rc.0", "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 92ceec2fb3..1df347b808 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,148 @@ # @objectstack/hono +## 17.0.0-rc.0 + +### Patch Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- c2d9098: feat(rest/protocol): extend droppedFields write-observability to the bulk paths + client SDK (#3455) + + Follow-up to #3448 (#3431 D2): the single-write PATCH/POST `/data` paths already + surface LEGALLY-stripped write fields (static `readonly` #2948 / `readonlyWhen` + #3042 / #3043 create ingress) as `droppedFields`. The **bulk** write paths did + not — the same strips happened silently on every batched row — and the typed + client warning + CORS mirror were deferred. This closes those out. + + **Bulk passthrough (metadata-protocol).** + + - `updateManyData` and `batchData` (update/upsert rows) now register a per-row + `onFieldsDropped` collector and attach the events to that row's result. + - `createManyData` diffs each supplied row against its #3043-stripped form and + returns an **aggregated** top-level `droppedFields` (one event per + object/reason with the union of field names) — its `{ records, count }` + response has no per-row slot, and the insert-time strip is static-`readonly` + only, so it is schema-uniform across rows and the aggregate is faithful. + - `insertManyData` keeps per-row precision, attaching `droppedFields` to each + outcome. + - **Correctness fix bundled in:** `updateManyData` and `batchData` never threaded + the caller's execution `context` to the engine — bulk writes ran context-less, + so RLS/FLS and `readonlyWhen` evaluated without the caller's principal, and the + batch create-ingress strip was hard-coded to a non-system context. All engine + calls in both methods now run under the resolved `context`. + + **Contract (spec).** `BatchOperationResultSchema` gains an optional per-row + `droppedFields` (covers `updateMany` + `batch`, which alias + `BatchUpdateResponseSchema`); `CreateManyDataResponseSchema` gains the optional + aggregated `droppedFields`. Both are omit-when-empty, so existing clients are + unaffected. `X-ObjectStack-Dropped-Fields` is deliberately **not** emitted for + batches — one response header cannot express per-row drops, so the per-row body + field is the canonical bulk channel. + + **Typed client warnings (@objectstack/client).** `CreateDataResult` / + `UpdateDataResult` gain `droppedFields?: DroppedFieldsEvent[]`, giving the body + channel a type instead of an untyped property. + + **CORS (@objectstack/hono, @objectstack/plugin-hono-server).** + `x-objectstack-dropped-fields` is added to the default `Access-Control-Expose-Headers` + allow-list (kept in lockstep across both Hono CORS sites) so a cross-origin + browser can read the single-write drop header. The body `droppedFields` remains + the primary, cross-origin-safe surface — this is a convenience mirror. + + **GraphQL — not applicable (documented).** #3455 lists a GraphQL mutation item, + but GraphQL has no runtime: `kernel.graphql` is unassigned everywhere and + `handleGraphQL` returns `501`, and discovery never advertises `/graphql`. There + is no schema generator or mutation resolver to expose a typed payload field on, + so there is nothing to wire until a GraphQL engine lands — at which point the + protocol-layer `droppedFields` is already present and only the GraphQL schema + projection would remain. + +- Updated dependencies [af5a224] +- Updated dependencies [879ea13] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [9f060e5] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [394b7a1] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [030125b] +- Updated dependencies [8e08bc3] +- Updated dependencies [3d5f726] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [810a3a2] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [e231abb] +- Updated dependencies [d8c4957] + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/plugin-hono-server@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 72f4e5401f..62eae537a1 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 6fbeb49534..e9506a5352 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,100 @@ # @objectstack/account +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index a77dc47acc..10d098846a 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 f3ee6708f6..94b59cdce3 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,100 @@ # @objectstack/setup +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index f8cae83eb1..341aa317bc 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 9852354dc4..f3085f6b58 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,100 @@ # @objectstack/studio +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 516004f58f..6bc1f0146a 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 6176b1ad13..969e26e62d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,1163 @@ # @objectstack/cli +## 17.0.0-rc.0 + +### Minor Changes + +- fdb4f50: feat(migrate): `os migrate files-to-references` — a data migration with a self-check, gated per deployment (#3617) + + The ADR-0104 file-as-reference migration ships as a command a deployment runs + against its own database, and the deployment-level flag it records is what may + later authorise irreversible behaviour — never the platform version. + + ```bash + os migrate files-to-references # dry run: reports, writes nothing + os migrate files-to-references --apply # converts, verifies, records the flag + ``` + + The run backfills legacy file-field values (inline metadata blobs, own-resolver + URLs, `data:` URIs) into owned `sys_file` references, reconciles the ownership + ledger against what records actually hold, and — only on an `--apply` run whose + reconciliation reports **zero blocking discrepancies** — records + `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. + + **Why a flag rather than a release note.** ObjectStack is a development + platform: third-party deployments upgrade on their own schedule and their data + is not observable by anyone else, so no release-side soak can vouch for them. + The evidence has to be produced where the data is. Consequences: + + - Installing a new version never starts deleting bytes. Running the migration + and passing its self-check is the consent. + - Not run, or not passed → files are retained forever. Wasted storage, zero + data loss. + - A later failing run **clears** `verified_at`: a deployment whose data has + drifted closes its own gate. + - A dry run writes nothing at all — not the conversions, and not the flag, + even when the self-check would pass. + - External URLs stay advisory. They are not `sys_file`s, so they can never + enter collection; whether to remodel them as a `url` field is the app + author's decision (ADR-0104 R7), not a gate. + + Ships alongside: + + - `@objectstack/spec` — `DataMigrationFlagSchema`, `FILE_REFERENCES_MIGRATION_ID`, + and the single `isDataMigrationFlagVerified` predicate both future consumers + (collection #3459, strict value-shape #3438) read, so the two gates cannot + disagree about the same fact. + - `@objectstack/platform-objects` — the `sys_migration` object plus + `readDataMigrationFlag` / `isDataMigrationVerified` / `recordDataMigrationRun`. + Reads fail toward "not verified": a gate that cannot read its evidence stays + closed. + - `@objectstack/objectql` — a read may now opt out of file-reference expansion + via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`, and the storage service's + bookkeeping/scan reads do. Without it the read resolver rewrites stored ids to + their expanded form before the reconciliation sees them, which reports held + references as absent — noisy `stale_owner` findings, and a missed + `unowned_reference` would have been a false pass of the collection gate. + +- 094fa34: feat(cli,client)!: drop `os environments create --template` and the + `template_id` body field — no control plane has ever read them (#3731) + + The CLI advertised `--template` as _"Built-in template id (e.g. crm, todo, + blank)"_ and forwarded it as `template_id` on `projects.create()`. Nothing + consumes it: `template_id` / `templateId` appears in **zero** non-test files in + the `cloud` repo, `sys_environment` has no such column, and the create route + whitelists what it reads (`displayName`, `organizationId`, `isDefault`, + `hostname`, `metadata`, …) — `template_id` is not in the list. The + `blank`/`crm`/`todo` registry the flag named was the `apps/server` + `createTemplatesRoutePlugin` snapshot, removed when the control plane moved to + `cloud`; the flag outlived it. + + So the flag was accepted, transmitted, and dropped — no seeding, no error, no + stored trace. That is worse than the 404 its listing counterpart returned + (`projects.listTemplates`, deleted in #3702): a 404 tells the caller something + is wrong, a silently ignored flag reports success. + + **Migration.** `os environments create --template ` → drop the flag; it + never did anything. Starter content comes from the App Marketplace: create the + environment, then install the package (`sys_package` rows with + `is_starter = true`, i.e. `client.projects.packages.install(envId, { packageId })`). + Callers passing `template_id` to `client.projects.create()` should delete the + property — TypeScript now rejects it, which is the point: an unknown field was + being silently discarded on the wire. + + Note this is **not** the same `--template` as `os init` / `create-objectstack` + (`app` / `plugin` / `empty` scaffolds) — those are local scaffolding templates + and are untouched. + +- 31468fc: feat(cli): `os i18n extract --check` — fail instead of writing when translation bundles have drifted + + Generated translation bundles had no freshness gate, so they rotted silently + until someone happened to re-run the extractor by hand. #3670 found three + distinct drifts sitting in the committed bundles at once: translations left + behind for schema keys that had been REMOVED, keys the schema had GAINED with + no entry at all, and an object whose labels were committed as empty strings + (which renders blank rather than falling back to anything readable). + + `--check` writes nothing and exits non-zero when a fresh extract differs from + what is committed in `--out`, listing each stale or missing file and printing + the exact regenerate command. It runs the identical render path as a real + extract — both branches iterate the same rendered set — so the check can never + disagree with what writing would produce. + + It runs in **merge mode** like any other extract, so it never asks anyone to + re-translate: an up-to-date bundle re-extracts byte-identically. Requires + `--out`, since there is nothing to compare against without it. + + In this repo it is wired up as `pnpm check:i18n` and gated in CI, but the flag + is on the CLI, so any consumer shipping generated bundles can gate them the + same way. + +- a8d1e24: feat(cli,spec): gate the whole declared surface for i18n, and translate inline object actions server-side (#3370) + + In a zh-CN workspace the platform chrome was localized while author-declared + labels leaked English — the approval drawer rendered **Approve / Reject / + Reassign** right beside the inbox's own 通过 / 拒绝. Two independent holes, both + closed here. + + **The lint gate could not see them.** `os lint`'s i18n coverage kept its own + walk of the metadata, separate from the one `os i18n extract` uses to scaffold + bundles, and the two had drifted: coverage only ever walked the _top-level_ + `actions` array, while `sys_approval_request` declares its decision actions + **inline on the object**. Those labels were extractable but ungated, so an + untranslated one could ship and no lint run would notice. Coverage now derives + its expected keys from `collectExpectedEntries()` — the extractor's walker — so + the gated surface and the scaffolded surface cannot disagree again. Newly gated + as a result: inline object actions, action `params` and `resultDialog` copy, + object-nested `listViews` (label / description / `emptyState`), object + `description`, field `help` / `placeholder`, and the `apps` / `dashboards` / + `pages` surfaces. Extract output is byte-identical — verified against the + committed plugin bundles. + + **It stays silent for projects that do not translate.** Which locales get + checked is the project's declaration, never an assumption: `os lint`, + `os i18n check` and `os i18n extract` now read the stack's own + `i18n.defaultLocale` / `i18n.supportedLocales`, falling back to the locales a + bundle already exists for, and finally to `en`. A project with neither is + checked against its default locale alone — which its inline labels already + satisfy — so it reports zero i18n issues. That also fixes a monolingual + _non-English_ project being told it owed `en` translations it never claimed to + speak. Locked by regression tests; the three bundled examples stay at 0 errors. + + **The server sent English regardless of locale.** `translateObject` walked an + object's `label` / `pluralLabel` / `description` / `fields` but never its inline + `actions`, so `GET /api/v1/meta/object/:name` returned the authored English + literals even though `@objectstack/plugin-approvals` ships `_actions` + translations for all eight decision actions in zh-CN / ja-JP / es-ES. The + Console compensated by re-resolving labels client-side against a separately + fetched bundle; every other consumer — mobile, plain HTTP, SDUI — rendered the + source language. It now runs inline actions through `translateAction`, without + stamping a synthetic `objectName` onto the response. + + Adds `os i18n extract --no-metadata-forms`. Whether the companion + `.metadata-forms.generated.ts` file is written was previously implicit: + every run emitted it, so `--check` demanded that file in packages that + deliberately do not commit one. The Studio metadata-form baseline is + registry-driven and identical for every stack, so exactly one package owns it + (`platform-objects`); a plugin translating only its own objects now opts out, + and its `--check` stops failing on a tree that is in sync. Defaults to emitting, + so `pnpm check:i18n` keeps covering all 8 platform bundles. + +- f2b8ac9: Navigation reachability vs. granted access (issue #3583, assessment R5) + + `validate-nav-access` joins what an app's navigation exposes against + `buildAccessMatrix` — the first lint consumer of the ADR-0090 D6 matrix, which + previously only backed `os compile`'s snapshot gate. An object in the menu that + no permission set grants read on renders as an entry and then fails + permission-denied when opened: it works while you browse as an administrator + (the platform's built-in `admin_full_access` carries a wildcard grant) and + breaks for exactly the users the app ships permission sets for. + + Advisory severity — a grant can legitimately come from a permission set another + installed package ships. Quiet by construction in three cases: platform-provided + objects (their own packages grant them), stacks that declare no permission sets + at all (permissions managed elsewhere, so flagging every entry says nothing), + and any stack where a set carries a wildcard `objects: { '*': … }` grant — the + shape `admin_full_access` itself uses, which the access matrix records under the + literal key `*`. + + Wired into `os validate`, `os lint`, and `os compile`. + +- 17749fc: Page-component field bindings and non-dashboard chart bindings (issue #3583, Phase 2) + + Two more reference-integrity rules from the #3583 assessment, both wired into + `os validate`, `os lint`, and `os compile`. + + **`validate-page-field-bindings`** — `PageComponent.properties` is an untyped + bag, so a highlights strip, KPI card, or details section can name a field the + bound object does not have; the component silently skips it. Which object a + component binds follows `dataSource.object` → `properties.object` → the page's + `object`, so multi-object pages are checked per element. `record:related_list` + resolves its columns/sort/filter against the **related** object and its + add-picker against that picker's own object. Advisory (matching + `FORM_FIELD_UNKNOWN`). Relationship paths, system fields, cross-package objects, + and unregistered component types are skipped. + + **`validate-chart-bindings`** — extends ADR-0021 axis checking past dashboards to + report charts (`report.chart` and `report.blocks[].chart`), list-view charts + (`views[].list`, `views[].listViews.*`, `objects[].listViews.*`), and + dataset-bound page chart components. An axis naming a raw field instead of a + declared measure is an **error** (the series comes back empty); an axis naming a + declared-but-unselected measure is a **warning**. The report shape needed its own + handling: `ReportChartSchema` narrows `xAxis`/`yAxis` to bare strings, which the + dashboard rule's array guard skips silently. The react `` block is + object-bound, not dataset-bound, and is deliberately left out — nothing defines + what its aggregate names the result column. + + **Fixes:** the page walk used by `validate-action-name-refs` read a top-level + `page.components` array, which `PageSchema` does not have — components live under + `regions[].components[]` and `slots`, and sub-trees nest inside the untyped + `properties` bag (`children`, `items[].children`, `body`, `footer`) rather than a + `children` key on the component. The rule was therefore visiting nothing on a + schema-parsed stack. Traversal now lives in one shared, tested module; on the + showcase app it reaches 194 components where the previous shape found 46. + Source-authored pages (`kind: 'html' | 'react' | 'jsx'`) are skipped — their + `regions` hold a derived cache the `source` wins over. + +- 67452d1: feat(spec): resolve page metadata i18n — `page:header` title/subtitle (#3589) + + Custom system pages authored as metadata (Installed Apps, Cloud Connection, + Connect an Agent) hard-code their `page:header` copy in + `properties.title` / `properties.subtitle`. Every other metadata type is + localized at the REST boundary, but `page` was not: the `pages` namespace + existed only on `AppTranslationBundleSchema` — a schema no runtime reads — + with no resolver behind it, so those headers stayed English in every locale + while the matching nav labels translated correctly. + + - `TranslationDataSchema` (the shape the i18n service actually serves) gains a + `pages` namespace: `pages..{label,description,title,subtitle}`. + - New `translatePage` in `@objectstack/spec/system` translates a page's own + `label` / `description` and overlays `title` / `subtitle` onto every + `page:header` in the page's regions. Registered in + `translateMetadataDocument`, so it rides the existing read path. + - `page` added to the REST boundary's `TRANSLATABLE_META_TYPES`. Locale + extraction, the locale-keyed ETag, and `Vary: Accept-Language` already + covered every metadata type — no new plumbing. + - `objectstack i18n extract` now emits page entries, including the + `page:header` copy, so the new namespace is not invisible to the tooling. + - zh-CN / ja-JP / es-ES translations shipped for the three Setup pages, plus + the missing `nav_cloud_connection` / `nav_connect_agent` nav labels (these + existed only in zh-CN). + + Header copy is keyed by **page name**, not by component id: `page:header` + instances carry no stable id. `title` falls back to `pages..label`, since + a page's header title and its nav label are normally the same string. + + Authoring is unchanged and English literals stay in metadata as the fallback — + a page with no `pages` entry renders exactly as before. Consumers of + `@object-ui` need no change: pages arrive already localized from the server. + +- 4340f13: feat(lint,cli): flag flow `update_record` writes to readonly fields at design time (#3425) + + A flow `update_record` node that writes a field the target object declares + `readonly: true`, under the default `runAs: 'user'` identity, is a **silent + no-op**: the objectql engine strips static-`readonly` fields from a non-system + UPDATE payload (#2948), so the intended write never lands — yet the step still + reports `success`. #3407/#3413 surfaced the strip as a run-time step warning; + this moves the discovery **left** to `os validate` / `os build` so an author + finds the mismatch at design time instead of by reading server WARN logs days + later. + + - New `@objectstack/lint` rule `validateReadonlyFlowWrites(stack)` — a pure + `(stack) => Finding[]` check (ADR-0019). A static `readonly:true` field + written by a literal `update_record` under `runAs !== 'system'` is a + 100%-certain no-op → **error** (gates the build). A `readonlyWhen` field is + per-record-state → **warning** (advisory). Deliberately narrow to stay + false-positive-free: `create_record` (INSERT is engine-exempt from the strip), + `runAs: 'system'` flows (the intended "automation maintains it" channel), + templated object names, and non-literal `fields` maps are all skipped. + - Wired into `os validate` and `os compile`/`os build`, mirroring the existing + security-posture gate (errors fail; advisories print dimmed). + + The formal contract, unchanged in behavior: `readonly` governs the end-user / + API surface (REST/UI and `runAs:'user'` flows strip it); trusted system writers + (`runAs:'system'`, system hooks, seeds) maintain it. To let a flow maintain a + readonly field, declare `runAs: 'system'`. + +- f163028: Reference-integrity validation for object and action names (issue #3583) + + A HotCRM audit found ~20 shipped instances of one bug class — metadata naming + something that does not exist — all passing `objectstack validate` / `lint` + cleanly and failing silently at runtime. This closes the object-name and + action-name half of that class. + + **New — `@objectstack/spec`:** `PLATFORM_PROVIDED_OBJECT_NAMES`, a curated + registry of every object name contributed by a platform package, official + plugin, or the cloud runtime, plus `isPlatformProvidedObjectName()` and + `hasPlatformObjectPrefix()`. This replaces the `startsWith('sys_')` prefix guess + that could not tell `sys_user` (real) from `sys_approval_process` (fictional — + removed by ADR-0019, registered by nothing), which is why every fictional + platform-prefixed reference shipped. A conformance test scans each package's + `*.object.ts` declarations and fails if the registry drifts. + + **New lint rules** (wired into both `os validate` and `os lint`): + + - `validate-object-references` — action-param `reference` / `objectOverride`, + dashboard `globalFilters[].optionsFrom.object`, and navigation + `requiresObject` gates. Severity follows resolvability: an unresolved + _unprefixed_ name is a typo (**error** — `object: 'user'` where the platform + object is `sys_user`); an unresolved _platform-prefixed_ name is **advisory**, + since a third-party package may still provide it. + - `validate-action-name-refs` — the surfaces that bind an action BY NAME: + list-view `bulkActions` / `rowActions`, page `record:quick_actions` + `actionNames`, and nav action items. A name matching no defined action is an + **error** (the button renders and does nothing), matching the existing + dashboard-action-target rule. + + **Fixes:** + + - `defineStack` cross-reference validation now walks `app.areas[].navigation` — + an areas-based app previously got no navigation checking at all — and recurses + into `children` on `object` nav items, not only `group` ones. + - `os lint` i18n coverage now reads field `options` in the canonical + `{value,label}[]` array shape; it only handled the record map, so option-label + coverage silently never fired for canonically-shaped select fields. + - Hook `condition` expressions are now field-checked when `object` is an ARRAY + of targets (previously only a single string target was checked, so a + multi-target hook filtering on a nonexistent field passed clean). Per-target + diagnostics are de-duplicated. + - A dashboard widget binding no `dataset` at all is now reported instead of + silently bypassing every binding and chart check on the raw-config + (`lint`/`doctor`) paths. `dataset` is schema-required, so this matches what + the parsed paths already enforce. + +### Patch Changes + +- 08b5a3d: fix(action): one precedence for `target` vs the deprecated `execute` — lower the alias, then drop it (#3713) + + `execute` is the deprecated alias of `target`, and three readers resolved "the + author declared both" in **two opposite directions**: + + | Reader | Preferred | + | ------------------------------------- | --------- | + | `ActionSchema` transform (spec) | `target` | + | objectui `ActionRunner.executeScript` | `execute` | + | CLI compile step (`lowerCallables`) | `execute` | + + So `defineAction({ type: 'script', target: 'preferredHandler', execute: 'legacyHandler' })` + ran `preferredHandler` server-side and `legacyHandler` client-side — two + different scripts for one button, silently, with no error anywhere. Low + frequency (it needs an author to set both, which happens mid-migration or by + copy-paste), but the failure mode is "the wrong code ran". + + **`target` now wins everywhere, and the alias is removed from the parsed + output** — the same "canonical wins, alias disappears" shape as + `agent.knowledge.topics` → `sources`. The conflict is now _unrepresentable_ + rather than merely agreed-upon: no renderer can see a second slot to disagree + about. Worth noting the server runtime never read `execute` at all + (`isHeadlessInvokableAction` gates on `target || body`; dispatch probes + `target`/`name`), so authoring `execute` worked _solely_ because it was lowered + at parse time — dropping it costs the server nothing. + + The CLI's inline-handler lowering had the same bug in compile-time form: with a + function in both slots it bundled the `execute` one and then overwrote + `action.target` with that ref, silently discarding the function the author + declared on `target`. It now probes `target` first and drops the alias. + + **Authoring is unchanged** — `execute` is still accepted on input (`ActionInput`), + still lowered to `target`, and still listed in the reference docs. Nothing to + migrate in your app metadata. + + **Consumers of the parsed metadata**, however, must read the canonical slot: + + - FROM: `parsedAction.execute` → TO: `parsedAction.target` + - One-line fix: delete the alias fallback, e.g. `action.execute || action.target` + becomes `action.target`. + + `z.infer` no longer carries `execute`, so any such reader + fails to compile rather than silently reading `undefined`. The objectui + `ActionRunner` counterpart ships separately. + +- 7fb436c: Multi-organization operation is an ENTITLEMENT again: the `group` posture no + longer activates without the enterprise runtime (ADR-0105 D12 correction). + + The first ADR-0105 wave read D12 as "the `group` wall ships open" and made the + posture self-activating — it never probed for `@objectstack/organizations`. That + turned `group` into a free multi-org path around the `isolated` gate (ADR-0081 + D2), and made the weaker isolation the free one, which is not a boundary anyone + would draw on purpose. + + The distinction that was missed: **open code is not free activation.** The wall's + implementation has always lived in the open packages — that is equally true of + `isolated`, whose Layer 0 wall sits in `plugin-security` and is gated on a + service the enterprise package registers. Cloud ADR-0016's 铁律 + (强制免费、治理收费) guarantees that a deployment RUNNING a multi-org shape is + safe; it is satisfied by REFUSING to run one unwalled, not by giving the posture + away. + + ## Changes + + - **`tenancy-service`**: `group` probes `org-scoping` exactly like `isolated`. + Without it the posture resolves to `single` and reports `degraded`. + - **`os serve`**: the ADR-0093 D5 boot guard keys off the resolved POSTURE + instead of `OS_MULTI_ORG_ENABLED`. Previously `OS_TENANCY_POSTURE=group` skipped + both the enterprise package load AND the fail-fast, silently degrading to an + unwalled deployment — the exact ADR-0049 class that guard exists to close. A + `group` request without the runtime now refuses to boot unless + `OS_ALLOW_DEGRADED_TENANCY=1`. + - **New seam — the runtime declares what it entitles.** `org-scoping` may expose + `supportedPostures` (`OrgScopingEntitlement`, `@objectstack/spec/security`); + the open side honours it and fails closed on anything not listed. Whether + `group` and `isolated` are one commercial tier or two is packaging policy, and + packaging policy belongs to the commercial runtime rather than hard-coded in + open core. Omitting the field entitles every walled posture, so existing + runtimes are unaffected. + - **`organization_id` stamping returns to the enterprise runtime.** The previous + wave moved auto-stamping into the open engine; that removed the closed + package's only load-bearing runtime duty, so a five-line forged `org-scoping` + registration would have produced a fully working multi-org deployment. With + stamping back where it was, a forged registration yields NULL-org rows the wall + hides — a broken deployment, not an unlicensed working one. + + **Write-side VALIDATION stays open and is unchanged**, including the + bulk-insert coverage: rejecting a forged `organization_id` is a security + property, not a packaging one. Only filling an ABSENT value moved back. + + - Default-organization bootstrap returns to `single`-only; every walled posture + keeps its existing owner (ADR-0081 D1). + + ## Note for operators + + `OS_TENANCY_POSTURE=group` without `@objectstack/organizations` installed now + **refuses to boot** rather than running single-org. This only affects + deployments that adopted `group` between the two waves. + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- e9b11df: fix(auth): app-declared organization roles are now storable, not just registerable (#3723) + + `AuthManagerOptions.additionalOrgRoles` registered every `permission` / + `position` name a stack declared with better-auth's organization plugin, so + `POST /organization/invite-member { role: 'sales_rep' }` passed the role check — + and then the write failed, because `sys_invitation.role` and `sys_member.role` + were closed selects listing `owner|admin|member` only: + + ``` + ValidationError: role must be one of: owner, admin, member + { field: 'role', code: 'invalid_option' } + ``` + + A select is enforced on write and better-auth's own inserts are not exempt (they + run through the ordinary ObjectQL validator), so any stack declaring role names + was registering roles that could be requested and never stored. + + Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single + normalizer; its output feeds better-auth's role map **and** the two `select` + option lists, so neither side can accept a name the other rejects. The built-in + roles (`owner`, `admin`, `delegated_admin`, `member`) live in + `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the + platform objects declare statically — app roles are appended at boot. + + New exports: + + - `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to + `identity/membership-role`; the package-level export path is unchanged). + - `@objectstack/plugin-auth` — `collectStackOrgRoles`, + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, + `withMembershipRoleOptions`. + + Hosts that boot `AuthPlugin` from a loaded stack should derive + `additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the + stack themselves — `objectstack serve`, the `@objectstack/verify` harness and + `DevPlugin` now all do. The harness previously passed none, which is why a + dogfood proof could boot a stack whose declared roles better-auth had never + heard of; `DevPlugin` documents itself as equivalent to the full stack and + silently excluded app roles from that equivalence. + + `additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and + `collectStackOrgRoles` now returns those descriptors. The label is what the + declaring `position` / `permission` metadata already says, so the role picker + shows `Executive` for a position declared as such instead of title-casing the + machine name into `Exec` — a third source of truth for one string. Presentation + only: better-auth sees just the name, and the stored value is always the name. + Passing `string[]` keeps working unchanged. + + Behaviour change worth noting: a declared role name that is not a valid machine + name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a + boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a + name would be registered verbatim and stored mangled — the same mismatch with + extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. + +- 96242ef: feat(auth): AuthPlugin derives app-declared organization roles itself — hosts pass nothing (#3723 follow-up, cloud#897) + + Five hosts boot `AuthPlugin` from a stack, and per-host `additionalOrgRoles` + wiring proved to be the defect pattern: three of them (the verify harness, + `DevPlugin`, cloud's `ArtifactKernelFactory`) at some point forgot it, and the + failure is silent — app-declared roles are simply absent. One host (cloud) + mounts `AuthPlugin` before the app metadata even exists, so no init-time walk + could ever cover it. + + `AuthPlugin` now derives the roles in its own `kernel:ready` hook — the one + point that fires after all metadata is registered in every host — via the new + `collectRegisteredOrgRoles(engine, metadataService?)` (the late-bound twin of + `collectStackOrgRoles`). Both consumers are updated from the derived union: + better-auth's org-plugin roles map (`applyConfigPatch`; the instance builds + lazily) and the `sys_invitation.role` / `sys_member.role` select options + (re-registration under the same package id — a supported registry path; no + DDL, options are validator/picker metadata). + + `objectstack serve`, the `@objectstack/verify` harness and `DevPlugin` no + longer pass `additionalOrgRoles` — deliberately, so the dogfood invite gate + only stays green if the auto-derivation works. The option remains for roles + declared OUTSIDE stack metadata; explicit entries are unioned with the derived + set. `collectStackOrgRoles` stays exported for hosts that want an init-time + walk of a raw stack object. + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- 5b89711: feat(spec,lint): freeze the `{current_user_id}` filter vocabulary and fail the build on unresolvable placeholders (#3574) + + A dashboard widget filtered on `{current_user}` rendered `0`. Not an error — a + zero, indistinguishable from a metric that is legitimately empty, with nothing + in the console or the server log. `service_dashboard.my_open_cases_by_priority` + in the HotCRM template had shipped broken this way since the day it was + written. + + The token had never been part of the contract. Date macros were frozen in + `date-macros.zod.ts` with a spec vocabulary, a lint-usable predicate, and a + single client resolver; `{current_user_id}` had only prose in an `app.zod.ts` + JSDoc and three ad-hoc client implementations that each handled one surface's + filter shape. Nothing could tell an author their token was wrong. + + - **`@objectstack/spec`** — new `data/context-tokens.zod.ts` freezing + `CONTEXT_TOKENS` (`current_user_id`, `current_org_id`) as the sibling of + `DATE_MACRO_TOKENS`, with `isContextToken` / `isKnownFilterToken` / + `classifyFilterToken` and a `CONTEXT_TOKEN_SUGGESTIONS` near-miss table. The + module documents what the tokens are _not_: presentation scope, never an + access boundary — that is RLS, which uses the unrelated `current_user.id` + expression root. + - **`@objectstack/lint`** — new `validateFilterTokens` (rule + `filter-token-unknown`, severity `error`). It walks `filter` / `filters` / + `runtimeFilter` subtrees across dashboards, objects, views, reports, + datasets, pages and apps, and reports any placeholder that resolves in + neither vocabulary. It scans for filter _keys_ rather than enumerating known + surfaces, so a new surface following the convention is covered the day it + ships — enumerating surfaces is how the dashboard was missed in the first + place. Navigation `recordId` / `params` are deliberately out of scope: they + resolve `AppContextSelector` ids, which are meaningless in a filter. + - **`@objectstack/cli`** — the gate runs in `os validate` and `os compile`. + + It is an error rather than a warning because of who authors this metadata. An + AI reads a query returning `0` as a correct answer and builds on it; its + correction loop is author → validate → fix, so a diagnostic only reaches it if + it can fail the build. The three spellings the suggestion table covers — + `{current_user}`, `{user_id}`, `{organization_id}` — are each correct + _somewhere else_ in the platform, which is exactly why authors reach for them. + + Also fixes a `ViewSchema` JSDoc example that documented `{user_id}`, a token + that resolves nowhere. + +- 169b58a: fix(#3426): build-time warning for unresolvable flow template paths + guard the formula re-read + + Two follow-ups to #3426 (the formula/lookup `{record.}` template gap that #3445 began closing). + + **Build-time signal (the issue's fallback ask).** `os validate` now flags a + record-change flow node whose `{record.}` template cannot resolve — + turning the previous SILENT blank into an advisory warning. Two cases, via the + new `@objectstack/lint` rule `validateFlowTemplatePaths`: + + - `flow-template-unknown-field` — `{record.}` where `` is neither a + declared field nor a system column (a typo like `{record.full_naem}`). + - `flow-template-lookup-traversal` — `{record..}`, a cross-object + hop the seeded record carries only as a scalar id (still unsupported; tracked + on #3426). + + Deliberately quiet: formula fields, bare lookup ids, numeric indexes into + `multiple` lookups (#1872), `json` sub-paths, and system columns are NOT flagged, + and flows bound to an object this stack does not define are skipped (no schema to + compare against). + + **Hydration re-read guards.** The `trigger-record-change` computed-field re-read + (#3445) is now (a) skipped when the object declares no `formula` field — the only + thing it adds — via the engine's optional `getObjectConfig`, and (b) memoized per + write on the shared HookContext, so N flows on one written record share ONE + re-read instead of N. Any uncertainty falls back to the prior unconditional + re-read (correctness over the optimization). + +- 189854c: chore(spec,cli): enroll `webhook` in the liveness GOVERNED set (#3462) + + Closes the final third of #3462 (umbrella #1878) — `report` and `dashboard` + landed in #3474; `webhook` was deferred for two reasons, both handled here. + + - **Not a registered metadata type.** `webhook` is absent from the metadata-type + registry, so the gate can't resolve it via `getMetadataTypeSchema`. Registering + it would switch on Studio webhook CRUD, `saveMetaItem` overlay acceptance, and + diagnostics sweeping — the wrong move while the authoring surface is still + disconnected (below). Instead the gate resolves it through a small + `SPEC_ONLY_SCHEMAS` override in `check-liveness.mts` (consulted before the + registry): the gate only needs to **walk** the schema, not register it. + - **The whole authoring surface is dead (#3461).** Nothing materializes an + authored `webhooks:` entry (stack/connector) into a `sys_webhook` dispatcher + row — the runtime reads only admin-authored `sys_webhook` rows. So + `packages/spec/liveness/webhook.json` classifies all 16 authorable props + **dead** and `authentication` **experimental** (HMAC-`secret`-only, its + existing marker). Per-prop notes record which props a future materializer + (#3461 option A) could remap (e.g. `object`→`object_name`, `isActive`→`active`) + vs which have no sink anywhere — doubling as that mapping table. + - **Author-warning wired (`@objectstack/cli`).** Added + `{ type: 'webhook', key: 'webhooks' }` to `TYPE_COLLECTIONS` in + `lint-liveness-properties.ts`, so `os compile` now advises authors that + `webhooks:` is a silent no-op. The required `url` prop carries the single + warning per webhook (one heads-up per artifact, not one per dead prop); + `isActive` is left unmarked (default(true) boolean). + + This is enrollment only — it does **not** decide #3461's build-the-bridge vs + retire-the-surface question. When that lands, the mapped props flip to live (cite + the materializer) or the ledger is removed with the schema. No spec shape/behavior + change (ledger + gate/lint config only). + +- dac6a08: feat(driver-sql)!: make index drift visible to `os migrate plan` — no more silent DDL at boot (#3728) + + The #3696 unique-scope migration converged **in place**: `syncTableIndexes` ran a + `DROP` + `CREATE UNIQUE INDEX` during `initObjects`, in every environment, + leaving one log line behind. `os migrate plan` showed nothing, because + `detectManagedDrift` was column-only — `ManagedDriftOp` had no index dimension at + all. An operator who wanted to review the DDL before it reached their database + had no way to, and a managed schema was being auto-altered in production, which + the #2186 contract explicitly forbids. + + Index drift is now a first-class dimension, reconciled through the same path as + column drift: + + - **`syncTableIndexes` is additive only.** It creates indexes; it never drops or + rewrites one. `dropLegacyGlobalUniques` is gone. + - **New `DriftOp` variants** — `replace_unique_index` (safe: retire the legacy + platform-wide unique in favour of the tenant composite), `create_index` (safe), + `recreate_index` (needs-confirm; destructive when it tightens to `UNIQUE`), and + `drop_index` (destructive). + - **`detectManagedDrift` reports them**, `os migrate plan` renders them (index + ops display as `table [index_name]`), and `os migrate apply` executes them. + Index DDL is portable, so it applies directly on every dialect — no SQLite + table rebuild. + - **`replace_unique_index` creates before it drops**, so uniqueness is never + unenforced mid-migration and a failed create leaves the schema untouched. + - **Declared `indexes[]` drift is covered too**: an index metadata declares but + the database lacks, and one whose definition no longer matches the declaration + (the additive sync skips those by name, so they could never self-heal). + - **Orphan detection is limited to ObjectStack's own generated naming** + (`uniq_…` / `idx_…`, plus the pre-#3696 `__unique` knex + spelling). A hand-rolled operational index is never reported as drift and + `--allow-destructive` will not delete it. + + **Behaviour change.** Boot no longer rewrites the index unconditionally. Dev + (`autoMigrate: 'safe'`, what `os dev` / `os serve` use) still self-heals on + restart, so local workflows are unchanged. Production now **warns** with an + actionable `os migrate` hint and leaves the schema alone — the deployment stays + on the legacy global unique (multi-tenant inserts still collide) until someone + runs `os migrate apply`. That is the deliberate trade: a visible, pre-inspectable + migration instead of an invisible one. + + Also fixed: `managedObjectIndexes` was never cleared when an object dropped its + `indexes[]`, so drift detection kept expecting an index nobody declared. + + `SchemaDiffEntryKind` gains `index_mismatch` and `unmapped_index`. + +- d1557d9: feat(driver-mongodb)!: declare the driver single-tenant and refuse to boot multi-tenant (#3724) + + `MongoDBDriver` implements **no row-level tenant isolation** — it never reads + `DriverOptions.tenantId`, so reads carry no tenant predicate and writes are not + stamped with a tenant column. The layer the SQL driver has (`resolveTenantField` + + - `applyTenantScope`) simply does not exist here, while everything above the + driver — object metadata's `tenancy` block, `applySystemFields` injecting + `organization_id`, the engine threading `tenantId` into every driver call — + operates on the assumption that tenant isolation is a platform guarantee. Point + a multi-tenant deployment's datasource at Mongo and every query read, updated + and deleted other tenants' documents, silently. + + Rather than serve unisolated, the driver now fails fast at startup: + + - The **constructor** and `connect()` call `assertSingleTenantPosture()`, which + refuses any tenancy posture other than `single` (`OS_TENANCY_POSTURE=group` / + `isolated`, including the posture derived from `OS_MULTI_ORG_ENABLED=true`), + resolved through the shared `resolveTenancyPosture()` so the driver can never + disagree with auth / the registry / the CLI about the mode. The check sits in + the constructor because that is the earliest seam — it fails before a host can + hand the driver anywhere — and `connect()` re-checks in case a host flips the + posture in between. (It originally had to live in the constructor because + `ObjectQLEngine.init()` _caught_ a driver's connect rejection and booted + anyway; that is fixed in the same release, #3741, so both seams abort boot.) + - `syncSchema()` / `syncSchemasBatch()` call `assertObjectsNotTenantScoped()` and + refuse objects declaring `tenancy.enabled: true`, naming every offender in one + message. + - `objectstack serve` / `dev` (CLI) now re-throw this error out of the + auto-driver-registration block instead of swallowing it, so boot exits 1 with + the actionable message — the same treatment `UnsupportedDriverError` already + gets. Matched duck-typed by `code`, so the CLI takes no dependency on the + driver package. + + Both throw `MongoDBMultiTenantUnsupportedError` with + `code === 'MONGODB_MULTI_TENANT_UNSUPPORTED'`, a message that names the detected + signal, the remedy, and `@objectstack/driver-sql` as the multi-tenant option. + + There is deliberately **no override env var**: an escape hatch would restore + exactly the silent non-isolation this guard removes. Single-tenant deployments — + every currently-working Mongo deployment — are unaffected. + + This is option B of #3724. Implementing real row-level isolation (option A) + remains open; the `unique` index shape stays single-field until then, which is + now correct by construction rather than by omission. + +- 97e9c30: fix(doctor): point the retired `reference_filters` hint at `lookupFilters` (#1878 §3 recheck) + + `os doctor`'s snake→camel rule table advised "Use `referenceFilters` + (camelCase)" — a key REMOVED from `FieldSchema` in #2377/ADR-0049, which the + non-strict schema silently strips. The live successor is `lookupFilters` (read + by the objectui lookup picker). The rule now matches both spellings and names + the right key. + +- 7aea626: fix(cli): include readonly flow-write warnings in `os validate --json` output + + The `readonlyWhen` flow-write advisory (`validateReadonlyFlowWrites`, #3465) was + printed in human mode but omitted from the `--json` summary's `warnings` array, + where every other advisory category is aggregated. `os validate --json` + consumers (CI, editors) therefore never saw those warnings. Added + `...readonlyWriteWarnings` to the summary array so JSON and human output agree. + +- 29ff3c2: feat(lint): warn on replay-unsafe `mode: 'insert'` seed datasets (#3434 follow-up) + + Seeds are replayed — they re-load on every dev-server boot and every package + re-publish, not applied once — so `mode: 'insert'` (the loader's one mode with + no existing-row check) duplicates its table on every restart. That footgun + shipped undetected until #3434 (showcase memberships grew 3 → 6 → 9). + + Adds `validateSeedReplaySafety` to `@objectstack/lint` (a pure `(stack) => Finding[]` + rule, ADR-0019) and wires it into `os validate` / `os lint`. Every `data[]` seed + declared with `mode: 'insert'` now gets an advisory warning that points at the + idempotent modes (`ignore` / `upsert`) and the `externalId` to match on — a + single natural-key field, or a COMPOSITE list of fields for a join / junction + table with no single key (`['team', 'project']`, the support #3434 added). It + catches the mistake at authoring time instead of on the second boot. + +- 95829a0: feat(lint): warn on seed values outside an object's declared state machine (#3433 follow-up) + + #3433 exempts seed writes from the `state_machine` validation rule, so a seeded + status the FSM does not declare is no longer rejected at write time. A field-level + `select` still catches a value outside its `options`, but a `state_machine` on a + free-text field — or a value that is a valid option yet not a declared FSM state — + now sails through silently: the exemption is a deliberate but blind back door. + + `validateSeedStateMachine` (a pure `(stack) => Finding[]` rule, run from + `os validate` / `os lint`, symmetric with the replay-safety rule from #3434) + re-adds that safety net at author time. It flags any seed record whose + `state_machine`-governed field carries a value outside the machine's declared + states — the union of `initialStates`, the transition-map keys, and the transition + targets. Advisory (`warning`): the exemption itself is legitimate, so the fix-it + points at either adding the state to the machine or correcting the typo, not a hard + build failure. New rule id: `seed-value-outside-state-machine`. + +- 9981c1d: Surface seed outcomes in the `os dev` / `os serve` boot banner (#3415). Seeds run inside the boot-quiet stdout window and SeedLoader's logs sit under the default warn level, so a fixture could silently lose most of its rows — the showcase shipped 1 of 5 projects with zero terminal signal. AppPlugin now stashes the per-boot seed counters on the kernel (`seed-summary` service) and the banner prints `Seeds: X inserted · Y updated · Z skipped`, escalating to a yellow `⚠ … N REJECTED` line when records were dropped. +- d60968c: Surface marketplace rehydrate/heal seed outcomes in the `os dev` / `os serve` boot banner (#3430), extending the config-app Seeds line from #3415. + + The seed pipeline's most useful result lines are all `logger.info`, but `os dev` forwards a default `warn` level and the serve boot-quiet window swallows stdout — so "marketplace package rehydrated onto a fresh DB with 0 rows", a fresh-DB self-heal, and row-level seed failures were all invisible unless you queried the database directly. + + The `seed-summary` kernel service is now a per-source list. AppPlugin (config apps) and the marketplace rehydrate/heal path each contribute a labelled entry, and the banner prints one combined line that ignores the log level: + + ``` + Seeds: showcase 162 rows · hotcrm(marketplace) 157 ok / 5 errors ⚠ + ``` + + Fresh-DB heals are marked `(healed on fresh db)`; a marketplace package that installed with seed datasets but landed 0 rows, and any run that dropped records, escalate to a yellow `⚠` line instead of passing silently. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [134df4f] +- Updated dependencies [fe67e34] +- Updated dependencies [3d3fddf] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [735f850] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [7101ca2] +- Updated dependencies [587fc91] +- Updated dependencies [415254c] +- Updated dependencies [1f8390b] +- Updated dependencies [3167e29] +- Updated dependencies [0a6fb1e] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [2ba560a] +- Updated dependencies [2dda6e7] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [d2a8695] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [d75edb9] +- Updated dependencies [9dcc0ae] +- Updated dependencies [96242ef] +- Updated dependencies [984396b] +- Updated dependencies [d0fea33] +- Updated dependencies [0cdb57a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [1b717e5] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [6ba3788] +- Updated dependencies [b96c11b] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [f243727] +- Updated dependencies [094fa34] +- Updated dependencies [5e55739] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [ce1f100] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [2c19383] +- Updated dependencies [c88eeda] +- Updated dependencies [5524f84] +- Updated dependencies [169b58a] +- Updated dependencies [9bf4588] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [307e0fe] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [ef5e72d] +- Updated dependencies [dac6a08] +- Updated dependencies [313d7be] +- Updated dependencies [5faeac6] +- Updated dependencies [394b7a1] +- Updated dependencies [7f4a8a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [1c8bf4f] +- Updated dependencies [7180ed5] +- Updated dependencies [d1557d9] +- Updated dependencies [f2b8ac9] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [fc5f126] +- Updated dependencies [adabaa8] +- Updated dependencies [030125b] +- Updated dependencies [605c23f] +- Updated dependencies [4e9e184] +- Updated dependencies [17749fc] +- Updated dependencies [67452d1] +- Updated dependencies [9bf4588] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [52281b0] +- Updated dependencies [db48ad5] +- Updated dependencies [4340f13] +- Updated dependencies [8e08bc3] +- Updated dependencies [16adb3c] +- Updated dependencies [6f55c63] +- Updated dependencies [1dc94f0] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [a137bbc] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [bbd902d] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [29ff3c2] +- Updated dependencies [abceb0d] +- Updated dependencies [95829a0] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [bd68f08] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [a629074] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [e889386] +- Updated dependencies [69f1dfd] +- Updated dependencies [c95ac80] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/rest@17.0.0-rc.0 + - @objectstack/driver-sql@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/service-storage@17.0.0-rc.0 + - @objectstack/client@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/plugin-auth@17.0.0-rc.0 + - @objectstack/lint@17.0.0-rc.0 + - @objectstack/plugin-security@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/mcp@17.0.0-rc.0 + - @objectstack/plugin-hono-server@17.0.0-rc.0 + - @objectstack/plugin-approvals@17.0.0-rc.0 + - @objectstack/service-analytics@17.0.0-rc.0 + - @objectstack/verify@17.0.0-rc.0 + - @objectstack/service-automation@17.0.0-rc.0 + - @objectstack/trigger-record-change@17.0.0-rc.0 + - @objectstack/plugin-pinyin-search@17.0.0-rc.0 + - @objectstack/console@17.0.0-rc.0 + - @objectstack/plugin-audit@17.0.0-rc.0 + - @objectstack/plugin-reports@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/service-job@17.0.0-rc.0 + - @objectstack/cloud-connection@17.0.0-rc.0 + - @objectstack/driver-mongodb@17.0.0-rc.0 + - @objectstack/metadata@17.0.0-rc.0 + - @objectstack/plugin-webhooks@17.0.0-rc.0 + - @objectstack/plugin-sharing@17.0.0-rc.0 + - @objectstack/service-settings@17.0.0-rc.0 + - @objectstack/account@17.0.0-rc.0 + - @objectstack/setup@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + - @objectstack/driver-memory@17.0.0-rc.0 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.0 + - @objectstack/plugin-email@17.0.0-rc.0 + - @objectstack/service-cache@17.0.0-rc.0 + - @objectstack/service-datasource@17.0.0-rc.0 + - @objectstack/service-messaging@17.0.0-rc.0 + - @objectstack/service-package@17.0.0-rc.0 + - @objectstack/service-queue@17.0.0-rc.0 + - @objectstack/service-realtime@17.0.0-rc.0 + - @objectstack/service-sms@17.0.0-rc.0 + - @objectstack/trigger-api@17.0.0-rc.0 + - @objectstack/trigger-schedule@17.0.0-rc.0 + ## 16.1.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 2de23d57cf..fce9753d11 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 c386400f45..c941066bb9 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,115 @@ # @objectstack/client-react +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [984396b] +- Updated dependencies [0cdb57a] +- Updated dependencies [9f060e5] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [1b717e5] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [094fa34] +- Updated dependencies [5e55739] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [a137bbc] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [48d5a1c] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/client@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 3c8f99dbb5..94c6251cb0 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 a718b96a65..1c6d408171 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,746 @@ # @objectstack/client +## 17.0.0-rc.0 + +### Major Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- a137bbc: feat(client)!: remove the `ai` namespace — three methods, none of which ever worked (#3718) + + `client.ai` held exactly three methods, and **no server in any repo has ever + mounted the URLs they build**: + + | Removed | Built | Why it 404ed | + | -------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `client.ai.nlq` | `POST /api/v1/ai/nlq` | declared in `DEFAULT_AI_ROUTES`, which has no runtime consumer — only the spec's own test reads it; `aiNlq?` is an optional protocol method nothing implements | + | `client.ai.suggest` | `POST /api/v1/ai/suggest` | same | + | `client.ai.insights` | `POST /api/v1/ai/insights` | same | + + Found by the AI route ledger (#3718, in `cloud`, where `service-ai` lives), + which enumerates the table `buildAIRoutes()` returns and matches the SDK's URLs + against it. The two sets are **disjoint**: the real AI surface is 12 routes — + `chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model` and six + `conversations` routes — and the SDK expressed none of them. + + **Removed, not deprecated.** A typed method that always throws is worse than no + method: it costs a runtime round-trip to discover, where absence is a compile + error. No working code can break, because there was no working behaviour. This + lands in the v17 major `@objectstack/client` is already taking, which is the + right window for a breaking removal rather than a reason to defer one. + + Expressing the real surface is tracked on #3718 as **new** API, not a rename of + what was removed. For chat, `useChat()` (`@ai-sdk/react`) already speaks the + Data Stream Protocol `POST /api/v1/ai/chat` serves. + + Also removed: the `AI_PLANE` exemption added to the capstone hours earlier + (#3727). With no method targeting `/api/v1/ai/`, an exemption there is a hole + with nothing to cover — the wildcard-only bound stays `0` and now reaches 0 + with nothing exempted to get there. + + The four AI tests in `client.test.ts` are **replaced, not deleted**. They were + the exact shape this audit keeps finding behind green suites: mock `fetch`, + assert the URL the client _built_, never assert that anything answered it. They + passed for years against three endpoints that did not exist. The replacement + asserts the one thing worth defending — the namespace is gone and must not + return without a route behind it. + + `Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported straight + from `@objectstack/spec/api`, so anyone holding those types keeps them. + Retiring the spec-side declarations is a separate change. + + Docs corrected: `client-sdk.mdx` carried three copy-pasteable examples that + 404ed, and `plugin-endpoints.mdx` had the AI surface **inverted** — it tabled + the three phantom routes and explicitly denied `/ai/chat`, which is mounted. It + now lists the 12 real ones. + +### Minor Changes + +- 0cdb57a: feat(client): `automation.resume()` / `automation.getScreen()` — finish a paused screen flow from the SDK (#3528) + + A `type: 'screen'` flow suspends when it reaches a `screen` node: `execute()` + returns `{ status: 'paused', runId, screen }` and the run waits for input. The + second half of that contract — `POST /automation/:flow/runs/:runId/resume` — + has shipped in the dispatcher since ADR-0019, but the client SDK's automation + surface stopped at `getFlow` / `execute` / `listRuns` / `getRun`. Anything built + on the SDK could therefore _start_ a screen flow and never finish it: the run + stayed suspended and the only way out was hand-rolling the HTTP call. That gap + is what stranded the Console's developer "Flow Runs" test runner, where every + test run of a screen flow orphaned a `paused` row. + + - **`automation.resume(flowName, runId, signal?)`** — posts the collected screen + values as `inputs` (applied as bare flow variables), plus the approval-style + `output` / `branchLabel` the dispatcher already accepts. Returns the next + `{ status: 'paused', screen }` of a multi-step wizard, or the terminal + `AutomationResult`. + - **`automation.getScreen(flowName, runId)`** — the screen a paused run is + waiting on, so a client that did not launch the run (a page reload, another + tab, an inbox) can render the pending step before resuming. + - Both are available on the environment-scoped client + (`client.project(id).automation.*`) as well as the unscoped one. + + Also covers the two dispatcher routes with tests — the resume and screen paths + had none, including the ordering guard that keeps `/runs/:runId/screen` from + being swallowed by the `/runs/:runId` run lookup. + +- d3f2ff6: feat(client): `actions` surface — the SDK path to server-registered actions (#3563 PR-2) + + `client.actions.invoke(object, action, { recordId, params })` and + `client.actions.invokeGlobal(action, opts)` dispatch handlers registered via + `engine.registerAction` (`POST /api/v1/actions/...`). This closes the largest + gap in the #3563 route audit: the whole `/actions` domain — the documented way + to expose custom server-side operations — was unreachable from the SDK, and + every console hand-rolled `fetch` for it. The record id travels in the body, + which both server URL shapes honor; the handler's own business failure comes + back as `{ success: false, error }` rather than a thrown exception. + + The route ledger flips all three `/actions` rows to `sdk` and the gap ratchet + drops 27 → 24. Also takes the documentation-drift findings from the audit: + the client README no longer documents six methods that do not exist, + `CLIENT_SPEC_COMPLIANCE.md` is retired to a tombstone pointing at the + CI-enforced ledger (its "FULLY COMPLIANT" verdict was measured against a + route table nothing consumes), and the docs-site SDK page documents the new + surface. + +- b7550d6: feat(client): `keys`, `shareLinks`, and `security` surfaces (#3563 PR-3) + + Three more domains the route audit found with zero SDK expression: + + - `client.keys.create({ name?, expiresAt? })` — mints a `sys_api_key` + (`POST /api/v1/keys`). The raw secret comes back exactly once; `user_id` + is pinned server-side. There was previously no SDK path to create an API + key at all. + - `client.shareLinks.create / list / revoke` — authenticated management of + record share links. Listing is server-constrained to the caller's own + links; the public token-consumption routes stay browser-only by design. + - `client.security.suggestedBindings.list / confirm / dismiss` — the + ADR-0090 admin surface for package audience-binding suggestions. + + The route ledger flips all seven rows to `sdk` and the gap ratchet drops + 24 → 17. + +- 0164f40: feat(client): the final six route-audit gaps — meta drafts/published/FSM + automation descriptors (#3563 PR-5) + + - `meta.getPublished(type, name)` — the published version of a metadata item + (ADR-0033; compound names pass through unencoded, matching `getItem`). + - `meta.listDrafts({ packageId?, type? })` — pending drafts the active-only + lists hide. + - `meta.getLegalNextStates(object, field, from?)` — ADR-0020 FSM + introspection ("from here, where can this record go?"). + - `automation.listActions({ paradigm?, source?, category? })` / + `automation.listConnectors({ type? })` — the ADR-0018/0022 descriptor + registries backing the Studio designer's pickers. + - `automation.getRuntimeStatus()` — per-flow enabled/bound engine state. + + With these, the #3563 gap ratchet reaches **0** (from 27): every dispatcher + route that should be SDK-expressible is, and the conformance guard keeps it + that way. + +- e295ad1: feat(client): the eleven package-lifecycle methods (#3563 PR-4) + + `client.packages` grows from install/enable to the full lifecycle the server + has shipped for three ADR generations: `update` (manifest edit), + `publish`, `publishDrafts` / `discardDrafts` (ADR-0033 whole-app draft + promotion), `listCommits` / `revertCommit` / `rollback` (ADR-0067 commit + timeline), `revert`, `export`, `adoptOrphans`, `duplicate` (ADR-0070 + portability). All eleven routes existed with no SDK expression — Studio + reached them via raw fetch. + + The route ledger flips all eleven rows to `sdk` and the gap ratchet drops + 17 → 6 (from 27 at the start of the audit). + +- 1003125: feat(client): close the approvals (6) + record-shares (3) REST gaps (#3587 batch 3/5) + + `client.approvals` gains the full request lifecycle beyond approve/reject: + `recall` (submitter withdraw), `revise` / `resubmit` (ADR-0044 send-back + round-trip), and the thread interactions `remind` / `requestInfo` / `comment`. + New `client.shares` namespace for per-record sharing grants: `list` / `grant` / + `revoke` (204-safe) under `/data/:object/:id/shares`. REST route-ledger + ratchet: 26 → 17. + +- 6e62a93: feat(client): close the sharing-rules (5) + security-explain (2) + search (1) REST gaps (#3587 batch 4/5) + + New `client.shares.rules` sub-namespace for tenant-wide sharing rules + (M10.17): `list` / `save` / `get` / `delete` (204-safe, grants cascade) / + `evaluate` (reconcile). `client.security.explain` speaks the ADR-0090 D6 + access-explanation contract via the POST transport (the GET query form is the + same `ExplainRequestSchema`). Top-level `client.search` covers global + cross-object search (M10.5). REST route-ledger ratchet: 17 → 9. + +- ecda20c: feat(client): close the 8 reports-family REST gaps (#3587 batch 2/5) + + New `client.reports` namespace speaking the plugin-reports REST surface: + `list` / `save` / `get` / `delete` (schedules cascade), `run`, `schedule`, + `listSchedules`, `unschedule`. The two DELETE routes return 204 — the client + methods return `{ deleted: true }` without attempting to parse an empty body. + Fixed path (`/api/v1/reports` is not in `ApiRoutesSchema`), matching the + keys / share-links precedent. REST route-ledger ratchet: 34 → 26. + +- 6e62a93: feat(client): close the final 9 REST gaps — ratchet 9 → 0 (#3587 batch 5/5) + + `data.clone` (enable.clone duplication) and `data.export` (streaming + CSV/JSON/XLSX; returns the raw `Response` — a file stream, not a JSON + envelope). New `email.send` (IEmailService; branch on the returned `status`). + `analytics.queryDataset` speaks the ADR-0021 REST dataset-query dialect. New + `datasources.external.*` federation admin: `listTables` / `draft` / `import` / + `refreshCatalog` / `validate` (ADR-0015 Addendum, 503-degrading). Every REST + route is now either SDK-expressed or carries a reviewed non-sdk disposition — + the #3587 gap ratchet rests at ZERO. + +- fc968af: feat(client): close the 9 metadata-family REST gaps the #3587 ledger carried (#3587) + + New `meta` surface: `getDiagnostics` (spec-validation sweep), `getReferences` + (reverse references), `getBookTree` (ADR-0046 §6 spine resolution), `getAudit` + (ADR-0010 §3.6 protection trail), `publishItem` / `rollbackItem` / `diffItem` + (ADR-0033 per-item draft lifecycle). The two compound-name routes + (`GET|PUT /meta/:type/:section/:name`) turned out to be already expressible — + `getItem`/`saveItem` pass slashes through unencoded — so they are flipped to + `sdk` with URL-pinning tests instead of new methods (the audit note claiming + an encoding barrier was wrong; only `deleteItem` encodes). REST route-ledger + ratchet: 43 → 34. + +- 7c7e246: feat(authz): expose the caller's delegable scope — the read half of the + delegated-administration gate (ADR-0090 D12 / ADR-0105 D8) + + `adminScope` decided writes but could not be READ: `assignablePermissionSets` + lived only inside `delegated-admin-gate.ts`, so a UI offering "place this + person in a unit, with these positions" (the D8 scoped-invitation form) had no + way to narrow its pickers. It would list the whole tree and let the user + discover the boundary by being refused — which turns an authorization gate into + a validator and makes the boundary invisible until it bites. + + `ISecurityService.describeDelegableScope(callerContext)` answers it, exposed as + `GET /api/v1/security/my-delegable-scope` and `client.security.describeDelegableScope()`: + + - `placeableBusinessUnitIds` — union of the subtrees where the caller may place + people (scopes granting `manageAssignments`); + - `assignablePositions` — positions whose every distributed permission set the + caller may hand out (containment check included); + - `scopes` — the held `adminScope`s with subtrees resolved, for attribution; + - `isTenantAdmin` — unconstrained, with everything enumerated so a consumer + renders ONE uniform picker instead of special-casing. + + Computed by the same helpers the write gate enforces with, so an option this + reports is one `assert()` accepts — a test asserts that agreement directly. It + NARROWS; the gate still decides. + + Strictly self-scoped: no target-user parameter, so it discloses nothing beyond + the authority the caller already holds (unlike `explain`, which has one and + gates it). Fail-closed — unresolvable scopes contribute nothing, a caller with + no delegated authority gets empty lists, and a deployment without + `@objectstack/plugin-security` gets 501. + +- 094fa34: feat(cli,client)!: drop `os environments create --template` and the + `template_id` body field — no control plane has ever read them (#3731) + + The CLI advertised `--template` as _"Built-in template id (e.g. crm, todo, + blank)"_ and forwarded it as `template_id` on `projects.create()`. Nothing + consumes it: `template_id` / `templateId` appears in **zero** non-test files in + the `cloud` repo, `sys_environment` has no such column, and the create route + whitelists what it reads (`displayName`, `organizationId`, `isDefault`, + `hostname`, `metadata`, …) — `template_id` is not in the list. The + `blank`/`crm`/`todo` registry the flag named was the `apps/server` + `createTemplatesRoutePlugin` snapshot, removed when the control plane moved to + `cloud`; the flag outlived it. + + So the flag was accepted, transmitted, and dropped — no seeding, no error, no + stored trace. That is worse than the 404 its listing counterpart returned + (`projects.listTemplates`, deleted in #3702): a 404 tells the caller something + is wrong, a silently ignored flag reports success. + + **Migration.** `os environments create --template ` → drop the flag; it + never did anything. Starter content comes from the App Marketplace: create the + environment, then install the package (`sys_package` rows with + `is_starter = true`, i.e. `client.projects.packages.install(envId, { packageId })`). + Callers passing `template_id` to `client.projects.create()` should delete the + property — TypeScript now rejects it, which is the point: an unknown field was + being silently discarded on the wire. + + Note this is **not** the same `--template` as `os init` / `create-objectstack` + (`app` / `plugin` / `empty` scaffolds) — those are local scaffolding templates + and are untouched. + +- 5e55739: feat(client)!: delete `projects.listTemplates()` — it targeted a route nothing + has ever mounted (#3702, #3655 finding) + + `client.projects.listTemplates()` built `GET /api/v1/cloud/templates`. That + path is mounted by **nothing**: none of the 17 registrars in `cloud`'s + `cloud-artifact-api-plugin.ts` (91 registrations, enumerated by driving them + against a capturing mock `IHttpServer`), and nothing in this repo — the string + occurred exactly once in each repo, at the call itself. Every invocation was a + 404 with a type signature promising a resolved value. + + "Templates" are real as **data** — `sys_package_templates`, the + `is_starter = true` view over `sys_package`, rendered as a console page — but + there has never been an HTTP route that lists them, and no caller in either + repo (nor in `objectui`) used the method. Mounting a route to satisfy a method + nobody calls is the wrong order: the client's declared shape + (`{ id, label, description, category? }`) does not match `sys_package`'s + columns, so picking that mapping is a product decision, not an implementation + detail. The method returns when a route exists to back it. + + Sixth instance of the `the method exists ≠ the method can be called` class this + audit family keeps finding, after `analytics.explain` / `analytics.meta` + (#3584), `meta.getView` (#3611) and `i18n.getTranslations` / `getFieldLabels` + (#3636) — and the first one only a cross-repo guard could see. The framework + capstone (#3642) exempts the `/api/v1/cloud/` prefix wholesale, because this + repo does not serve those routes; `cloud`'s control-plane ledger (#3655) is + where the mounted set and the SDK are both in scope, and it pins the absence. + + Callers who somehow depended on it were already receiving a 404; read starter + packages through the `sys_package` view (`is_starter = true`) instead. + +- c2d9098: feat(rest/protocol): extend droppedFields write-observability to the bulk paths + client SDK (#3455) + + Follow-up to #3448 (#3431 D2): the single-write PATCH/POST `/data` paths already + surface LEGALLY-stripped write fields (static `readonly` #2948 / `readonlyWhen` + #3042 / #3043 create ingress) as `droppedFields`. The **bulk** write paths did + not — the same strips happened silently on every batched row — and the typed + client warning + CORS mirror were deferred. This closes those out. + + **Bulk passthrough (metadata-protocol).** + + - `updateManyData` and `batchData` (update/upsert rows) now register a per-row + `onFieldsDropped` collector and attach the events to that row's result. + - `createManyData` diffs each supplied row against its #3043-stripped form and + returns an **aggregated** top-level `droppedFields` (one event per + object/reason with the union of field names) — its `{ records, count }` + response has no per-row slot, and the insert-time strip is static-`readonly` + only, so it is schema-uniform across rows and the aggregate is faithful. + - `insertManyData` keeps per-row precision, attaching `droppedFields` to each + outcome. + - **Correctness fix bundled in:** `updateManyData` and `batchData` never threaded + the caller's execution `context` to the engine — bulk writes ran context-less, + so RLS/FLS and `readonlyWhen` evaluated without the caller's principal, and the + batch create-ingress strip was hard-coded to a non-system context. All engine + calls in both methods now run under the resolved `context`. + + **Contract (spec).** `BatchOperationResultSchema` gains an optional per-row + `droppedFields` (covers `updateMany` + `batch`, which alias + `BatchUpdateResponseSchema`); `CreateManyDataResponseSchema` gains the optional + aggregated `droppedFields`. Both are omit-when-empty, so existing clients are + unaffected. `X-ObjectStack-Dropped-Fields` is deliberately **not** emitted for + batches — one response header cannot express per-row drops, so the per-row body + field is the canonical bulk channel. + + **Typed client warnings (@objectstack/client).** `CreateDataResult` / + `UpdateDataResult` gain `droppedFields?: DroppedFieldsEvent[]`, giving the body + channel a type instead of an untyped property. + + **CORS (@objectstack/hono, @objectstack/plugin-hono-server).** + `x-objectstack-dropped-fields` is added to the default `Access-Control-Expose-Headers` + allow-list (kept in lockstep across both Hono CORS sites) so a cross-origin + browser can read the single-write drop header. The body `droppedFields` remains + the primary, cross-origin-safe surface — this is a convenience mirror. + + **GraphQL — not applicable (documented).** #3455 lists a GraphQL mutation item, + but GraphQL has no runtime: `kernel.graphql` is unassigned everywhere and + `handleGraphQL` returns `501`, and discovery never advertises `/graphql`. There + is no schema generator or mutation resolver to expose a typed payload field on, + so there is nothing to wire until a GraphQL engine lands — at which point the + protocol-layer `droppedFields` is already present and only the GraphQL schema + projection would remain. + +- 7ffc3d3: feat(client,spec)!: delete the 21 dead SDK methods and the four ghost route + tables that underwrote them (#3612, #3587 finding) + + Five client surface families built URLs that exist on NO server surface — + not the dispatcher, not `@objectstack/rest`, not the autonomous service + mounts — so every call was a guaranteed 404: + + - `permissions` (check, getObjectPermissions, getEffectivePermissions) + - `realtime` (connect, disconnect, subscribe, unsubscribe, setPresence, + getPresence) — `service-realtime` registers zero HTTP routes and the + dispatcher deliberately never advertises `/realtime` + - `workflow` (getConfig, getState, transition) + - `views` CRUD (list, get, create, update, delete) — no `/ui/views` route + anywhere + - `notifications` device/preference helpers (registerDevice, + unregisterDevice, getPreferences, updatePreferences) — the ADR-0012 + server side was never built + + Each family was underwritten only by an unconsumed spec `DEFAULT_*_ROUTES` + table — the same disease `DEFAULT_DISPATCHER_ROUTES` had (#3586) — so + `DEFAULT_PERMISSION_ROUTES`, `DEFAULT_VIEW_ROUTES`, `DEFAULT_WORKFLOW_ROUTES`, + and `DEFAULT_REALTIME_ROUTES` are deleted with them; + `getDefaultRouteRegistrations()` now returns 9 registrations. + `ApiRouteType` loses its client-only `'views' | 'permissions'` extras. + + Kept: `client.events` (explicitly local in-memory buffer, no HTTP), + `notifications.list/markRead/markAllRead` (dispatcher-served), + `approvals.*` (ADR-0019 — the real approval decision API), and + `meta.getLegalNextStates` (the real FSM read). + + Breaking for anyone calling the removed methods — a repo-wide and + objectui-wide sweep found one consumer (`useClientNotifications`'s dead + device/preference delegates, trimmed in the objectui companion change); + shipped as minor per the launch-window convention (cf. #3562/#3581/#3595). + Re-adding any of these surfaces requires the server route to exist and a + route-ledger row proving it (#3569/#3609 guards). + +### Patch Changes + +- 37b1346: feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1) + + `POST /api/v1/storage/upload/complete` now returns the opaque `sys_file` id + (`data.fileId`), and `client.storage.upload()` surfaces it on the returned + `FileMetadata`. Previously the commit response omitted the id — the caller + could not learn which id to persist after committing an upload, so a file + field could never store a reference. + + Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the + client falls back to the presigned id when talking to an older server). This is + the enabling foundation for file-as-reference; the storage model itself is + unchanged in this PR. + +- 0bab8bb: fix(client,runtime): analytics.meta/explain now call routes that actually exist (#3584) + + The route audit (#3563) ledgered four dispatcher↔client shape mismatches. + Re-verification showed the two analytics shapes the client spoke — + `GET /analytics/meta/:cube` and `POST /analytics/explain` — were served by + **nothing**: not the dispatcher, not `@objectstack/rest`, not + `service-analytics`. Both methods 404ed against every deployment. + + - `analytics.meta(cube?)` — FROM `GET /analytics/meta/:cube` TO + `GET /analytics/meta[?cube=]`. The cube argument is now optional; when + given, the dispatcher threads it into `AnalyticsService.getMeta(cubeName?)`, + which always supported the filter. Responses now use the dispatcher envelope + (`{ success, data }`). + - `analytics.explain(payload)` — FROM `POST /analytics/explain` TO + `POST /analytics/sql` (the dispatcher's SQL dry-run route, backed by + `generateSql`). Method name unchanged. + + No migration is expected in practice: a method that unconditionally 404ed can + have no working callers (none exist in objectstack or objectui). Anyone who + had hand-rolled fetches against the imaginary shapes should switch to the + routes above. + + The two storage rows from the same audit are deliberately NOT reshaped: the + presigned/chunked protocol the SDK speaks is registered autonomously by + `service-storage` on any http-server and stays canonical; the dispatcher's + bare `POST /storage/upload` / `GET /storage/file/:id` are reclassified in the + route ledger as a `server-only` low-level compat surface. + +- 984396b: test(plugin-auth): enumerate better-auth's route table — the `/auth/**` wildcard becomes 55 exact rows (#3656) + + The widest hole the #3642 capstone measured. That guard reports how many SDK + calls match only a `**` prefix family rather than a resolvable route, and the + answer was 60 of ~196 — with 54 on `* /auth/**`, the largest and most + security-relevant namespace in the client. `auth.me` builds + `/api/v1/auth/get-session`; a prefix claim cannot tell you better-auth still + calls it that, and better-auth is a third-party dependency on its own release + cadence (this repo already chased its 1.7 column drift in #3624 / #3647). + + `plugin-auth` mounts it with a single catch-all, so there are no per-route + registration calls to capture the way tranche 3 captured + `registerStorageRoutes`. The seam is `auth.api`: every better-auth endpoint + carries `.path` and `.options.method`, so a live instance is the route table. + + `auth-route-ledger.ts` reads it, in two halves checked differently on purpose: + + - **55 reviewed rows** — every route the SDK calls, each naming its client + method, checked strictly against the live table. This is the rename detector. + - **129-path mounted-surface inventory** — checked for exact equality both + ways, so a version bump that adds publicly-mounted auth endpoints becomes a + reviewable CI diff. Machine-maintained rather than reviewed prose: demanding + a rationale for all 129 would make every better-auth upgrade a hundred-row + review and the ledger would rot into rubber-stamping. + + Enumeration is config-dependent, so the inventory is pinned at the + configuration enabling every plugin the SDK targets — the maximal surface — + with the participating `OS_*` env vars cleared so a developer's shell cannot + produce a spurious diff. Mutation-checked: renaming a ledgered route fails the + suite naming it. + + The capstone guard now includes this ledger in its union and prefers exact rows + over wildcard families when matching — without that ordering fix every + `/auth/*` URL would still have been absorbed by `* /auth/**` and the new ledger + would have changed nothing. Wildcard-only matches fall **60 → 3**; the ratchet + moves with them. What remains is `* /ai/**`, whose routes `service-ai` builds + at plugin start. + + No runtime change: a ledger, a guard, and the header/audit-doc notes. + +- 1b717e5: test(client): close the route audit's reverse direction — every SDK URL must match a route some surface mounts (#3642) + + The capstone of the #3563 route audit. The dispatcher (#3563), REST (#3587) and + service-mount (#3636) ledgers all run server → client: enumerate what a surface + mounts, demand a reviewed disposition, and for `sdk` rows demand the named + client method exists. None of them asked the reverse question — does the URL + the client _builds_ match anything a server _mounts_? — so a method could name + a real function, carry a green ledger row, and 404 everywhere. + + That shipped four times, found one at a time by hand: `analytics.explain` and + `analytics.meta` (#3584), `meta.getView` (#3611), and `i18n.getTranslations` / + `getFieldLabels` (#3636) — the last pair having carried green `sdk` rows since + tranche 1. + + `client-url-conformance.test.ts` drives every method on a real client with a + recording `fetch` and matches each captured URL against the union of all four + ledgers. A real drive rather than a hand-written "method X targets route Y" + table, because such a table is an assertion _about_ the code that the code can + drift away from — the exact failure being fixed. Mutation-checked: re-injecting + the #3636 dialect bug fails the suite. + + The sweep's own completeness is asserted, since that is what rots silently — a + new method must be driven or declared `NON_HTTP` with a reason; a driven method + emitting zero requests fails (stale placeholder args are how a sweep quietly + stops covering anything); a URL containing `undefined` fails; and the + `__api-endpoint` `(unmatched)` catch-all is excluded from the pattern set so it + cannot match everything and make the suite vacuous. + + 196 of ~219 methods matched. Two bounds are reported rather than papered over: + `/api/v1/cloud/*` (23 `projects.*` methods) belongs to the sibling `cloud` repo + and is exempt by prefix, bounded so no other namespace can use it (#3655); and + 60 of ~196 matched calls rest only on a `**` prefix claim rather than a + resolvable route — 54 of those on `* /auth/**` — a count the guard ratchets so + it can only shrink (#3656). + + No runtime change: this is a guard plus the ledger-header and audit-doc notes + recording what it does and does not cover. + +- 16adb3c: fix(rest,client)!: reconcile the two REST↔client mismatches the #3587 audit + ledgered (#3610, #3611) + + **#3610 — `POST /api/v1/packages` publish-vs-install collision.** The REST + package registrar claimed the bare `POST /packages` for _marketplace publish_ + (`{manifest, metadata}`), while the dispatcher packages domain gives the same + verb+path _install_ semantics — and REST registers first in the production + stack (first-match-wins), so every `client.packages.install` call landed on + the publish handler and 400'd. Marketplace publish moves to + `POST /api/v1/packages/publish` (breaking for direct callers; a repo-wide and + objectui-wide sweep found zero). The dispatcher's `POST /packages/:id/publish` + (ADR-0033 draft publish) is two segments — different shape, no clash. The + dispatcher already writes both stores on install (`protocol.installPackage`) + and fully uninstalls on DELETE (`protocol.deletePackage`), so the remaining + REST GET/GET/DELETE shadows stay — they are compatible. + + **#3611 — UI view dialect split.** `meta.getView` spoke the `?type=` query + dialect that only the dispatcher `/ui` domain understands; the REST surface + mounts only the path form `/ui/view/:object/:type`, so the query form 404'd + wherever REST serves (e.g. project-scoped bases). The client now sends the + path form both surfaces accept; a URL-pinning test keeps it that way. + + REST route ledger updated: the two `mismatch` rows are resolved (packages + publish row is `server-only` publisher tooling; the ui row flips to `sdk`). + The ledger now carries zero mismatches. + +- 3d5f726: feat(rest): route audit tranche 2 — the REST surface gets its own ledger + + conformance guard (#3587, follow-up to #3563) + + The dispatcher tranche closed its 27 gaps and guards them (#3569…#3579), but + `@objectstack/rest` mounts a second, larger surface the client also reaches — + 89 routes, never audited. `rest-route-ledger.ts` now records a reviewed + disposition for every one of them (38 sdk, 43 gap, 3 server-only, 3 public, + 2 mismatch), and the guard is real enumeration on both sources: RouteManager + routes via the `getRoutes()` introspection seam, and the two + RouteManager-bypassing registrars (`package-routes.ts`, + `external-datasource-routes.ts`) via captured mock-server registrations — no + pinned-by-hand list. The client half + (`rest-route-ledger-coverage.test.ts`) verifies every claimed method exists; + a 43-gap ratchet is wired into CI. Every guard direction was negative-tested. + + Notable dispositions the audit surfaced: `POST /api/v1/packages` is a + publish/install shape collision between REST and the dispatcher (REST + registers first and wins) — ledgered `mismatch`; the REST + `GET /ui/view/:object/:type` path dialect is unreachable by the SDK's + query-param dialect — ledgered `mismatch`; `service-storage` / + `service-i18n` mount a third route surface outside `@objectstack/rest`, + explicitly out of scope here and tracked under #3587. + + No behavior change — data + tests only, plus a scope-note refresh in the + runtime ledger pointing at the new REST ledger. + +- f1a8114: fix(client,service-i18n): ledger the autonomously-mounted service routes, and repair the two i18n calls that reached nothing (#3636) + + Tranche 3 of the #3563 route audit — the last un-audited server surface. The + dispatcher ledger (#3563) and the REST ledger (#3587) each stop at their own + package boundary, and two services mount routes outside both: they reach for + the `http-server` service and register straight on `IHttpServer`, so neither + `RouteManager` nor `RestServer.getRoutes()` has ever seen them. That left the + SDK's entire storage surface, plus all of i18n, in the pre-#3563 posture: + expressed, working, guarded by nothing. + + **Ledgers + guards.** `storage-route-ledger.ts` (10 routes) and + `i18n-route-ledger.ts` (3) sit next to the registrars that mount them, each + enumerated for real — the registrar runs against a capturing mock + `IHttpServer` and its registration calls _are_ the route set, so a new route + lands with a reviewed disposition or fails CI. The client half is + `packages/client/src/service-route-ledger-coverage.test.ts`; ledgers cross the + boundary as relative source imports, never a service→client package edge. + + **Two wire-level 404s fixed.** `i18n.getTranslations` sent + `/i18n/translations?locale=xx` and `i18n.getFieldLabels` sent + `/i18n/labels/:object?locale=xx`, while every serving surface — service-i18n's + mounts, the dispatcher's HTTP mounts, and the `plugin-rest-api.zod.ts` + contract — mounts only the path form. Neither call could ever be answered. + Both had carried a green `sdk` row in the dispatcher ledger since tranche 1, + because that guard asks whether the client _method_ exists, not whether it + speaks a URL anything mounts. The client now sends the path dialect, the same + resolution #3611 gave `meta.getView`, and a new suite drives the real client + at a real router so a revert cannot pass quietly. + + **One response-shape fix.** service-i18n's success bodies omitted the + `success` flag that `ObjectStackClient.unwrapResponse` keys on, so the SDK + returned the raw `{ data: … }` wrapper against that provider while returning + the declared unwrapped shape against the dispatcher — one method, two shapes, + decided by which plugin mounted the route. Its three handlers now emit the + `{ success: true, data }` envelope the `i18n` route group declares. `data` did + not move, so direct body readers are unaffected. + + Storage audited clean: 7 routes SDK-expressed, 3 reviewed `server-only` (the + browser capability URL objectql stamps into file-field payloads, and the two + local-driver loopbacks). The chunked-upload family, flagged for triage, turned + out fully expressed. Both ledgers ratchet `gap` and `mismatch` at zero. + + Filed, not fixed: `GET {base}/_local/file/:key` is built by three call sites + and mounted by none (#3641); the cross-surface URL conformance guard that would + have caught all of the above mechanically is the capstone (#3642). + +- 48d5a1c: Route ledger + conformance guard for the dispatcher↔client surface (#3563) + + #3528's root-cause class — a route that exists and works while + `@objectstack/client` has no way to express it — now has an inventory and a + ratchet. `route-ledger.ts` records the audited disposition of every dispatcher + route (sdk / gap / server-only / public / dynamic / mismatch); + The guard is split along the package boundary (a runtime→client edge is a + build cycle): runtime's `route-ledger.conformance.test.ts` fails when a + dispatcher domain lands with no ledger entry and ratchets the audited gap + count (27 at PR-1); client's `route-ledger-coverage.test.ts` fails when a + ledger entry claims a client method that doesn't exist. Findings and follow-up slicing live + in `docs/audits/2026-07-dispatcher-client-route-coverage.md`. No runtime + behavior change. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/client/package.json b/packages/client/package.json index a7a11340ee..1dd69859cd 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 1bd6a57c68..c2af4bf21a 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,207 @@ # @objectstack/cloud-connection +## 17.0.0-rc.0 + +### Patch Changes + +- 402f534: fix(objectql): bridge late-registered manifest objects into the metadata service + + Marketplace-installed template packages register through the `manifest` + service on `kernel:ready` (install) or later (HTTP install), but the one-shot + SchemaRegistry→metadata bridge runs once during `ObjectQLPlugin.start()` — + so their objects only ever reached the ObjectQL registry. Every + IMetadataService consumer (AI `describe_object`, Studio object lists, + `metadata.listObjects`) missed them; only the seed loader had grown an + engine-side fallback (#3422). + + The manifest service's `register` now bridges the manifest's own objects into + the metadata service after registering them with the engine, resolving the + service at call time and mirroring the startup bridge's contract: + `register('object', name, obj, { notify: false })` (#3112), skip entries it + did not bridge itself, refresh its own copy on same-package re-install (hot + upgrade). Armed only after `start()` has run the one-shot bridge, and never + on project kernels — boot-time behavior is unchanged. `register` now returns + a promise; the marketplace install/rehydrate paths await it so metadata reads + right after an install are deterministic. + +- 1c8bf4f: fix(marketplace): heal missing sample data when rehydrating installed packages onto a new database + + The install ledger (`.objectstack/installed-packages/`) is anchored to the + project directory while the database can be swapped out from under it — + `os dev --fresh`, a deleted `dev.db`, a `--database` switch. Rehydrate + deliberately never re-seeds (existing rows must not be re-upserted over user + edits on every boot), which left a rehydrated marketplace package PERMANENTLY + empty on a new database: app in the switcher, tables created, zero rows — the + "HotCRM installed but every KPI is 0 / Sales Pipeline all-empty" state. + + Rehydrate now runs the bundled seed datasets iff the manifest actually bundles + them, the user never explicitly purged them, the runtime is single-tenant + (multi-tenant seeding stays owned by the per-org replay), and EVERY seeded + object is empty — one surviving row anywhere means the data is still there and + nothing is touched, so the heal is idempotent across restarts and can never + revert user edits. + + Also fixed along the way: a purge now stamps `sampleDataPurged` on the ledger + entry (so healed restarts respect the deliberate empty baseline), and install + marks `withSampleData: true` when the seed run reports all rows _skipped_ + (already present, e.g. a reinstall over live demo data) instead of leaving the + flag false over a seeded database. + +- 810a3a2: fix(runtime,cloud-connection): multi-tenant seed replay covers every source, not just the first (#3453) + + In multi-tenant deployments (enterprise `@objectstack/organizations`) a brand-new org + gets its own private copy of demo data by replaying the kernel's `seed-datasets` list + on the `sys_organization` insert. That list is meant to hold the union of every seed + source — every config-declared app AND every marketplace package — but two framework + traps (the same pair #3444 fixed for seed-summary) shrank it to just the first source: + + - The standard `PluginContext` exposes `getService`/`registerService` but has NO + `.kernel` handle, so `(ctx as any).kernel?.getService('seed-datasets')` always read + `undefined`. Each source then saw "nothing registered" and overwrote the list with + only its own datasets instead of extending it. + - `registerService` throws on a duplicate name, so the second source's re-register was + swallowed by the surrounding try/catch — its datasets (and, for a config app, its + replayer) silently lost. + + Net effect: with two config apps, or a config app plus marketplace packages, a new org + replayed only the first app's seeds. + + The fix mirrors #3444's seed-summary hardening: `seed-datasets` is now a single shared + array, registered once and mutated in place by every source through a new + `mergeSeedDatasets` helper that reads via the context's own resolver first. AppPlugin's + per-org replayer reads that live list at invoke time instead of a captured snapshot, so + it replays the full union — including datasets merged after its closure was built — and + the replayer itself is registered once and reused by later config apps. + + Covered by seam-level unit tests (accumulation across app + marketplace sources; the + replayer reads the live union). True multi-tenant end-to-end coverage requires the + enterprise `@objectstack/organizations` plugin, which lives in the cloud repo. + +- d60968c: Surface marketplace rehydrate/heal seed outcomes in the `os dev` / `os serve` boot banner (#3430), extending the config-app Seeds line from #3415. + + The seed pipeline's most useful result lines are all `logger.info`, but `os dev` forwards a default `warn` level and the serve boot-quiet window swallows stdout — so "marketplace package rehydrated onto a fresh DB with 0 rows", a fresh-DB self-heal, and row-level seed failures were all invisible unless you queried the database directly. + + The `seed-summary` kernel service is now a per-source list. AppPlugin (config apps) and the marketplace rehydrate/heal path each contribute a labelled entry, and the banner prints one combined line that ignores the log level: + + ``` + Seeds: showcase 162 rows · hotcrm(marketplace) 157 ok / 5 errors ⚠ + ``` + + Fresh-DB heals are marked `(healed on fresh db)`; a marketplace package that installed with seed datasets but landed 0 rows, and any run that dropped records, escalate to a yellow `⚠` line instead of passing silently. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 0ddf650c51..e115cd3151 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 eb093e1cfe..7827961cc0 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/connector-mcp +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 45edcf4f1f..0b4475d7db 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 167dcc00f2..9272fa872b 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/connector-openapi +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 3b2166e832..52ca123413 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 f42fc67541..2d0d3d161f 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/connector-rest +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index b00d850642..5490c1dc50 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 4688105eeb..851120b941 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/connector-slack +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 6df727924f..b22d62db71 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 6bc0632d79..4445af8e8c 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/console +## 17.0.0-rc.0 + +### Minor Changes + +- b96c11b: Console (objectui) refreshed to `2cb8d78e24ad`. Frontend changes in this range: + + - fix(console): dispatch flow actions from every surface + cover the screen-flow round trip (framework#3528) (#2833) + - feat(approvals): typed output pickers, quick-path guard, expression completion (framework#3447, #2829) (#2831) + - fix(console): make a paused screen flow completable, and stop the runner from tearing down its host (framework#3528) (#2830) + - feat(fields): adopt the file-as-reference value shape — ObjectStack ADR-0104 D3 wave 2 (PR-7) (#2828) + - fix(console): resolve a modal action's `target` as a page, not an object (#3530) (#2826) + - feat(approvals): dynamic decision-output fields + expression approver editing (framework#3447 P2) (#2827) + - feat: render the server's effective API operation set (#3391 PR-4) (#2823) + - fix(console): approval timeline attachment chip shows its name and opens (#2820) (#2821) + - fix(i18n): localize FileField upload widget + approvals snapshot field labels (#2819) + - feat(report)!: drop SpecReportColumn/SpecReportGrouping re-exports + retire the legacy ReportViewer chart fallback (#3463) (#2816) + - feat(plugin-grid): "Import as historical data" option in the Import Wizard (framework #3479) (#2815) + - feat(app-shell): toast when a save silently dropped read-only fields (framework #3431/#3455) (#2814) + - fix(app-shell): remove never-firing `record-change` option from the flow trigger picker (#3427) (#2812) + - fix(form): scroll+focus the first errored field on invalid submit (#2793) (#2813) + - feat(approvals): label pending-approver chips with their group (objectui#2807) (#2811) + - feat(approvals): label pending-approver chips with their group (objectui#2807) (#2811) + - fix(approvals): surface the admin override for a stuck request in the inbox (#3424) (#2810) + - feat(studio): first-class notify flow node in the Studio palette + inspector (#2808) + - feat(app-shell): Studio flow start node offers a "Record created or updated" trigger (#3427) (#2809) + - fix: read spec-canonical keys for dashboard header title and field length rules (#2806) + - fix(kanban): surface off-column records in an Uncategorized lane (#2792) (#2804) + - fix(approvals): Approval Center density + amount emphasis (#2762 P2) (#2805) + - fix(i18n): 补齐记录详情审批按钮与弹窗的国际化文案 (#2791) + - fix(approvals): Approval Center triage + drawer readability pass (#2762 P1-2/3/4/5, P2) (#2803) + - feat(app-shell): surface step warnings in the Flow Runs panel (#3407) (#2802) + - feat(studio): surface the enable.searchable toggle in ObjectSettingsPanel (#2800) (#2801) + - feat(app-shell): localize the automations flow designer & inspector (en-US + zh-CN) (#2796) + - feat(form): consume spec-aligned FormView buttons/defaults in ObjectForm (#2790) + - fix(approvals): Approval Center UX pass — badge nowrap, approve confirm, progress bar, localized declared actions (#2762) (#2789) + - feat(app-shell): group/coalesce repeat notifications in the message center (#2765) (#2788) + - fix(app-shell): 首页与消息中心的未国际化文案 (#2787) + - fix(app-shell): give inline `lookup` action params a real record picker (#3405) (#2786) + - fix(app-shell): map raw sys_activity rows in the inbox Activity tab (#2781) (#2782) + - fix(app-shell): i18n the "Switch Object" breadcrumb dropdown label (#2783) + - fix(data-table): keep right-pinned action column header sticky on horizontal scroll (#2785) + - fix(app-shell): keep list-origin back link when switching detail tabs (#2775) + + objectui range: `cf2d56e32a11...2cb8d78e24ad` + +### Patch Changes + +- 6ba3788: Console (objectui) refreshed to `09c6a177bb4a`. Frontend changes in this range: + + - fix(grid): localize import result errors (objectstack#3566) (#2861) + + objectui range: `c6cfdf1288b6...09c6a177bb4a` + ## 16.1.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 38f91978e0..333592af68 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 abf6957e91..69bf4aef9d 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,211 @@ # @objectstack/core +## 17.0.0-rc.0 + +### Minor Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 1415c8261a..2b960c147f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 5ba4d492aa..af1490ca37 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,67 @@ # create-objectstack +## 17.0.0-rc.0 + +### Patch Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- 4e9e184: chore(deps): OSV security batch — bump tar to ^7.5.21 (GHSA-r292-9mhp-454m) and + js-yaml to ^5.2.2 (GHSA-pm4m-ph32-ghv5) + + Both are declared-range bumps to the patched releases, so downstream installs + resolve the fixed versions from the published manifests, not just this + workspace's lockfile. The same batch clears the remaining transitive advisories + (next 16.2.11 in apps/docs; workspace overrides for brace-expansion, sharp, + react-router, @sveltejs/kit, @hono/node-server) — those live in pnpm-workspace.yaml + and the private docs app, which do not ship. + ## 16.1.0 ## 16.0.0 diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 9676ab51e2..0587314f14 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "16.1.0", + "version": "17.0.0-rc.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/create-objectstack/src/templates/blank/objectstack.config.ts b/packages/create-objectstack/src/templates/blank/objectstack.config.ts index 67c7699d64..5e1435a077 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.config.ts +++ b/packages/create-objectstack/src/templates/blank/objectstack.config.ts @@ -16,7 +16,7 @@ export default defineStack({ // refuse this package at the boundary with the exact migration command, // instead of crashing later. Kept in lockstep with releases by // scripts/sync-template-versions.mjs. - engines: { protocol: '^16' }, + engines: { protocol: '^17' }, }, // `automation` backs flow execution and, per ADR-0097, materializes any diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 5b627698d1..4cabeb9cf7 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -11,16 +11,16 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@objectstack/spec": "^16.0.0", - "@objectstack/runtime": "^16.0.0", - "@objectstack/driver-memory": "^16.0.0", - "@objectstack/plugin-hono-server": "^16.0.0", - "@objectstack/connector-rest": "^16.0.0", - "@objectstack/connector-openapi": "^16.0.0", - "@objectstack/connector-mcp": "^16.0.0" + "@objectstack/spec": "^17.0.0", + "@objectstack/runtime": "^17.0.0", + "@objectstack/driver-memory": "^17.0.0", + "@objectstack/plugin-hono-server": "^17.0.0", + "@objectstack/connector-rest": "^17.0.0", + "@objectstack/connector-openapi": "^17.0.0", + "@objectstack/connector-mcp": "^17.0.0" }, "devDependencies": { - "@objectstack/cli": "^16.0.0", + "@objectstack/cli": "^17.0.0", "typescript": "^6.0.0" } } diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index cc52d44e70..1ba79e3194 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,104 @@ # @objectstack/formula +## 17.0.0-rc.0 + +### Minor Changes + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 7227c1fa3c..ebafcbde47 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 a1f6e9b4d7..aa099d281b 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,729 @@ # @objectstack/lint +## 17.0.0-rc.0 + +### Minor Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508) + + - `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a + designer must source each approver row's `value`: `user`/`team`/`department`/`position` + are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` / + `sys_business_unit` / `sys_position`; `position` commits the machine **name**, the + others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`), + `manager` is auto-resolved, `field` names a trigger-object field, and `queue` is + unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`. + - `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep + loading and rendering) but is published in `xEnumDeprecated`, so designers stop + offering it — the runtime has no queue resolution and the slot routes to nobody. The + approver `value` xRef now also maps `manager`, so designers can render its + auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries + `{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete + `user`) until a real ownership-queue implementation lands. + - `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue` + approver is skipped. + - `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver + types that are declared but not implemented by the runtime. + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +- 9bf4588: feat(lint): flag never-firing record trigger tokens at authoring time (#3427) + + New `flow-trigger-unknown-event` rule in `validateFlowTriggerReadiness`: a flow + start node whose `triggerType` is record-lifecycle-shaped + (`record-before|after-`) but names an op the record-change trigger cannot map + — e.g. a typo like `record-after-updated` — binds to the record-change trigger + yet maps to no ObjectQL hook and never fires, with only a runtime warning. The + rule surfaces that never-fire defect at `os validate` time. Warning severity; + bare `record-` shapes (e.g. `record-change`) are out of scope. + +- f2b8ac9: Navigation reachability vs. granted access (issue #3583, assessment R5) + + `validate-nav-access` joins what an app's navigation exposes against + `buildAccessMatrix` — the first lint consumer of the ADR-0090 D6 matrix, which + previously only backed `os compile`'s snapshot gate. An object in the menu that + no permission set grants read on renders as an entry and then fails + permission-denied when opened: it works while you browse as an administrator + (the platform's built-in `admin_full_access` carries a wildcard grant) and + breaks for exactly the users the app ships permission sets for. + + Advisory severity — a grant can legitimately come from a permission set another + installed package ships. Quiet by construction in three cases: platform-provided + objects (their own packages grant them), stacks that declare no permission sets + at all (permissions managed elsewhere, so flagging every entry says nothing), + and any stack where a set carries a wildcard `objects: { '*': … }` grant — the + shape `admin_full_access` itself uses, which the access matrix records under the + literal key `*`. + + Wired into `os validate`, `os lint`, and `os compile`. + +- 2a5f04a: `` aggregate result-column naming is now a contract, and its axis bindings are validated (issue #3701) + + Split out of #3583 Phase 2 (#3684), which extended ADR-0021 axis checking to + report charts, list-view charts, and dataset-bound page chart components but had + to leave the react `` block out: it is OBJECT-bound (`objectName` + + an inline `aggregate`), `aggregate` existed in the contract only as the + description string `'{ field, function, groupBy }'`, and nothing in the repo said + what the aggregated result columns were called. Without that, `xAxis`/`yAxis` had + nothing to resolve against, and guessing a convention would have manufactured + false positives (ADR-0072 D1). + + **The convention, recorded rather than invented.** Every path that can serve an + object-bound chart already agreed — the engine's structured-`groupBy` aggregate + (whose alias objectui sets to `field || function`), the legacy analytics query + (which remaps its measure key back to `field`), the client-side fallback, and the + console's own chart-view wiring (`xAxisKey: groupBy`, `series[].dataKey: field`). + `packages/spec/src/ui/chart-aggregate.ts` writes it down and exports it: + + - an object-bound aggregate returns rows keyed by the **raw field names** — + `groupBy` for the category column, `field` for the value column, the literal + `count` for a fieldless count, plus `__comparison` under a comparison + overlay; + - `chartAggregateCategoryKey` / `chartAggregateValueKey` / `chartAggregateResultKeys` + derive those columns so producers and checkers cannot re-derive them apart; + - `ChartAggregateSchema` replaces the description string with a real Zod schema + and rejects a non-`count` function with no `field` (which used to reach the + renderer as `sum(undefined)` and render blank). + + This is the deliberate opposite of the dataset path, whose rows are keyed by the + declared measure `name` (`sum_amount`) — the trap `chart-measure-unknown` catches. + Only the dataset path has an author-chosen name to key by. + + **``'s contract now names the props it actually reads.** The block + consumes `xAxisKey` and `series[].dataKey`; `ChartConfig`'s `xAxis`/`yAxis`/`series` + shapes reached it and were silently dropped, which ADR-0078 forbids. They are + removed from the block's `dataProps`; `chartType`, `xAxisKey`, and `series` are + declared in the React overlay where the other bindings live. + + **`validate-react-page-props` now reads attribute VALUES**, not just names, for + ``: + + - `react-chart-field-unknown` (error) — `aggregate.field` / `aggregate.groupBy` + naming a field the bound object does not declare; + - `react-chart-aggregate-invalid` (error) — an unimplemented aggregation + function, or a non-`count` function with nothing to aggregate; + - `react-chart-axis-unknown` (error) — `xAxisKey` / `series[].dataKey` naming a + column the aggregate does not return (including a dataset-style `sum_total`), + or a category axis bound to the value column; + - `react-chart-axis-inert` (warning) — the `xAxis` / `yAxis` shapes this block + never reads. + + Value reading is opt-in per block and evaluates only static literals: a prop + driven by React state or a variable, a usage carrying a `{...spread}`, a chart + given inline `data`, and objects another package defines are all skipped + silently — an unresolvable binding is not a wrong one. + +- 4f740b0: ``'s author contract is the spec `ChartConfig` shape again (issue #3729) + + #3701 trimmed `xAxis`/`yAxis`/`series` out of the `` contract + because the renderer read `xAxisKey`/`series[].dataKey` and silently dropped the + ChartConfig shapes — an honest record of the runtime gap, not the target state. + objectui#2880 closed the gap the other way round (the renderer now honors + `ChartConfig` through one normalization boundary), so the contract follows the + protocol again (ADR-0082 D1: the spec schema IS the protocol). + + **Contract.** `type`, `xAxis`, `yAxis`, `series`, `subtitle`, `showDataLabels`, + `annotations` and `interaction` are published from `ChartConfigSchema`; the + internal `chartType`/`xAxisKey`/`series[].dataKey` spellings leave the author + contract. `annotations` and `interaction` gained the `.describe()` they never + had, so the generated contract stops publishing bare `object[]` with no meaning. + + **The `type` exception.** `ChartConfig.type` is the chart family, but on any + surface that flattens chart config into a props bag `type` is already the SDUI + envelope's component discriminator — an author writing `type="bar"` used to + replace `object-chart` and the block stopped resolving. The collision is created + by the flattening and is resolved there (objectui's react-page wrapper), so the + contract can publish `type` as the spec spells it. The contract generator's + blanket `type` skip is now overridable by an explicit `dataProps` allow-list, + since for this one block `type` is a real author prop. + + **Lint.** `validate-react-page-props` reads the axes in the spec spelling — + `xAxis.field`, `yAxis[].field`, `series[].name` — and keeps accepting the + internal spellings silently, because dashboards and the console's own chart-view + wiring emit them. `react-chart-axis-inert` is retired: the props it warned about + are honored now, so the warning would be false. The three binding-integrity + rules from #3701 are unchanged. + + **Spec.** `chart-aggregate.ts` records the constraint the whole result-column + convention rests on: an inline `aggregate` is SINGLE-MEASURE. Keying rows by the + raw field name only works because there is exactly one measure to key; two + measures over one field would collide, and resolving that needs an author-chosen + name per measure — which is what a dataset is. Widening `ChartAggregateSchema` + into a measures array would silently invalidate every axis binding these rules + validate, so the boundary is now written down rather than left to be rediscovered. + + The chart taxonomy note is corrected too: grouped/stacked bar and stacked area + are absent from `ChartTypeSchema` not because they render as their base chart, + but because stacking is a property of the SERIES (`ChartSeries.stack`), not a + chart family — one `bar` family plus a series stack group expresses all three. + `ChartInteraction.zoom` is now marked declared-not-delivered in its own + description rather than reading as shipped. + +- 17749fc: Page-component field bindings and non-dashboard chart bindings (issue #3583, Phase 2) + + Two more reference-integrity rules from the #3583 assessment, both wired into + `os validate`, `os lint`, and `os compile`. + + **`validate-page-field-bindings`** — `PageComponent.properties` is an untyped + bag, so a highlights strip, KPI card, or details section can name a field the + bound object does not have; the component silently skips it. Which object a + component binds follows `dataSource.object` → `properties.object` → the page's + `object`, so multi-object pages are checked per element. `record:related_list` + resolves its columns/sort/filter against the **related** object and its + add-picker against that picker's own object. Advisory (matching + `FORM_FIELD_UNKNOWN`). Relationship paths, system fields, cross-package objects, + and unregistered component types are skipped. + + **`validate-chart-bindings`** — extends ADR-0021 axis checking past dashboards to + report charts (`report.chart` and `report.blocks[].chart`), list-view charts + (`views[].list`, `views[].listViews.*`, `objects[].listViews.*`), and + dataset-bound page chart components. An axis naming a raw field instead of a + declared measure is an **error** (the series comes back empty); an axis naming a + declared-but-unselected measure is a **warning**. The report shape needed its own + handling: `ReportChartSchema` narrows `xAxis`/`yAxis` to bare strings, which the + dashboard rule's array guard skips silently. The react `` block is + object-bound, not dataset-bound, and is deliberately left out — nothing defines + what its aggregate names the result column. + + **Fixes:** the page walk used by `validate-action-name-refs` read a top-level + `page.components` array, which `PageSchema` does not have — components live under + `regions[].components[]` and `slots`, and sub-trees nest inside the untyped + `properties` bag (`children`, `items[].children`, `body`, `footer`) rather than a + `children` key on the component. The rule was therefore visiting nothing on a + schema-parsed stack. Traversal now lives in one shared, tested module; on the + showcase app it reaches 194 components where the previous shape found 46. + Source-authored pages (`kind: 'html' | 'react' | 'jsx'`) are skipped — their + `regions` hold a derived cache the `source` wins over. + +- 4340f13: feat(lint,cli): flag flow `update_record` writes to readonly fields at design time (#3425) + + A flow `update_record` node that writes a field the target object declares + `readonly: true`, under the default `runAs: 'user'` identity, is a **silent + no-op**: the objectql engine strips static-`readonly` fields from a non-system + UPDATE payload (#2948), so the intended write never lands — yet the step still + reports `success`. #3407/#3413 surfaced the strip as a run-time step warning; + this moves the discovery **left** to `os validate` / `os build` so an author + finds the mismatch at design time instead of by reading server WARN logs days + later. + + - New `@objectstack/lint` rule `validateReadonlyFlowWrites(stack)` — a pure + `(stack) => Finding[]` check (ADR-0019). A static `readonly:true` field + written by a literal `update_record` under `runAs !== 'system'` is a + 100%-certain no-op → **error** (gates the build). A `readonlyWhen` field is + per-record-state → **warning** (advisory). Deliberately narrow to stay + false-positive-free: `create_record` (INSERT is engine-exempt from the strip), + `runAs: 'system'` flows (the intended "automation maintains it" channel), + templated object names, and non-literal `fields` maps are all skipped. + - Wired into `os validate` and `os compile`/`os build`, mirroring the existing + security-posture gate (errors fail; advisories print dimmed). + + The formal contract, unchanged in behavior: `readonly` governs the end-user / + API surface (REST/UI and `runAs:'user'` flows strip it); trusted system writers + (`runAs:'system'`, system hooks, seeds) maintain it. To let a flow maintain a + readonly field, declare `runAs: 'system'`. + +- f163028: Reference-integrity validation for object and action names (issue #3583) + + A HotCRM audit found ~20 shipped instances of one bug class — metadata naming + something that does not exist — all passing `objectstack validate` / `lint` + cleanly and failing silently at runtime. This closes the object-name and + action-name half of that class. + + **New — `@objectstack/spec`:** `PLATFORM_PROVIDED_OBJECT_NAMES`, a curated + registry of every object name contributed by a platform package, official + plugin, or the cloud runtime, plus `isPlatformProvidedObjectName()` and + `hasPlatformObjectPrefix()`. This replaces the `startsWith('sys_')` prefix guess + that could not tell `sys_user` (real) from `sys_approval_process` (fictional — + removed by ADR-0019, registered by nothing), which is why every fictional + platform-prefixed reference shipped. A conformance test scans each package's + `*.object.ts` declarations and fails if the registry drifts. + + **New lint rules** (wired into both `os validate` and `os lint`): + + - `validate-object-references` — action-param `reference` / `objectOverride`, + dashboard `globalFilters[].optionsFrom.object`, and navigation + `requiresObject` gates. Severity follows resolvability: an unresolved + _unprefixed_ name is a typo (**error** — `object: 'user'` where the platform + object is `sys_user`); an unresolved _platform-prefixed_ name is **advisory**, + since a third-party package may still provide it. + - `validate-action-name-refs` — the surfaces that bind an action BY NAME: + list-view `bulkActions` / `rowActions`, page `record:quick_actions` + `actionNames`, and nav action items. A name matching no defined action is an + **error** (the button renders and does nothing), matching the existing + dashboard-action-target rule. + + **Fixes:** + + - `defineStack` cross-reference validation now walks `app.areas[].navigation` — + an areas-based app previously got no navigation checking at all — and recurses + into `children` on `object` nav items, not only `group` ones. + - `os lint` i18n coverage now reads field `options` in the canonical + `{value,label}[]` array shape; it only handled the record map, so option-label + coverage silently never fired for canonically-shaped select fields. + - Hook `condition` expressions are now field-checked when `object` is an ARRAY + of targets (previously only a single string target was checked, so a + multi-target hook filtering on a nonexistent field passed clean). Per-target + diagnostics are de-duplicated. + - A dashboard widget binding no `dataset` at all is now reported instead of + silently bypassing every binding and chart check on the raw-config + (`lint`/`doctor`) paths. `dataset` is schema-required, so this matches what + the parsed paths already enforce. + +### Patch Changes + +- 1bd5652: feat(auth): give ADR-0105 D8's scope-bounded issuance a caller — the + `delegated_admin` org role, capped so it cannot mint authority (#3697) + + D8 authorizes invitation _placement_ against the issuer's `adminScope` + (ADR-0090 D12), so a delegated plant admin may invite only into their own + subtree. That gate is implemented, unit-proven and reachable — but no principal + could reach it in a state where it did anything: + + - better-auth grants `invitation: ["create"]` to `owner` and `admin` only + (`memberAc` holds `invitation: []`, and roles registered through + `additionalOrgRoles` inherited that empty statement); + - under a wall-enforcing posture, owners and admins are auto-elevated to + `organization_admin` (`auto-org-admin-grant.ts`), which carries the wildcard + `modifyAllRecords` that makes `isTenantAdmin()` true — and the gate + short-circuits on tenant admins. + + The two sets were disjoint. Issuance placement was bounded by the Layer 0 org + wall (real, and correct) but never by `adminScope`, so D8's motivating story — + "a plant admin invites into their own subtree without a platform admin + finishing the job" — could not happen. + + **Two pieces, and they only ship together.** + + **1. The role.** `delegated_admin` is now registered with the organization + plugin as `memberAc.statements` plus `invitation: ["create"]` — the one + membership grade that may reach `/organization/invite-member` without being an + org admin. Deliberately _not_ `invitation: ["cancel"]`: better-auth's cancel + route checks the permission with no inviterId attribution, so it would mean + "cancel anyone's pending invitation in the org". + + The role carries no ObjectStack authority by construction — `mapMembershipRole` + passes it through as a position name, and with no `sys_position_permission_set` + binding that name resolves to nothing. Role = _can reach the endpoint_; + `adminScope` = _what the endpoint permits_. + + `sys_member.role` and `sys_invitation.role` each gain `delegated_admin` as a + fourth option. Those selects are **enforced on write** — better-auth's own + invitation and membership inserts are validated like any other row — so + registering the role with the org plugin without listing it in both would have + produced a role nobody could hold and nobody could hand out + (`ValidationError: role must be one of: owner, admin, member`). That is exactly + how the end-to-end regression caught it, twice; neither unit test could. The + three non-English translation bundles carry the English label for the new option + until localized. + + **2. The role cap**, in the framework's own `beforeCreateInvitation` hook, + beside the D8 placement gate. Registering the role alone would have been a + four-step privilege escalation: better-auth's only role-level cap on _what role + you may invite someone as_ is its `creatorRole` check (default `owner`), which + blocks inviting an **owner** but not an **admin** — and an accepted `admin` + membership is auto-elevated to `organization_admin` → `isTenantAdmin()`. A + subtree-scoped delegate could have manufactured a tenant admin, with every + existing defense off the path (`sys_member` is not a `GOVERNED_OBJECT`, and the + acceptance-time membership write runs under better-auth's context, not the + issuer's). + + The cap refuses an invitation whose role outranks the issuer's own, and + restricts a below-admin issuer to plain `member` — not merely "not admin/owner", + because an app-registered role projects into `current_user.positions` and may be + bound to permission sets, making it a capability channel too. A delegate's + channel for capability is the invitation's _placement_ intent, which the D12 + gate allowlists position-by-position. The cap applies to every invitation, + placement-carrying or not (the escalation is independent of placement), and + fails closed: an issuer role that cannot be resolved confers nothing above a + plain member. + + **What changes for deployments.** One new class of principal exists: members + holding the `delegated_admin` org role, who can invite into the org — as + `member` only, into the subtree their `adminScope` allows. It is opt-in twice + over (someone must set the membership role _and_ grant an adminScope set), so a + default deployment changes not at all. Org owners and admins are unaffected. + + Also exported: `MEMBERSHIP_ROLE_DELEGATED_ADMIN` from `@objectstack/spec`, so + console and control-plane surfaces name the role from one place. + +- 9dcc0ae: fix(automation): array-form flow `triggerType` fails loudly instead of silently never firing (#3481) + + An array `triggerType` on a flow start node — the shape an author (or an AI + authoring pass) naturally reaches for to fire on more than one event, e.g. + + ```ts + config: { objectName: 'app_task', triggerType: ['record-after-create', 'record-after-delete'] } + ``` + + was accepted everywhere and armed nowhere. Multi-event unions are deliberately + unsupported (only the single tokens plus the `record-after-write` create-OR-update + union exist — see #3457), but nothing said so: `defineFlow` passed the array + (start-node `config` is an open record), the engine's `typeof === 'string'` check + folded it to no trigger and misclassified the flow as **manual**, so it never + entered the trigger-binding audit, and the flow-trigger-readiness lint used the + same `typeof` narrowing and produced no finding. The flow bound to nothing and + never fired, with zero output at any layer — the same silent-never-fire class as + #3427 / #3472, and the last authoring shape still slipping past every guard. + + This is a **defensive** fix — arrays remain unsupported; they now fail loudly: + + - **lint** (`validate-flow-trigger-readiness`): an array `triggerType` containing + any `record-*` element now yields a `flow-trigger-unknown-event` warning at + `os validate` time, steering to `record-after-write` (for created-or-updated) or + one flow per event. + - **engine** (`resolveTriggerBinding`): such an array is routed to the + `record_change` trigger — exactly as an unmappable single token is — instead of + being folded to a manual flow, so it reaches the trigger's bind-time rejection. + - **trigger** (`record-change`): the bind-time rejection detects the array shape + and emits a targeted warning (naming the flow, pointing at `record-after-write` + and #3457) rather than the generic unknown-token line. + +- 5b89711: feat(spec,lint): freeze the `{current_user_id}` filter vocabulary and fail the build on unresolvable placeholders (#3574) + + A dashboard widget filtered on `{current_user}` rendered `0`. Not an error — a + zero, indistinguishable from a metric that is legitimately empty, with nothing + in the console or the server log. `service_dashboard.my_open_cases_by_priority` + in the HotCRM template had shipped broken this way since the day it was + written. + + The token had never been part of the contract. Date macros were frozen in + `date-macros.zod.ts` with a spec vocabulary, a lint-usable predicate, and a + single client resolver; `{current_user_id}` had only prose in an `app.zod.ts` + JSDoc and three ad-hoc client implementations that each handled one surface's + filter shape. Nothing could tell an author their token was wrong. + + - **`@objectstack/spec`** — new `data/context-tokens.zod.ts` freezing + `CONTEXT_TOKENS` (`current_user_id`, `current_org_id`) as the sibling of + `DATE_MACRO_TOKENS`, with `isContextToken` / `isKnownFilterToken` / + `classifyFilterToken` and a `CONTEXT_TOKEN_SUGGESTIONS` near-miss table. The + module documents what the tokens are _not_: presentation scope, never an + access boundary — that is RLS, which uses the unrelated `current_user.id` + expression root. + - **`@objectstack/lint`** — new `validateFilterTokens` (rule + `filter-token-unknown`, severity `error`). It walks `filter` / `filters` / + `runtimeFilter` subtrees across dashboards, objects, views, reports, + datasets, pages and apps, and reports any placeholder that resolves in + neither vocabulary. It scans for filter _keys_ rather than enumerating known + surfaces, so a new surface following the convention is covered the day it + ships — enumerating surfaces is how the dashboard was missed in the first + place. Navigation `recordId` / `params` are deliberately out of scope: they + resolve `AppContextSelector` ids, which are meaningless in a filter. + - **`@objectstack/cli`** — the gate runs in `os validate` and `os compile`. + + It is an error rather than a warning because of who authors this metadata. An + AI reads a query returning `0` as a correct answer and builds on it; its + correction loop is author → validate → fix, so a diagnostic only reaches it if + it can fail the build. The three spellings the suggestion table covers — + `{current_user}`, `{user_id}`, `{organization_id}` — are each correct + _somewhere else_ in the platform, which is exactly why authors reach for them. + + Also fixes a `ViewSchema` JSDoc example that documented `{user_id}`, a token + that resolves nowhere. + +- 5524f84: feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475) + + A record-change flow can now declare `expand: ['', …]` on its start + node config so node templates resolve `{record..}` (e.g. + `{record.account.name}` in a notify title, closing the #3426 gap for lookups). + + The engine re-reads the declared relations AFTER identity resolution, as the + run's OWN principal — `resolveRunDataContext` honors `runAs`, so a `runAs:'user'` + run reads the referenced object as the **triggering user** (its RLS/FLS enforced) + rather than system-elevated. This is what made expansion unsafe to do in the + trigger's re-read (which has no resolved grants) and is why it lives in the + engine (new `AutomationEngine.setRecordExpander`, bridged by the plugin to the + same data engine the CRUD nodes use). + + Only the declared relation keys are grafted onto the run record, so bare lookup + ids and `multiple` lookup arrays (#1872) on other relations — and the formula + fields the trigger already hydrated — are untouched. Opt-in ⇒ zero cost when + unused; best-effort ⇒ a re-read failure leaves the record unexpanded and never + breaks the flow. + + The `os validate` lint rule `flow-template-lookup-traversal` (#3426/#3472) is now + suppressed for a relation once the flow declares it in `config.expand`. + +- 169b58a: fix(#3426): build-time warning for unresolvable flow template paths + guard the formula re-read + + Two follow-ups to #3426 (the formula/lookup `{record.}` template gap that #3445 began closing). + + **Build-time signal (the issue's fallback ask).** `os validate` now flags a + record-change flow node whose `{record.}` template cannot resolve — + turning the previous SILENT blank into an advisory warning. Two cases, via the + new `@objectstack/lint` rule `validateFlowTemplatePaths`: + + - `flow-template-unknown-field` — `{record.}` where `` is neither a + declared field nor a system column (a typo like `{record.full_naem}`). + - `flow-template-lookup-traversal` — `{record..}`, a cross-object + hop the seeded record carries only as a scalar id (still unsupported; tracked + on #3426). + + Deliberately quiet: formula fields, bare lookup ids, numeric indexes into + `multiple` lookups (#1872), `json` sub-paths, and system columns are NOT flagged, + and flows bound to an object this stack does not define are skipped (no schema to + compare against). + + **Hydration re-read guards.** The `trigger-record-change` computed-field re-read + (#3445) is now (a) skipped when the object declares no `formula` field — the only + thing it adds — via the engine's optional `getObjectConfig`, and (b) memoized per + write on the shared HookContext, so N flows on one written record share ONE + re-read instead of N. Any uncertainty falls back to the prior unconditional + re-read (correctness over the optimization). + +- 7f4a8a1: fix(lint): flag every never-firing `record-`-prefixed trigger token, incl. `record-change` (#3427) + + Generalizes the `flow-trigger-unknown-event` rule: it now flags ANY `record-`-prefixed + `triggerType` that is not a valid firing token + (`record-{before,after}-{create,insert,update,delete,write}`) — not just + `record-(before|after)-` typos. This closes the `record-change` trap: the + engine routes `record-change` ("Record changed (any)") to the record-change trigger, + which maps it to no hook so it never fires — now caught at `os validate` time instead + of only a runtime warn. Also covers bad-phase tokens like `record-during-update`. + Warning severity, unchanged. + +- 29ff3c2: feat(lint): warn on replay-unsafe `mode: 'insert'` seed datasets (#3434 follow-up) + + Seeds are replayed — they re-load on every dev-server boot and every package + re-publish, not applied once — so `mode: 'insert'` (the loader's one mode with + no existing-row check) duplicates its table on every restart. That footgun + shipped undetected until #3434 (showcase memberships grew 3 → 6 → 9). + + Adds `validateSeedReplaySafety` to `@objectstack/lint` (a pure `(stack) => Finding[]` + rule, ADR-0019) and wires it into `os validate` / `os lint`. Every `data[]` seed + declared with `mode: 'insert'` now gets an advisory warning that points at the + idempotent modes (`ignore` / `upsert`) and the `externalId` to match on — a + single natural-key field, or a COMPOSITE list of fields for a join / junction + table with no single key (`['team', 'project']`, the support #3434 added). It + catches the mistake at authoring time instead of on the second boot. + +- 95829a0: feat(lint): warn on seed values outside an object's declared state machine (#3433 follow-up) + + #3433 exempts seed writes from the `state_machine` validation rule, so a seeded + status the FSM does not declare is no longer rejected at write time. A field-level + `select` still catches a value outside its `options`, but a `state_machine` on a + free-text field — or a value that is a valid option yet not a declared FSM state — + now sails through silently: the exemption is a deliberate but blind back door. + + `validateSeedStateMachine` (a pure `(stack) => Finding[]` rule, run from + `os validate` / `os lint`, symmetric with the replay-safety rule from #3434) + re-adds that safety net at author time. It flags any seed record whose + `state_machine`-governed field carries a value outside the machine's declared + states — the union of `initialStates`, the transition-map keys, and the transition + targets. Advisory (`warning`): the exemption itself is legitimate, so the fix-it + points at either adding the state to the machine or correcting the typo, not a hard + build failure. New rule id: `seed-value-outside-state-machine`. + +- 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list. +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/sdui-parser@17.0.0-rc.0 + ## 16.1.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index fb6f5fbc00..372e10d841 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 3da529abe0..6712ce2ec1 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,213 @@ # @objectstack/plugin-mcp-server +## 17.0.0-rc.0 + +### Patch Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 53e012ce14..9fae0de8a1 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 8113f1aae2..d158ac69b0 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,139 @@ # @objectstack/metadata-core +## 17.0.0-rc.0 + +### Patch Changes + +- db48ad5: fix(security,approvals,metadata-core): restore batch routes on the eight objects the #3391 P1 companion fix missed (#3026) + + The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch + request is admitted only when the object grants the `bulk` **primitive** and the + batched child operation is itself allowed. Before that, the `*Many` routes + checked only the child verb, so a boilerplate CRUD-five whitelist + (`['get','list','create','update','delete']`) batched fine. + + The companion fix — adding the `bulk` primitive wherever an explicit whitelist + survived — was applied only inside `platform-objects`. Eight objects carrying + the same boilerplate live in other packages and kept the gap, so `/batch`, + `createMany`, `updateMany` and `deleteMany` answered `405 +OBJECT_API_METHOD_NOT_ALLOWED` on objects whose single-record create/update/ + delete were wide open. `data-objectstack` rethrows that 405 without falling back + to per-row writes, which surfaced as a hard error on multi-select delete in the + Setup grids. + + Objects reclaimed (whitelist now `['get','list','create','update','delete','bulk']`): + `sys_capability`, `sys_permission_set`, `sys_position`, + `sys_position_permission_set`, `sys_user_permission_set`, `sys_user_position` + (plugin-security); `sys_approval_delegation` (plugin-approvals); + `sys_view_definition` (metadata-core). + + No new authority is granted: `bulk` only permits batching verbs each object + already exposes one record at a time, and every batched row still passes the + same row- and field-level permission checks. The whitelists stay explicit rather + than being deleted — seven of the eight are `managedBy`, and + `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array + `apiMethods`, so dropping the line would silently disable the managed-write + backstop. + +- c073b8c: refactor(metadata-core): drop `sys_view_definition`'s all-six `apiMethods` whitelist (#3026) + + #3745 completed this object's boilerplate CRUD-five whitelist to all six + primitives so its batch routes stopped 405-ing. A whitelist naming all six is + equivalent to no whitelist — except it stops tracking primitives the enum grows + later — so the #3543 audit rule applies and the declaration is removed. + + No behaviour change: `undefined` resolves to `unrestricted`, whose effective + operation set is identical to `restricted` holding all six. + + Removing it is safe HERE specifically because the object has no `managedBy`: + `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array + `apiMethods`, so for a managed object an absent whitelist would take the + managed-write backstop with it. That is why the RBAC objects reclaimed by #3745 + keep their explicit arrays and this one does not. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index d22ebea1ae..d337d55ffb 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 1dcfa12b9f..d872cd2203 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,13 @@ # @objectstack/metadata-fs +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [db48ad5] +- Updated dependencies [c073b8c] + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index f533bf07b2..3a5638b967 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 489dd6124c..71e885c30d 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,298 @@ # @objectstack/metadata-protocol +## 17.0.0-rc.0 + +### Minor Changes + +- c2d9098: feat(rest/protocol): extend droppedFields write-observability to the bulk paths + client SDK (#3455) + + Follow-up to #3448 (#3431 D2): the single-write PATCH/POST `/data` paths already + surface LEGALLY-stripped write fields (static `readonly` #2948 / `readonlyWhen` + #3042 / #3043 create ingress) as `droppedFields`. The **bulk** write paths did + not — the same strips happened silently on every batched row — and the typed + client warning + CORS mirror were deferred. This closes those out. + + **Bulk passthrough (metadata-protocol).** + + - `updateManyData` and `batchData` (update/upsert rows) now register a per-row + `onFieldsDropped` collector and attach the events to that row's result. + - `createManyData` diffs each supplied row against its #3043-stripped form and + returns an **aggregated** top-level `droppedFields` (one event per + object/reason with the union of field names) — its `{ records, count }` + response has no per-row slot, and the insert-time strip is static-`readonly` + only, so it is schema-uniform across rows and the aggregate is faithful. + - `insertManyData` keeps per-row precision, attaching `droppedFields` to each + outcome. + - **Correctness fix bundled in:** `updateManyData` and `batchData` never threaded + the caller's execution `context` to the engine — bulk writes ran context-less, + so RLS/FLS and `readonlyWhen` evaluated without the caller's principal, and the + batch create-ingress strip was hard-coded to a non-system context. All engine + calls in both methods now run under the resolved `context`. + + **Contract (spec).** `BatchOperationResultSchema` gains an optional per-row + `droppedFields` (covers `updateMany` + `batch`, which alias + `BatchUpdateResponseSchema`); `CreateManyDataResponseSchema` gains the optional + aggregated `droppedFields`. Both are omit-when-empty, so existing clients are + unaffected. `X-ObjectStack-Dropped-Fields` is deliberately **not** emitted for + batches — one response header cannot express per-row drops, so the per-row body + field is the canonical bulk channel. + + **Typed client warnings (@objectstack/client).** `CreateDataResult` / + `UpdateDataResult` gain `droppedFields?: DroppedFieldsEvent[]`, giving the body + channel a type instead of an untyped property. + + **CORS (@objectstack/hono, @objectstack/plugin-hono-server).** + `x-objectstack-dropped-fields` is added to the default `Access-Control-Expose-Headers` + allow-list (kept in lockstep across both Hono CORS sites) so a cross-origin + browser can read the single-write drop header. The body `droppedFields` remains + the primary, cross-origin-safe surface — this is a convenience mirror. + + **GraphQL — not applicable (documented).** #3455 lists a GraphQL mutation item, + but GraphQL has no runtime: `kernel.graphql` is unassigned everywhere and + `handleGraphQL` returns `501`, and discovery never advertises `/graphql`. There + is no schema generator or mutation resolver to expose a typed payload field on, + so there is nothing to wire until a GraphQL engine lands — at which point the + protocol-layer `droppedFields` is already present and only the GraphQL schema + projection would remain. + +- 5ac93d4: feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431) + + #3413 (closes #3407) built the engine-level strip-observability channel + (`WriteObservabilityOptions.onFieldsDropped`) and wired the flow side + (`update_record` / `create_record` emit a step warning + `droppedFields`). The + **REST write path was never wired**, so an external API caller writing N fields + still got a bare `200 + record` when `readonly` (#2948) / `readonlyWhen` (#3042) + stripping meant `< N` actually landed — the same silent-success class #3407 + fixed flow-side, just on HTTP. The only way to notice was a per-field diff of + the returned row (which need not echo every field). This wires the channel + through the protocol → REST, on both write verbs. + + **Passthrough (metadata-protocol).** `updateData` now registers an + `onFieldsDropped` collector on `engine.update` and returns the events on the + response as `droppedFields`. `createData` surfaces the #3043 static-`readonly` + INGRESS strip too — that strip runs at the protocol ingress + (`stripReadonlyForInsert`), _before_ the engine, so it is recovered by diffing + the supplied payload against the stripped one (the engine's `onFieldsDropped` is + also wired for a future insert-side engine strip). A faulty listener never + breaks the write — the engine catches and logs. + + **Contract (spec).** `UpdateDataResponseSchema` / `CreateDataResponseSchema` + gain an **optional** `droppedFields: DroppedFieldsEvent[]` — present only when + ≥1 field was dropped. Optional + omit-when-empty keeps the response shape + backward-compatible for clients that only read `record`. + + **REST surface.** PATCH `/data/:object/:id` and POST `/data/:object` echo the + drops as an `X-ObjectStack-Dropped-Fields` response header + (`field;reason=` tokens, comma-joined — e.g. + `approval_status;reason=readonly`) and keep the structured `droppedFields` on + the body. **Status/success semantics are unchanged** (200 update / 201 create) — + a strip is legitimate semantics, not a failure (same principle as #3413). The + FLS write gate is untouched (it already fails closed with 403). + + Out of scope (issue #3431 D2 open questions, deferred): bulk + (`updateManyData` / `createManyData` / `batchData`) and GraphQL mutation wiring, + typed `@objectstack/client` warnings, and adding the header to the Hono CORS + `exposeHeaders` allow-list for cross-origin browser reads (the body + `droppedFields` is the cross-origin-safe channel meanwhile). + +- 20cb232: feat(metadata-protocol,objectql): MetadataProtocolPlugin + `registerProtocol` opt-out — ADR-0076 Step 2 PR-A (#2462) + + `createMetadataProtocolPlugin()` now owns what `ObjectQLPlugin` historically + assembled inline: the `ObjectStackProtocolImplementation` construction + + `protocol` registration, the metadata-storage platform objects, and the D12 + `degraded` analytics fallback (pattern: plugin-security — named plugin, + `dependencies` on the engine, `ctx.getService('objectql')`). `ObjectQLPlugin` + grows `registerProtocol?: boolean` (default `true`, fully backward + compatible): pass `false` when mounting the new plugin. Protocol CONSUMERS + stay on the engine plugin either way — DB hydration and the authored + hook/action rebind resolve `protocol` lazily (the rebind arms from `start()` + in delegated mode) and degrade gracefully. Mixing both assemblies fails fast + with the fix in the message. This is the additive first leg of the + cross-repo sequence; cloud's 3 boot sites flip in PR-B, the built-in + assembly + re-exports retire in PR-C. + +- e231abb: feat(objectql,metadata-protocol)!: single-source the protocol assembly; drop objectql's protocol re-exports — ADR-0076 Step 2 PR-C (#2462) + + The ONE assembly now lives in `@objectstack/metadata-protocol` as + `assembleMetadataProtocol()` — `createMetadataProtocolPlugin()` (delegated + mode, cloud) and `ObjectQLPlugin`'s built-in convenience mode + (`registerProtocol !== false`, single-kernel/dev boots) both mount the same + code path (~112 inline lines deleted from the engine plugin). objectql's six + protocol re-exports (`ObjectStackProtocolImplementation`, + `SysMetadataRepository`, `SeedLoaderService`, `runBuildProbes` + types) are + removed — import them from `@objectstack/metadata-protocol` directly + (breaking, shipped as minor per the launch-window convention; the only known + importers were five test files, repointed). Scope note vs the original Step-2 + recipe: the objectql→metadata-protocol dependency is deliberately KEPT for + the convenience mount — `@objectstack/objectql/core` was already + protocol-free, and forcing 20 framework boot sites to mount two plugins buys + no runtime win. "Zero protocol dependency" lands as "zero assembly ownership, + single source". + +### Patch Changes + +- abceb0d: fix(seed-loader): support a composite `externalId` so join-table seeds dedupe on replay (#3434) + + A junction / join table has no single-field natural key — the PAIR of its + foreign keys is what's unique — so its seed could only run `mode: 'insert'`, + which re-inserts every row on each replay boot with no existing-row check + (`decideWriteAction`'s `insert` case returns `insert` unconditionally). The + table duplicated on every restart: the showcase `showcase_project_membership` + fixture (3 rows) grew 3 → 6 → 9. It was masked until #3415 let the master-detail + parents seed at all. + + - `SeedSchema.externalId` now accepts a **list** of field names + (`externalId: ['team', 'project']`) in addition to a single field name, + declaring a composite natural key. Default stays `'name'`. + - `SeedLoaderService` builds the uniqueness key from all listed fields (joined + with a `\u0000` separator that can't occur in a natural-key value). Reference + key fields are compared by their RESOLVED parent ids — which the existing DB + row already stores — so a composite of foreign keys matches across restarts. + A partial key (any component absent) is treated as no key, falling back to + insert, exactly as a missing single-field key already did. + - A composite-key target does not participate in single-value reference + resolution (a reference is one natural-key string), so such objects keep the + `'name'` default when referenced by another dataset. + + The showcase membership fixture switches to `mode: 'ignore'` + + `externalId: ['team', 'project']`, so replay boots leave the three rows + untouched instead of duplicating them. + +- 4c5a584: fix(seed-loader): resolve lookup/master_detail references for objects that only live in the engine registry (marketplace installs) + + `SeedLoaderService.buildDependencyGraph` consulted only `metadata.getObject()` + when building the reference graph. Marketplace-installed packages register + their objects through the `manifest` service straight into the ObjectQL + registry — after the boot-time `bridgeObjectsToMetadataService` pass — so the + metadata service never lists them. The reference graph came back empty for + those objects and every lookup / master_detail seed value was written + verbatim: `crm_contact.crm_account` held the authored natural key + (`"Acme Corporation"`) instead of the target record's id. + + The damage compounded under RLS: `crm_contact` declares + `sharingModel: controlled_by_parent`, whose row filter compiles to a join on + the parent reference. With every reference dangling, the join matched nothing + and the whole object went invisible to everyone — platform admins included — + while the rows sat in the table (REST list `total=0`, single GET 404). + + The loader now falls back to the engine's own schema registry + (feature-detected `engine.getSchema()`, which the ObjectQL engine exposes) + whenever the metadata service has no definition for a seeded object. The + metadata service remains the preferred source; engines without a schema + registry keep the old behavior. + +- 0c302a7: Exempt curated seed writes from `state_machine` validation (#3433). + + A seed is a snapshot of established facts — a project already `completed`, an + opportunity already `closed_won` — not a record walking its lifecycle. But once + an object declared `state_machine.initialStates` (#3165), the write path enforced + the FSM entry point on **every** insert, so seed replay silently rejected every + mid-lifecycle row and cascaded its master-detail children. That is the "installed + but no data" failure for the showcase board (1 of 5 projects), and it would hit + every marketplace template (a `closed_won` opportunity, a `closed` case) plus the + rehydrate-heal and per-org replay paths. + + `SeedLoaderService` now marks its writes with a server-set `ExecutionContext.seedReplay` + flag; the engine passes `skipStateMachine` to the rule evaluator for those writes, + which skips the `state_machine` rule on both insert (`initialStates`) and update + (transitions). The exemption is scoped to `state_machine` only — a seed must still + satisfy every other validation (`format`, `cross_field`, `script`, `json_schema`, + `conditional`). Because all seed paths funnel through `SeedLoaderService.SEED_OPTIONS`, + the fix covers boot inline seed, marketplace install/heal, and per-org replay at once. + + The showcase project seed drops its three-phase FSM-walk workaround (#3415) and + seeds each project directly at its real status again. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 9ccaad222f..775db84c8c 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 d511cdf5f3..5fbf51147f 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,117 @@ # @objectstack/metadata +## 17.0.0-rc.0 + +### Patch Changes + +- 4e9e184: chore(deps): OSV security batch — bump tar to ^7.5.21 (GHSA-r292-9mhp-454m) and + js-yaml to ^5.2.2 (GHSA-pm4m-ph32-ghv5) + + Both are declared-range bumps to the patched releases, so downstream installs + resolve the fixed versions from the published manifests, not just this + workspace's lockfile. The same batch clears the remaining transitive advisories + (next 16.2.11 in apps/docs; workspace overrides for brace-expansion, sharp, + react-router, @sveltejs/kit, @hono/node-server) — those live in pnpm-workspace.yaml + and the private docs app, which do not ship. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + - @objectstack/metadata-fs@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index 592d882abc..3ff0ee5457 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 ea02856f10..4ee93776ca 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,858 @@ # @objectstack/objectql +## 17.0.0-rc.0 + +### Minor Changes + +- 6169615: feat(objectql)!: media value shapes enforce once THIS deployment has verified its file migration (#3438 D1 media half, gated by #3617) + + A `file` / `image` / `avatar` / `video` / `audio` value that does not match the + stored contract (an opaque `sys_file` id) now **rejects with `invalid_type`** + instead of warning — but only on a deployment that has run + `os migrate files-to-references --apply` and passed its self-check. + + **Why this is not a version-wide flip.** The legacy media values this rejects — + inline `{url, name, …}` blobs, bare URLs — are exactly what that migration + converts. A deployment that has run it has been _shown_ to hold none; a + deployment that has not would have every media-field update start failing the + moment it upgraded. So the enforcement follows the evidence, per deployment, + rather than the release. Nothing changes for a deployment until it migrates. + + **Upgrading:** + + ```bash + os migrate files-to-references # dry run: reports what would convert + os migrate files-to-references --apply # convert, verify, record the flag + ``` + + If a write starts failing after you migrate, the value genuinely does not match + the contract — the error names the field. `OS_ALLOW_LAX_MEDIA_VALUES=1` re-opens + media leniency while you diagnose. + + **Scope — deliberately only media.** `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` is + unchanged and still opts every class into strict (and still forces media strict + on a deployment that has not migrated). Reference types (`lookup`, `user`, …) + and structured JSON (`location`, `address`, `repeater`, …) stay warn-first: the + file migration is evidence about file values and says nothing about whether a + `location` is well formed, so gating them on its flag would be borrowing + evidence for a fact it does not cover. They flip when something can vouch for + them — see #3438. + + **Cost.** Dormant unless the written object declares a media field, and the + flag read is memoized, so this is one query per process for apps that store + files and zero for those that do not. A running server picks up a + newly-recorded migration on restart, or via `engine.invalidateDataMigrationFlags()`. + +- fa3d0cf: feat(spec): field runtime value-shape contract — ADR-0104 phase 1 (D1) + + `@objectstack/spec/data` now owns the runtime VALUE shape of every field type + (`field-value.zod.ts`): semantic type classes (`STRING_VALUE_TYPES`, + `NUMERIC_VALUE_TYPES`, `REFERENCE_VALUE_TYPES`, `FILE_REFERENCE_TYPES`, + `STRUCTURED_JSON_TYPES`, `MULTI_CAPABLE_TYPES`, …), the shared + `isMultiValueField`, and `valueSchemaFor(field, 'stored' | 'expanded')`. The + four consumers that each hand-copied this knowledge (objectql record-validator, + rest import-coerce, driver-sql column classification, qa conformance) now + derive from the spec, and the field-zoo round-trip MATRIX is asserted against + the contract so the two cannot drift. + + **Write-path change (objectql, warn-first):** previously-unvalidated types — + single `lookup`/`master_detail`/`user`/`tree`, `file`/`image`/`avatar`/ + `video`/`audio`, `location`, `address`, `composite`, `repeater`, `record`, + `vector` — are now checked against the contract. A violation **logs a warning + and passes** in this release (legacy rows must not strand their records); + set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` to enforce as a + `400 VALIDATION_FAILED`. The flip to strict-by-default rides a later minor + (ADR-0104 R1/R2). + + **Deprecations (removal rides the next spec major), FROM → TO:** + + - `CurrencyValueSchema` (`{value, currency}`) → none. A `currency` field's + value is a **bare number** everywhere in the runtime (validator, SQL `float` + column, import coercion, field-zoo oracle); the currency code lives in field + config. Use `valueSchemaFor({type: 'currency'})`. + - `LocationCoordinatesSchema` (`{latitude, longitude}`) → `LocationValueSchema` + (`{lat, lng}`) — the shape the platform actually stores. + - `AddressSchema` is **adopted** (unchanged) as the enforced `address` value + contract via `AddressValueSchema`. + + No stored data changes shape; the contract codifies deployed reality + ("reality wins", ADR-0104 D1). + +- a749273: feat(objectql): resolve file-field id references on read — ADR-0104 D3 wave 2 (PR-2) + + The engine read path now resolves a `file`/`image`/`avatar`/`video`/`audio` + value stored as an opaque `sys_file` id string into its expanded + `FileValueSchema` form — `{ id, name, size, mimeType, url }`, with `url` derived + from the stable `/api/v1/storage/files/:fileId` resolver (never stored). One + batched `sys_file` `id $in […]` read per query (no N+1), mirroring the + lookup-`$expand` batch pattern. + + **Dual-mode safe.** An inline-blob value (an object) passes through unchanged, + and only an **opaque id token** (uuid/nanoid-shaped) is treated as a reference — + a URL-shaped value (`https://…`, `/api/…`, `data:…`, `blob:…`), which a file + field legitimately holds in the legacy world, is never looked up. The step + fires zero reads unless a file field actually holds an id token (the blob/URL + case is free), and it no-ops entirely when `sys_file` is not registered. + + This makes a stored `fileId` (surfaced by PR-1) actually usable on read, ahead + of the v17 cutover that narrows the stored form to an id. + +- fdb4f50: feat(migrate): `os migrate files-to-references` — a data migration with a self-check, gated per deployment (#3617) + + The ADR-0104 file-as-reference migration ships as a command a deployment runs + against its own database, and the deployment-level flag it records is what may + later authorise irreversible behaviour — never the platform version. + + ```bash + os migrate files-to-references # dry run: reports, writes nothing + os migrate files-to-references --apply # converts, verifies, records the flag + ``` + + The run backfills legacy file-field values (inline metadata blobs, own-resolver + URLs, `data:` URIs) into owned `sys_file` references, reconciles the ownership + ledger against what records actually hold, and — only on an `--apply` run whose + reconciliation reports **zero blocking discrepancies** — records + `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. + + **Why a flag rather than a release note.** ObjectStack is a development + platform: third-party deployments upgrade on their own schedule and their data + is not observable by anyone else, so no release-side soak can vouch for them. + The evidence has to be produced where the data is. Consequences: + + - Installing a new version never starts deleting bytes. Running the migration + and passing its self-check is the consent. + - Not run, or not passed → files are retained forever. Wasted storage, zero + data loss. + - A later failing run **clears** `verified_at`: a deployment whose data has + drifted closes its own gate. + - A dry run writes nothing at all — not the conversions, and not the flag, + even when the self-check would pass. + - External URLs stay advisory. They are not `sys_file`s, so they can never + enter collection; whether to remodel them as a `url` field is the app + author's decision (ADR-0104 R7), not a gate. + + Ships alongside: + + - `@objectstack/spec` — `DataMigrationFlagSchema`, `FILE_REFERENCES_MIGRATION_ID`, + and the single `isDataMigrationFlagVerified` predicate both future consumers + (collection #3459, strict value-shape #3438) read, so the two gates cannot + disagree about the same fact. + - `@objectstack/platform-objects` — the `sys_migration` object plus + `readDataMigrationFlag` / `isDataMigrationVerified` / `recordDataMigrationRun`. + Reads fail toward "not verified": a gate that cannot read its evidence stays + closed. + - `@objectstack/objectql` — a read may now opt out of file-reference expansion + via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`, and the storage service's + bookkeeping/scan reads do. Without it the read resolver rewrites stored ids to + their expanded form before the reconciliation sees them, which reports held + references as absent — noisy `stale_owner` findings, and a missed + `unowned_reference` would have been a false pass of the collection gate. + +- 030125b: feat(objectql)!: `init()` refuses to boot when a data driver fails to connect (#3741) + + `ObjectQLEngine.init()` wrapped every driver's `connect()` in a try/catch, logged + one error line, and carried on. A server whose database was unreachable therefore + "started successfully" — health endpoints could even stay green — and then failed + every request with an error that reads nothing like _the database is down_. The + warning it printed (`Operations may recover via lazy reconnection or fail at query +time`) was half fiction: grep the repo and no reconnection exists in `driver-sql` + or `driver-mongodb`, so only the "fail at query time" half was ever real. The + caller made it worse — `ObjectQLPlugin.start()` runs `syncRegisteredSchemas()` + immediately after `init()`, issuing DDL against a driver that isn't there. + + The structural half of the bug was worse than the operational one: the catch + removed a driver's ability to **refuse startup at all**. Any fatal startup check — + licence, server version, incompatible configuration, missing capability, not just + an unreachable socket — is expressed by throwing from `connect()`, and every one + of them was silently downgraded to a runtime error. That is why driver-mongodb's + multi-tenancy guard (#3724 / #3734) had to be hoisted into its constructor. + + - `init()` now **throws** `DriverConnectError` (`code: 'ERR_DRIVER_CONNECT'`) + when any boot-registered driver's `connect()` rejects, aborting kernel + bootstrap. It still attempts every driver first, so one failed boot names all + of them. The message is self-contained — each failed driver and its cause — + because the CLI prints `error.message` alone; the first cause is also attached + as `error.cause`. Exported from both `@objectstack/objectql` and + `@objectstack/objectql/core`. + - `connect()` is now a supported place for a driver to veto boot. Startup + validation that needs a live connection (server version, capability probes) + no longer has to be forced into a constructor. + - The misleading "lazy reconnection" warning is gone. + - New escape hatch `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` + (`resolveAllowDriverConnectFailure()` in `@objectstack/types`) restores the old + lenient boot, but loudly: a `DEGRADED BOOT` banner names the failed drivers and + states that they are never retried or reconnected and that every query and + schema sync routed to them will fail for the process lifetime. The banner goes + to stderr as well as the logger, because `os serve` swallows all of stdout + during boot and `Logger` routes `warn` there — logger-only, the one message + that matters would be invisible in exactly the deployment the flag is for. + Defaults off. + + **Migration.** No code or config change is needed for a correctly configured + deployment — a driver that connected before still connects. A deployment that was + _silently_ booting without its database now fails the boot instead, with the + driver name and cause in the error; fix the datasource configuration (typically + `OS_DATABASE_URL`, credentials, or network reachability). To keep booting without + it — deliberately, and knowing every request that touches it will fail — set + `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`. + +- 8e08bc3: feat(runtime): `/ready` reports 503 when a data driver stops answering (#3756) + + `/health` returned `{status: 'ok'}` unconditionally and `/ready` only checked + whether the kernel state was `running` — a flag set once when bootstrap finishes + and never revisited. Neither probe touched the data layer. So a database that + went away _after_ boot (restart, failover, network policy change, pool exhausted, + credentials rotated) left both probes green: the load balancer kept routing to a + replica that failed 100% of its requests, and the orchestrator saw nothing wrong. + The driver's `checkHealth()` already existed and was cheap (`SELECT 1` / + `db.command({ping:1})`) but was only consumed by `datasource-admin`'s + `testConnection` — no probe path called it, and `ObjectQL` exposed no way to ask + (`drivers` is private with no accessor). + + This is the runtime-side half of #3741, which fixed only the boot-time version + of the same defect. + + - New `ObjectQL.checkDriversHealth({ timeoutMs })` pings every registered driver + and returns a `DriverHealth[]` verdict. Each probe is settled independently and + bounded (default 2s) — `checkHealth()` swallows its own errors, but on a dead + knex pool it does not return at all, waiting out `acquireConnectionTimeout` + (60s by default), and a probe that hangs is as useless as one that lies. A + driver implementing no `checkHealth()` is reported healthy: absence of a probe + is not evidence of failure. + - `GET /ready` now returns 503 with the failing driver names when the kernel is + running but a driver is down, on top of the existing booting/shutting-down + cases. The result is memoized for ~1s so Kubernetes' few-second polling does + not become one database round-trip per probe per replica. + - `GET /health` deliberately still checks nothing, and now says why in the code. + A failing _liveness_ probe restarts the pod, which cannot fix an unreachable + database but would put every replica into a restart storm for the length of the + outage. Readiness — leave the rotation — is the failure mode that helps. + + The readiness check **fails open**: a kernel with no data engine (lite kernels, + edge, metadata-only hosts), an engine predating `checkDriversHealth`, or a probe + that itself throws all read as ready, exactly as before. Readiness gates whether + a replica receives any traffic at all, so an inconclusive answer must not + black-hole a working deployment. Only a driver that positively reports itself + unhealthy takes the replica out. + + **Migration.** None. Deployments already wiring `/api/v1/ready` as their + readiness probe get the stricter check automatically; deployments that pointed a + _liveness_ probe at `/ready` should move it to `/health`, which is the endpoint + that never fails on a dependency. + +- 20cb232: feat(metadata-protocol,objectql): MetadataProtocolPlugin + `registerProtocol` opt-out — ADR-0076 Step 2 PR-A (#2462) + + `createMetadataProtocolPlugin()` now owns what `ObjectQLPlugin` historically + assembled inline: the `ObjectStackProtocolImplementation` construction + + `protocol` registration, the metadata-storage platform objects, and the D12 + `degraded` analytics fallback (pattern: plugin-security — named plugin, + `dependencies` on the engine, `ctx.getService('objectql')`). `ObjectQLPlugin` + grows `registerProtocol?: boolean` (default `true`, fully backward + compatible): pass `false` when mounting the new plugin. Protocol CONSUMERS + stay on the engine plugin either way — DB hydration and the authored + hook/action rebind resolve `protocol` lazily (the rebind arms from `start()` + in delegated mode) and degrade gracefully. Mixing both assemblies fails fast + with the fix in the message. This is the additive first leg of the + cross-repo sequence; cloud's 3 boot sites flip in PR-B, the built-in + assembly + re-exports retire in PR-C. + +- e231abb: feat(objectql,metadata-protocol)!: single-source the protocol assembly; drop objectql's protocol re-exports — ADR-0076 Step 2 PR-C (#2462) + + The ONE assembly now lives in `@objectstack/metadata-protocol` as + `assembleMetadataProtocol()` — `createMetadataProtocolPlugin()` (delegated + mode, cloud) and `ObjectQLPlugin`'s built-in convenience mode + (`registerProtocol !== false`, single-kernel/dev boots) both mount the same + code path (~112 inline lines deleted from the engine plugin). objectql's six + protocol re-exports (`ObjectStackProtocolImplementation`, + `SysMetadataRepository`, `SeedLoaderService`, `runBuildProbes` + types) are + removed — import them from `@objectstack/metadata-protocol` directly + (breaking, shipped as minor per the launch-window convention; the only known + importers were five test files, repointed). Scope note vs the original Step-2 + recipe: the objectql→metadata-protocol dependency is deliberately KEPT for + the convenience mount — `@objectstack/objectql/core` was already + protocol-free, and forcing 20 framework boot sites to mount two plugins buys + no runtime win. "Zero protocol dependency" lands as "zero assembly ownership, + single source". + +- b95577a: feat(automation): surface silently-stripped write fields as step warnings (#3407) + + `update_record` used to report an unconditional `success` even when the data + layer legally stripped the requested write fields — static `readonly` (#2948) + or a TRUE `readonlyWhen` predicate (#3042). The only trace was a server-side + logger warn, invisible in the flow run trace: an author saw a clean 3ms + `success` while the DB truth never changed (how #3356's approval stage + write-backs failed unnoticed). + + - **spec**: new `DroppedFieldsEventSchema` / `DroppedFieldsEvent` + (`{ object, fields, reason: 'readonly' | 'readonly_when' }`) in + `data/data-engine.zod.ts`, and a `WriteObservabilityOptions` + (`onFieldsDropped` listener) mixin on `IDataEngine.insert/update` option + params in `contracts/data-engine.ts`. The listener is a TS-contract-level, + in-process-only channel — deliberately NOT part of the serializable Zod + options schemas or the RPC boundary. + - **objectql**: `engine.update()` reports each strip pass's dropped keys + + reason through `options.onFieldsDropped` (all four strip sites: single-id + + bulk × readonly + readonlyWhen). A throwing listener never breaks the write. + System-context writes skip the readonly strip and therefore report nothing, + as before. `insert()` accepts the option for symmetry but strips nothing + today (INSERT is readonly-exempt; FLS write denial throws). + - **service-automation**: `NodeExecutionResult` and `StepLogEntry` gain + advisory `warnings?: string[]`; `update_record` / `create_record` attach one + warning per strip event naming the dropped fields, plus a structured + `droppedFields` output (`{.droppedFields}`) for downstream nodes. + `success` semantics are unchanged — stripping stays legal, it just is no + longer silent. + +### Patch Changes + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- d44dbfa: feat(spec)!: shrink the `ApiMethod` enum to the six primitives — legacy values are stripped at parse, never honored (#3543, P2 of #3391) + + **BREAKING** (the `!` marker and this changeset are the breaking-change + record; the train ships as the v17 major — see the `v17-rc-anchor` changeset): + the authored `enable.apiMethods` enum is now exactly the six + primitives (`get`, `list`, `create`, `update`, `delete`, `bulk`). The eight + legacy values (`upsert`, `aggregate`, `history`, `search`, `restore`, `purge`, + `import`, `export`) are no longer authorable — they are DERIVED effective + operations, resolved by the server's single derivation table. + + **Migration (FROM → TO).** Replace each legacy value with the primitives it + derives from, then de-duplicate; if the result names all six primitives, delete + the `apiMethods` key entirely (equivalent to default-open, and it tracks future + primitives): + + | FROM (legacy) | TO (primitives) | why | + | ------------- | -------------------- | ---------------------------------------------- | + | `upsert` | `create`, `update` | upsert ⊆ create ∧ update | + | `import` | `create`, `update` | import ⊆ create ∨ update (writeMode-precise) | + | `export` | `list` | export ⊆ list | + | `aggregate` | `list` | aggregate ⊆ list | + | `search` | `list` | search ⊆ list ∧ `searchable` | + | `history` | `get` | history ⊆ get ∧ `trackHistory` | + | `restore` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + | `purge` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + + Reporter codemod: `node scripts/codemod/apimethods-legacy-to-primitives.mjs` + (scans, reports the exact replacement per site, and flags whitelists the + mapping would WIDEN so the edit stays reviewable). + + **Stored metadata keeps parsing — permanent tolerance, narrowing only.** Real + metadata does not upgrade in lockstep with the spec, so a stored legacy value + is NOT a parse error: `stripLegacyApiMethods` (new export) strips it with a + FROM→TO warning (canonicalize-and-warn). Stripping only ever NARROWS exposure — + the derivation table still grants every legacy verb that derives from the + primitives you declared. Two cliffs to know: + + 1. A whitelist of ONLY legacy values (e.g. `['upsert']`) strips to `[]` = + **deny-all** — the object's API closes instead of widening. The strip + warning and the objectql registration diagnostic both call this out. + 2. A legacy value NOT derivable from your declared primitives (e.g. + `['get', 'export']` — export needs `list`) was honored by the P1 + "explicit wins" path and is now denied. Declare the underlying primitive. + + **Type split — authored vs effective vocabulary.** `ApiMethod` (authored) is + now six values; the NEW `ApiOperation` type / `ApiOperationSchema` / + `API_OPERATION_ORDER` (fourteen values, byte-stable pre-shrink wire order) + carry the EFFECTIVE vocabulary. The wire contract is unchanged: the 405 + `allowed` array and `/me/permissions` `apiOperations` still serialize derived + verbs (`export`, `search`, …), and `EffectiveObjectPermissionSchema.apiOperations` + now validates against `ApiOperationSchema`. `EffectiveApiMethods.explicitLegacy` + is removed (nothing is honored verbatim anymore); `API_METHOD_ORDER` remains as + a deprecated alias of `API_OPERATION_ORDER`. + + **Fail-closed tightening (#3545):** a PRESENT but non-array `apiMethods` (only + producible by a raw/out-of-band metadata write) now resolves to `deny-all` + instead of unrestricted — a policy that exists but cannot be read fails CLOSED. + + **Published JSON Schema diverges deliberately:** `data/ApiMethod.json` is the + strict six-value enum (a `z.preprocess` is not representable in JSON Schema), + so external JSON-Schema validators reject legacy values that the zod parse + would strip-and-warn. Treat the JSON Schema as the authored contract; the zod + tolerance exists for stored metadata. + + **objectql:** the P1 "explicit wins" transition is reclaimed — + `warnDeprecatedExplicitApiMethods` is replaced by `warnStrippedLegacyApiMethods` + (a permanent per-object diagnostic for schemas that reach the registry without + passing through Zod; the parse-time strip warning carries no object name). + + **platform-objects:** whitelist audit — `sys_business_unit`, + `sys_business_unit_member` (P1's explicit `import`/`export` reclaimed) and + `sys_user_preference` dropped their `apiMethods` entirely (each named all six + primitives = default-open). Read-only and deny-all whitelists are unchanged; + the seven `[]` declarations are deliberately KEPT as defense-in-depth alongside + `apiEnabled: false`. + +- b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456) + + The record lock is keyed on a **pending** `sys_approval_request`, and it could + not tell _the run that owns that request_ from _an unrelated user editing the + record_. So a flow that touched its own target record while its own approval was + still pending — a manual `resume` with no decision, or a node that writes the + record between opening the approval and the decision — died on its own + `RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery + existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it + self-healing. + + Both halves are now closed. + + **Prevention — the owning run may write its own record.** The automation engine + stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it + travels with every data node's ObjectQL context into `ctx.provenance`. The lock + hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`. + It is keyed on run identity rather than elevation on purpose: a `runAs:'user'` + run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance — + server-constructed like `isSystem`, never client-supplied, evaluated by no + security middleware, and the only write it permits is to the one record its own + run already holds a pending request against. + + **Recovery — a sweep releases records held by runs that died anyway.** A pending + request whose owning run has reached a terminal state (`completed`, `failed`, + `cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled` + — releasing the lock — and audited under the reserved actor `system:dead-run` + with the run and its status in the comment, so it is never mistaken for a + submitter's withdrawal. It runs on the existing approvals sweep clock, which also + covers the case no in-band handler can: a run killed by a process crash. + + The sweep is fail-safe by construction. It acts only on an explicit terminal + status from a closed set; `paused` (the normal state of a live approval), + `running`, an unrecognised status, an unknown run, a `getRun` that throws, and a + deployment with no automation engine are all read as "still alive". The failure + mode is "a dead run's lock survives until an admin recalls it" — today's + behaviour — never "a live approval is destroyed". + + Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for + a run id rather than the latest. A run that pauses and later finishes records two + entries under one id, so every suspend-then-finish run — every approval, screen + and wait flow — reported itself as `paused` forever, both on the Runs + observability surface and to this sweep. + + One shape was left out here and closed separately in #3712: a `runAs:'user'` run + with no trigger user (a schedule) resolved no ObjectQL context at all, so it + carried no `flowRunId` and stayed subject to the lock. It now passes a + provenance-only context — the run id and nothing the security middleware keys on + — so it is attributable without acquiring a principal, and its documented + unscoped posture (#1888) is unchanged. + +- c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712) + + #3456 let the run that opened a pending approval write its own target record, + keyed on `flowRunId`. It worked for every run that resolves an identity and + missed the one that doesn't: an effective `runAs:'user'` run with **no trigger + user** — a schedule being the canonical case — passed no ObjectQL context at + all, so nothing carried the run id and the run still died on its own + `RECORD_LOCKED`. + + The blocker was never the lock. It was that "no identity" and "no context" were + the same thing on the wire, so a run could not say _who it was_ without also + claiming _what it was allowed to do_. + + **A run with no principal now passes provenance alone.** + `resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`, + no `permissions`, not even `isSystem: false`. Every principal gate keys on one + of those fields (the elevation short-circuit on `isSystem`, the ADR-0103 + engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`, + the empty-principal fall-open on all three), so this context authorizes + **identically to no context at all**. The run keeps the documented #1888 + unscoped posture, its loud `[runAs]` warning, and the + `flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch + changed — only that it can now be attributed. + + **Provenance moved out of the hook session, into `ctx.provenance`.** `session` + answers _who is calling_ and is absent when no identity envelope was supplied — + a distinction real gates depend on (the attachment access gate skips bare-kernel + writes on exactly that test). Folding a run id into `session` would have forced + an identity-less run to present an empty session, silently turning "no caller" + into "an anonymous caller" and narrowing the #1888 fail-open for attachments + alone. `HookContext.provenance.flowRunId` says what produced the write; the + approvals lock reads it there. + + Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope + (`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time + defaults, which made them _required_ on a caller-supplied option and asserted + something untrue — that every data-engine context carries a principal. Callers + have always passed slices (`{ isSystem: true }` for a system read); the type now + says so. + + Migration: nothing to change unless you read the run id inside a hook. If you + wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the + field never shipped under the old name. + +- 0e3a226: fix(authz): widen the driver's native tenant scope to the membership union + under the `group` posture — ADR-0105 D2 finally reaches the wire (#3623) + + The Layer 0 wall correctly compiled `organization_id IN accessible_org_ids` + under `group`, but the ObjectQL engine also propagated the active-org + `tenantId` into `DriverOptions` unconditionally, and the SQL driver's native + scoping ANDed `organization_id = tenantId` under the union — collapsing every + group read back to active-org (isolated) reach. Found by the cloud-side + `ee-group-showcase` dogfood (cloud#880), the first end-to-end boot of `group` + against a real driver. + + - `DriverOptions.tenantIds` (spec): the union tenant access set. Drivers with + native scoping widen reads/updates/deletes/aggregates to `IN (...)`, + keeping the NULL-tenant global-row carve-out; inserts still stamp from + `tenantId` (the active organization is the write target, D5). Absent or + empty ⇒ equality fallback — fail toward isolation, never toward exposure. + - ObjectQL engine threads `ExecutionContext.accessible_org_ids` as + `tenantIds` when the tenancy posture is `group`, reported by a new + `setTenancyPostureProvider` seam. + - SecurityPlugin wires that provider at start — deliberately from the + enforcement layer, so the driver wall only widens while the Layer 0 union + wall enforces above it. Embeddings without plugin-security keep active-org + equality. + +- 81ce41a: feat(rest): `treatAsHistorical` import also preserves the original audit timeline (#3493) + + Follow-up to #3479/#3483. `treatAsHistorical` solved the FSM half — mid-lifecycle + rows are no longer rejected by `initialStates` — but the OTHER half of a historical + migration, preserving the original timeline, still didn't hold: an imported ticket + that closed in 2021 stored `updated_at` = the import day (and `updated_by` = the + importer), and a `writeMode: 'upsert'` refresh silently dropped business `readonly` + fields (`closed_at`, `resolved_by`). Reports, audit, and "recently modified" + sorting all came out wrong. + + Three layers were force-overwriting the timeline; all three now respect a single + new opt-in flag, `ExecutionContext.preserveAudit`, which `treatAsHistorical` sets + alongside `skipStateMachine`: + + - **spec**: `ExecutionContext.preserveAudit` (server-set only, never client-supplied) + and `DriverOptions.preserveAudit` (threaded to the driver's update stamp). + - **objectql** — the built-in audit hook (`plugin.ts`) now treats `updated_at` / + `updated_by` as CLIENT-PREFERRED (`?? now` / `?? userId`) under `preserveAudit`, + symmetric with how `created_at` / `created_by` already behave on insert; and the + static-`readonly` write strip (`stripReadonlyFields`) admits a WHITELIST — the + audit/timestamp family plus author-declared business `readonly` fields — so an + upsert refresh no longer drops them. + - **driver-sql** — the SQL `update` path keeps a supplied `updated_at` instead of + force-advancing it to `now` when `DriverOptions.preserveAudit` is set (fills-only- + empty, mirroring the insert stamp). + - **rest** — the import runner sets `preserveAudit` on the write context iff the + request opts into `treatAsHistorical`. + + Deliberately a WHITELIST, not the blanket `isSystem` exemption: platform-managed + `system` columns OUTSIDE the audit family (`organization_id` / tenancy, generated + columns) STAY stripped, so a historical import reinstates established facts without + becoming a backdoor to forge tenancy. Permissions / RLS / field-level security are + unaffected — this changes only which audit/readonly values the runtime overwrites, + never who may write the record. Fully opt-in: a normal write still auto-stamps + `updated_at`/`updated_by` and strips `readonly` exactly as before. The objectui + "Import as historical data" checkbox (objectui#2815) now drives both halves — no new + UI. + +- 85e1e4e: feat(rest): `treatAsHistorical` import option — skip the state machine for historical-data migration (#3479) + + Sibling of #3433 (seed exemption), one entry point over. #3165's `initialStates` enforced + the FSM entry point on every INSERT, so importing established historical facts — + a batch of already-`closed` tickets, `closed_won` deals, `completed` projects — + was rejected row-by-row with `invalid_initial_state`, blocking the core + data-migration path. Unlike the seed case it was visible (per-row errors), but it + still functionally blocked a legitimate use. + + - **spec**: `ExecutionContext.skipStateMachine` — a general, server-set flag (the + seed-specific `seedReplay`'s sibling) that skips the `state_machine` rule for a + write; `ImportRequestSchema.treatAsHistorical` (default `false`) — the user-facing + import option. + - **objectql**: the engine now skips the state machine for `seedReplay` OR + `skipStateMachine` (one helper), covering both seed replay and historical import. + - **rest**: the import runner sets `skipStateMachine` on the write context iff the + request opts into `treatAsHistorical`; default off, so a normal import still walks + the FSM (the strict behavior is the default). Import **undo** now also carries + `skipStateMachine`, since restoring a prior snapshot re-writes an earlier state + that need not be a legal transition from where the row is now. + - **platform-objects**: `sys_import_job.treat_as_historical` audit column (additive). + + Scope is identical to the seed exemption: ONLY the `state_machine` rule is skipped; + field shape, `format`, `cross_field`, `script` all still run. The objectui import + wizard checkbox is a separate follow-up. + +- e1fa8d5: fix(objectql): arm the late-manifest metadata bridge on project kernels too + + The per-manifest bridge added for marketplace installs (#3428) armed itself + inside the same `environmentId === undefined` gate as the one-shot startup + bridge — but `os dev` boots the kernel project-scoped (environmentId + 'env_local'), which is marketplace install-local's primary home, so the fix + was inert exactly where it matters. Caught by browser-dogfooding the install + flow. + + The gate is correct for the one-shot bridge (it copies the entire + process-wide SchemaRegistry, which would leak sibling-project objects on + multi-environment servers) but does not apply to the per-manifest bridge: it + only copies the objects of the one package this kernel just registered. + Arming now happens unconditionally at the end of `start()`; boot-time + behavior on every kernel shape is unchanged (the flag still flips only after + the startup path has run), and the one-shot bridge keeps its gate. + +- 402f534: fix(objectql): bridge late-registered manifest objects into the metadata service + + Marketplace-installed template packages register through the `manifest` + service on `kernel:ready` (install) or later (HTTP install), but the one-shot + SchemaRegistry→metadata bridge runs once during `ObjectQLPlugin.start()` — + so their objects only ever reached the ObjectQL registry. Every + IMetadataService consumer (AI `describe_object`, Studio object lists, + `metadata.listObjects`) missed them; only the seed loader had grown an + engine-side fallback (#3422). + + The manifest service's `register` now bridges the manifest's own objects into + the metadata service after registering them with the engine, resolving the + service at call time and mirroring the startup bridge's contract: + `register('object', name, obj, { notify: false })` (#3112), skip entries it + did not bridge itself, refresh its own copy on same-package re-install (hot + upgrade). Armed only after `start()` has run the one-shot bridge, and never + on project kernels — boot-time behavior is unchanged. `register` now returns + a promise; the marketplace install/rehydrate paths await it so metadata reads + right after an install are deterministic. + +- 0c302a7: Exempt curated seed writes from `state_machine` validation (#3433). + + A seed is a snapshot of established facts — a project already `completed`, an + opportunity already `closed_won` — not a record walking its lifecycle. But once + an object declared `state_machine.initialStates` (#3165), the write path enforced + the FSM entry point on **every** insert, so seed replay silently rejected every + mid-lifecycle row and cascaded its master-detail children. That is the "installed + but no data" failure for the showcase board (1 of 5 projects), and it would hit + every marketplace template (a `closed_won` opportunity, a `closed` case) plus the + rehydrate-heal and per-org replay paths. + + `SeedLoaderService` now marks its writes with a server-set `ExecutionContext.seedReplay` + flag; the engine passes `skipStateMachine` to the rule evaluator for those writes, + which skips the `state_machine` rule on both insert (`initialStates`) and update + (transitions). The exemption is scoped to `state_machine` only — a seed must still + satisfy every other validation (`format`, `cross_field`, `script`, `json_schema`, + `conditional`). Because all seed paths funnel through `SeedLoaderService.SEED_OPTIONS`, + the fix covers boot inline seed, marketplace install/heal, and per-org replay at once. + + The showcase project seed drops its three-phase FSM-walk workaround (#3415) and + seeds each project directly at its real status again. + +- 5f0852f: fix(driver-sql): bucket a SQLite `Field.datetime` by its stored instant instead of collapsing every row into one `(null)` (#3773) + + On SQLite, any trend chart bucketed by day/week/month/year over a + `Field.datetime` column put **every record in a single `(null)` bucket** — one + bar, carrying the whole total. The measure was right; only the bucket key was + wrong. `Field.date` (ISO TEXT storage) was unaffected, so the same dashboard + could show one column working and the next one flat. + + better-sqlite3 stores a `Field.datetime` as INTEGER epoch **milliseconds** (knex + binds a JS `Date` as `.getTime()`), and `buildDateBucketExpr` emitted a flat + `strftime('%Y-%m', col)`. SQLite reads a bare integer as a **Julian day + number**; an epoch-ms value is far outside the legal range, so `strftime` + returned NULL for every row. Nothing downstream noticed: SQLite advertises + `queryDateGranularity.month`, so `engine.aggregate` pushes the bucketing down, + and its in-memory fallback only engages for an _unsupported_ granularity or a + non-UTC timezone. + + The SQLite expression is now storage-aware, sharing one `isEpochStoredDatetime` + predicate with the filter-comparand coercion added for the same root cause in + \#2034 — a window and a bucket that disagree about storage is exactly how an + epoch column ended up correctly filtered and then entirely bucketed as NULL. + Postgres and MySQL are untouched: `defineColumn` maps `Field.datetime` to a + native timestamp there, which is also why their comparands are left alone. + + Two details are load-bearing and pinned by tests: + + - The conversion dispatches on each **stored value's** type, not just the + declared one. A SQLite `Field.datetime` column is genuinely mixed-form — + `formatInput` passes datetime values through, so a `Date` lands as INTEGER + while an ISO string (including an unresolved `defaultValue: 'NOW()'`) lands as + TEXT. Dividing TEXT by 1000 coerces it to its leading year, filing live rows + under 1970 — worse than the NULL it replaced. + - Division is `/1000.0`, not `/1000`. Integer division truncates toward zero, so + a pre-1970 instant (`-1` ms) would surface as 1970-01-01. + + `bucketDateValue` (the in-memory fallback in `@objectstack/objectql`) now reads a + finite **number** as epoch milliseconds. `new Date(String(1767225600000))` is an + Invalid Date, so a driver handing back raw storage values bucketed as `'(null)'` + there while the pushed-down SQL bucketed correctly — fixing only the driver would + have traded one wrong answer for two different ones, and the two paths have to + label the same instant identically for a drill-down to survive crossing them. + + `SqliteWasmDriver` inherits `buildDateBucketExpr`, so it carried the bug and gets + the fix. + +- cde1975: fix(dev): eliminate three fixed startup log warnings so official examples boot clean (#3420) + + `os dev` on the stock showcase printed three fixed noise sources on every boot, + with zero example-side changes — training users to ignore warnings. + + - **spec** — add a field-level `ackPlaintextMasking: true` opt-out for the + generic `password` author-time warning (ADR-0100). A deliberately-masked + field (like field-zoo's `f_password`) can now affirm intent instead of + printing an un-actionable "safe to ignore" on every boot; the warning text + points authors at the flag. + - **plugin-auth** — pass better-auth's documented + `silenceWarnings.oauthAuthServerConfig` to `oauthProvider(...)`. We already + mount the `/.well-known/oauth-authorization-server` documents ourselves at + the issuer root, so the plugin's "please ensure it exists" reminder was a + false positive (printed twice); silencing it removes both. + - **objectql** — route the Registry's re-register / package-overwrite lines + (normal rebuild / HMR / seed-replay paths) through a new debug-only + `SchemaRegistry.debug()` so they stay out of the default `info` boot log. Adds + a `logLevel` construction option (and matching `OS_REGISTRY_LOG` env var) so + the debug-gated housekeeping is discoverable for troubleshooting. + +- 54f479a: fix(objectql): accept relative and inline URLs on `url` fields + + The record-validator's `url`-type check required an absolute `scheme://` URL, + so it rejected the **root-relative** value the platform's own storage service + returns for an uploaded file. The console avatar uploader + (`createObjectStackUploadAdapter`) PUTs the image to storage and then writes + `sys_user.image` (a `Field.url`) = `/api/v1/storage/files/`; that failed + `invalid_url` and — on the better-auth `update-user` path — surfaced as a + failed profile save (the "上传用户头像报错" avatar-upload bug). + + `URL_RE` now also accepts root-/protocol-relative refs (`/path`, `//host/path`) + and the `data:` / `blob:` inline forms, in addition to `scheme://…`. A bare + scheme-less string with no leading `/` (e.g. `"notaurl"`) is still rejected. + Verified end-to-end in the running Console: avatar upload → display → replace → + remove all succeed. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [4c5a584] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/metadata-protocol@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 412741fa04..4bb8702d39 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 d9ef79cf5e..350e1304d5 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/observability +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 5d6c8e1a7f..e03ff56b98 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 b4ef07c41e..254dbc057b 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,795 @@ # @objectstack/platform-objects +## 17.0.0-rc.0 + +### Major Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +### Minor Changes + +- fdb4f50: feat(migrate): `os migrate files-to-references` — a data migration with a self-check, gated per deployment (#3617) + + The ADR-0104 file-as-reference migration ships as a command a deployment runs + against its own database, and the deployment-level flag it records is what may + later authorise irreversible behaviour — never the platform version. + + ```bash + os migrate files-to-references # dry run: reports, writes nothing + os migrate files-to-references --apply # converts, verifies, records the flag + ``` + + The run backfills legacy file-field values (inline metadata blobs, own-resolver + URLs, `data:` URIs) into owned `sys_file` references, reconciles the ownership + ledger against what records actually hold, and — only on an `--apply` run whose + reconciliation reports **zero blocking discrepancies** — records + `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. + + **Why a flag rather than a release note.** ObjectStack is a development + platform: third-party deployments upgrade on their own schedule and their data + is not observable by anyone else, so no release-side soak can vouch for them. + The evidence has to be produced where the data is. Consequences: + + - Installing a new version never starts deleting bytes. Running the migration + and passing its self-check is the consent. + - Not run, or not passed → files are retained forever. Wasted storage, zero + data loss. + - A later failing run **clears** `verified_at`: a deployment whose data has + drifted closes its own gate. + - A dry run writes nothing at all — not the conversions, and not the flag, + even when the self-check would pass. + - External URLs stay advisory. They are not `sys_file`s, so they can never + enter collection; whether to remodel them as a `url` field is the app + author's decision (ADR-0104 R7), not a gate. + + Ships alongside: + + - `@objectstack/spec` — `DataMigrationFlagSchema`, `FILE_REFERENCES_MIGRATION_ID`, + and the single `isDataMigrationFlagVerified` predicate both future consumers + (collection #3459, strict value-shape #3438) read, so the two gates cannot + disagree about the same fact. + - `@objectstack/platform-objects` — the `sys_migration` object plus + `readDataMigrationFlag` / `isDataMigrationVerified` / `recordDataMigrationRun`. + Reads fail toward "not verified": a gate that cannot read its evidence stays + closed. + - `@objectstack/objectql` — a read may now opt out of file-reference expansion + via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`, and the storage service's + bookkeeping/scan reads do. Without it the read resolver rewrites stored ids to + their expanded form before the reconciliation sees them, which reports held + references as absent — noisy `stale_owner` findings, and a missed + `unowned_reference` would have been a false pass of the collection gate. + +- 1bd5652: feat(auth): give ADR-0105 D8's scope-bounded issuance a caller — the + `delegated_admin` org role, capped so it cannot mint authority (#3697) + + D8 authorizes invitation _placement_ against the issuer's `adminScope` + (ADR-0090 D12), so a delegated plant admin may invite only into their own + subtree. That gate is implemented, unit-proven and reachable — but no principal + could reach it in a state where it did anything: + + - better-auth grants `invitation: ["create"]` to `owner` and `admin` only + (`memberAc` holds `invitation: []`, and roles registered through + `additionalOrgRoles` inherited that empty statement); + - under a wall-enforcing posture, owners and admins are auto-elevated to + `organization_admin` (`auto-org-admin-grant.ts`), which carries the wildcard + `modifyAllRecords` that makes `isTenantAdmin()` true — and the gate + short-circuits on tenant admins. + + The two sets were disjoint. Issuance placement was bounded by the Layer 0 org + wall (real, and correct) but never by `adminScope`, so D8's motivating story — + "a plant admin invites into their own subtree without a platform admin + finishing the job" — could not happen. + + **Two pieces, and they only ship together.** + + **1. The role.** `delegated_admin` is now registered with the organization + plugin as `memberAc.statements` plus `invitation: ["create"]` — the one + membership grade that may reach `/organization/invite-member` without being an + org admin. Deliberately _not_ `invitation: ["cancel"]`: better-auth's cancel + route checks the permission with no inviterId attribution, so it would mean + "cancel anyone's pending invitation in the org". + + The role carries no ObjectStack authority by construction — `mapMembershipRole` + passes it through as a position name, and with no `sys_position_permission_set` + binding that name resolves to nothing. Role = _can reach the endpoint_; + `adminScope` = _what the endpoint permits_. + + `sys_member.role` and `sys_invitation.role` each gain `delegated_admin` as a + fourth option. Those selects are **enforced on write** — better-auth's own + invitation and membership inserts are validated like any other row — so + registering the role with the org plugin without listing it in both would have + produced a role nobody could hold and nobody could hand out + (`ValidationError: role must be one of: owner, admin, member`). That is exactly + how the end-to-end regression caught it, twice; neither unit test could. The + three non-English translation bundles carry the English label for the new option + until localized. + + **2. The role cap**, in the framework's own `beforeCreateInvitation` hook, + beside the D8 placement gate. Registering the role alone would have been a + four-step privilege escalation: better-auth's only role-level cap on _what role + you may invite someone as_ is its `creatorRole` check (default `owner`), which + blocks inviting an **owner** but not an **admin** — and an accepted `admin` + membership is auto-elevated to `organization_admin` → `isTenantAdmin()`. A + subtree-scoped delegate could have manufactured a tenant admin, with every + existing defense off the path (`sys_member` is not a `GOVERNED_OBJECT`, and the + acceptance-time membership write runs under better-auth's context, not the + issuer's). + + The cap refuses an invitation whose role outranks the issuer's own, and + restricts a below-admin issuer to plain `member` — not merely "not admin/owner", + because an app-registered role projects into `current_user.positions` and may be + bound to permission sets, making it a capability channel too. A delegate's + channel for capability is the invitation's _placement_ intent, which the D12 + gate allowlists position-by-position. The cap applies to every invitation, + placement-carrying or not (the escalation is independent of placement), and + fails closed: an issuer role that cannot be resolved confers nothing above a + plain member. + + **What changes for deployments.** One new class of principal exists: members + holding the `delegated_admin` org role, who can invite into the org — as + `member` only, into the subtree their `adminScope` allows. It is opt-in twice + over (someone must set the membership role _and_ grant an adminScope set), so a + default deployment changes not at all. Org owners and admins are unaffected. + + Also exported: `MEMBERSHIP_ROLE_DELEGATED_ADMIN` from `@objectstack/spec`, so + console and control-plane surfaces name the role from one place. + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 67452d1: feat(spec): resolve page metadata i18n — `page:header` title/subtitle (#3589) + + Custom system pages authored as metadata (Installed Apps, Cloud Connection, + Connect an Agent) hard-code their `page:header` copy in + `properties.title` / `properties.subtitle`. Every other metadata type is + localized at the REST boundary, but `page` was not: the `pages` namespace + existed only on `AppTranslationBundleSchema` — a schema no runtime reads — + with no resolver behind it, so those headers stayed English in every locale + while the matching nav labels translated correctly. + + - `TranslationDataSchema` (the shape the i18n service actually serves) gains a + `pages` namespace: `pages..{label,description,title,subtitle}`. + - New `translatePage` in `@objectstack/spec/system` translates a page's own + `label` / `description` and overlays `title` / `subtitle` onto every + `page:header` in the page's regions. Registered in + `translateMetadataDocument`, so it rides the existing read path. + - `page` added to the REST boundary's `TRANSLATABLE_META_TYPES`. Locale + extraction, the locale-keyed ETag, and `Vary: Accept-Language` already + covered every metadata type — no new plumbing. + - `objectstack i18n extract` now emits page entries, including the + `page:header` copy, so the new namespace is not invisible to the tooling. + - zh-CN / ja-JP / es-ES translations shipped for the three Setup pages, plus + the missing `nav_cloud_connection` / `nav_connect_agent` nav labels (these + existed only in zh-CN). + + Header copy is keyed by **page name**, not by component id: `page:header` + instances carry no stable id. `title` falls back to `pages..label`, since + a page's header title and its nav label are normally the same string. + + Authoring is unchanged and English literals stay in metadata as the fallback — + a page with no `pages` entry renders exactly as before. Consumers of + `@object-ui` need no change: pages arrive already localized from the server. + +- aa8b847: feat(authz): scoped invitations — placement intent on an invitation, gated by + the issuer's adminScope and applied on acceptance (ADR-0105 D8) + + An invitation may now carry PLACEMENT INTENT — the business unit the invitee + lands in and the positions they are assigned — so a delegated (plant) admin's + invitee arrives already in the right unit and role instead of waiting on a + platform admin. This closes the structural gap ADR-0105 D8 names for + `single`-posture deployments and is the natural admission path under `group`. + + The two halves ship together, deliberately: + + - **Issuance is authorized** against the ISSUER's `adminScope` (ADR-0090 D12), + by dry-running the existing `DelegatedAdminGate` against the very + `sys_user_position` rows the acceptance would write. The gate is reused + verbatim — no second copy of the subtree/allowlist logic to drift — so an + invitation can never place what its issuer could not have assigned directly. + Without that gate the feature would be an escalation hole: the built-in + `organization_admin` is deliberately read-only on the RBAC tables precisely + so a fresh org admin cannot rebind themselves, and applying an unchecked + invitation payload under system context would hand that authority straight + back. + - **Acceptance applies it**, idempotently and failure-isolated: a replayed + acceptance converges instead of duplicating assignments, and a placement + miss never undoes a valid membership. + + Surface: + + - `sys_invitation` gains `business_unit_id` + `positions` (ADR-0092 extension + fields, registered in the D7 collision-guarded whitelist; NOT generically + editable — placement is set only at issuance, through the gate). + - `@objectstack/plugin-security` registers the `invitation-placement` service + (`assertIssuable` / `apply`). + - `@objectstack/plugin-auth` wires better-auth's `beforeCreateInvitation` / + `afterAcceptInvitation` to it. **Fail closed**: an invitation that requests + placement in a deployment without the delegated-administration runtime is + refused, never silently placed unchecked. + + Existing invitations are unaffected — an invitation without placement intent + never consults the gate and behaves exactly as before. + +### Patch Changes + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- d44dbfa: feat(spec)!: shrink the `ApiMethod` enum to the six primitives — legacy values are stripped at parse, never honored (#3543, P2 of #3391) + + **BREAKING** (the `!` marker and this changeset are the breaking-change + record; the train ships as the v17 major — see the `v17-rc-anchor` changeset): + the authored `enable.apiMethods` enum is now exactly the six + primitives (`get`, `list`, `create`, `update`, `delete`, `bulk`). The eight + legacy values (`upsert`, `aggregate`, `history`, `search`, `restore`, `purge`, + `import`, `export`) are no longer authorable — they are DERIVED effective + operations, resolved by the server's single derivation table. + + **Migration (FROM → TO).** Replace each legacy value with the primitives it + derives from, then de-duplicate; if the result names all six primitives, delete + the `apiMethods` key entirely (equivalent to default-open, and it tracks future + primitives): + + | FROM (legacy) | TO (primitives) | why | + | ------------- | -------------------- | ---------------------------------------------- | + | `upsert` | `create`, `update` | upsert ⊆ create ∧ update | + | `import` | `create`, `update` | import ⊆ create ∨ update (writeMode-precise) | + | `export` | `list` | export ⊆ list | + | `aggregate` | `list` | aggregate ⊆ list | + | `search` | `list` | search ⊆ list ∧ `searchable` | + | `history` | `get` | history ⊆ get ∧ `trackHistory` | + | `restore` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + | `purge` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + + Reporter codemod: `node scripts/codemod/apimethods-legacy-to-primitives.mjs` + (scans, reports the exact replacement per site, and flags whitelists the + mapping would WIDEN so the edit stays reviewable). + + **Stored metadata keeps parsing — permanent tolerance, narrowing only.** Real + metadata does not upgrade in lockstep with the spec, so a stored legacy value + is NOT a parse error: `stripLegacyApiMethods` (new export) strips it with a + FROM→TO warning (canonicalize-and-warn). Stripping only ever NARROWS exposure — + the derivation table still grants every legacy verb that derives from the + primitives you declared. Two cliffs to know: + + 1. A whitelist of ONLY legacy values (e.g. `['upsert']`) strips to `[]` = + **deny-all** — the object's API closes instead of widening. The strip + warning and the objectql registration diagnostic both call this out. + 2. A legacy value NOT derivable from your declared primitives (e.g. + `['get', 'export']` — export needs `list`) was honored by the P1 + "explicit wins" path and is now denied. Declare the underlying primitive. + + **Type split — authored vs effective vocabulary.** `ApiMethod` (authored) is + now six values; the NEW `ApiOperation` type / `ApiOperationSchema` / + `API_OPERATION_ORDER` (fourteen values, byte-stable pre-shrink wire order) + carry the EFFECTIVE vocabulary. The wire contract is unchanged: the 405 + `allowed` array and `/me/permissions` `apiOperations` still serialize derived + verbs (`export`, `search`, …), and `EffectiveObjectPermissionSchema.apiOperations` + now validates against `ApiOperationSchema`. `EffectiveApiMethods.explicitLegacy` + is removed (nothing is honored verbatim anymore); `API_METHOD_ORDER` remains as + a deprecated alias of `API_OPERATION_ORDER`. + + **Fail-closed tightening (#3545):** a PRESENT but non-array `apiMethods` (only + producible by a raw/out-of-band metadata write) now resolves to `deny-all` + instead of unrestricted — a policy that exists but cannot be read fails CLOSED. + + **Published JSON Schema diverges deliberately:** `data/ApiMethod.json` is the + strict six-value enum (a `z.preprocess` is not representable in JSON Schema), + so external JSON-Schema validators reject legacy values that the zod parse + would strip-and-warn. Treat the JSON Schema as the authored contract; the zod + tolerance exists for stored metadata. + + **objectql:** the P1 "explicit wins" transition is reclaimed — + `warnDeprecatedExplicitApiMethods` is replaced by `warnStrippedLegacyApiMethods` + (a permanent per-object diagnostic for schemas that reach the registry without + passing through Zod; the parse-time strip warning carries no object name). + + **platform-objects:** whitelist audit — `sys_business_unit`, + `sys_business_unit_member` (P1's explicit `import`/`export` reclaimed) and + `sys_user_preference` dropped their `apiMethods` entirely (each named all six + primitives = default-open). Read-only and deny-all whitelists are unchanged; + the seven `[]` declarations are deliberately KEPT as defense-in-depth alongside + `apiEnabled: false`. + +- e9b11df: fix(auth): app-declared organization roles are now storable, not just registerable (#3723) + + `AuthManagerOptions.additionalOrgRoles` registered every `permission` / + `position` name a stack declared with better-auth's organization plugin, so + `POST /organization/invite-member { role: 'sales_rep' }` passed the role check — + and then the write failed, because `sys_invitation.role` and `sys_member.role` + were closed selects listing `owner|admin|member` only: + + ``` + ValidationError: role must be one of: owner, admin, member + { field: 'role', code: 'invalid_option' } + ``` + + A select is enforced on write and better-auth's own inserts are not exempt (they + run through the ordinary ObjectQL validator), so any stack declaring role names + was registering roles that could be requested and never stored. + + Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single + normalizer; its output feeds better-auth's role map **and** the two `select` + option lists, so neither side can accept a name the other rejects. The built-in + roles (`owner`, `admin`, `delegated_admin`, `member`) live in + `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the + platform objects declare statically — app roles are appended at boot. + + New exports: + + - `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to + `identity/membership-role`; the package-level export path is unchanged). + - `@objectstack/plugin-auth` — `collectStackOrgRoles`, + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, + `withMembershipRoleOptions`. + + Hosts that boot `AuthPlugin` from a loaded stack should derive + `additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the + stack themselves — `objectstack serve`, the `@objectstack/verify` harness and + `DevPlugin` now all do. The harness previously passed none, which is why a + dogfood proof could boot a stack whose declared roles better-auth had never + heard of; `DevPlugin` documents itself as equivalent to the full stack and + silently excluded app roles from that equivalence. + + `additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and + `collectStackOrgRoles` now returns those descriptors. The label is what the + declaring `position` / `permission` metadata already says, so the role picker + shows `Executive` for a position declared as such instead of title-casing the + machine name into `Exec` — a third source of truth for one string. Presentation + only: better-auth sees just the name, and the stored value is always the name. + Passing `string[]` keeps working unchanged. + + Behaviour change worth noting: a declared role name that is not a valid machine + name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a + boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a + name would be registered verbatim and stored mangled — the same mismatch with + extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. + +- bc17d39: fix(auth): provision the better-auth 1.7 columns `sys_team` / `sys_team_member` / `sys_two_factor` were missing (#3624) + + better-auth 1.7.0-rc.1 added fields to three models that the platform objects + never provisioned and `auth-schema-config.ts` never mapped. Because an unmapped + field keeps its camelCase name, the adapter emitted columns no table had: + + | model | field | column now provisioned | + | :----------- | :---------------------------------------- | :---------------------------------------------------------- | + | `team` | `memberCount` | `sys_team.member_count` | + | `teamMember` | `membershipKey` | `sys_team_member.membership_key` | + | `twoFactor` | `failedVerificationCount` / `lockedUntil` | `sys_two_factor.failed_verification_count` / `locked_until` | + + The team pair broke org creation outright. The organization plugin's team + sub-feature is on by default, so `POST /api/v1/auth/organization/create` + auto-creates a default team — and that insert died with `table sys_team has no +column named memberCount` _after_ the organization row had already committed. + Callers got an HTTP 500 on top of a half-created org: a real org row with no + default team behind it. Every multi-org deployment's create-org flow hit this. + + The two-factor pair broke the 2FA lockout path the same way: better-auth + guard-increments `failedVerificationCount` on each wrong code and stamps + `lockedUntil` past the threshold, so a wrong code 500'd instead of being + counted. All four columns are better-auth's own state — provisioned, readable, + and never written from the ObjectStack side. + + Existing environments pick the columns up through the driver's additive schema + sync; no data migration is needed. `member_count` backfills to 0 and + better-auth's own `syncTeamMemberCount` reconciles it on the next membership + change, and `membership_key` stays null on pre-upgrade rows, which better-auth + tolerates by falling back to the `(team_id, user_id)` pair. + + A new drift gate (`better-auth-schema-parity.test.ts`) now asserts that every + column the installed better-auth version can write exists on the platform + object backing it, across the auth manager's whole model surface. The ADR-0092 + D7 guard only ever caught _collisions_ between our extension fields and + better-auth's, so a bump that adds a brand-new field passed the build and failed + at runtime — twice now, counting the 1.7 `oauthAccessToken.authorizationCodeId` + regression. The next one fails the build instead. + +- 524151c: fix(i18n): clear the accumulated drift in the generated translation bundles + + The committed bundles had fallen behind the spec on three independent axes. + `os i18n extract` (merge mode — every existing translation is preserved) + reconciles all of them: + + **Keys the spec no longer has**, still carrying translations in + `*.metadata-forms.generated.ts`. All three were removed deliberately and are + now _rejected_ by the schema, so their entries were dead weight: + + - `capabilities.trash` / `capabilities.mru` — `enable.trash`/`enable.mru` + retired in the 16.x line (#2377), with tombstone guidance in + `UNKNOWN_KEY_GUIDANCE`. + - agent `visibility` — removed 2026-07 (#1901). + + **Keys the spec gained** but the bundles never learned: the + `summaryOperations.*` sub-fields (`object` / `function` / `field` / + `relationshipField` / `filter`), and `sys_invitation.business_unit_id` / + `positions` from the ADR-0105 D8 placement work. + + **Objects stuck on empty strings.** `sys_migration`'s labels and help text were + committed as `""` in the ja-JP and es-ES bundles, which renders as _blank_ in + those locales rather than falling back to anything readable. They now carry the + schema text like every other untranslated key. + + No API or schema change — this only affects what the UI displays. + +- d1cabaa: fix(i18n): translate the SSO / SCIM / user-position / import-job admin objects + + Four live, UI-facing system objects were registered but never added to their + package's i18n extract config, so non-English admins saw raw English `label` + metadata: + + - `sys_sso_provider`, `sys_scim_provider` (platform-objects) — identity-provider + admin grids plus the register / verify-domain actions. + - `sys_user_position` (plugin-security) — delegated position assignment + (`userActions` create/edit/delete); its sibling `sys_user_permission_set` was + already translated, so this closes an inconsistency. + - `sys_import_job` (platform-objects) — import history / progress, alongside the + already-translated `sys_job` / `sys_job_run`. + + Adds each object to its package's `scripts/i18n-extract.config.ts` and supplies + real zh-CN / ja-JP / es-ES translations across all four locale bundles, and + extends the bundle-ownership guards' `OWNED_OBJECTS` to cover them. The + orphan-only guards from #3502 could not catch this "owned-and-live-but-never- + extracted" gap. + +- 85e1e4e: feat(rest): `treatAsHistorical` import option — skip the state machine for historical-data migration (#3479) + + Sibling of #3433 (seed exemption), one entry point over. #3165's `initialStates` enforced + the FSM entry point on every INSERT, so importing established historical facts — + a batch of already-`closed` tickets, `closed_won` deals, `completed` projects — + was rejected row-by-row with `invalid_initial_state`, blocking the core + data-migration path. Unlike the seed case it was visible (per-row errors), but it + still functionally blocked a legitimate use. + + - **spec**: `ExecutionContext.skipStateMachine` — a general, server-set flag (the + seed-specific `seedReplay`'s sibling) that skips the `state_machine` rule for a + write; `ImportRequestSchema.treatAsHistorical` (default `false`) — the user-facing + import option. + - **objectql**: the engine now skips the state machine for `seedReplay` OR + `skipStateMachine` (one helper), covering both seed replay and historical import. + - **rest**: the import runner sets `skipStateMachine` on the write context iff the + request opts into `treatAsHistorical`; default off, so a normal import still walks + the FSM (the strict behavior is the default). Import **undo** now also carries + `skipStateMachine`, since restoring a prior snapshot re-writes an earlier state + that need not be a legal transition from where the row is now. + - **platform-objects**: `sys_import_job.treat_as_historical` audit column (additive). + + Scope is identical to the seed exemption: ONLY the `state_machine` rule is skipped; + field shape, `format`, `cross_field`, `script` all still run. The objectui import + wizard checkbox is a separate follow-up. + +- 5487c20: fix(auth): provision `sys_scim_provider.provider_key` — SCIM provider creation failed the moment SCIM was switched on (#3653) + + `@better-auth/scim` declares `providerKey` as `required: true, unique: true` + and writes it on every provider insert — a derived `:` + uniqueness key it owns end to end. `sys_scim_provider` never provisioned the + column, so the adapter emitted a `provider_key` no table had: the same failure + shape as #3624, waiting behind the `OS_SCIM_ENABLED` flag. + + Found by extending the better-auth parity gate to `@better-auth/sso` and + `@better-auth/scim`. Neither accepts a `schema` option, so `getAuthTables()` is + blind to them and they were excluded when that gate shipped; the gate now reads + each plugin's own declared schema and resolves columns the way the adapter + actually does for a bridged model. `@better-auth/sso` came back fully covered. + + Existing environments pick the column up through the driver's additive schema + sync; it stays null on pre-upgrade rows, which the nullable UNIQUE index admits. + +- 9aa5510: fix(i18n): ship the missing object-translation keys for the better-auth 1.7 and ADR-0105 D6 fields (#3624 follow-up) + + The generated object-translation bundles predate two rounds of field additions, + so six fields had no entry in **any** locale and fell back to their raw schema + labels in every UI surface that reads the bundle: + + - `sys_team.member_count`, `sys_team_member.membership_key`, + `sys_two_factor.failed_verification_count` / `locked_until` — the better-auth + 1.7 columns provisioned in #3647. + - `sys_organization.parent_organization_id` / `sort_order` — the same gap left + by the earlier ADR-0105 D6 group-structure work. + + Regenerated with `os i18n extract` (merge mode, so every existing translation is + preserved — the diff is purely additive). No API or schema change; the fields + themselves already shipped. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 7502b10758..8497aaa9af 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 7f220fc31f..07ea8195f9 100644 --- a/packages/plugins/driver-memory/CHANGELOG.md +++ b/packages/plugins/driver-memory/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/driver-memory +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/driver-memory/package.json b/packages/plugins/driver-memory/package.json index 24696a313d..405fb770de 100644 --- a/packages/plugins/driver-memory/package.json +++ b/packages/plugins/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 f54b574833..3d118cb182 100644 --- a/packages/plugins/driver-mongodb/CHANGELOG.md +++ b/packages/plugins/driver-mongodb/CHANGELOG.md @@ -1,5 +1,255 @@ # @objectstack/driver-mongodb +## 17.0.0-rc.0 + +### Minor Changes + +- d1557d9: feat(driver-mongodb)!: declare the driver single-tenant and refuse to boot multi-tenant (#3724) + + `MongoDBDriver` implements **no row-level tenant isolation** — it never reads + `DriverOptions.tenantId`, so reads carry no tenant predicate and writes are not + stamped with a tenant column. The layer the SQL driver has (`resolveTenantField` + + - `applyTenantScope`) simply does not exist here, while everything above the + driver — object metadata's `tenancy` block, `applySystemFields` injecting + `organization_id`, the engine threading `tenantId` into every driver call — + operates on the assumption that tenant isolation is a platform guarantee. Point + a multi-tenant deployment's datasource at Mongo and every query read, updated + and deleted other tenants' documents, silently. + + Rather than serve unisolated, the driver now fails fast at startup: + + - The **constructor** and `connect()` call `assertSingleTenantPosture()`, which + refuses any tenancy posture other than `single` (`OS_TENANCY_POSTURE=group` / + `isolated`, including the posture derived from `OS_MULTI_ORG_ENABLED=true`), + resolved through the shared `resolveTenancyPosture()` so the driver can never + disagree with auth / the registry / the CLI about the mode. The check sits in + the constructor because that is the earliest seam — it fails before a host can + hand the driver anywhere — and `connect()` re-checks in case a host flips the + posture in between. (It originally had to live in the constructor because + `ObjectQLEngine.init()` _caught_ a driver's connect rejection and booted + anyway; that is fixed in the same release, #3741, so both seams abort boot.) + - `syncSchema()` / `syncSchemasBatch()` call `assertObjectsNotTenantScoped()` and + refuse objects declaring `tenancy.enabled: true`, naming every offender in one + message. + - `objectstack serve` / `dev` (CLI) now re-throw this error out of the + auto-driver-registration block instead of swallowing it, so boot exits 1 with + the actionable message — the same treatment `UnsupportedDriverError` already + gets. Matched duck-typed by `code`, so the CLI takes no dependency on the + driver package. + + Both throw `MongoDBMultiTenantUnsupportedError` with + `code === 'MONGODB_MULTI_TENANT_UNSUPPORTED'`, a message that names the detected + signal, the remedy, and `@objectstack/driver-sql` as the multi-tenant option. + + There is deliberately **no override env var**: an escape hatch would restore + exactly the silent non-isolation this guard removes. Single-tenant deployments — + every currently-working Mongo deployment — are unaffected. + + This is option B of #3724. Implementing real row-level isolation (option A) + remains open; the `unique` index shape stays single-field until then, which is + now correct by construction rather than by omission. + +- b90086a: fix(driver-sql)!: `unique` materializes per tenant, ending its contradiction with the per-tenant autonumber sequence (#3696) + + `unique: true` became a **single-column global index that ignored `tenancy` + entirely**, while the autonumber sequence table is keyed by + `(object, tenant_id, field, scope)` and hands every tenant its own counter + starting at 1. Two subsystems of the same platform contradicted each other: + tenant B's `PROD-00001` was rejected by an index it could not see — **no user + did anything wrong**, the platform's left hand refused what its right hand + issued. + + The rejection also doubled as a **cross-tenant existence oracle**: a UNIQUE + violation told tenant B that some _other_ tenant held the value, enumerable by + probing emails / codes / names. + + **The contract now:** + + | Declaration | Materializes as | + | -------------------------------- | --------------------------------------------------------------- | + | `unique: true` + tenant column | composite `(tenantField, field)` — unique **within** the tenant | + | `unique: true`, no tenant column | single-column — single-tenant DDL is byte-identical to before | + | `unique: 'global'` | single-column, always platform-wide | + + The tenant column comes first in the composite, so the index also serves the + `WHERE tenant = ?` prefix scans every tenant-scoped read issues. + + **Declared `indexes[]` are deliberately unchanged.** They are materialized over + exactly the columns listed — no tenant column is injected. The author already + spells them out, per-tenant ones have always been written explicitly + (`fields: ['organization_id', 'code']`), and many are legitimately platform-wide + (a DNS hostname, a reserved slug, an external provider id). `'global'` is + accepted there as a synonym of `true` so one vocabulary covers both spellings. + + **Migration is automatic and cannot fail.** Legacy indexes + (`
__unique` from knex, `uniq_
_` from the drift-rebuild + path) are retired inline at schema-sync time. The old global constraint is + strictly stronger than the new per-tenant one, so existing rows satisfy the + replacement by construction — no dedup, no cleanup, no data touched. It + converges at sync rather than waiting for a deliberate `os migrate` run because + a deployment that never ran migrate would otherwise stay broken. + + **Upgrading — audit your `unique: true` fields.** On a tenant-scoped object the + constraint is now per tenant. Anything that must stay platform-wide has to say + so: + + ```ts + hostname: Field.text({ unique: "global" }); // no two tenants may claim it + ``` + + Note the reach: `applySystemFields` injects `organization_id` into every + registered object unless it opts out, and the driver falls back to that column + when no `tenancy.tenantField` is declared — so most objects are tenant-scoped. + Typical candidates for `'global'`: DNS hostnames, reserved slugs, external + provider ids (Stripe customer/subscription), device identities. + + Postgres materializes `col.unique()` as a table CONSTRAINT rather than a bare + index, so the retirement tries `DROP CONSTRAINT` before `DROP INDEX` — + `DROP INDEX` alone would have made the migration a no-op on exactly the + deployments that matter most. + + `@objectstack/driver-mongodb` accepts the new declaration but keeps single-field + indexes: it implements no row-level tenancy at all (no tenant predicate on read, + no tenant stamp on write), so a `(tenant, field)` index would advertise an + isolation it does not deliver. Tracked separately. + +### Patch Changes + +- 030125b: feat(objectql)!: `init()` refuses to boot when a data driver fails to connect (#3741) + + `ObjectQLEngine.init()` wrapped every driver's `connect()` in a try/catch, logged + one error line, and carried on. A server whose database was unreachable therefore + "started successfully" — health endpoints could even stay green — and then failed + every request with an error that reads nothing like _the database is down_. The + warning it printed (`Operations may recover via lazy reconnection or fail at query +time`) was half fiction: grep the repo and no reconnection exists in `driver-sql` + or `driver-mongodb`, so only the "fail at query time" half was ever real. The + caller made it worse — `ObjectQLPlugin.start()` runs `syncRegisteredSchemas()` + immediately after `init()`, issuing DDL against a driver that isn't there. + + The structural half of the bug was worse than the operational one: the catch + removed a driver's ability to **refuse startup at all**. Any fatal startup check — + licence, server version, incompatible configuration, missing capability, not just + an unreachable socket — is expressed by throwing from `connect()`, and every one + of them was silently downgraded to a runtime error. That is why driver-mongodb's + multi-tenancy guard (#3724 / #3734) had to be hoisted into its constructor. + + - `init()` now **throws** `DriverConnectError` (`code: 'ERR_DRIVER_CONNECT'`) + when any boot-registered driver's `connect()` rejects, aborting kernel + bootstrap. It still attempts every driver first, so one failed boot names all + of them. The message is self-contained — each failed driver and its cause — + because the CLI prints `error.message` alone; the first cause is also attached + as `error.cause`. Exported from both `@objectstack/objectql` and + `@objectstack/objectql/core`. + - `connect()` is now a supported place for a driver to veto boot. Startup + validation that needs a live connection (server version, capability probes) + no longer has to be forced into a constructor. + - The misleading "lazy reconnection" warning is gone. + - New escape hatch `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` + (`resolveAllowDriverConnectFailure()` in `@objectstack/types`) restores the old + lenient boot, but loudly: a `DEGRADED BOOT` banner names the failed drivers and + states that they are never retried or reconnected and that every query and + schema sync routed to them will fail for the process lifetime. The banner goes + to stderr as well as the logger, because `os serve` swallows all of stdout + during boot and `Logger` routes `warn` there — logger-only, the one message + that matters would be invisible in exactly the deployment the flag is for. + Defaults off. + + **Migration.** No code or config change is needed for a correctly configured + deployment — a driver that connected before still connects. A deployment that was + _silently_ booting without its database now fails the boot instead, with the + driver name and cause in the error; fix the datasource configuration (typically + `OS_DATABASE_URL`, credentials, or network reachability). To keep booting without + it — deliberately, and knowing every request that touches it will fail — set + `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/driver-mongodb/package.json b/packages/plugins/driver-mongodb/package.json index 409123d743..d312f30b14 100644 --- a/packages/plugins/driver-mongodb/package.json +++ b/packages/plugins/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 2c28d87cbf..825c917bdf 100644 --- a/packages/plugins/driver-sql/CHANGELOG.md +++ b/packages/plugins/driver-sql/CHANGELOG.md @@ -1,5 +1,350 @@ # @objectstack/driver-sql +## 17.0.0-rc.0 + +### Minor Changes + +- dac6a08: feat(driver-sql)!: make index drift visible to `os migrate plan` — no more silent DDL at boot (#3728) + + The #3696 unique-scope migration converged **in place**: `syncTableIndexes` ran a + `DROP` + `CREATE UNIQUE INDEX` during `initObjects`, in every environment, + leaving one log line behind. `os migrate plan` showed nothing, because + `detectManagedDrift` was column-only — `ManagedDriftOp` had no index dimension at + all. An operator who wanted to review the DDL before it reached their database + had no way to, and a managed schema was being auto-altered in production, which + the #2186 contract explicitly forbids. + + Index drift is now a first-class dimension, reconciled through the same path as + column drift: + + - **`syncTableIndexes` is additive only.** It creates indexes; it never drops or + rewrites one. `dropLegacyGlobalUniques` is gone. + - **New `DriftOp` variants** — `replace_unique_index` (safe: retire the legacy + platform-wide unique in favour of the tenant composite), `create_index` (safe), + `recreate_index` (needs-confirm; destructive when it tightens to `UNIQUE`), and + `drop_index` (destructive). + - **`detectManagedDrift` reports them**, `os migrate plan` renders them (index + ops display as `table [index_name]`), and `os migrate apply` executes them. + Index DDL is portable, so it applies directly on every dialect — no SQLite + table rebuild. + - **`replace_unique_index` creates before it drops**, so uniqueness is never + unenforced mid-migration and a failed create leaves the schema untouched. + - **Declared `indexes[]` drift is covered too**: an index metadata declares but + the database lacks, and one whose definition no longer matches the declaration + (the additive sync skips those by name, so they could never self-heal). + - **Orphan detection is limited to ObjectStack's own generated naming** + (`uniq_…` / `idx_…`, plus the pre-#3696 `
__unique` knex + spelling). A hand-rolled operational index is never reported as drift and + `--allow-destructive` will not delete it. + + **Behaviour change.** Boot no longer rewrites the index unconditionally. Dev + (`autoMigrate: 'safe'`, what `os dev` / `os serve` use) still self-heals on + restart, so local workflows are unchanged. Production now **warns** with an + actionable `os migrate` hint and leaves the schema alone — the deployment stays + on the legacy global unique (multi-tenant inserts still collide) until someone + runs `os migrate apply`. That is the deliberate trade: a visible, pre-inspectable + migration instead of an invisible one. + + Also fixed: `managedObjectIndexes` was never cleared when an object dropped its + `indexes[]`, so drift detection kept expecting an index nobody declared. + + `SchemaDiffEntryKind` gains `index_mismatch` and `unmapped_index`. + +- b90086a: fix(driver-sql)!: `unique` materializes per tenant, ending its contradiction with the per-tenant autonumber sequence (#3696) + + `unique: true` became a **single-column global index that ignored `tenancy` + entirely**, while the autonumber sequence table is keyed by + `(object, tenant_id, field, scope)` and hands every tenant its own counter + starting at 1. Two subsystems of the same platform contradicted each other: + tenant B's `PROD-00001` was rejected by an index it could not see — **no user + did anything wrong**, the platform's left hand refused what its right hand + issued. + + The rejection also doubled as a **cross-tenant existence oracle**: a UNIQUE + violation told tenant B that some _other_ tenant held the value, enumerable by + probing emails / codes / names. + + **The contract now:** + + | Declaration | Materializes as | + | -------------------------------- | --------------------------------------------------------------- | + | `unique: true` + tenant column | composite `(tenantField, field)` — unique **within** the tenant | + | `unique: true`, no tenant column | single-column — single-tenant DDL is byte-identical to before | + | `unique: 'global'` | single-column, always platform-wide | + + The tenant column comes first in the composite, so the index also serves the + `WHERE tenant = ?` prefix scans every tenant-scoped read issues. + + **Declared `indexes[]` are deliberately unchanged.** They are materialized over + exactly the columns listed — no tenant column is injected. The author already + spells them out, per-tenant ones have always been written explicitly + (`fields: ['organization_id', 'code']`), and many are legitimately platform-wide + (a DNS hostname, a reserved slug, an external provider id). `'global'` is + accepted there as a synonym of `true` so one vocabulary covers both spellings. + + **Migration is automatic and cannot fail.** Legacy indexes + (`
__unique` from knex, `uniq_
_` from the drift-rebuild + path) are retired inline at schema-sync time. The old global constraint is + strictly stronger than the new per-tenant one, so existing rows satisfy the + replacement by construction — no dedup, no cleanup, no data touched. It + converges at sync rather than waiting for a deliberate `os migrate` run because + a deployment that never ran migrate would otherwise stay broken. + + **Upgrading — audit your `unique: true` fields.** On a tenant-scoped object the + constraint is now per tenant. Anything that must stay platform-wide has to say + so: + + ```ts + hostname: Field.text({ unique: "global" }); // no two tenants may claim it + ``` + + Note the reach: `applySystemFields` injects `organization_id` into every + registered object unless it opts out, and the driver falls back to that column + when no `tenancy.tenantField` is declared — so most objects are tenant-scoped. + Typical candidates for `'global'`: DNS hostnames, reserved slugs, external + provider ids (Stripe customer/subscription), device identities. + + Postgres materializes `col.unique()` as a table CONSTRAINT rather than a bare + index, so the retirement tries `DROP CONSTRAINT` before `DROP INDEX` — + `DROP INDEX` alone would have made the migration a no-op on exactly the + deployments that matter most. + + `@objectstack/driver-mongodb` accepts the new declaration but keeps single-field + indexes: it implements no row-level tenancy at all (no tenant predicate on read, + no tenant stamp on write), so a `(tenant, field)` index would advertise an + isolation it does not deliver. Tracked separately. + +### Patch Changes + +- fa3d0cf: feat(spec): field runtime value-shape contract — ADR-0104 phase 1 (D1) + + `@objectstack/spec/data` now owns the runtime VALUE shape of every field type + (`field-value.zod.ts`): semantic type classes (`STRING_VALUE_TYPES`, + `NUMERIC_VALUE_TYPES`, `REFERENCE_VALUE_TYPES`, `FILE_REFERENCE_TYPES`, + `STRUCTURED_JSON_TYPES`, `MULTI_CAPABLE_TYPES`, …), the shared + `isMultiValueField`, and `valueSchemaFor(field, 'stored' | 'expanded')`. The + four consumers that each hand-copied this knowledge (objectql record-validator, + rest import-coerce, driver-sql column classification, qa conformance) now + derive from the spec, and the field-zoo round-trip MATRIX is asserted against + the contract so the two cannot drift. + + **Write-path change (objectql, warn-first):** previously-unvalidated types — + single `lookup`/`master_detail`/`user`/`tree`, `file`/`image`/`avatar`/ + `video`/`audio`, `location`, `address`, `composite`, `repeater`, `record`, + `vector` — are now checked against the contract. A violation **logs a warning + and passes** in this release (legacy rows must not strand their records); + set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` to enforce as a + `400 VALIDATION_FAILED`. The flip to strict-by-default rides a later minor + (ADR-0104 R1/R2). + + **Deprecations (removal rides the next spec major), FROM → TO:** + + - `CurrencyValueSchema` (`{value, currency}`) → none. A `currency` field's + value is a **bare number** everywhere in the runtime (validator, SQL `float` + column, import coercion, field-zoo oracle); the currency code lives in field + config. Use `valueSchemaFor({type: 'currency'})`. + - `LocationCoordinatesSchema` (`{latitude, longitude}`) → `LocationValueSchema` + (`{lat, lng}`) — the shape the platform actually stores. + - `AddressSchema` is **adopted** (unchanged) as the enforced `address` value + contract via `AddressValueSchema`. + + No stored data changes shape; the contract codifies deployed reality + ("reality wins", ADR-0104 D1). + +- 0e3a226: fix(authz): widen the driver's native tenant scope to the membership union + under the `group` posture — ADR-0105 D2 finally reaches the wire (#3623) + + The Layer 0 wall correctly compiled `organization_id IN accessible_org_ids` + under `group`, but the ObjectQL engine also propagated the active-org + `tenantId` into `DriverOptions` unconditionally, and the SQL driver's native + scoping ANDed `organization_id = tenantId` under the union — collapsing every + group read back to active-org (isolated) reach. Found by the cloud-side + `ee-group-showcase` dogfood (cloud#880), the first end-to-end boot of `group` + against a real driver. + + - `DriverOptions.tenantIds` (spec): the union tenant access set. Drivers with + native scoping widen reads/updates/deletes/aggregates to `IN (...)`, + keeping the NULL-tenant global-row carve-out; inserts still stamp from + `tenantId` (the active organization is the write target, D5). Absent or + empty ⇒ equality fallback — fail toward isolation, never toward exposure. + - ObjectQL engine threads `ExecutionContext.accessible_org_ids` as + `tenantIds` when the tenancy posture is `group`, reported by a new + `setTenancyPostureProvider` seam. + - SecurityPlugin wires that provider at start — deliberately from the + enforcement layer, so the driver wall only widens while the Layer 0 union + wall enforces above it. Embeddings without plugin-security keep active-org + equality. + +- 81ce41a: feat(rest): `treatAsHistorical` import also preserves the original audit timeline (#3493) + + Follow-up to #3479/#3483. `treatAsHistorical` solved the FSM half — mid-lifecycle + rows are no longer rejected by `initialStates` — but the OTHER half of a historical + migration, preserving the original timeline, still didn't hold: an imported ticket + that closed in 2021 stored `updated_at` = the import day (and `updated_by` = the + importer), and a `writeMode: 'upsert'` refresh silently dropped business `readonly` + fields (`closed_at`, `resolved_by`). Reports, audit, and "recently modified" + sorting all came out wrong. + + Three layers were force-overwriting the timeline; all three now respect a single + new opt-in flag, `ExecutionContext.preserveAudit`, which `treatAsHistorical` sets + alongside `skipStateMachine`: + + - **spec**: `ExecutionContext.preserveAudit` (server-set only, never client-supplied) + and `DriverOptions.preserveAudit` (threaded to the driver's update stamp). + - **objectql** — the built-in audit hook (`plugin.ts`) now treats `updated_at` / + `updated_by` as CLIENT-PREFERRED (`?? now` / `?? userId`) under `preserveAudit`, + symmetric with how `created_at` / `created_by` already behave on insert; and the + static-`readonly` write strip (`stripReadonlyFields`) admits a WHITELIST — the + audit/timestamp family plus author-declared business `readonly` fields — so an + upsert refresh no longer drops them. + - **driver-sql** — the SQL `update` path keeps a supplied `updated_at` instead of + force-advancing it to `now` when `DriverOptions.preserveAudit` is set (fills-only- + empty, mirroring the insert stamp). + - **rest** — the import runner sets `preserveAudit` on the write context iff the + request opts into `treatAsHistorical`. + + Deliberately a WHITELIST, not the blanket `isSystem` exemption: platform-managed + `system` columns OUTSIDE the audit family (`organization_id` / tenancy, generated + columns) STAY stripped, so a historical import reinstates established facts without + becoming a backdoor to forge tenancy. Permissions / RLS / field-level security are + unaffected — this changes only which audit/readonly values the runtime overwrites, + never who may write the record. Fully opt-in: a normal write still auto-stamps + `updated_at`/`updated_by` and strips `readonly` exactly as before. The objectui + "Import as historical data" checkbox (objectui#2815) now drives both halves — no new + UI. + +- 5f0852f: fix(driver-sql): bucket a SQLite `Field.datetime` by its stored instant instead of collapsing every row into one `(null)` (#3773) + + On SQLite, any trend chart bucketed by day/week/month/year over a + `Field.datetime` column put **every record in a single `(null)` bucket** — one + bar, carrying the whole total. The measure was right; only the bucket key was + wrong. `Field.date` (ISO TEXT storage) was unaffected, so the same dashboard + could show one column working and the next one flat. + + better-sqlite3 stores a `Field.datetime` as INTEGER epoch **milliseconds** (knex + binds a JS `Date` as `.getTime()`), and `buildDateBucketExpr` emitted a flat + `strftime('%Y-%m', col)`. SQLite reads a bare integer as a **Julian day + number**; an epoch-ms value is far outside the legal range, so `strftime` + returned NULL for every row. Nothing downstream noticed: SQLite advertises + `queryDateGranularity.month`, so `engine.aggregate` pushes the bucketing down, + and its in-memory fallback only engages for an _unsupported_ granularity or a + non-UTC timezone. + + The SQLite expression is now storage-aware, sharing one `isEpochStoredDatetime` + predicate with the filter-comparand coercion added for the same root cause in + \#2034 — a window and a bucket that disagree about storage is exactly how an + epoch column ended up correctly filtered and then entirely bucketed as NULL. + Postgres and MySQL are untouched: `defineColumn` maps `Field.datetime` to a + native timestamp there, which is also why their comparands are left alone. + + Two details are load-bearing and pinned by tests: + + - The conversion dispatches on each **stored value's** type, not just the + declared one. A SQLite `Field.datetime` column is genuinely mixed-form — + `formatInput` passes datetime values through, so a `Date` lands as INTEGER + while an ISO string (including an unresolved `defaultValue: 'NOW()'`) lands as + TEXT. Dividing TEXT by 1000 coerces it to its leading year, filing live rows + under 1970 — worse than the NULL it replaced. + - Division is `/1000.0`, not `/1000`. Integer division truncates toward zero, so + a pre-1970 instant (`-1` ms) would surface as 1970-01-01. + + `bucketDateValue` (the in-memory fallback in `@objectstack/objectql`) now reads a + finite **number** as epoch milliseconds. `new Date(String(1767225600000))` is an + Invalid Date, so a driver handing back raw storage values bucketed as `'(null)'` + there while the pushed-down SQL bucketed correctly — fixing only the driver would + have traded one wrong answer for two different ones, and the two paths have to + label the same instant identically for a drill-down to survive crossing them. + + `SqliteWasmDriver` inherits `buildDateBucketExpr`, so it carried the bug and gets + the fix. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/driver-sql/package.json b/packages/plugins/driver-sql/package.json index e6626c008f..f0b972987b 100644 --- a/packages/plugins/driver-sql/package.json +++ b/packages/plugins/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 baa6c0bd83..1ba9a5b7ec 100644 --- a/packages/plugins/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/plugins/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,95 @@ # @objectstack/driver-sqlite-wasm +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/driver-sql@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/driver-sqlite-wasm/package.json b/packages/plugins/driver-sqlite-wasm/package.json index 0d408cfca2..a385f5b936 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": "16.1.0", + "version": "17.0.0-rc.0", "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 6960724e75..3a22f0a605 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/embedder-openai +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 64a62c331a..e6a868276c 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 d4851f2447..7c36f81146 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/knowledge-memory +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/service-knowledge@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 9c00525188..c09bbd4f65 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 f949c0e54e..8876e9734f 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/knowledge-ragflow +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/service-knowledge@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index 6310c68158..6a21cb392f 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 3036ec0c88..f5a2e6af3a 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,575 @@ # @objectstack/plugin-approvals +## 17.0.0-rc.0 + +### Minor Changes + +- d75edb9: Approval nodes now resolve `field` / `manager` approvers against the record's **live** state at node entry, not the trigger snapshot the flow froze at submit time (#3447). An earlier step — or the approver of an earlier step — can now write the field that routes a later step's approvers, enabling dynamic routing / dynamic co-sign (e.g. a lead reviewer picking which departments co-review, then those departments resolving as parallel approvers). Graph approvers (team / position / department / tier) already resolved live; this brings the in-record types into line. + + Also fixes two latent defects on the same path: a multi-select user field now fans out into one approver slot per user (previously the array was stringified to a single bogus id), and out-of-office delegation is applied per fanned-out user (previously silently skipped for multi-value fields). When the record can't be re-read (hard-deleted mid-flow, or a backend that can't serve a point read), resolution falls back to the trigger snapshot and warns rather than wedging the flow. + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +- 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list. + +### Patch Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 2ba560a: fix(plugin-approvals): give the decision actions a visual hierarchy (objectui#2762 P1-5) + + The `sys_approval_request` decision actions all declared as equal-weight + buttons, so the drawer's action bar rendered five identical outlined + buttons with no emphasis on the primary path. `approval_approve` now + declares `variant: 'primary'` and `approval_reject` declares + `variant: 'danger'`, so a metadata-driven renderer highlights Approve and + styles Reject as destructive — matching the hierarchy the mobile card + already has. Pure metadata; the secondary levers stay unstyled (tertiary). + +- 2dda6e7: fix(plugin-approvals): localize the declared decision-action labels (objectui#2762 P0-3) + + The Approval Center's decision drawer rendered the `sys_approval_request` + declared actions with their literal metadata labels — English **Approve / + Reject / Reassign / Send back / Request info** in a zh-CN workspace, sitting + next to the same page's localized 通过 / 拒绝 inbox buttons. The plugin's + translation bundle covered fields and views but had no `_actions` node, so + the console's `_actions..label` resolution had nothing to hit. + + - Re-ran `os i18n extract` against the plugin's config: the bundles now carry + `_actions` translations (label, confirmText, successMessage, param labels + and helpText) for all eight decision actions — `approval_approve`, + `approval_reject`, `approval_reassign`, `approval_send_back`, + `approval_request_info`, `approval_remind`, `approval_recall`, + `approval_resubmit` — in zh-CN, ja-JP and es-ES (en keeps the metadata + literals). + - The extract also surfaced other untranslated gaps, now filled in all three + locales: the `returned` status option, the `sys_approval_action.action` + audit options (`reassign` / `remind` / `request_info` / `comment` / + `revise` / `resubmit` / `ooo_substitute`), the `attachments` field, and the + `my_pending` / `recent` view empty states. + +- 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508) + + - `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a + designer must source each approver row's `value`: `user`/`team`/`department`/`position` + are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` / + `sys_business_unit` / `sys_position`; `position` commits the machine **name**, the + others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`), + `manager` is auto-resolved, `field` names a trigger-object field, and `queue` is + unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`. + - `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep + loading and rendering) but is published in `xEnumDeprecated`, so designers stop + offering it — the runtime has no queue resolution and the slot routes to nobody. The + approver `value` xRef now also maps `manager`, so designers can render its + auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries + `{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete + `user`) until a real ownership-queue implementation lands. + - `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue` + approver is skipped. + - `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver + types that are declared but not implemented by the runtime. + +- 0bc685a: fix(approvals): return decision attachments as file values, not "[object Object]" (#3504) + + `sys_approval_action.attachments` is a `Field.file`, so the column **stores an + opaque `sys_file` id** (ADR-0104 D3 — the stored form of every media field). The + ObjectQL read path resolves that id into its expanded + `{ id, name, size, mimeType, url }` form on the way out. But `rowFromAction` + mapped the column with `.map(String)`, collapsing each expanded value to the + literal string `"[object Object]"`. Every `listActions` consumer (the approval + inbox timeline) then received garbage: the attachment chip had no filename and + its id was `"[object Object]"`, so opening it 404'd. + + - `ApprovalActionRow.attachments` is now `ApprovalActionAttachment[]` — the + expanded file value plus its id, so a consumer can label and open an + attachment without needing read access to the system `sys_file` object (which + regular approvers do not have). + - Three read forms are accepted: the expanded value (the normal case), a bare id + (nothing to expand it into — storage service absent, file not committed), and + a legacy inline blob written before file-as-reference (`file_id` / + `mime_type`), until the backfill converts it. The id test reuses the + platform's `isFileIdToken`, so this and the engine's read resolver cannot + disagree about what counts as an id. + - The decision _input_ (`ApprovalDecisionInput.attachments`) is unchanged — it + still takes fileId strings, which is also exactly what the column stores. Only + the read shape changed. + +- b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456) + + The record lock is keyed on a **pending** `sys_approval_request`, and it could + not tell _the run that owns that request_ from _an unrelated user editing the + record_. So a flow that touched its own target record while its own approval was + still pending — a manual `resume` with no decision, or a node that writes the + record between opening the approval and the decision — died on its own + `RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery + existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it + self-healing. + + Both halves are now closed. + + **Prevention — the owning run may write its own record.** The automation engine + stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it + travels with every data node's ObjectQL context into `ctx.provenance`. The lock + hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`. + It is keyed on run identity rather than elevation on purpose: a `runAs:'user'` + run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance — + server-constructed like `isSystem`, never client-supplied, evaluated by no + security middleware, and the only write it permits is to the one record its own + run already holds a pending request against. + + **Recovery — a sweep releases records held by runs that died anyway.** A pending + request whose owning run has reached a terminal state (`completed`, `failed`, + `cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled` + — releasing the lock — and audited under the reserved actor `system:dead-run` + with the run and its status in the comment, so it is never mistaken for a + submitter's withdrawal. It runs on the existing approvals sweep clock, which also + covers the case no in-band handler can: a run killed by a process crash. + + The sweep is fail-safe by construction. It acts only on an explicit terminal + status from a closed set; `paused` (the normal state of a live approval), + `running`, an unrecognised status, an unknown run, a `getRun` that throws, and a + deployment with no automation engine are all read as "still alive". The failure + mode is "a dead run's lock survives until an admin recalls it" — today's + behaviour — never "a live approval is destroyed". + + Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for + a run id rather than the latest. A run that pauses and later finishes records two + entries under one id, so every suspend-then-finish run — every approval, screen + and wait flow — reported itself as `paused` forever, both on the Runs + observability surface and to this sweep. + + One shape was left out here and closed separately in #3712: a `runAs:'user'` run + with no trigger user (a schedule) resolved no ObjectQL context at all, so it + carried no `flowRunId` and stayed subject to the lock. It now passes a + provenance-only context — the run id and nothing the security middleware keys on + — so it is attributable without acquiring a principal, and its documented + unscoped posture (#1888) is unchanged. + +- be1c52c: fix(approvals): admin override for a request routed to an unstaffed approver (#3424) + + An `approval` node routed to a `position` (or `team`/`department`) with **no + holders** resolved to only the unresolvable `position:` literal in + `pending_approvers` — no concrete user was in the slate. Every normal + `decide` / `reassign` / `recall` then returned `FORBIDDEN` (not a pending + approver) and, with `lockRecord`, the target record stayed `RECORD_LOCKED` + forever: a data-availability dead-end with no in-product recovery (the only exit + was editing the DB by hand). Very easy to hit in fresh/demo orgs (positions + seeded, holders not) and whenever a role is vacated in production. + + A **platform or tenant admin** — the same posture the engine's superuser bypass + already trusts — may now act on any _pending_ request to release it: **approve, + reject, reassign** it to a real approver, or **recall** it. The override finalizes + the request (which releases the record lock, keyed on a pending request); a + tenant admin's authority is org-scoped, a platform admin's is not, and the + decision is audited under the admin's own id. An admin approval is authoritative, + finalizing the node even under `unanimous` / `quorum` / `per_group` rather than + counting as one vote among the (empty) slate. + + - `sys_approval_request.viewer` gains `can_override` (server-computed): true for a + privileged admin on a pending request. The `approve` / `reject` / `reassign` + declared actions OR it into their `visible` gate, so the console surfaces the + recovery path without a hand-wired button. Existing approver/submitter gating is + unchanged. + - `openNodeRequest` now logs a loud warning when a node resolves to **no concrete + approver**, so the misconfiguration is visible instead of silently locking the + record. The literal-fallback behavior (kept for 15.x slot back-compat) is + otherwise unchanged. + +- c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712) + + #3456 let the run that opened a pending approval write its own target record, + keyed on `flowRunId`. It worked for every run that resolves an identity and + missed the one that doesn't: an effective `runAs:'user'` run with **no trigger + user** — a schedule being the canonical case — passed no ObjectQL context at + all, so nothing carried the run id and the run still died on its own + `RECORD_LOCKED`. + + The blocker was never the lock. It was that "no identity" and "no context" were + the same thing on the wire, so a run could not say _who it was_ without also + claiming _what it was allowed to do_. + + **A run with no principal now passes provenance alone.** + `resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`, + no `permissions`, not even `isSystem: false`. Every principal gate keys on one + of those fields (the elevation short-circuit on `isSystem`, the ADR-0103 + engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`, + the empty-principal fall-open on all three), so this context authorizes + **identically to no context at all**. The run keeps the documented #1888 + unscoped posture, its loud `[runAs]` warning, and the + `flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch + changed — only that it can now be attributed. + + **Provenance moved out of the hook session, into `ctx.provenance`.** `session` + answers _who is calling_ and is absent when no identity envelope was supplied — + a distinction real gates depend on (the attachment access gate skips bare-kernel + writes on exactly that test). Folding a run id into `session` would have forced + an identity-less run to present an empty session, silently turning "no caller" + into "an anonymous caller" and narrowing the #1888 fail-open for attachments + alone. `HookContext.provenance.flowRunId` says what produced the write; the + approvals lock reads it there. + + Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope + (`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time + defaults, which made them _required_ on a caller-supplied option and asserted + something untrue — that every data-engine context carries a principal. Callers + have always passed slices (`{ isSystem: true }` for a system read); the type now + says so. + + Migration: nothing to change unless you read the run id inside a hook. If you + wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the + field never shipped under the old name. + +- d2a8695: fix(approvals)!: an approval request is visible to its participants, not to the whole tenant (#3590) + + `getRequest` / `listRequests` / `countRequests` deliberately query with + `SYSTEM_CTX` to bypass RLS — as the code comments say, the approver-visibility + rule spans identity forms RLS cannot model cleanly, so it has to be expressed in + the service. Only the **tenant** half of that rule was ever applied. The + participant half was named in the comment and never written, so **any + authenticated user could read any approval request in their tenant** — its + payload snapshot, its full decision history, and (once decision attachments + derived their access from the request, #3580) its files. + + `approverId` on `listRequests` is a _filter_, not authorization: omitting it + returned the whole tenant. + + A caller now sees a request when they are a participant — the submitter, a + current approver (via the normalized approver index, so every identity form the + write path recorded is covered), or someone who has already acted on it (a past + approver whose slot has moved on, a commenter). Admins with override authority + keep the unrestricted view the "all requests" console surface depends on, and a + tokenless context sees nothing. + + Keying on the concrete user id is sufficient rather than an approximation: + position/team/manager/field approvers are resolved to concrete user ids at open + time, and the `type:value` literal is only the fallback for a spec that resolved + to _nobody_ — a slot no one can act on either way. So this cannot hide a request + from someone who could actually act on it. + + **A write path's own result is not re-gated.** Every operation echoes back the + request it just changed; the operation already authorized itself, and re-asking + would answer wrong for a context carrying no `userId` (a flow-driven resume, a + service-to-service call), turning a successful write into `null`. + + Marked breaking because a client that listed requests without an `approverId` + filter and expected the whole tenant will now receive only its own — which is + the point. + +- 84e7be9: feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807) + + `per_group` (会签) requests now carry `pending_approver_groups` on the + enriched row — a map from each still-pending approver id to the group key(s) + it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label + each "waiting on" chip with the group it represents instead of showing + duplicate, context-free names. + + - Resolved in `attachDecisionProgress` from the same open-time + `__approverGroups` snapshot the `decision_progress` groups already use, so + the two never disagree. + - Only the **pending** slots are mapped (a resolved approver has left + `pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are + dropped — a `· #0` sub-tag would be noise. + - Absent for non-`per_group` behaviors. Display-only; the engine's + finalization tally stays authoritative. + - Added to the `ApprovalRequestRow` contract in `@objectstack/spec`. + +- debc23a: feat(approvals): enrich inbox rows with `payload_labels` (snapshot field labels) + + The approvals inbox summary title-cased raw snapshot machine keys + (`assessment_status` → "Assessment Status") because the API sent no field + labels. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot + field key → the target object's field label), symmetric with the existing + `payload_display` (which resolves the values), and `ApprovalRequestRow` gains + the field. For a single-locale project the schema label is already the + localized string, so a client can render the human field name (e.g. "考核状态") + instead of a prettified English key. + +- 376a061: Surface the approval node's author-declared `decisionOutputs` keys on the request read as `ApprovalRequestRow.decision_outputs` (#3447 P2 UI enablement). The set varies per request (each node declares its own), so it rides the row rather than the object's static action params — a decision UI renders one input per key and POSTs `outputs` with the decision. +- deb538f: fix(storage): let an object delegate file-read authorization to its service + + Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a + **legitimate approver could see a decision attachment's filename but got 403 + opening it**, found by driving app-showcase in a browser as a real non-admin + approver. + + Cause: a field-owned file's download was authorized by testing whether the + caller can READ the owning row. For an ordinary business object that is right — + row readability _is_ the access rule. For `sys_approval_action` it is the wrong + authority: the audit table is deliberately closed to ordinary approver + positions (`operation 'find' … is not permitted for positions [auditor, +everyone]`), so the test denied the very approver the attachment was filed for. + The approvals _service_ has always had the real rule, which is why the timeline + listing the attachment returned 200 while the bytes returned 403. + + An object may now name a service to answer the question instead: + + - `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes + downloads of files owned by that object's media fields. + - `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract. + - `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead` + reuses the _same_ gate `listActions` applies (visibility of the parent + request) rather than inventing a second, looser rule for the bytes. + + **Fails closed**: a declared delegate that is missing or does not implement the + method denies, rather than silently reverting to the raw read it was declared to + replace. Objects without the declaration are unchanged. + + Verified in the browser against app-showcase, both sides of the gate: the + approver now downloads the real PDF (200), and an anonymous request is still + refused (401) — the anonymous capability URL the original change closed stays + closed. A decision attachment ends up exactly as readable as the decision it + hangs off: never more, and no longer less. + +- db48ad5: fix(security,approvals,metadata-core): restore batch routes on the eight objects the #3391 P1 companion fix missed (#3026) + + The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch + request is admitted only when the object grants the `bulk` **primitive** and the + batched child operation is itself allowed. Before that, the `*Many` routes + checked only the child verb, so a boilerplate CRUD-five whitelist + (`['get','list','create','update','delete']`) batched fine. + + The companion fix — adding the `bulk` primitive wherever an explicit whitelist + survived — was applied only inside `platform-objects`. Eight objects carrying + the same boilerplate live in other packages and kept the gap, so `/batch`, + `createMany`, `updateMany` and `deleteMany` answered `405 +OBJECT_API_METHOD_NOT_ALLOWED` on objects whose single-record create/update/ + delete were wide open. `data-objectstack` rethrows that 405 without falling back + to per-row writes, which surfaced as a hard error on multi-select delete in the + Setup grids. + + Objects reclaimed (whitelist now `['get','list','create','update','delete','bulk']`): + `sys_capability`, `sys_permission_set`, `sys_position`, + `sys_position_permission_set`, `sys_user_permission_set`, `sys_user_position` + (plugin-security); `sys_approval_delegation` (plugin-approvals); + `sys_view_definition` (metadata-core). + + No new authority is granted: `bulk` only permits batching verbs each object + already exposes one record at a time, and every batched row still passes the + same row- and field-level permission checks. The whitelists stay explicit rather + than being deleted — seven of the eight are `managedBy`, and + `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array + `apiMethods`, so dropping the line would silently disable the managed-write + backstop. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index ee21745681..6f05b9858a 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 9e1474446a..de1a0215f2 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,147 @@ # @objectstack/plugin-audit +## 17.0.0-rc.0 + +### Minor Changes + +- f243727: remove(plugin-audit): drop the kernel's built-in assignment notifications; move the policy to user-space automation (#3403) + + **Breaking (behavioral).** `plugin-audit` no longer emits a `collab.assignment` + notification when an owner/assignee field changes on a record. Deciding that an + assignment warrants a bell is a business policy, not a platform default — the + kernel version guessed "who is the assignee" from field names (`owner_id`, + `assigned_to`, `assignee_id`, `owner`, `assignee`), which misfired on system + records like `sys_file` and spammed users with "…assigned to you" noise on file + uploads (#3402). + + **What was removed:** the `writeAssignmentNotifications` writer, the `OWNER_FIELDS` + heuristic, and the `messages.assignedToYou` translation key (en / zh-CN / ja-JP / + es-ES). **Unaffected:** `sys_audit_log` / `sys_activity` capture, and `@mention` + notifications (`collab.mention`) — those remain platform behavior. The + `owner_of:` messaging audience and `service-messaging`'s `DEFAULT_OWNER_FIELDS` + are a separate, caller-requested mechanism and are unchanged. + + **FROM → TO migration.** If you relied on the automatic bell, configure an + automation flow on the target object (`record-after-update` / `record-after-create` + trigger + a `notify` node). The `condition` can read the pre-update row via + `previous`, and `notify`'s `recipients` / `title` / `actionUrl` all interpolate + record fields. Ready-made example: `showcase_task_assigned_notify` in + `examples/app-showcase/src/automation/flows/index.ts`: + + ```ts + { id: 'start', type: 'start', config: { + objectName: 'your_object', + triggerType: 'record-after-update', + condition: 'assignee != previous.assignee', + } }, + { id: 'notify_assignee', type: 'notify', config: { + topic: 'task.assigned', + recipients: ['{record.assignee}'], + channels: ['inbox'], + title: 'New assignment: {record.title}', + actionUrl: '/your_object/{record.id}', + } }, + ``` + + Notes on parity: the flow template renders a single language (the kernel version + localized the title to the recipient's locale); a flow fires on every real change + (the `previous` condition already gates that) and, unless you add an actor guard, + also notifies self-assignments — the kernel version suppressed those. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index b8433c287e..a9fcc0dd7f 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 70a09bd814..4330582123 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,763 @@ # Changelog +## 17.0.0-rc.0 + +### Major Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +### Minor Changes + +- 1bd5652: feat(auth): give ADR-0105 D8's scope-bounded issuance a caller — the + `delegated_admin` org role, capped so it cannot mint authority (#3697) + + D8 authorizes invitation _placement_ against the issuer's `adminScope` + (ADR-0090 D12), so a delegated plant admin may invite only into their own + subtree. That gate is implemented, unit-proven and reachable — but no principal + could reach it in a state where it did anything: + + - better-auth grants `invitation: ["create"]` to `owner` and `admin` only + (`memberAc` holds `invitation: []`, and roles registered through + `additionalOrgRoles` inherited that empty statement); + - under a wall-enforcing posture, owners and admins are auto-elevated to + `organization_admin` (`auto-org-admin-grant.ts`), which carries the wildcard + `modifyAllRecords` that makes `isTenantAdmin()` true — and the gate + short-circuits on tenant admins. + + The two sets were disjoint. Issuance placement was bounded by the Layer 0 org + wall (real, and correct) but never by `adminScope`, so D8's motivating story — + "a plant admin invites into their own subtree without a platform admin + finishing the job" — could not happen. + + **Two pieces, and they only ship together.** + + **1. The role.** `delegated_admin` is now registered with the organization + plugin as `memberAc.statements` plus `invitation: ["create"]` — the one + membership grade that may reach `/organization/invite-member` without being an + org admin. Deliberately _not_ `invitation: ["cancel"]`: better-auth's cancel + route checks the permission with no inviterId attribution, so it would mean + "cancel anyone's pending invitation in the org". + + The role carries no ObjectStack authority by construction — `mapMembershipRole` + passes it through as a position name, and with no `sys_position_permission_set` + binding that name resolves to nothing. Role = _can reach the endpoint_; + `adminScope` = _what the endpoint permits_. + + `sys_member.role` and `sys_invitation.role` each gain `delegated_admin` as a + fourth option. Those selects are **enforced on write** — better-auth's own + invitation and membership inserts are validated like any other row — so + registering the role with the org plugin without listing it in both would have + produced a role nobody could hold and nobody could hand out + (`ValidationError: role must be one of: owner, admin, member`). That is exactly + how the end-to-end regression caught it, twice; neither unit test could. The + three non-English translation bundles carry the English label for the new option + until localized. + + **2. The role cap**, in the framework's own `beforeCreateInvitation` hook, + beside the D8 placement gate. Registering the role alone would have been a + four-step privilege escalation: better-auth's only role-level cap on _what role + you may invite someone as_ is its `creatorRole` check (default `owner`), which + blocks inviting an **owner** but not an **admin** — and an accepted `admin` + membership is auto-elevated to `organization_admin` → `isTenantAdmin()`. A + subtree-scoped delegate could have manufactured a tenant admin, with every + existing defense off the path (`sys_member` is not a `GOVERNED_OBJECT`, and the + acceptance-time membership write runs under better-auth's context, not the + issuer's). + + The cap refuses an invitation whose role outranks the issuer's own, and + restricts a below-admin issuer to plain `member` — not merely "not admin/owner", + because an app-registered role projects into `current_user.positions` and may be + bound to permission sets, making it a capability channel too. A delegate's + channel for capability is the invitation's _placement_ intent, which the D12 + gate allowlists position-by-position. The cap applies to every invitation, + placement-carrying or not (the escalation is independent of placement), and + fails closed: an issuer role that cannot be resolved confers nothing above a + plain member. + + **What changes for deployments.** One new class of principal exists: members + holding the `delegated_admin` org role, who can invite into the org — as + `member` only, into the subtree their `adminScope` allows. It is opt-in twice + over (someone must set the membership role _and_ grant an adminScope set), so a + default deployment changes not at all. Org owners and admins are unaffected. + + Also exported: `MEMBERSHIP_ROLE_DELEGATED_ADMIN` from `@objectstack/spec`, so + console and control-plane surfaces name the role from one place. + +- 7fb436c: Multi-organization operation is an ENTITLEMENT again: the `group` posture no + longer activates without the enterprise runtime (ADR-0105 D12 correction). + + The first ADR-0105 wave read D12 as "the `group` wall ships open" and made the + posture self-activating — it never probed for `@objectstack/organizations`. That + turned `group` into a free multi-org path around the `isolated` gate (ADR-0081 + D2), and made the weaker isolation the free one, which is not a boundary anyone + would draw on purpose. + + The distinction that was missed: **open code is not free activation.** The wall's + implementation has always lived in the open packages — that is equally true of + `isolated`, whose Layer 0 wall sits in `plugin-security` and is gated on a + service the enterprise package registers. Cloud ADR-0016's 铁律 + (强制免费、治理收费) guarantees that a deployment RUNNING a multi-org shape is + safe; it is satisfied by REFUSING to run one unwalled, not by giving the posture + away. + + ## Changes + + - **`tenancy-service`**: `group` probes `org-scoping` exactly like `isolated`. + Without it the posture resolves to `single` and reports `degraded`. + - **`os serve`**: the ADR-0093 D5 boot guard keys off the resolved POSTURE + instead of `OS_MULTI_ORG_ENABLED`. Previously `OS_TENANCY_POSTURE=group` skipped + both the enterprise package load AND the fail-fast, silently degrading to an + unwalled deployment — the exact ADR-0049 class that guard exists to close. A + `group` request without the runtime now refuses to boot unless + `OS_ALLOW_DEGRADED_TENANCY=1`. + - **New seam — the runtime declares what it entitles.** `org-scoping` may expose + `supportedPostures` (`OrgScopingEntitlement`, `@objectstack/spec/security`); + the open side honours it and fails closed on anything not listed. Whether + `group` and `isolated` are one commercial tier or two is packaging policy, and + packaging policy belongs to the commercial runtime rather than hard-coded in + open core. Omitting the field entitles every walled posture, so existing + runtimes are unaffected. + - **`organization_id` stamping returns to the enterprise runtime.** The previous + wave moved auto-stamping into the open engine; that removed the closed + package's only load-bearing runtime duty, so a five-line forged `org-scoping` + registration would have produced a fully working multi-org deployment. With + stamping back where it was, a forged registration yields NULL-org rows the wall + hides — a broken deployment, not an unlicensed working one. + + **Write-side VALIDATION stays open and is unchanged**, including the + bulk-insert coverage: rejecting a forged `organization_id` is a security + property, not a packaging one. Only filling an ABSENT value moved back. + + - Default-organization bootstrap returns to `single`-only; every walled posture + keeps its existing owner (ADR-0081 D1). + + ## Note for operators + + `OS_TENANCY_POSTURE=group` without `@objectstack/organizations` installed now + **refuses to boot** rather than running single-org. This only affects + deployments that adopted `group` between the two waves. + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 96242ef: feat(auth): AuthPlugin derives app-declared organization roles itself — hosts pass nothing (#3723 follow-up, cloud#897) + + Five hosts boot `AuthPlugin` from a stack, and per-host `additionalOrgRoles` + wiring proved to be the defect pattern: three of them (the verify harness, + `DevPlugin`, cloud's `ArtifactKernelFactory`) at some point forgot it, and the + failure is silent — app-declared roles are simply absent. One host (cloud) + mounts `AuthPlugin` before the app metadata even exists, so no init-time walk + could ever cover it. + + `AuthPlugin` now derives the roles in its own `kernel:ready` hook — the one + point that fires after all metadata is registered in every host — via the new + `collectRegisteredOrgRoles(engine, metadataService?)` (the late-bound twin of + `collectStackOrgRoles`). Both consumers are updated from the derived union: + better-auth's org-plugin roles map (`applyConfigPatch`; the instance builds + lazily) and the `sys_invitation.role` / `sys_member.role` select options + (re-registration under the same package id — a supported registry path; no + DDL, options are validator/picker metadata). + + `objectstack serve`, the `@objectstack/verify` harness and `DevPlugin` no + longer pass `additionalOrgRoles` — deliberately, so the dogfood invite gate + only stays green if the auto-derivation works. The option remains for roles + declared OUTSIDE stack metadata; explicit entries are unioned with the derived + set. `collectStackOrgRoles` stays exported for hosts that want an init-time + walk of a raw stack object. + +- 313d7be: feat(auth): `onInvitationAccepted` host seam — better-auth's + `afterAcceptInvitation` forwarded to the host (ADR-0105 D8 prerequisite) + + An invitation may carry placement intent (target business unit + positions, + extension fields on `sys_invitation` per the ADR-0092 whitelist), but there + was no server-side seam to apply it when the invitation is accepted — + better-auth's org-plugin models don't fire core `databaseHooks` (framework + #3541 D8 note). + + `AuthManagerConfig.onInvitationAccepted` mirrors `onOrganizationCreated`: + invoked from `organizationHooks.afterAcceptInvitation` with the mapped ids + (`invitationId`, `organizationId`, `userId`, `memberId`, `role`, `email`) + plus the RAW `invitation` / `member` rows so a host reads its own extension + columns without a second query. Failure-isolated — acceptance never rolls + back on a side-effect miss; hosts needing effectively-atomic placement + should make the callback idempotent and reconcile on retry. + +- aa8b847: feat(authz): scoped invitations — placement intent on an invitation, gated by + the issuer's adminScope and applied on acceptance (ADR-0105 D8) + + An invitation may now carry PLACEMENT INTENT — the business unit the invitee + lands in and the positions they are assigned — so a delegated (plant) admin's + invitee arrives already in the right unit and role instead of waiting on a + platform admin. This closes the structural gap ADR-0105 D8 names for + `single`-posture deployments and is the natural admission path under `group`. + + The two halves ship together, deliberately: + + - **Issuance is authorized** against the ISSUER's `adminScope` (ADR-0090 D12), + by dry-running the existing `DelegatedAdminGate` against the very + `sys_user_position` rows the acceptance would write. The gate is reused + verbatim — no second copy of the subtree/allowlist logic to drift — so an + invitation can never place what its issuer could not have assigned directly. + Without that gate the feature would be an escalation hole: the built-in + `organization_admin` is deliberately read-only on the RBAC tables precisely + so a fresh org admin cannot rebind themselves, and applying an unchecked + invitation payload under system context would hand that authority straight + back. + - **Acceptance applies it**, idempotently and failure-isolated: a replayed + acceptance converges instead of duplicating assignments, and a placement + miss never undoes a valid membership. + + Surface: + + - `sys_invitation` gains `business_unit_id` + `positions` (ADR-0092 extension + fields, registered in the D7 collision-guarded whitelist; NOT generically + editable — placement is set only at issuance, through the gate). + - `@objectstack/plugin-security` registers the `invitation-placement` service + (`assertIssuable` / `apply`). + - `@objectstack/plugin-auth` wires better-auth's `beforeCreateInvitation` / + `afterAcceptInvitation` to it. **Fail closed**: an invitation that requests + placement in a deployment without the delegated-administration runtime is + refused, never silently placed unchecked. + + Existing invitations are unaffected — an invitation without placement intent + never consults the gate and behaves exactly as before. + +### Patch Changes + +- 735f850: fix(security): resolve the ISSUER's real grants when authorizing invitation + placement (ADR-0105 D8) + + Scoped-invitation issuance dry-runs `DelegatedAdminGate` against the + `sys_user_position` rows the acceptance would write. The gate reads authority + off `context.positions` / `context.permissions` — but the invitation hook + handed it a hand-built `{ userId, tenantId }`, which carries neither. Every + delegated administrator therefore resolved to the additive baseline alone and + was refused: + + > requires tenant-level administration or a delegated adminScope (ADR-0090 D12) + + Fail-closed, but dead: only a tenant admin could ever issue a placement, which + is the one case the feature was not for. Caught by cloud's group-posture + dogfood, which exercises the real HTTP path with a real delegate. + + `assertIssuable` now takes `actorUserId` instead of a caller-built + `actorContext` and resolves that user's grants itself through the single authz + resolver (`@objectstack/core` `resolveUserAuthzGrants`) — the same envelope a + transport would have carried, from the same reads. There is no request to + resolve a context from inside a better-auth hook, so the id is what the caller + can honestly supply and the resolution belongs behind the boundary. + + A principal-less call still reaches the gate with an empty context on purpose: + the gate owns that refusal too, so the security boundary keeps exactly one + place an issuance can be denied. + +- e9b11df: fix(auth): app-declared organization roles are now storable, not just registerable (#3723) + + `AuthManagerOptions.additionalOrgRoles` registered every `permission` / + `position` name a stack declared with better-auth's organization plugin, so + `POST /organization/invite-member { role: 'sales_rep' }` passed the role check — + and then the write failed, because `sys_invitation.role` and `sys_member.role` + were closed selects listing `owner|admin|member` only: + + ``` + ValidationError: role must be one of: owner, admin, member + { field: 'role', code: 'invalid_option' } + ``` + + A select is enforced on write and better-auth's own inserts are not exempt (they + run through the ordinary ObjectQL validator), so any stack declaring role names + was registering roles that could be requested and never stored. + + Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single + normalizer; its output feeds better-auth's role map **and** the two `select` + option lists, so neither side can accept a name the other rejects. The built-in + roles (`owner`, `admin`, `delegated_admin`, `member`) live in + `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the + platform objects declare statically — app roles are appended at boot. + + New exports: + + - `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to + `identity/membership-role`; the package-level export path is unchanged). + - `@objectstack/plugin-auth` — `collectStackOrgRoles`, + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, + `withMembershipRoleOptions`. + + Hosts that boot `AuthPlugin` from a loaded stack should derive + `additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the + stack themselves — `objectstack serve`, the `@objectstack/verify` harness and + `DevPlugin` now all do. The harness previously passed none, which is why a + dogfood proof could boot a stack whose declared roles better-auth had never + heard of; `DevPlugin` documents itself as equivalent to the full stack and + silently excluded app roles from that equivalence. + + `additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and + `collectStackOrgRoles` now returns those descriptors. The label is what the + declaring `position` / `permission` metadata already says, so the role picker + shows `Executive` for a position declared as such instead of title-casing the + machine name into `Exec` — a third source of truth for one string. Presentation + only: better-auth sees just the name, and the stored value is always the name. + Passing `string[]` keeps working unchanged. + + Behaviour change worth noting: a declared role name that is not a valid machine + name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a + boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a + name would be registered verbatim and stored mangled — the same mismatch with + extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. + +- 984396b: test(plugin-auth): enumerate better-auth's route table — the `/auth/**` wildcard becomes 55 exact rows (#3656) + + The widest hole the #3642 capstone measured. That guard reports how many SDK + calls match only a `**` prefix family rather than a resolvable route, and the + answer was 60 of ~196 — with 54 on `* /auth/**`, the largest and most + security-relevant namespace in the client. `auth.me` builds + `/api/v1/auth/get-session`; a prefix claim cannot tell you better-auth still + calls it that, and better-auth is a third-party dependency on its own release + cadence (this repo already chased its 1.7 column drift in #3624 / #3647). + + `plugin-auth` mounts it with a single catch-all, so there are no per-route + registration calls to capture the way tranche 3 captured + `registerStorageRoutes`. The seam is `auth.api`: every better-auth endpoint + carries `.path` and `.options.method`, so a live instance is the route table. + + `auth-route-ledger.ts` reads it, in two halves checked differently on purpose: + + - **55 reviewed rows** — every route the SDK calls, each naming its client + method, checked strictly against the live table. This is the rename detector. + - **129-path mounted-surface inventory** — checked for exact equality both + ways, so a version bump that adds publicly-mounted auth endpoints becomes a + reviewable CI diff. Machine-maintained rather than reviewed prose: demanding + a rationale for all 129 would make every better-auth upgrade a hundred-row + review and the ledger would rot into rubber-stamping. + + Enumeration is config-dependent, so the inventory is pinned at the + configuration enabling every plugin the SDK targets — the maximal surface — + with the participating `OS_*` env vars cleared so a developer's shell cannot + produce a spurious diff. Mutation-checked: renaming a ledgered route fails the + suite naming it. + + The capstone guard now includes this ledger in its union and prefers exact rows + over wildcard families when matching — without that ordering fix every + `/auth/*` URL would still have been absorbed by `* /auth/**` and the new ledger + would have changed nothing. Wildcard-only matches fall **60 → 3**; the ratchet + moves with them. What remains is `* /ai/**`, whose routes `service-ai` builds + at plugin start. + + No runtime change: a ledger, a guard, and the header/audit-doc notes. + +- d0fea33: fix(auth): map ObjectQL `ValidationError` to a 4xx on the better-auth paths (#3398) + + A field-level validation failure raised by the ObjectQL record-validator + (e.g. an invalid `image` on `POST /api/v1/auth/update-user`) surfaced to the + HTTP client as a **raw 500 with an empty body**. better-auth only maps its own + `APIError`s to structured responses; any other error thrown from an adapter + method propagates to better-call's router as an unhandled fault → `500 {}`. + + Added the auth-path analogue of the REST layer's `mapDataError`: the objectql + adapter now detects the ObjectQL validation envelope at its boundary (duck-typed + by `code` / `name`, so plugin-auth keeps no hard dependency on + `@objectstack/objectql` and cross-realm `instanceof` can't bite) and re-throws + it as `APIError('BAD_REQUEST', …)`. `update-user` and friends now answer with a + `400 { code: 'VALIDATION_FAILED', message, fields }` instead of an opaque 500. + +- bc17d39: fix(auth): provision the better-auth 1.7 columns `sys_team` / `sys_team_member` / `sys_two_factor` were missing (#3624) + + better-auth 1.7.0-rc.1 added fields to three models that the platform objects + never provisioned and `auth-schema-config.ts` never mapped. Because an unmapped + field keeps its camelCase name, the adapter emitted columns no table had: + + | model | field | column now provisioned | + | :----------- | :---------------------------------------- | :---------------------------------------------------------- | + | `team` | `memberCount` | `sys_team.member_count` | + | `teamMember` | `membershipKey` | `sys_team_member.membership_key` | + | `twoFactor` | `failedVerificationCount` / `lockedUntil` | `sys_two_factor.failed_verification_count` / `locked_until` | + + The team pair broke org creation outright. The organization plugin's team + sub-feature is on by default, so `POST /api/v1/auth/organization/create` + auto-creates a default team — and that insert died with `table sys_team has no +column named memberCount` _after_ the organization row had already committed. + Callers got an HTTP 500 on top of a half-created org: a real org row with no + default team behind it. Every multi-org deployment's create-org flow hit this. + + The two-factor pair broke the 2FA lockout path the same way: better-auth + guard-increments `failedVerificationCount` on each wrong code and stamps + `lockedUntil` past the threshold, so a wrong code 500'd instead of being + counted. All four columns are better-auth's own state — provisioned, readable, + and never written from the ObjectStack side. + + Existing environments pick the columns up through the driver's additive schema + sync; no data migration is needed. `member_count` backfills to 0 and + better-auth's own `syncTeamMemberCount` reconciles it on the next membership + change, and `membership_key` stays null on pre-upgrade rows, which better-auth + tolerates by falling back to the `(team_id, user_id)` pair. + + A new drift gate (`better-auth-schema-parity.test.ts`) now asserts that every + column the installed better-auth version can write exists on the platform + object backing it, across the auth manager's whole model surface. The ADR-0092 + D7 guard only ever caught _collisions_ between our extension fields and + better-auth's, so a bump that adds a brand-new field passed the build and failed + at runtime — twice now, counting the 1.7 `oauthAccessToken.authorizationCodeId` + regression. The next one fails the build instead. + +- 65ac468: fix(import): sanitize row errors — never leak raw SQL, map constraint failures to human wording (#3566) + + A failing import row surfaced the driver's raw error verbatim. When a write hit + a DB constraint (e.g. `sys_user.phone_number` is `unique`), the query builder + embeds the entire failing statement in `err.message`, and `toFailedResult` + handed that straight back — so the importer saw `` insert into `sys_user` +(...) values (...) - UNIQUE constraint failed: sys_user.phone_number ``. That is + both unreadable and an information disclosure of the schema. + + - `sanitizeRowError()` (import-runner) maps the common constraint failures — + SQLite / MySQL / Postgres `UNIQUE` and `NOT NULL` — to human wording + ("A record with this `` already exists.", "`` is required.") + and, as a backstop, never lets a message that still reads as a SQL statement + reach the client (it salvages the driver's trailing reason, or falls back to + a generic message). Already-friendly messages (e.g. better-auth's "User + already exists") pass through unchanged. Applies to every import path. + - `isLikelyEmail` now rejects non-ASCII addresses, so an address like + `x@柴仟.com` fails the import **dry-run** pre-check instead of passing client + and dry-run validation only to be rejected by better-auth's strict ASCII + validator at real-import time. + +- 5faeac6: fix(auth): spell isLikelyEmail's ASCII guard with printable bounds (no control char) + + The non-ASCII guard added in framework#3566 was written as `[^\x00-\x7f]`, whose + regex literal embeds a control character (`\x00`). Rewrite it as `[^\x20-\x7e]` — + identical behaviour (anything outside printable ASCII fails the email + pre-filter), but the pattern no longer carries a control character (eslint + `no-control-regex`), and it matches the objectui side's `isPlausibleEmail`. + +- cde1975: fix(dev): eliminate three fixed startup log warnings so official examples boot clean (#3420) + + `os dev` on the stock showcase printed three fixed noise sources on every boot, + with zero example-side changes — training users to ignore warnings. + + - **spec** — add a field-level `ackPlaintextMasking: true` opt-out for the + generic `password` author-time warning (ADR-0100). A deliberately-masked + field (like field-zoo's `f_password`) can now affirm intent instead of + printing an un-actionable "safe to ignore" on every boot; the warning text + points authors at the flag. + - **plugin-auth** — pass better-auth's documented + `silenceWarnings.oauthAuthServerConfig` to `oauthProvider(...)`. We already + mount the `/.well-known/oauth-authorization-server` documents ourselves at + the issuer root, so the plugin's "please ensure it exists" reminder was a + false positive (printed twice); silencing it removes both. + - **objectql** — route the Registry's re-register / package-overwrite lines + (normal rebuild / HMR / seed-replay paths) through a new debug-only + `SchemaRegistry.debug()` so they stay out of the default `info` boot log. Adds + a `logLevel` construction option (and matching `OS_REGISTRY_LOG` env var) so + the debug-gated housekeeping is discoverable for troubleshooting. + +- a629074: fix(auth): the second factor now obeys the operator's lockout policy instead of better-auth's defaults (#3690) + + `auth-manager.ts` constructed `twoFactor()` with a schema and nothing else, so + better-auth's built-in `accountLockout` defaults — on, 10 attempts, 15 minutes — + governed two-factor verification no matter what the admin configured. An operator + who tightened **Setup → Authentication → Account lockout threshold** to 3 got a + password stage that locked at 3 and a second factor that still locked at 10: the + stricter door was the looser one, with nothing in the UI saying so. + + `lockout_threshold` / `lockout_duration_minutes` are now projected onto + better-auth's own `accountLockout` shape (`enabled` / `maxFailedAttempts` / + `durationSeconds`, minutes converted to seconds) rather than growing a parallel + `two_factor_lockout_*` pair — one policy, one mental model, and a future upstream + field arrives as a new option instead of a conflict. The projection goes through + `applyConfigPatch`, which resets the cached better-auth instance, so a settings + change takes effect without a restart. + + Threshold `0` is deliberately **not** forwarded as `enabled: false`. It is the + password stage's "off", and a deployment may leave that stage unlocked because + rate limiting or an IdP covers it; the second factor is the last check before a + session is issued, so it keeps better-auth's default rather than being switched + off by a setting that never mentioned it. + + The threshold field is also no longer hidden behind `email_password_enabled` — + two-factor verification exists in passwordless deployments, where the setting was + previously unreachable. + + The admin **Unlock Account** action now clears both stages. It only ever reset + `sys_user`, so a user locked at the second factor had no admin escape hatch and + had to wait the duration out — survivable while that lock needed 10 failures, + routine once an operator can set the threshold to 3. The second-factor clear is + best-effort and runs after the primary write, so an account with no enrolment + still unlocks normally. + + Note the plugin caps attempts at 5 per challenge (`beginAttempt(5)`), which no + option reaches; a threshold above 5 forces a fresh challenge rather than raising + that cap. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [ce1f100] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [ef5e72d] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [bbd902d] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/rest@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index f707b859c5..e6dac6e25c 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 5e50fa6aaf..0d5986eb35 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,189 @@ # @objectstack/plugin-dev +## 17.0.0-rc.0 + +### Patch Changes + +- 96242ef: feat(auth): AuthPlugin derives app-declared organization roles itself — hosts pass nothing (#3723 follow-up, cloud#897) + + Five hosts boot `AuthPlugin` from a stack, and per-host `additionalOrgRoles` + wiring proved to be the defect pattern: three of them (the verify harness, + `DevPlugin`, cloud's `ArtifactKernelFactory`) at some point forgot it, and the + failure is silent — app-declared roles are simply absent. One host (cloud) + mounts `AuthPlugin` before the app metadata even exists, so no init-time walk + could ever cover it. + + `AuthPlugin` now derives the roles in its own `kernel:ready` hook — the one + point that fires after all metadata is registered in every host — via the new + `collectRegisteredOrgRoles(engine, metadataService?)` (the late-bound twin of + `collectStackOrgRoles`). Both consumers are updated from the derived union: + better-auth's org-plugin roles map (`applyConfigPatch`; the instance builds + lazily) and the `sys_invitation.role` / `sys_member.role` select options + (re-registration under the same package id — a supported registry path; no + DDL, options are validator/picker metadata). + + `objectstack serve`, the `@objectstack/verify` harness and `DevPlugin` no + longer pass `additionalOrgRoles` — deliberately, so the dogfood invite gate + only stays green if the auto-derivation works. The option remains for roles + declared OUTSIDE stack metadata; explicit entries are unioned with the derived + set. `collectStackOrgRoles` stays exported for hosts that want an init-time + walk of a raw stack object. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [735f850] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [96242ef] +- Updated dependencies [984396b] +- Updated dependencies [d0fea33] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [ce1f100] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [307e0fe] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [ef5e72d] +- Updated dependencies [dac6a08] +- Updated dependencies [313d7be] +- Updated dependencies [5faeac6] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [8e08bc3] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [bbd902d] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [bd68f08] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [a629074] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/rest@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/plugin-auth@17.0.0-rc.0 + - @objectstack/plugin-security@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/plugin-hono-server@17.0.0-rc.0 + - @objectstack/service-i18n@17.0.0-rc.0 + - @objectstack/account@17.0.0-rc.0 + - @objectstack/setup@17.0.0-rc.0 + - @objectstack/driver-memory@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 74f91bf246..f0ddaeb476 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 fa5e08093c..309c21a8fa 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,102 @@ # @objectstack/plugin-email +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 9b4cd60297..6640c7e80a 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 895999be7e..a7eeae5fda 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,503 @@ # @objectstack/plugin-hono-server +## 17.0.0-rc.0 + +### Minor Changes + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710) + + **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a + record and taking a bulk machine-readable copy of the whole table are different + privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite + "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so. + + ### Migration — FROM → TO + + | | before | after | + | -------------------- | ----------------------------------- | -------------------------- | + | `allowExport` unset | export **allowed** (inherited read) | export **denied** | + | `allowExport: false` | export denied | export denied (unchanged) | + | `allowExport: true` | export allowed | export allowed (unchanged) | + + **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'` + wildcard) of every permission set whose holders should keep exporting. + + ```ts + objects: { + deal: { allowRead: true, allowExport: true }, // ← add the grant + } + ``` + + Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set + that never exported is unaffected. + + **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the + built-ins are handled for you — `admin_full_access` and `organization_admin` now + carry `allowExport: true` explicitly. **Environment-authored sets are not**: any + custom set whose users export must be edited. `member_default` deliberately does + NOT carry the grant, so ordinary authenticated users lose export until an admin + grants it — that is the point of the flip, not an oversight. + + **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set + granting `true` grants export. `false` and unset are the same outcome; `false` + is authoring intent, not a veto, because permission sets are additive capability + containers (ADR-0090). + + **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no + longer confer export. Separating "may see all data" from "may take a bulk copy" + is the segregation-of-duties case the axis exists for. + + ### Also in this change + + - **spec** — a set carrying `allowExport` is now **high-privilege** + (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` / + `guest` audience anchors. Without this the opt-in was defeatable by binding an + export-granting set to `everyone`. One predicate, so the runtime anchor gate, + the `@objectstack/lint` security-posture rule and the install-time suggestion + surface all pick it up together. + - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so + `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It + explains as `read ∧ the export grant`: `object_crud` reports the conjunction + and attributes the granting set, while every data-shaped layer + (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed + as the `find` the export actually performs — asking the RLS compiler about an + `export` operation would match no policy and wrongly report "no RLS applies". + `readFilter` is surfaced for `export` as it is for `read`. + - **plugin-reports** — closes the reports side door (#3710). A report rendered + as `csv`/`json` is the same bulk copy of the same object, so it is gated by + the same `ISecurityService.canExport`. Enforced in `executeReport`, which the + interactive run, the ad-hoc run and the scheduled dispatch all funnel through; + `scheduleReport` additionally refuses at create time so an author is not told + at 3am. A schedule created while granted stops delivering once the grant is + revoked. `html_table` stays a read — it is a rendered view, not a bulk copy. + Deployments without `plugin-security` are unaffected (no permission sets + exist, so the axis does not apply). + +- d8c4957: feat: user-level export permission axis (#3544, #3391 follow-up) + + `export` is a user-gated operation, not just "anyone who can list". A permission + set can now deny export on an object while keeping read — matching Salesforce + "Export Reports" / Dynamics "Export to Excel" / NetSuite "Export Lists" / SAP + S_GUI 61. + + - **spec** `ObjectPermissionSchema` gains an optional `allowExport` bit. It is + deliberately OPTIONAL with **no default** so it is a backward-compatible + opt-out: unset → inherits read (today's "can-list ⇒ can-export"), `false` → + export denied while read is kept, `true` → granted. + - **plugin-hono-server** `annotateEffectiveApiOperations` derives + `userExportAllowed = allowExport !== false` from the resolved per-object + permission and threads it into `resolveEffectiveApiMethods` — so `export` + derives from `list ∧ userExportAllowed`. When the axis removes `export` from + an otherwise-open object, the object is now annotated (the effective set minus + `export`) so the client hides the Export button; an unrestricted object with + export still allowed stays unannotated (client default-allow). + + Wires the `userExportAllowed` slot reserved in #3391 P1 — zero contract change + to the derivation table or the frontend (it already consumes the effective + `apiOperations`). Backward-compatible: existing permission sets (no + `allowExport`) keep today's behavior everywhere. + +### Patch Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- c2d9098: feat(rest/protocol): extend droppedFields write-observability to the bulk paths + client SDK (#3455) + + Follow-up to #3448 (#3431 D2): the single-write PATCH/POST `/data` paths already + surface LEGALLY-stripped write fields (static `readonly` #2948 / `readonlyWhen` + #3042 / #3043 create ingress) as `droppedFields`. The **bulk** write paths did + not — the same strips happened silently on every batched row — and the typed + client warning + CORS mirror were deferred. This closes those out. + + **Bulk passthrough (metadata-protocol).** + + - `updateManyData` and `batchData` (update/upsert rows) now register a per-row + `onFieldsDropped` collector and attach the events to that row's result. + - `createManyData` diffs each supplied row against its #3043-stripped form and + returns an **aggregated** top-level `droppedFields` (one event per + object/reason with the union of field names) — its `{ records, count }` + response has no per-row slot, and the insert-time strip is static-`readonly` + only, so it is schema-uniform across rows and the aggregate is faithful. + - `insertManyData` keeps per-row precision, attaching `droppedFields` to each + outcome. + - **Correctness fix bundled in:** `updateManyData` and `batchData` never threaded + the caller's execution `context` to the engine — bulk writes ran context-less, + so RLS/FLS and `readonlyWhen` evaluated without the caller's principal, and the + batch create-ingress strip was hard-coded to a non-system context. All engine + calls in both methods now run under the resolved `context`. + + **Contract (spec).** `BatchOperationResultSchema` gains an optional per-row + `droppedFields` (covers `updateMany` + `batch`, which alias + `BatchUpdateResponseSchema`); `CreateManyDataResponseSchema` gains the optional + aggregated `droppedFields`. Both are omit-when-empty, so existing clients are + unaffected. `X-ObjectStack-Dropped-Fields` is deliberately **not** emitted for + batches — one response header cannot express per-row drops, so the per-row body + field is the canonical bulk channel. + + **Typed client warnings (@objectstack/client).** `CreateDataResult` / + `UpdateDataResult` gain `droppedFields?: DroppedFieldsEvent[]`, giving the body + channel a type instead of an untyped property. + + **CORS (@objectstack/hono, @objectstack/plugin-hono-server).** + `x-objectstack-dropped-fields` is added to the default `Access-Control-Expose-Headers` + allow-list (kept in lockstep across both Hono CORS sites) so a cross-origin + browser can read the single-write drop header. The body `droppedFields` remains + the primary, cross-origin-safe surface — this is a convenience mirror. + + **GraphQL — not applicable (documented).** #3455 lists a GraphQL mutation item, + but GraphQL has no runtime: `kernel.graphql` is unassigned everywhere and + `handleGraphQL` returns `501`, and discovery never advertises `/graphql`. There + is no schema generator or mutation resolver to expose a typed payload field on, + so there is nothing to wire until a GraphQL engine lands — at which point the + protocol-layer `droppedFields` is already present and only the GraphQL schema + projection would remain. + +- 9613396: feat(security): ENFORCE the user-level export axis on the server (#3544) + + `allowExport` landed as a spec bit plus a `/me/permissions` annotation, which + hid the client's Export button — and nothing else. Because `export ⊆ list`, the + REST export route streams through `findData` and the engine middleware sees an + ordinary `find` gated by `allowRead`, so no code path ever read the bit: a caller + holding `allowExport: false` could still `curl +/api/v1/data/:object/export` and drain the whole table. Declared, not enforced. + + - **plugin-security** `PermissionEvaluator.checkObjectPermission('export', …)` is + now a real decision: `export` = read granted ∧ not explicitly denied. + `allowExport` stays out of `OPERATION_TO_PERMISSION` on purpose — that map + means "the bit must be truthy", which would have denied export to every + permission set authored before the axis existed. The new exported + `resolveUserExportAllowed()` folds the tri-state across sets (`true` beats + `false` beats unset) exactly as the `/me/permissions` merge does. + - **spec** `ISecurityService` gains `canExport(object, context)` — the question a + bulk-egress door outside the engine middleware has to ask before it reads. + Fails CLOSED; `isSystem` and an empty set resolution bypass, mirroring the + middleware. + - **rest** `GET /data/:object/export` calls it and answers **403 + `EXPORT_NOT_PERMITTED`** before the first chunk is fetched. Distinct from the + object-level 405 `OBJECT_API_METHOD_NOT_ALLOWED`, which still runs first: 405 + says the object exposes no export, 403 says this caller may not use it. No + security service (no `plugin-security` ⇒ no permission sets) → allowed, the + same fail-open posture as every other permission gate in that layer; service + present but unable to answer → denied. + - **plugin-hono-server** the `/me/permissions` annotation now falls back to the + `'*'` entry's export bit when a per-object entry declares none, matching the + evaluator's own wildcard fallback — so a set that denies export wholesale via + `'*'` no longer offers a button the server refuses. + + Backward-compatible: `allowExport` is still an opt-out with no default, so an + unset bit inherits read and existing permission sets behave exactly as before. + Only a permission set that explicitly sets `allowExport: false` changes — and it + now changes on the server, which is the point. + + Implementers of `ISecurityService` outside this repo must add `canExport`; the + interface member is required, matching how `getReadableFields` was added. + Consumers still feature-detect (`typeof svc.canExport === 'function'`), so a + partial implementation degrades rather than throwing. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 6d1455abac..1673bb7c82 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": "16.1.0", + "version": "17.0.0-rc.0", "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 4b09367e12..ed5e8a7d9d 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/plugin-pinyin-search +## 17.0.0-rc.0 + +### Patch Changes + +- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the + production-dependency batch from #3517 + + **better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`, + `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the + adapter/telemetry overrides). `@better-auth/scim` deliberately stays on + 1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the + `scimProvider` model and the generate-token endpoint are gone), which is a + feature migration, not a version bump. Its peer range accepts rc.2 core, and the + advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed. + + **BREAKING — account identity.** better-auth renamed `account.accountId` to + `account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now + resolves accounts by `(issuer, providerAccountId)`. + + - FROM `fields: { accountId: 'account_id' }` → TO + `fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider + account id keeps its `account_id` column — only the better-auth-side name + moved — and `sys_account` gains an `issuer` column. + - FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO + `createAccount({ providerId, issuer, providerAccountId, … })`. A local + password account carries the issuer better-auth mints for itself, + `local:credential`. + - FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO + `unlink({ accountId })`, where `accountId` is now the account ROW id (the `id` + from `accounts.list()`), matching better-auth's narrowed body. + `accounts.list()` returns `issuer` + `providerAccountId` in place of + `accountId`. + + **Existing deployments:** rows written before 1.7 have no issuer and are + invisible to sign-in until stamped. The auth plugin now runs an idempotent + boot-time backfill that stamps what it can derive — `local:credential` for + password accounts, `local:oauth:` for configured social providers, + and the registered IdP's real `iss` from `sys_sso_provider` for federated ones. + Accounts from a federated IdP that is no longer registered cannot be derived; + they are logged with their provider id and row count rather than guessed, and + those users cannot sign in through that provider until the row is stamped with + the IdP's issuer or removed so a fresh login re-links it. + + **Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both + now implemented over the kernel cache service — `getAndDelete` (single-use + verification values) and `increment` (fixed-window rate-limit counter; + `rateLimit.storage: 'secondary-storage'` throws at boot without it). + + The rest of #3517's production-dependency batch rides along: `@oclif/core` + 4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4, + `pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps. + +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [a749273] +- Updated dependencies [fdb4f50] +- Updated dependencies [879ea13] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [b949059] +- Updated dependencies [c5ff96d] +- Updated dependencies [0e3a226] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [030125b] +- Updated dependencies [8e08bc3] +- Updated dependencies [0c302a7] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index ef0f57ebf7..b6cb906bd9 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": "16.1.0", + "version": "17.0.0-rc.0", "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 0bb7f85ad6..331fcf2c93 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,172 @@ # @objectstack/plugin-reports +## 17.0.0-rc.0 + +### Major Changes + +- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710) + + **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a + record and taking a bulk machine-readable copy of the whole table are different + privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite + "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so. + + ### Migration — FROM → TO + + | | before | after | + | -------------------- | ----------------------------------- | -------------------------- | + | `allowExport` unset | export **allowed** (inherited read) | export **denied** | + | `allowExport: false` | export denied | export denied (unchanged) | + | `allowExport: true` | export allowed | export allowed (unchanged) | + + **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'` + wildcard) of every permission set whose holders should keep exporting. + + ```ts + objects: { + deal: { allowRead: true, allowExport: true }, // ← add the grant + } + ``` + + Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set + that never exported is unaffected. + + **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the + built-ins are handled for you — `admin_full_access` and `organization_admin` now + carry `allowExport: true` explicitly. **Environment-authored sets are not**: any + custom set whose users export must be edited. `member_default` deliberately does + NOT carry the grant, so ordinary authenticated users lose export until an admin + grants it — that is the point of the flip, not an oversight. + + **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set + granting `true` grants export. `false` and unset are the same outcome; `false` + is authoring intent, not a veto, because permission sets are additive capability + containers (ADR-0090). + + **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no + longer confer export. Separating "may see all data" from "may take a bulk copy" + is the segregation-of-duties case the axis exists for. + + ### Also in this change + + - **spec** — a set carrying `allowExport` is now **high-privilege** + (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` / + `guest` audience anchors. Without this the opt-in was defeatable by binding an + export-granting set to `everyone`. One predicate, so the runtime anchor gate, + the `@objectstack/lint` security-posture rule and the install-time suggestion + surface all pick it up together. + - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so + `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It + explains as `read ∧ the export grant`: `object_crud` reports the conjunction + and attributes the granting set, while every data-shaped layer + (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed + as the `find` the export actually performs — asking the RLS compiler about an + `export` operation would match no policy and wrongly report "no RLS applies". + `readFilter` is surfaced for `export` as it is for `read`. + - **plugin-reports** — closes the reports side door (#3710). A report rendered + as `csv`/`json` is the same bulk copy of the same object, so it is gated by + the same `ISecurityService.canExport`. Enforced in `executeReport`, which the + interactive run, the ad-hoc run and the scheduled dispatch all funnel through; + `scheduleReport` additionally refuses at create time so an author is not told + at 3am. A schedule created while granted stops delivering once the grant is + revoked. `html_table` stays a read — it is a rendered view, not a bulk copy. + Deployments without `plugin-security` are unaffected (no permission sets + exist, so the axis does not apply). + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 0fe029548c..1495f3af1d 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-reports", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 fe69fc785d..853136c829 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,689 @@ # @objectstack/plugin-security +## 17.0.0-rc.0 + +### Major Changes + +- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710) + + **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a + record and taking a bulk machine-readable copy of the whole table are different + privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite + "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so. + + ### Migration — FROM → TO + + | | before | after | + | -------------------- | ----------------------------------- | -------------------------- | + | `allowExport` unset | export **allowed** (inherited read) | export **denied** | + | `allowExport: false` | export denied | export denied (unchanged) | + | `allowExport: true` | export allowed | export allowed (unchanged) | + + **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'` + wildcard) of every permission set whose holders should keep exporting. + + ```ts + objects: { + deal: { allowRead: true, allowExport: true }, // ← add the grant + } + ``` + + Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set + that never exported is unaffected. + + **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the + built-ins are handled for you — `admin_full_access` and `organization_admin` now + carry `allowExport: true` explicitly. **Environment-authored sets are not**: any + custom set whose users export must be edited. `member_default` deliberately does + NOT carry the grant, so ordinary authenticated users lose export until an admin + grants it — that is the point of the flip, not an oversight. + + **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set + granting `true` grants export. `false` and unset are the same outcome; `false` + is authoring intent, not a veto, because permission sets are additive capability + containers (ADR-0090). + + **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no + longer confer export. Separating "may see all data" from "may take a bulk copy" + is the segregation-of-duties case the axis exists for. + + ### Also in this change + + - **spec** — a set carrying `allowExport` is now **high-privilege** + (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` / + `guest` audience anchors. Without this the opt-in was defeatable by binding an + export-granting set to `everyone`. One predicate, so the runtime anchor gate, + the `@objectstack/lint` security-posture rule and the install-time suggestion + surface all pick it up together. + - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so + `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It + explains as `read ∧ the export grant`: `object_crud` reports the conjunction + and attributes the granting set, while every data-shaped layer + (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed + as the `find` the export actually performs — asking the RLS compiler about an + `export` operation would match no policy and wrongly report "no RLS applies". + `readFilter` is surfaced for `export` as it is for `read`. + - **plugin-reports** — closes the reports side door (#3710). A report rendered + as `csv`/`json` is the same bulk copy of the same object, so it is gated by + the same `ISecurityService.canExport`. Enforced in `executeReport`, which the + interactive run, the ad-hoc run and the scheduled dispatch all funnel through; + `scheduleReport` additionally refuses at create time so an author is not told + at 3am. A schedule created while granted stops delivering once the grant is + revoked. `html_table` stays a read — it is a rendered view, not a bulk copy. + Deployments without `plugin-security` are unaffected (no permission sets + exist, so the axis does not apply). + +### Minor Changes + +- 7fb436c: Multi-organization operation is an ENTITLEMENT again: the `group` posture no + longer activates without the enterprise runtime (ADR-0105 D12 correction). + + The first ADR-0105 wave read D12 as "the `group` wall ships open" and made the + posture self-activating — it never probed for `@objectstack/organizations`. That + turned `group` into a free multi-org path around the `isolated` gate (ADR-0081 + D2), and made the weaker isolation the free one, which is not a boundary anyone + would draw on purpose. + + The distinction that was missed: **open code is not free activation.** The wall's + implementation has always lived in the open packages — that is equally true of + `isolated`, whose Layer 0 wall sits in `plugin-security` and is gated on a + service the enterprise package registers. Cloud ADR-0016's 铁律 + (强制免费、治理收费) guarantees that a deployment RUNNING a multi-org shape is + safe; it is satisfied by REFUSING to run one unwalled, not by giving the posture + away. + + ## Changes + + - **`tenancy-service`**: `group` probes `org-scoping` exactly like `isolated`. + Without it the posture resolves to `single` and reports `degraded`. + - **`os serve`**: the ADR-0093 D5 boot guard keys off the resolved POSTURE + instead of `OS_MULTI_ORG_ENABLED`. Previously `OS_TENANCY_POSTURE=group` skipped + both the enterprise package load AND the fail-fast, silently degrading to an + unwalled deployment — the exact ADR-0049 class that guard exists to close. A + `group` request without the runtime now refuses to boot unless + `OS_ALLOW_DEGRADED_TENANCY=1`. + - **New seam — the runtime declares what it entitles.** `org-scoping` may expose + `supportedPostures` (`OrgScopingEntitlement`, `@objectstack/spec/security`); + the open side honours it and fails closed on anything not listed. Whether + `group` and `isolated` are one commercial tier or two is packaging policy, and + packaging policy belongs to the commercial runtime rather than hard-coded in + open core. Omitting the field entitles every walled posture, so existing + runtimes are unaffected. + - **`organization_id` stamping returns to the enterprise runtime.** The previous + wave moved auto-stamping into the open engine; that removed the closed + package's only load-bearing runtime duty, so a five-line forged `org-scoping` + registration would have produced a fully working multi-org deployment. With + stamping back where it was, a forged registration yields NULL-org rows the wall + hides — a broken deployment, not an unlicensed working one. + + **Write-side VALIDATION stays open and is unchanged**, including the + bulk-insert coverage: rejecting a forged `organization_id` is a security + property, not a packaging one. Only filling an ABSENT value moved back. + + - Default-organization bootstrap returns to `single`-only; every walled posture + keeps its existing owner (ADR-0081 D1). + + ## Note for operators + + `OS_TENANCY_POSTURE=group` without `@objectstack/organizations` installed now + **refuses to boot** rather than running single-org. This only affects + deployments that adopted `group` between the two waves. + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 7c7e246: feat(authz): expose the caller's delegable scope — the read half of the + delegated-administration gate (ADR-0090 D12 / ADR-0105 D8) + + `adminScope` decided writes but could not be READ: `assignablePermissionSets` + lived only inside `delegated-admin-gate.ts`, so a UI offering "place this + person in a unit, with these positions" (the D8 scoped-invitation form) had no + way to narrow its pickers. It would list the whole tree and let the user + discover the boundary by being refused — which turns an authorization gate into + a validator and makes the boundary invisible until it bites. + + `ISecurityService.describeDelegableScope(callerContext)` answers it, exposed as + `GET /api/v1/security/my-delegable-scope` and `client.security.describeDelegableScope()`: + + - `placeableBusinessUnitIds` — union of the subtrees where the caller may place + people (scopes granting `manageAssignments`); + - `assignablePositions` — positions whose every distributed permission set the + caller may hand out (containment check included); + - `scopes` — the held `adminScope`s with subtrees resolved, for attribution; + - `isTenantAdmin` — unconstrained, with everything enumerated so a consumer + renders ONE uniform picker instead of special-casing. + + Computed by the same helpers the write gate enforces with, so an option this + reports is one `assert()` accepts — a test asserts that agreement directly. It + NARROWS; the gate still decides. + + Strictly self-scoped: no target-user parameter, so it discloses nothing beyond + the authority the caller already holds (unlike `explain`, which has one and + gates it). Fail-closed — unresolvable scopes contribute nothing, a caller with + no delegated authority gets empty lists, and a deployment without + `@objectstack/plugin-security` gets 501. + +- 9613396: feat(security): ENFORCE the user-level export axis on the server (#3544) + + `allowExport` landed as a spec bit plus a `/me/permissions` annotation, which + hid the client's Export button — and nothing else. Because `export ⊆ list`, the + REST export route streams through `findData` and the engine middleware sees an + ordinary `find` gated by `allowRead`, so no code path ever read the bit: a caller + holding `allowExport: false` could still `curl +/api/v1/data/:object/export` and drain the whole table. Declared, not enforced. + + - **plugin-security** `PermissionEvaluator.checkObjectPermission('export', …)` is + now a real decision: `export` = read granted ∧ not explicitly denied. + `allowExport` stays out of `OPERATION_TO_PERMISSION` on purpose — that map + means "the bit must be truthy", which would have denied export to every + permission set authored before the axis existed. The new exported + `resolveUserExportAllowed()` folds the tri-state across sets (`true` beats + `false` beats unset) exactly as the `/me/permissions` merge does. + - **spec** `ISecurityService` gains `canExport(object, context)` — the question a + bulk-egress door outside the engine middleware has to ask before it reads. + Fails CLOSED; `isSystem` and an empty set resolution bypass, mirroring the + middleware. + - **rest** `GET /data/:object/export` calls it and answers **403 + `EXPORT_NOT_PERMITTED`** before the first chunk is fetched. Distinct from the + object-level 405 `OBJECT_API_METHOD_NOT_ALLOWED`, which still runs first: 405 + says the object exposes no export, 403 says this caller may not use it. No + security service (no `plugin-security` ⇒ no permission sets) → allowed, the + same fail-open posture as every other permission gate in that layer; service + present but unable to answer → denied. + - **plugin-hono-server** the `/me/permissions` annotation now falls back to the + `'*'` entry's export bit when a per-object entry declares none, matching the + evaluator's own wildcard fallback — so a set that denies export wholesale via + `'*'` no longer offers a button the server refuses. + + Backward-compatible: `allowExport` is still an opt-out with no default, so an + unset bit inherits read and existing permission sets behave exactly as before. + Only a permission set that explicitly sets `allowExport: false` changes — and it + now changes on the server, which is the point. + + Implementers of `ISecurityService` outside this repo must add `canExport`; the + interface member is required, matching how `getReadableFields` was added. + Consumers still feature-detect (`typeof svc.canExport === 'function'`), so a + partial implementation degrades rather than throwing. + +- aa8b847: feat(authz): scoped invitations — placement intent on an invitation, gated by + the issuer's adminScope and applied on acceptance (ADR-0105 D8) + + An invitation may now carry PLACEMENT INTENT — the business unit the invitee + lands in and the positions they are assigned — so a delegated (plant) admin's + invitee arrives already in the right unit and role instead of waiting on a + platform admin. This closes the structural gap ADR-0105 D8 names for + `single`-posture deployments and is the natural admission path under `group`. + + The two halves ship together, deliberately: + + - **Issuance is authorized** against the ISSUER's `adminScope` (ADR-0090 D12), + by dry-running the existing `DelegatedAdminGate` against the very + `sys_user_position` rows the acceptance would write. The gate is reused + verbatim — no second copy of the subtree/allowlist logic to drift — so an + invitation can never place what its issuer could not have assigned directly. + Without that gate the feature would be an escalation hole: the built-in + `organization_admin` is deliberately read-only on the RBAC tables precisely + so a fresh org admin cannot rebind themselves, and applying an unchecked + invitation payload under system context would hand that authority straight + back. + - **Acceptance applies it**, idempotently and failure-isolated: a replayed + acceptance converges instead of duplicating assignments, and a placement + miss never undoes a valid membership. + + Surface: + + - `sys_invitation` gains `business_unit_id` + `positions` (ADR-0092 extension + fields, registered in the D7 collision-guarded whitelist; NOT generically + editable — placement is set only at issuance, through the gate). + - `@objectstack/plugin-security` registers the `invitation-placement` service + (`assertIssuable` / `apply`). + - `@objectstack/plugin-auth` wires better-auth's `beforeCreateInvitation` / + `afterAcceptInvitation` to it. **Fail closed**: an invitation that requests + placement in a deployment without the delegated-administration runtime is + refused, never silently placed unchecked. + + Existing invitations are unaffected — an invitation without placement intent + never consults the gate and behaves exactly as before. + +- d318b24: feat: `security.getReadableFields` query surface for export column projection (#3547, #3391 follow-up) + + The REST export route projected its columns by inferring readability from the + first chunk of already-masked data rows (#3498). That has two known + compromises: a readable column whose first-chunk values are all null (and thus + omitted by the driver) drops out of the header, and an empty result set leaves + nothing to narrow. This adds the long-term-correct path. + + - **plugin-security** — the `security` service gains + `getReadableFields(object, context)`. It resolves the caller's permission + sets and builds the field-permission map with the SAME evaluator + + `requiredPermissions` fold the read middleware's `FieldMasker` uses (and the + same on-behalf-of delegator intersection, fail-closed on a dangling + delegator), then returns every schema field NOT masked non-readable — the + exact complement of what the mask deletes, so it can never drift from + data-plane FLS. Computed from schema + context, never from data rows: immune + to null values and empty result sets. A system context bypasses FLS; an + unresolvable schema returns `undefined` so callers fall back. + - **rest** — the `GET /data/:object/export` route asks the environment's + `security` service for `getReadableFields(object, context)` and projects the + schema-derived header to that set BEFORE streaming. When no security service + is reachable (no plugin-security / single-kernel without a provider) it + degrades to the existing masked-row inference, so there is zero regression. + Explicit `?fields=` requests are still honored verbatim. + + Contract-neutral: export columns already equal list's readable columns + (`export ⊆ list`, #3391); this makes the projection authoritative instead of + inferred. + +### Patch Changes + +- 735f850: fix(security): resolve the ISSUER's real grants when authorizing invitation + placement (ADR-0105 D8) + + Scoped-invitation issuance dry-runs `DelegatedAdminGate` against the + `sys_user_position` rows the acceptance would write. The gate reads authority + off `context.positions` / `context.permissions` — but the invitation hook + handed it a hand-built `{ userId, tenantId }`, which carries neither. Every + delegated administrator therefore resolved to the additive baseline alone and + was refused: + + > requires tenant-level administration or a delegated adminScope (ADR-0090 D12) + + Fail-closed, but dead: only a tenant admin could ever issue a placement, which + is the one case the feature was not for. Caught by cloud's group-posture + dogfood, which exercises the real HTTP path with a real delegate. + + `assertIssuable` now takes `actorUserId` instead of a caller-built + `actorContext` and resolves that user's grants itself through the single authz + resolver (`@objectstack/core` `resolveUserAuthzGrants`) — the same envelope a + transport would have carried, from the same reads. There is no request to + resolve a context from inside a better-auth hook, so the id is what the caller + can honestly supply and the resolution belongs behind the boundary. + + A principal-less call still reaches the gate with an empty context on purpose: + the gate owns that refusal too, so the security boundary keeps exactly one + place an issuance can be denied. + +- 307e0fe: fix(security): govern `sys_member` writes — organization membership is not a delegable capability (#3697 follow-up) + + `DelegatedAdminGate`'s `GOVERNED_OBJECTS` covered the four RBAC link tables but + not `sys_member`, so the table that decides _who is an org admin_ was the one + authority surface the delegated-administration gate never saw. + + That matters because a membership row is an authority dial: `role` containing + `owner`/`admin` is auto-elevated to `organization_admin` by + `auto-org-admin-grant.ts`, and that set's wildcard `modifyAllRecords` is exactly + what `isTenantAdmin()` tests. Writing one mints a tenant admin — the same + escalation the invitation role cap closes on the issuance path, one layer down + at the table. + + **Not exploitable today, and this changes no working behaviour.** Every + `sys_member` writer is a better-auth path running under `isSystem`, which + short-circuits the whole security middleware before this gate; the ADR-0092 D2 + identity write guard refuses user-context writes to better-auth-managed tables + upstream of it. The gate is added so the chain cannot silently reopen the day a + direct-write surface is introduced — a `case` label is not enforcement, and the + call site is what decides (AGENTS.md Prime Directive #10). + + The rule is tenant-admin-only rather than scope-delegable, deliberately: no axis + of `AdminScope` expresses "organization membership" (its vocabulary is BU + subtree, action flags and an assignable-set allowlist), so there is nothing for + a delegated scope to approve part of — and a delegate who could write one would + mint authority strictly greater than their own, which is what ADR-0090 D12 + exists to prevent. Adding people to an organization already has a delegable + path: the **invitation**, whose placement is authorized against the issuer's + `adminScope` and whose role is capped at the issuer's own grade. The refusal + message says so. + +- 0e3a226: fix(authz): widen the driver's native tenant scope to the membership union + under the `group` posture — ADR-0105 D2 finally reaches the wire (#3623) + + The Layer 0 wall correctly compiled `organization_id IN accessible_org_ids` + under `group`, but the ObjectQL engine also propagated the active-org + `tenantId` into `DriverOptions` unconditionally, and the SQL driver's native + scoping ANDed `organization_id = tenantId` under the union — collapsing every + group read back to active-org (isolated) reach. Found by the cloud-side + `ee-group-showcase` dogfood (cloud#880), the first end-to-end boot of `group` + against a real driver. + + - `DriverOptions.tenantIds` (spec): the union tenant access set. Drivers with + native scoping widen reads/updates/deletes/aggregates to `IN (...)`, + keeping the NULL-tenant global-row carve-out; inserts still stamp from + `tenantId` (the active organization is the write target, D5). Absent or + empty ⇒ equality fallback — fail toward isolation, never toward exposure. + - ObjectQL engine threads `ExecutionContext.accessible_org_ids` as + `tenantIds` when the tenancy posture is `group`, reported by a new + `setTenancyPostureProvider` seam. + - SecurityPlugin wires that provider at start — deliberately from the + enforcement layer, so the driver wall only widens while the Layer 0 union + wall enforces above it. Embeddings without plugin-security keep active-org + equality. + +- d1cabaa: fix(i18n): translate the SSO / SCIM / user-position / import-job admin objects + + Four live, UI-facing system objects were registered but never added to their + package's i18n extract config, so non-English admins saw raw English `label` + metadata: + + - `sys_sso_provider`, `sys_scim_provider` (platform-objects) — identity-provider + admin grids plus the register / verify-domain actions. + - `sys_user_position` (plugin-security) — delegated position assignment + (`userActions` create/edit/delete); its sibling `sys_user_permission_set` was + already translated, so this closes an inconsistency. + - `sys_import_job` (platform-objects) — import history / progress, alongside the + already-translated `sys_job` / `sys_job_run`. + + Adds each object to its package's `scripts/i18n-extract.config.ts` and supplies + real zh-CN / ja-JP / es-ES translations across all four locale bundles, and + extends the bundle-ownership guards' `OWNED_OBJECTS` to cover them. The + orphan-only guards from #3502 could not catch this "owned-and-live-but-never- + extracted" gap. + +- 7180ed5: fix(security): fail closed when an object's security posture can't be resolved + (#3545) + + #3545 accepted the API-exposure gate's fail-open on unresolvable metadata on one + load-bearing premise: that gate is a SURFACE-AREA control, while the real + authorization boundary — auth + the ObjectQL security middleware (CRUD/FLS/RLS) + — enforces unconditionally on the data call whatever the gate answers. + + Verifying that premise rather than assuming it shows it did not hold. The + middleware does run unconditionally, but two of its INPUTS were read from the + same object metadata and defaulted permissively when it could not be resolved, + so the very trigger the issue is about reached one layer PAST the gate, into the + boundary itself: an unresolved `access.default` read as PUBLIC (so a plain `'*'` + wildcard covered an object ADR-0066 D2 excludes from it) and an unresolved + `requiredPermissions` read as NO CONTRACT (so the D3 capability AND-gate was + skipped entirely). + + `getObjectSecurityMeta` now flags `unresolved`, and the three consumers that turn + posture into an access decision fail closed on it: the middleware denies (with an + error log, so a persistent metadata outage is observable rather than a silent + blanket-allow), `canExport` denies, and `getReadableFields` exposes no columns — + the same stance already taken for a permission-resolution failure and a dangling + delegator. `computeLayeredRlsFilter` keeps consuming the defaults deliberately: + there the permissive value WITHHOLDS the cross-tenant exemption, so it is already + the closed direction. + + Blast radius is bounded to the risky case. System/boot writes (`isSystem`) and + principal-less/anonymous contexts short-circuit earlier in the middleware, so + reaching the new check means an authenticated principal with resolved grants + asking for an object whose declaration is missing; the cold-start window is + served by those short-circuits, not by the permissive default. The exposure + gate's own tiered decision (transient unavailability → fail open) is therefore + unchanged — it now rests on a boundary that actually holds. + + The explain engine reports the denial on its existing `object_crud` layer naming + the real cause, so the "why am I denied?" surface cannot drift from enforcement. + +- db48ad5: fix(security,approvals,metadata-core): restore batch routes on the eight objects the #3391 P1 companion fix missed (#3026) + + The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch + request is admitted only when the object grants the `bulk` **primitive** and the + batched child operation is itself allowed. Before that, the `*Many` routes + checked only the child verb, so a boilerplate CRUD-five whitelist + (`['get','list','create','update','delete']`) batched fine. + + The companion fix — adding the `bulk` primitive wherever an explicit whitelist + survived — was applied only inside `platform-objects`. Eight objects carrying + the same boilerplate live in other packages and kept the gap, so `/batch`, + `createMany`, `updateMany` and `deleteMany` answered `405 +OBJECT_API_METHOD_NOT_ALLOWED` on objects whose single-record create/update/ + delete were wide open. `data-objectstack` rethrows that 405 without falling back + to per-row writes, which surfaced as a hard error on multi-select delete in the + Setup grids. + + Objects reclaimed (whitelist now `['get','list','create','update','delete','bulk']`): + `sys_capability`, `sys_permission_set`, `sys_position`, + `sys_position_permission_set`, `sys_user_permission_set`, `sys_user_position` + (plugin-security); `sys_approval_delegation` (plugin-approvals); + `sys_view_definition` (metadata-core). + + No new authority is granted: `bulk` only permits batching verbs each object + already exposes one record at a time, and every batched row still passes the + same row- and field-level permission checks. The whitelists stay explicit rather + than being deleted — seven of the eight are `managedBy`, and + `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array + `apiMethods`, so dropping the line would silently disable the managed-write + backstop. + +- 1659072: feat(spec): publish `ISecurityService` — the `security` service surface becomes an enforced contract + + The `security` service registers seven cross-package methods (`getReadFilter`, + `getReadableFields`, `resolvePermissionSetNames`, `explain`, and the three + audience-binding suggestion calls) but had no contract in + `@objectstack/spec/contracts`. Consumers duck-typed it, and each one invented its + own fallback for a missing method or an "empty" answer — with more consumers + arriving, that is a drift surface. + + `ISecurityService` now documents the surface, and both ends are typed against it + so it is **enforced rather than declared**: `plugin-security` assigns its + registration to `ISecurityService` (a renamed, dropped, or re-typed method fails + that build), and the REST layer resolves the service as a `Partial` + (so call sites must keep feature-detecting instead of assuming the full surface). + + The contract makes explicit the one thing consumers cannot guess — that the + methods do **not** share a failure convention: + + - `getReadFilter` fails **CLOSED**: a resolution failure yields a deny filter + matching zero rows, never `undefined`. `undefined` means "no row restriction", + and nothing else. + - `getReadableFields` fails **SOFT**: `undefined` means "no answer, use your own + projection", while `[]` is authoritative and means "no field is readable" — + opposite instructions that a consumer must not conflate. + + Typing the producer immediately caught one real discrepancy, fixed here: + `getReadFilter` declared `Promise | null | undefined>` + while every return path yields a filter or `undefined` (`filter ?? undefined` + normalizes the null away). The dead `| null` is removed, so "no restriction" has + exactly one representation. Type-level only — no runtime behaviour changes. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 7b8522d0dc..9c80b77271 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 a314cc7021..bf0c940aa1 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,152 @@ # @objectstack/plugin-sharing +## 17.0.0-rc.0 + +### Minor Changes + +- 503be86: feat(security)!: reconcile the SharingRule authoring surface with the enforced runtime — rename `group` → `team`, add `business_unit`, prune `guest` + owner-type rules (#1878) + + The authoring `ShareRecipientType` enum had drifted behind the ADR-0090 D3 + rename and the enforced runtime: the runtime expands `team` (via + `sys_team`/`sys_team_member`) and `business_unit`, but the authoring enum + still offered the pre-rename `group` (silently skipped at seed time) and + omitted the two live recipients. After this change **every authorable + recipient and rule type is enforced** — nothing on the SharingRule surface + validates and then silently does nothing (ADR-0078). + + - **`sharedWith.type: 'group'` → `'team'`** (wire-rename): the enum member is + renamed to match the runtime vocabulary and now maps through the seed + bootstrap to the live `TeamGraphService` expansion. Flat `sys_team` + membership; enforced. + - **`business_unit` added** to the authoring enum — exactly one business + unit's members (no subtree; use `unit_and_subordinates` for the subtree). + The runtime + bootstrap already enforced it; only the enum omitted it. + - **`guest` removed** — it had no runtime recipient mapping. Anonymous access + is served by the public-form grant and share links, not sharing rules. + - **Owner-type rules removed** (`type: 'owner'`, `ownedBy`, + `OwnerSharingRuleSchema` + its type export): they depend on live + team/position membership, which the static materialiser cannot track, so + they validated but never materialised a share. They return as an enforced + form if membership-reactive re-materialisation is designed. + `SharingRuleSchema` is now the criteria form; the `queue` recipient stays + runtime-reserved (no `sys_queue` yet) and deliberately non-authorable. + + **Migration** (stale definitions now fail parse with the valid options listed): + + - `sharedWith: { type: 'group', … }` → `sharedWith: { type: 'team', … }`. + - `sharedWith: { type: 'guest', … }` → delete the rule; expose the records + via a public form or share link instead. + - `type: 'owner'` rules → rewrite as a `type: 'criteria'` rule scoping the + rows by field values (see the migrated examples: + `share_open_tasks_with_manager` in app-showcase, + `share_active_leads_with_manager` in app-crm), or use a scope-depth grant. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 73f8ba1ee8..01b89e7715 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 4678ff33ab..7af16f72ae 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,169 @@ # @objectstack/plugin-webhooks +## 17.0.0-rc.0 + +### Minor Changes + +- 69f1dfd: fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461) + + A webhook authored declaratively — `defineStack({ webhooks })` / `defineWebhook()`, + validated against the spec `WebhookSchema` — was a **silent no-op**. The runtime + dispatcher (`AutoEnqueuer`) fans out off `sys_webhook` DATA rows (`object_name` / + `active`), which until now were only ever written by hand through the object's + CRUD UI. Nothing turned a declared webhook (`object` / `isActive`) into a + dispatchable row, so authoring `webhooks:` on a stack produced `webhook` metadata + that never fired (ADR-0078). The showcase app itself shipped a `webhooks:` entry + that did nothing. + + `@objectstack/plugin-webhooks` now bridges the two on boot: + + - **`bootstrapDeclaredWebhooks`** reads declared `webhook` metadata from the + ObjectQL registry (where the manifest decomposition already parks + `stack.webhooks`), validates each through `WebhookSchema.parse()` — the spec + schema finally has a real consumer — and materializes it into a `sys_webhook` + row, mapping `object → object_name`, `isActive → active`, and stashing the full + envelope (headers / secret / retry / timeout) in `definition_json`. The + auto-enqueuer's first cache refresh then picks the row up and dispatches it. + - **Seed-not-clobber provenance** (mirrors `sys_sharing_rule`, #2909): `sys_webhook` + gains `managed_by` / `customized` columns. Declared webhooks re-seed every boot + as `managed_by: 'package'`, but a row an admin created (`managed_by: 'admin'`) or + edited in Setup (`customized: true`, stamped by a `beforeUpdate` hook) is never + overwritten — a deactivated noisy webhook survives redeploys. + + Connector-declared `webhooks` remain not-yet-enforced (that is a separate seam, + #3197). Registering `webhook` as a first-class metadata type + enrolling it in the + liveness `GOVERNED` set is a tracked follow-up. + + Migration: none required. Existing hand-authored `sys_webhook` rows default to + `managed_by: 'admin'` and are never touched by the seeder. Anyone who authored + `webhooks:` on a stack expecting it to fire will find it now does — review those + declarations (especially `url` / `isActive`) before upgrading. + +### Patch Changes + +- 52281b0: chore(i18n): purge the dead sys_webhook_delivery translation block and guard against recurrence + + `sys_webhook_delivery` was removed when webhook delivery moved to + `@objectstack/service-messaging` (`sys_http_delivery`, ADR-0018 M3), but a full + translation block for it lingered in the four generated plugin-webhooks i18n + bundles (en/zh-CN/ja-JP/es-ES) — dead weight bound to an object that no longer + exists, and destined to be dropped silently (with any curated strings) on the + next `os i18n extract`. + + - Removed the stale `sys_webhook_delivery` block from all four locale bundles + (surgical; the `sys_webhook` block is untouched). + - Corrected three stale `sys_webhook_delivery` doc comments (platform-objects + `integration/index.ts` + `setup.app.ts`, plugin-webhooks `sys-webhook.object.ts`) + that still named it as a plugin-webhooks-owned object. + - Rolled out the platform-objects `bundle-ownership` test guard (#2834 ⑤ / + ADR-0029 D8) to the eight packages that own i18n bundles, so a stray object + block in a generated bundle now fails the build instead of dying silently. + - That guard immediately surfaced a live-object omission: `sys_capability` was + present in plugin-security's bundles with curated translations but had been + dropped from its extract config — re-added to the config so the strings are + preserved, rather than deleted. + +- c95ac80: chore(plugin-webhooks): drop the dead sys_webhook_delivery i18n blocks + + `sys_webhook_delivery` was removed from `@objectstack/plugin-webhooks` when + outbound delivery moved to `@objectstack/service-messaging` (`sys_http_delivery`, + ADR-0018 M3), but its translation blocks lingered in all four generated locale + bundles (en / zh-CN / ja-JP / es-ES) — loaded at runtime yet referenced by + nothing, since the object no longer exists in this plugin. + + - Removed the `sys_webhook_delivery` node from each `*.objects.generated.ts` + bundle; `WebhooksTranslations` now carries only `sys_webhook`. + - Corrected the stale ownership comment on `SysWebhook` that still named + `sys_webhook_delivery` as a live sibling. + + (The dangling `SysWebhookDelivery` import in `scripts/i18n-extract.config.ts` + was fixed independently on `main` by #3489, so it is not part of this change.) + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/service-messaging@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 419650f6c5..b48290dcdc 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 26ff455a4d..c9dd891c12 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,150 @@ # @objectstack/dogfood +## 0.0.40-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [134df4f] +- Updated dependencies [fe67e34] +- Updated dependencies [3d3fddf] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [735f850] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [7101ca2] +- Updated dependencies [587fc91] +- Updated dependencies [415254c] +- Updated dependencies [1f8390b] +- Updated dependencies [3167e29] +- Updated dependencies [0a6fb1e] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [96242ef] +- Updated dependencies [984396b] +- Updated dependencies [d0fea33] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [f243727] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [2c19383] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [307e0fe] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [dac6a08] +- Updated dependencies [313d7be] +- Updated dependencies [5faeac6] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [7180ed5] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [fc5f126] +- Updated dependencies [adabaa8] +- Updated dependencies [030125b] +- Updated dependencies [605c23f] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [52281b0] +- Updated dependencies [db48ad5] +- Updated dependencies [8e08bc3] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [bd68f08] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [a629074] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [e889386] +- Updated dependencies [69f1dfd] +- Updated dependencies [c95ac80] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/service-storage@17.0.0-rc.0 + - @objectstack/plugin-auth@17.0.0-rc.0 + - @objectstack/plugin-security@17.0.0-rc.0 + - @objectstack/mcp@17.0.0-rc.0 + - @objectstack/service-analytics@17.0.0-rc.0 + - @objectstack/verify@17.0.0-rc.0 + - @objectstack/plugin-audit@17.0.0-rc.0 + - @objectstack/plugin-webhooks@17.0.0-rc.0 + - @objectstack/example-crm@4.0.92-rc.0 + - @objectstack/example-showcase@0.3.14-rc.0 + - @objectstack/connector-mcp@17.0.0-rc.0 + - @objectstack/connector-openapi@17.0.0-rc.0 + - @objectstack/connector-rest@17.0.0-rc.0 + - @objectstack/service-messaging@17.0.0-rc.0 + ## 0.0.39 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 782bb72ddd..952c5d8a92 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.39", + "version": "0.0.40-rc.0", "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 e58748b158..02db8b8573 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/downstream-contract +## 0.0.38-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 0.0.37 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index d72325fd5e..7802949431 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.37", + "version": "0.0.38-rc.0", "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 0fc0c0710b..11a93b172e 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/http-conformance +## 0.0.6-rc.0 + +### Patch Changes + +- Updated dependencies [879ea13] + - @objectstack/core@17.0.0-rc.0 + ## 0.0.5 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 8fe60e50cf..5898f53ffb 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.5", + "version": "0.0.6-rc.0", "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 ee8daf5ba4..f0e56f33a6 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,840 @@ # @objectstack/rest +## 17.0.0-rc.0 + +### Minor Changes + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- 7c7e246: feat(authz): expose the caller's delegable scope — the read half of the + delegated-administration gate (ADR-0090 D12 / ADR-0105 D8) + + `adminScope` decided writes but could not be READ: `assignablePermissionSets` + lived only inside `delegated-admin-gate.ts`, so a UI offering "place this + person in a unit, with these positions" (the D8 scoped-invitation form) had no + way to narrow its pickers. It would list the whole tree and let the user + discover the boundary by being refused — which turns an authorization gate into + a validator and makes the boundary invisible until it bites. + + `ISecurityService.describeDelegableScope(callerContext)` answers it, exposed as + `GET /api/v1/security/my-delegable-scope` and `client.security.describeDelegableScope()`: + + - `placeableBusinessUnitIds` — union of the subtrees where the caller may place + people (scopes granting `manageAssignments`); + - `assignablePositions` — positions whose every distributed permission set the + caller may hand out (containment check included); + - `scopes` — the held `adminScope`s with subtrees resolved, for attribution; + - `isTenantAdmin` — unconstrained, with everything enumerated so a consumer + renders ONE uniform picker instead of special-casing. + + Computed by the same helpers the write gate enforces with, so an option this + reports is one `assert()` accepts — a test asserts that agreement directly. It + NARROWS; the gate still decides. + + Strictly self-scoped: no target-user parameter, so it discloses nothing beyond + the authority the caller already holds (unlike `explain`, which has one and + gates it). Fail-closed — unresolvable scopes contribute nothing, a caller with + no delegated authority gets empty lists, and a deployment without + `@objectstack/plugin-security` gets 501. + +- 9613396: feat(security): ENFORCE the user-level export axis on the server (#3544) + + `allowExport` landed as a spec bit plus a `/me/permissions` annotation, which + hid the client's Export button — and nothing else. Because `export ⊆ list`, the + REST export route streams through `findData` and the engine middleware sees an + ordinary `find` gated by `allowRead`, so no code path ever read the bit: a caller + holding `allowExport: false` could still `curl +/api/v1/data/:object/export` and drain the whole table. Declared, not enforced. + + - **plugin-security** `PermissionEvaluator.checkObjectPermission('export', …)` is + now a real decision: `export` = read granted ∧ not explicitly denied. + `allowExport` stays out of `OPERATION_TO_PERMISSION` on purpose — that map + means "the bit must be truthy", which would have denied export to every + permission set authored before the axis existed. The new exported + `resolveUserExportAllowed()` folds the tri-state across sets (`true` beats + `false` beats unset) exactly as the `/me/permissions` merge does. + - **spec** `ISecurityService` gains `canExport(object, context)` — the question a + bulk-egress door outside the engine middleware has to ask before it reads. + Fails CLOSED; `isSystem` and an empty set resolution bypass, mirroring the + middleware. + - **rest** `GET /data/:object/export` calls it and answers **403 + `EXPORT_NOT_PERMITTED`** before the first chunk is fetched. Distinct from the + object-level 405 `OBJECT_API_METHOD_NOT_ALLOWED`, which still runs first: 405 + says the object exposes no export, 403 says this caller may not use it. No + security service (no `plugin-security` ⇒ no permission sets) → allowed, the + same fail-open posture as every other permission gate in that layer; service + present but unable to answer → denied. + - **plugin-hono-server** the `/me/permissions` annotation now falls back to the + `'*'` entry's export bit when a per-object entry declares none, matching the + evaluator's own wildcard fallback — so a set that denies export wholesale via + `'*'` no longer offers a button the server refuses. + + Backward-compatible: `allowExport` is still an opt-out with no default, so an + unset bit inherits read and existing permission sets behave exactly as before. + Only a permission set that explicitly sets `allowExport: false` changes — and it + now changes on the server, which is the point. + + Implementers of `ISecurityService` outside this repo must add `canExport`; the + interface member is required, matching how `getReadableFields` was added. + Consumers still feature-detect (`typeof svc.canExport === 'function'`), so a + partial implementation degrades rather than throwing. + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +- 16adb3c: fix(rest,client)!: reconcile the two REST↔client mismatches the #3587 audit + ledgered (#3610, #3611) + + **#3610 — `POST /api/v1/packages` publish-vs-install collision.** The REST + package registrar claimed the bare `POST /packages` for _marketplace publish_ + (`{manifest, metadata}`), while the dispatcher packages domain gives the same + verb+path _install_ semantics — and REST registers first in the production + stack (first-match-wins), so every `client.packages.install` call landed on + the publish handler and 400'd. Marketplace publish moves to + `POST /api/v1/packages/publish` (breaking for direct callers; a repo-wide and + objectui-wide sweep found zero). The dispatcher's `POST /packages/:id/publish` + (ADR-0033 draft publish) is two segments — different shape, no clash. The + dispatcher already writes both stores on install (`protocol.installPackage`) + and fully uninstalls on DELETE (`protocol.deletePackage`), so the remaining + REST GET/GET/DELETE shadows stay — they are compatible. + + **#3611 — UI view dialect split.** `meta.getView` spoke the `?type=` query + dialect that only the dispatcher `/ui` domain understands; the REST surface + mounts only the path form `/ui/view/:object/:type`, so the query form 404'd + wherever REST serves (e.g. project-scoped bases). The client now sends the + path form both surfaces accept; a URL-pinning test keeps it that way. + + REST route ledger updated: the two `mismatch` rows are resolved (packages + publish row is `server-only` publisher tooling; the ui row flips to `sdk`). + The ledger now carries zero mismatches. + +- bbd902d: feat(rest): unify request→environment resolution on the host's `kernel-resolver` seam — ADR-0076 D11 step ④ (#2462) + + The REST server kept its own parallel hostname/`X-Environment-Id` resolution + chain (duplicated inline in three places), while the HTTP dispatcher resolves + the same question through the host-injected ADR-0006 `kernel-resolver` seam — + so the same unscoped request could be attributed to different environments + depending on which HTTP surface served it. + + `RestApiPlugin` now adapts the host's `kernel-resolver` service (registered by + the cloud runtime next to `env-registry`; no cloud-side change needed) into a + new `RestRequestEnvResolver` seam, and `resolveRequestEnvironmentId` becomes + the single entry point every per-environment decision (protocol, i18n, + exec-ctx) flows through. Where a resolver is wired, its answer — including the + session-driven fallbacks the REST chain never had — is final; the legacy + built-in chain remains for OSS single-environment boots (no resolver + registered) and as the degradation path if the resolver throws. + +- 5ac93d4: feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431) + + #3413 (closes #3407) built the engine-level strip-observability channel + (`WriteObservabilityOptions.onFieldsDropped`) and wired the flow side + (`update_record` / `create_record` emit a step warning + `droppedFields`). The + **REST write path was never wired**, so an external API caller writing N fields + still got a bare `200 + record` when `readonly` (#2948) / `readonlyWhen` (#3042) + stripping meant `< N` actually landed — the same silent-success class #3407 + fixed flow-side, just on HTTP. The only way to notice was a per-field diff of + the returned row (which need not echo every field). This wires the channel + through the protocol → REST, on both write verbs. + + **Passthrough (metadata-protocol).** `updateData` now registers an + `onFieldsDropped` collector on `engine.update` and returns the events on the + response as `droppedFields`. `createData` surfaces the #3043 static-`readonly` + INGRESS strip too — that strip runs at the protocol ingress + (`stripReadonlyForInsert`), _before_ the engine, so it is recovered by diffing + the supplied payload against the stripped one (the engine's `onFieldsDropped` is + also wired for a future insert-side engine strip). A faulty listener never + breaks the write — the engine catches and logs. + + **Contract (spec).** `UpdateDataResponseSchema` / `CreateDataResponseSchema` + gain an **optional** `droppedFields: DroppedFieldsEvent[]` — present only when + ≥1 field was dropped. Optional + omit-when-empty keeps the response shape + backward-compatible for clients that only read `record`. + + **REST surface.** PATCH `/data/:object/:id` and POST `/data/:object` echo the + drops as an `X-ObjectStack-Dropped-Fields` response header + (`field;reason=` tokens, comma-joined — e.g. + `approval_status;reason=readonly`) and keep the structured `droppedFields` on + the body. **Status/success semantics are unchanged** (200 update / 201 create) — + a strip is legitimate semantics, not a failure (same principle as #3413). The + FLS write gate is untouched (it already fails closed with 403). + + Out of scope (issue #3431 D2 open questions, deferred): bulk + (`updateManyData` / `createManyData` / `batchData`) and GraphQL mutation wiring, + typed `@objectstack/client` warnings, and adding the header to the Hono CORS + `exposeHeaders` allow-list for cross-origin browser reads (the body + `droppedFields` is the cross-origin-safe channel meanwhile). + +- d318b24: feat: `security.getReadableFields` query surface for export column projection (#3547, #3391 follow-up) + + The REST export route projected its columns by inferring readability from the + first chunk of already-masked data rows (#3498). That has two known + compromises: a readable column whose first-chunk values are all null (and thus + omitted by the driver) drops out of the header, and an empty result set leaves + nothing to narrow. This adds the long-term-correct path. + + - **plugin-security** — the `security` service gains + `getReadableFields(object, context)`. It resolves the caller's permission + sets and builds the field-permission map with the SAME evaluator + + `requiredPermissions` fold the read middleware's `FieldMasker` uses (and the + same on-behalf-of delegator intersection, fail-closed on a dangling + delegator), then returns every schema field NOT masked non-readable — the + exact complement of what the mask deletes, so it can never drift from + data-plane FLS. Computed from schema + context, never from data rows: immune + to null values and empty result sets. A system context bypasses FLS; an + unresolvable schema returns `undefined` so callers fall back. + - **rest** — the `GET /data/:object/export` route asks the environment's + `security` service for `getReadableFields(object, context)` and projects the + schema-derived header to that set BEFORE streaming. When no security service + is reachable (no plugin-security / single-kernel without a provider) it + degrades to the existing masked-row inference, so there is zero regression. + Explicit `?fields=` requests are still honored verbatim. + + Contract-neutral: export columns already equal list's readable columns + (`export ⊆ list`, #3391); this makes the projection authoritative instead of + inferred. + +### Patch Changes + +- fa3d0cf: feat(spec): field runtime value-shape contract — ADR-0104 phase 1 (D1) + + `@objectstack/spec/data` now owns the runtime VALUE shape of every field type + (`field-value.zod.ts`): semantic type classes (`STRING_VALUE_TYPES`, + `NUMERIC_VALUE_TYPES`, `REFERENCE_VALUE_TYPES`, `FILE_REFERENCE_TYPES`, + `STRUCTURED_JSON_TYPES`, `MULTI_CAPABLE_TYPES`, …), the shared + `isMultiValueField`, and `valueSchemaFor(field, 'stored' | 'expanded')`. The + four consumers that each hand-copied this knowledge (objectql record-validator, + rest import-coerce, driver-sql column classification, qa conformance) now + derive from the spec, and the field-zoo round-trip MATRIX is asserted against + the contract so the two cannot drift. + + **Write-path change (objectql, warn-first):** previously-unvalidated types — + single `lookup`/`master_detail`/`user`/`tree`, `file`/`image`/`avatar`/ + `video`/`audio`, `location`, `address`, `composite`, `repeater`, `record`, + `vector` — are now checked against the contract. A violation **logs a warning + and passes** in this release (legacy rows must not strand their records); + set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` to enforce as a + `400 VALIDATION_FAILED`. The flip to strict-by-default rides a later minor + (ADR-0104 R1/R2). + + **Deprecations (removal rides the next spec major), FROM → TO:** + + - `CurrencyValueSchema` (`{value, currency}`) → none. A `currency` field's + value is a **bare number** everywhere in the runtime (validator, SQL `float` + column, import coercion, field-zoo oracle); the currency code lives in field + config. Use `valueSchemaFor({type: 'currency'})`. + - `LocationCoordinatesSchema` (`{latitude, longitude}`) → `LocationValueSchema` + (`{lat, lng}`) — the shape the platform actually stores. + - `AddressSchema` is **adopted** (unchanged) as the enforced `address` value + contract via `AddressValueSchema`. + + No stored data changes shape; the contract codifies deployed reality + ("reality wins", ADR-0104 D1). + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 1986594: feat(analytics): honour widget `dateGranularity`, `sortBy`/`sortOrder`, and `limit` in the dataset query (#3588) + + Three presentation options were accepted by the metadata layer and then dropped + by the analytics query builder. They reached no SQL, produced no error, and the + only way to notice was to read the `sql` a dataset response echoes — so a + dashboard could declare `dateGranularity: 'month'` and quietly render one bar + per record. + + - **`dateGranularity` now buckets.** `DatasetSelection` gained an optional + `dateGranularity`, applied to every selected `date` dimension. Precedence per + dimension: an explicit `timeDimensions` granularity, then the selection's, + then the dataset dimension's own default. A widget can bucket a trend by month + without the dataset committing every other consumer to that granularity. + - **`order` / `limit` / `offset` now apply on every path.** They are applied to + the ASSEMBLED grid — after measure-scoped sub-queries merge, after `compareTo` + columns attach, and after derived measures are computed — so a derived measure + is a valid sort key and the ObjectQL aggregate path (which has no ordering + grammar, and which native SQL hands every date-bucketed query to) orders + identically to native SQL. A single-query selection still pushes the window + down into the statement. An `order` key that names nothing the selection + projects is now rejected (400) rather than silently ignored. + - **`limit` is deterministic.** Without an `order`, a limit orders by the + selected dimensions first, so it truncates a reproducible window instead of an + arbitrary subset. + - **Widget `options` is a contract again.** The four query-affecting keys + (`dateGranularity`, `sortBy`, `sortOrder`, `limit`) plus `stageOrder` are + declared on `DashboardWidgetOptionsSchema`, so a typo like `sortDirection` is + an author-time error. The bag stays open — renderer extras (`icon`, `columns`, + `striped`, …) pass through untouched. + + Two latent bugs surfaced while fixing the above and are fixed here too: + + - `order`/`limit` were forwarded to EVERY sub-query. A measure-scoped + supplementary query selects one measure, so an inherited `ORDER BY` named a + column it never selected, and an inherited `LIMIT` truncated it before the + merge — dropping rows from the assembled grid. Nothing hit this only because + nothing passed `order`. + - The `compareTo` pass built its query by hand and skipped granularity + resolution, so a month-bucketed primary grid was merged against raw-timestamp + comparison rows. No dimension key matched and every `__compare` + column came back empty. + + `ObjectQLStrategy` now also echoes a representative `sql` (with `date_trunc`, + `WHERE`, `ORDER BY`, and `LIMIT`; filter values parameterized, never inlined). + Previously the `sql` field simply vanished from the response whenever a query + was date-bucketed, leaving an author unable to tell "not implemented" from "this + strategy doesn't report". + +- 3c8cfd1: fix(rest): make the API-exposure gate's metadata fail-open observable (#3545, #3391 follow-up) + + The object API-exposure gate (`apiEnabled` / `apiMethods`) fails OPEN when object + metadata can't be resolved, so a transient metadata outage doesn't 405 every + request. #3545 evaluated the residual risk of that path and confirmed it is + acceptable — the gate is a **surface-area control, not the authorization + boundary**: every request still passes auth and the ObjectQL security middleware + (CRUD / FLS / RLS) on the data call regardless of the gate's outcome, so a + fail-open can never bypass data authorization. + + The one gap was that the fail-open was **silent** — a persistent metadata fault + (store down / corrupt schema doc), during which the gate allows every operation + unchecked, looked identical to healthy operation. + + - **rest** `loadObjectItems` now LOGS a _thrown_ metadata read (a real fault) + while leaving a legitimately-empty registry (a cold-start `[]`) silent — so a + genuine outage is diagnosable without false alarms during normal startup. The + behavior is unchanged (still returns `[]` → gate abstains → data path + security + enforce). + - **runtime** `api-exposure.ts` records the #3545 tiered decision in its + contract doc: keep fail-open when the whole metadata service is unavailable + (failing closed would break the cold-start window for no security gain); the + narrow "object resolvable but its `enable` policy is present-yet-unreadable" + widen (unreachable through Zod-validated registration) is deferred to the + exposure-semantics window (#3543). + + No contract or behavior change to the gate itself — observability + decision + record only. + +- 1003125: feat(client): close the approvals (6) + record-shares (3) REST gaps (#3587 batch 3/5) + + `client.approvals` gains the full request lifecycle beyond approve/reject: + `recall` (submitter withdraw), `revise` / `resubmit` (ADR-0044 send-back + round-trip), and the thread interactions `remind` / `requestInfo` / `comment`. + New `client.shares` namespace for per-record sharing grants: `list` / `grant` / + `revoke` (204-safe) under `/data/:object/:id/shares`. REST route-ledger + ratchet: 26 → 17. + +- 6e62a93: feat(client): close the sharing-rules (5) + security-explain (2) + search (1) REST gaps (#3587 batch 4/5) + + New `client.shares.rules` sub-namespace for tenant-wide sharing rules + (M10.17): `list` / `save` / `get` / `delete` (204-safe, grants cascade) / + `evaluate` (reconcile). `client.security.explain` speaks the ADR-0090 D6 + access-explanation contract via the POST transport (the GET query form is the + same `ExplainRequestSchema`). Top-level `client.search` covers global + cross-object search (M10.5). REST route-ledger ratchet: 17 → 9. + +- ecda20c: feat(client): close the 8 reports-family REST gaps (#3587 batch 2/5) + + New `client.reports` namespace speaking the plugin-reports REST surface: + `list` / `save` / `get` / `delete` (schedules cascade), `run`, `schedule`, + `listSchedules`, `unschedule`. The two DELETE routes return 204 — the client + methods return `{ deleted: true }` without attempting to parse an empty body. + Fixed path (`/api/v1/reports` is not in `ApiRoutesSchema`), matching the + keys / share-links precedent. REST route-ledger ratchet: 34 → 26. + +- 6e62a93: feat(client): close the final 9 REST gaps — ratchet 9 → 0 (#3587 batch 5/5) + + `data.clone` (enable.clone duplication) and `data.export` (streaming + CSV/JSON/XLSX; returns the raw `Response` — a file stream, not a JSON + envelope). New `email.send` (IEmailService; branch on the returned `status`). + `analytics.queryDataset` speaks the ADR-0021 REST dataset-query dialect. New + `datasources.external.*` federation admin: `listTables` / `draft` / `import` / + `refreshCatalog` / `validate` (ADR-0015 Addendum, 503-degrading). Every REST + route is now either SDK-expressed or carries a reviewed non-sdk disposition — + the #3587 gap ratchet rests at ZERO. + +- fc968af: feat(client): close the 9 metadata-family REST gaps the #3587 ledger carried (#3587) + + New `meta` surface: `getDiagnostics` (spec-validation sweep), `getReferences` + (reverse references), `getBookTree` (ADR-0046 §6 spine resolution), `getAudit` + (ADR-0010 §3.6 protection trail), `publishItem` / `rollbackItem` / `diffItem` + (ADR-0033 per-item draft lifecycle). The two compound-name routes + (`GET|PUT /meta/:type/:section/:name`) turned out to be already expressible — + `getItem`/`saveItem` pass slashes through unencoded — so they are flipped to + `sdk` with URL-pinning tests instead of new methods (the audit note claiming + an encoding barrier was wrong; only `deleteItem` encodes). REST route-ledger + ratchet: 43 → 34. + +- ce1f100: fix(rest): export emits the projected header row on an empty result set (#3547) + + `GET /data/:object/export` wrote a zero-byte file whenever the query matched no + rows — the header was only ever written alongside the first data chunk. With the + `getReadableFields` column projection the readable column set is derived from + schema + context, so it is known even when no rows come back: an empty CSV/xlsx + export now carries the exact readable header, which also makes it a usable + import template. + + The header is emitted only when the column set is AUTHORITATIVE — the security + service's readable projection, or an explicit `?fields=` request. When the header + is schema-derived and the projection was unavailable, the export stays headerless + as before: the masked-row fallback has no rows to narrow with, and writing the + full schema header would name FLS-hidden columns. `header=false` still suppresses + the header in every case. + +- 81ce41a: feat(rest): `treatAsHistorical` import also preserves the original audit timeline (#3493) + + Follow-up to #3479/#3483. `treatAsHistorical` solved the FSM half — mid-lifecycle + rows are no longer rejected by `initialStates` — but the OTHER half of a historical + migration, preserving the original timeline, still didn't hold: an imported ticket + that closed in 2021 stored `updated_at` = the import day (and `updated_by` = the + importer), and a `writeMode: 'upsert'` refresh silently dropped business `readonly` + fields (`closed_at`, `resolved_by`). Reports, audit, and "recently modified" + sorting all came out wrong. + + Three layers were force-overwriting the timeline; all three now respect a single + new opt-in flag, `ExecutionContext.preserveAudit`, which `treatAsHistorical` sets + alongside `skipStateMachine`: + + - **spec**: `ExecutionContext.preserveAudit` (server-set only, never client-supplied) + and `DriverOptions.preserveAudit` (threaded to the driver's update stamp). + - **objectql** — the built-in audit hook (`plugin.ts`) now treats `updated_at` / + `updated_by` as CLIENT-PREFERRED (`?? now` / `?? userId`) under `preserveAudit`, + symmetric with how `created_at` / `created_by` already behave on insert; and the + static-`readonly` write strip (`stripReadonlyFields`) admits a WHITELIST — the + audit/timestamp family plus author-declared business `readonly` fields — so an + upsert refresh no longer drops them. + - **driver-sql** — the SQL `update` path keeps a supplied `updated_at` instead of + force-advancing it to `now` when `DriverOptions.preserveAudit` is set (fills-only- + empty, mirroring the insert stamp). + - **rest** — the import runner sets `preserveAudit` on the write context iff the + request opts into `treatAsHistorical`. + + Deliberately a WHITELIST, not the blanket `isSystem` exemption: platform-managed + `system` columns OUTSIDE the audit family (`organization_id` / tenancy, generated + columns) STAY stripped, so a historical import reinstates established facts without + becoming a backdoor to forge tenancy. Permissions / RLS / field-level security are + unaffected — this changes only which audit/readonly values the runtime overwrites, + never who may write the record. Fully opt-in: a normal write still auto-stamps + `updated_at`/`updated_by` and strips `readonly` exactly as before. The objectui + "Import as historical data" checkbox (objectui#2815) now drives both halves — no new + UI. + +- 85e1e4e: feat(rest): `treatAsHistorical` import option — skip the state machine for historical-data migration (#3479) + + Sibling of #3433 (seed exemption), one entry point over. #3165's `initialStates` enforced + the FSM entry point on every INSERT, so importing established historical facts — + a batch of already-`closed` tickets, `closed_won` deals, `completed` projects — + was rejected row-by-row with `invalid_initial_state`, blocking the core + data-migration path. Unlike the seed case it was visible (per-row errors), but it + still functionally blocked a legitimate use. + + - **spec**: `ExecutionContext.skipStateMachine` — a general, server-set flag (the + seed-specific `seedReplay`'s sibling) that skips the `state_machine` rule for a + write; `ImportRequestSchema.treatAsHistorical` (default `false`) — the user-facing + import option. + - **objectql**: the engine now skips the state machine for `seedReplay` OR + `skipStateMachine` (one helper), covering both seed replay and historical import. + - **rest**: the import runner sets `skipStateMachine` on the write context iff the + request opts into `treatAsHistorical`; default off, so a normal import still walks + the FSM (the strict behavior is the default). Import **undo** now also carries + `skipStateMachine`, since restoring a prior snapshot re-writes an earlier state + that need not be a legal transition from where the row is now. + - **platform-objects**: `sys_import_job.treat_as_historical` audit column (additive). + + Scope is identical to the seed exemption: ONLY the `state_machine` rule is skipped; + field shape, `format`, `cross_field`, `script` all still run. The objectui import + wizard checkbox is a separate follow-up. + +- 65ac468: fix(import): sanitize row errors — never leak raw SQL, map constraint failures to human wording (#3566) + + A failing import row surfaced the driver's raw error verbatim. When a write hit + a DB constraint (e.g. `sys_user.phone_number` is `unique`), the query builder + embeds the entire failing statement in `err.message`, and `toFailedResult` + handed that straight back — so the importer saw `` insert into `sys_user` +(...) values (...) - UNIQUE constraint failed: sys_user.phone_number ``. That is + both unreadable and an information disclosure of the schema. + + - `sanitizeRowError()` (import-runner) maps the common constraint failures — + SQLite / MySQL / Postgres `UNIQUE` and `NOT NULL` — to human wording + ("A record with this `` already exists.", "`` is required.") + and, as a backstop, never lets a message that still reads as a SQL statement + reach the client (it salvages the driver's trailing reason, or falls back to + a generic message). Already-friendly messages (e.g. better-auth's "User + already exists") pass through unchanged. Applies to every import path. + - `isLikelyEmail` now rejects non-ASCII addresses, so an address like + `x@柴仟.com` fails the import **dry-run** pre-check instead of passing client + and dry-run validation only to be rejected by better-auth's strict ASCII + validator at real-import time. + +- ef5e72d: fix(rest): undo of a historical import now preserves the audit timeline (#3549) + + A `treatAsHistorical` import writes with `preserveAudit` (#3493), keeping the + original `updated_at`/`updated_by` and business `readonly` fields instead of + stamping-now / stripping them. Its undo route, however, restored the captured + pre-import snapshot with a plain write context — so the audit auto-stamp + re-wrote `updated_at`/`updated_by` to "now", silently corrupting the very + timeline the historical import had preserved. + + The undo write context now mirrors the import's own: it carries + `preserveAudit` iff the job row is flagged `treat_as_historical`, so restoring + `u.before` re-writes the snapshotted audit/timestamp values verbatim. A normal + import's undo is unchanged (default stamp/strip). + +- 67452d1: feat(spec): resolve page metadata i18n — `page:header` title/subtitle (#3589) + + Custom system pages authored as metadata (Installed Apps, Cloud Connection, + Connect an Agent) hard-code their `page:header` copy in + `properties.title` / `properties.subtitle`. Every other metadata type is + localized at the REST boundary, but `page` was not: the `pages` namespace + existed only on `AppTranslationBundleSchema` — a schema no runtime reads — + with no resolver behind it, so those headers stayed English in every locale + while the matching nav labels translated correctly. + + - `TranslationDataSchema` (the shape the i18n service actually serves) gains a + `pages` namespace: `pages..{label,description,title,subtitle}`. + - New `translatePage` in `@objectstack/spec/system` translates a page's own + `label` / `description` and overlays `title` / `subtitle` onto every + `page:header` in the page's regions. Registered in + `translateMetadataDocument`, so it rides the existing read path. + - `page` added to the REST boundary's `TRANSLATABLE_META_TYPES`. Locale + extraction, the locale-keyed ETag, and `Vary: Accept-Language` already + covered every metadata type — no new plumbing. + - `objectstack i18n extract` now emits page entries, including the + `page:header` copy, so the new namespace is not invisible to the tooling. + - zh-CN / ja-JP / es-ES translations shipped for the three Setup pages, plus + the missing `nav_cloud_connection` / `nav_connect_agent` nav labels (these + existed only in zh-CN). + + Header copy is keyed by **page name**, not by component id: `page:header` + instances carry no stable id. `title` falls back to `pages..label`, since + a page's header title and its nav label are normally the same string. + + Authoring is unchanged and English literals stay in metadata as the fallback — + a page with no `pages` entry renders exactly as before. Consumers of + `@object-ui` need no change: pages arrive already localized from the server. + +- 3d5f726: feat(rest): route audit tranche 2 — the REST surface gets its own ledger + + conformance guard (#3587, follow-up to #3563) + + The dispatcher tranche closed its 27 gaps and guards them (#3569…#3579), but + `@objectstack/rest` mounts a second, larger surface the client also reaches — + 89 routes, never audited. `rest-route-ledger.ts` now records a reviewed + disposition for every one of them (38 sdk, 43 gap, 3 server-only, 3 public, + 2 mismatch), and the guard is real enumeration on both sources: RouteManager + routes via the `getRoutes()` introspection seam, and the two + RouteManager-bypassing registrars (`package-routes.ts`, + `external-datasource-routes.ts`) via captured mock-server registrations — no + pinned-by-hand list. The client half + (`rest-route-ledger-coverage.test.ts`) verifies every claimed method exists; + a 43-gap ratchet is wired into CI. Every guard direction was negative-tested. + + Notable dispositions the audit surfaced: `POST /api/v1/packages` is a + publish/install shape collision between REST and the dispatcher (REST + registers first and wins) — ledgered `mismatch`; the REST + `GET /ui/view/:object/:type` path dialect is unreachable by the SDK's + query-param dialect — ledgered `mismatch`; `service-storage` / + `service-i18n` mount a third route surface outside `@objectstack/rest`, + explicitly out of scope here and tracked under #3587. + + No behavior change — data + tests only, plus a scope-note refresh in the + runtime ledger pointing at the new REST ledger. + +- 1659072: feat(spec): publish `ISecurityService` — the `security` service surface becomes an enforced contract + + The `security` service registers seven cross-package methods (`getReadFilter`, + `getReadableFields`, `resolvePermissionSetNames`, `explain`, and the three + audience-binding suggestion calls) but had no contract in + `@objectstack/spec/contracts`. Consumers duck-typed it, and each one invented its + own fallback for a missing method or an "empty" answer — with more consumers + arriving, that is a drift surface. + + `ISecurityService` now documents the surface, and both ends are typed against it + so it is **enforced rather than declared**: `plugin-security` assigns its + registration to `ISecurityService` (a renamed, dropped, or re-typed method fails + that build), and the REST layer resolves the service as a `Partial` + (so call sites must keep feature-detecting instead of assuming the full surface). + + The contract makes explicit the one thing consumers cannot guess — that the + methods do **not** share a failure convention: + + - `getReadFilter` fails **CLOSED**: a resolution failure yields a deny filter + matching zero rows, never `undefined`. `undefined` means "no row restriction", + and nothing else. + - `getReadableFields` fails **SOFT**: `undefined` means "no answer, use your own + projection", while `[]` is authoritative and means "no field is readable" — + opposite instructions that a consumer must not conflate. + + Typing the producer immediately caught one real discrepancy, fixed here: + `getReadFilter` declared `Promise | null | undefined>` + while every return path yields a filter or `undefined` (`filter ?? undefined` + normalizes the null away). The dead `| null` is removed, so "no restriction" has + exactly one representation. Type-level only — no runtime behaviour changes. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + - @objectstack/service-package@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 5e15315640..d3e5ff7b01 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 3f351169df..372d9060e5 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,864 @@ # @objectstack/runtime +## 17.0.0-rc.0 + +### Minor Changes + +- af5a224: feat: enforce declared action-param contract at dispatch — ADR-0104 phase 2 (D2) + + An action's declared `params[]` (`type` / `required` / `multiple` / `options` / + `reference`) was a complete value contract that only ever informed the client + dialog — the server passed `reqBody.params` straight to the handler unvalidated + (REST `handleActions` and the MCP `invokeBusinessAction` path), and handlers + read an untyped bag. D2 makes the declaration enforced and typed. + + - **`@objectstack/spec/ui`** now exports `validateActionParams` (+ + `ResolvedActionParam`, `ActionParamIssue`, `ACTION_PARAM_BUILTIN_KEYS`): a + pure check that validates a params bag against resolved param declarations, + reusing the D1 `valueSchemaFor` so option membership, `multiple` arrays and + reference-id shape all ride the one value contract. Also exports the typed + authoring surface `ActionHandler` / `ActionHandlerContext` / + `ActionEngineFacade` — annotate a handler with `ActionHandler` instead of + `(ctx: any)`. + - **Dispatch (runtime)**: both the REST and MCP action paths resolve the + action's declared params (field-backed params resolved through the referenced + object field) and validate the request bag **before the handler runs** — + required presence, per-type value shape, and unknown keys (the dispatcher's + own `recordId` / `objectName` are allowlisted). + + **Warn-first rollout (ADR-0104 R3).** A violation is **logged and passes** by + default — params that were silently wrong before keep working while the drift + becomes visible. Set `OS_ACTION_PARAMS_STRICT_ENABLED=1` to reject with a + `400 VALIDATION` (REST) / an error (MCP). Actions that declare no `params` are + untouched (nothing to validate against). The flip to strict-by-default rides a + later minor once telemetry is quiet. + + Not included: file/image params becoming `sys_file` references — that depends + on file-as-reference (ADR-0104 D3). Per-name static typing of `ctx.params` from + the literal `params` array is a deferred DX nicety; the runtime guarantee holds + regardless. + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- 394b7a1: feat(job): honor the authored `retryPolicy` / `timeout` in the job scheduler (#3494) + + `JobSchema.retryPolicy` and `JobSchema.timeout` used to be parsed-but-ignored + (the 2026-06 liveness audit's aspirational-config cluster). They are now + enforced end to end — built rather than pruned, since retry/backoff and + per-run time limits are semantics job authors reasonably expect: + + - **spec**: `IJobService.schedule` gains an optional 4th `options` argument + (`JobScheduleOptions` with `retryPolicy` / `timeout`, mirroring the + authorable schema); new `JobRetryPolicy` type. Backward compatible — + existing 3-arg implementations and callers are unaffected. + - **service-job**: new `runWithPolicy` helper (exported, with + `JobTimeoutError`) wraps every handler invocation in `CronJobAdapter` and + `IntervalJobAdapter`; `DbJobAdapter` threads options through to its inner + adapters. Failed attempts (including timeouts) retry with exponential + backoff `backoffMs * backoffMultiplier^(retry-1)` up to `maxRetries`; + an attempt exceeding `timeout` is recorded with execution status + `'timeout'`. No `options` → exactly the legacy single-attempt behavior. + - **runtime**: declarative-jobs registration in AppPlugin forwards the + authored `retryPolicy` / `timeout` to the scheduler. + + Note: JavaScript cannot forcibly cancel an in-flight handler — a timed-out + attempt is abandoned, not killed. The retry delay caps only via the + multiplier arithmetic (no maxDelay knob yet). + + Refs #3494, #1878, #1893. + +- 8e08bc3: feat(runtime): `/ready` reports 503 when a data driver stops answering (#3756) + + `/health` returned `{status: 'ok'}` unconditionally and `/ready` only checked + whether the kernel state was `running` — a flag set once when bootstrap finishes + and never revisited. Neither probe touched the data layer. So a database that + went away _after_ boot (restart, failover, network policy change, pool exhausted, + credentials rotated) left both probes green: the load balancer kept routing to a + replica that failed 100% of its requests, and the orchestrator saw nothing wrong. + The driver's `checkHealth()` already existed and was cheap (`SELECT 1` / + `db.command({ping:1})`) but was only consumed by `datasource-admin`'s + `testConnection` — no probe path called it, and `ObjectQL` exposed no way to ask + (`drivers` is private with no accessor). + + This is the runtime-side half of #3741, which fixed only the boot-time version + of the same defect. + + - New `ObjectQL.checkDriversHealth({ timeoutMs })` pings every registered driver + and returns a `DriverHealth[]` verdict. Each probe is settled independently and + bounded (default 2s) — `checkHealth()` swallows its own errors, but on a dead + knex pool it does not return at all, waiting out `acquireConnectionTimeout` + (60s by default), and a probe that hangs is as useless as one that lies. A + driver implementing no `checkHealth()` is reported healthy: absence of a probe + is not evidence of failure. + - `GET /ready` now returns 503 with the failing driver names when the kernel is + running but a driver is down, on top of the existing booting/shutting-down + cases. The result is memoized for ~1s so Kubernetes' few-second polling does + not become one database round-trip per probe per replica. + - `GET /health` deliberately still checks nothing, and now says why in the code. + A failing _liveness_ probe restarts the pod, which cannot fix an unreachable + database but would put every replica into a restart storm for the length of the + outage. Readiness — leave the rotation — is the failure mode that helps. + + The readiness check **fails open**: a kernel with no data engine (lite kernels, + edge, metadata-only hosts), an engine predating `checkDriversHealth`, or a probe + that itself throws all read as ready, exactly as before. Readiness gates whether + a replica receives any traffic at all, so an inconclusive answer must not + black-hole a working deployment. Only a driver that positively reports itself + unhealthy takes the replica out. + + **Migration.** None. Deployments already wiring `/api/v1/ready` as their + readiness probe get the stricter check automatically; deployments that pointed a + _liveness_ probe at `/ready` should move it to `/health`, which is the endpoint + that never fails on a dependency. + +- 3216344: feat(runtime): extract the action-execution subsystem from the dispatcher — ADR-0076 D11 step ③, PR-8 (#2462) + + The 16-helper machinery behind server-registered business actions + (declaration collection/resolution, ADR-0104 param enforcement, the + permission/AI-exposure gates, the engine facade + session shape, invocation, + and the `callData` protocol/ObjectQL bridge — ~560 lines) moves to + `action-execution.ts`, depending only on the narrow `ActionExecutionDeps` + slice (resolveService + getObjectQL; NO env-resolution state). The + ADR-0104 warn-once statics ride along as module functions. The dispatcher + keeps four thin delegates with in-class callers; twelve internal-only + helpers are called directly on the module. This is the pre-cut that turns + the `/actions` and `/mcp` domain extractions into mechanical moves (PR-9). + Zero behavior change — runtime 649, http-conformance 41, dogfood 351 green. + +- f5bfac8: feat(runtime): extract the /actions and /mcp dispatcher domain bodies — ADR-0076 D11 step ③, PR-9 (#2462) + + The two deep-coupled domains ride the PR-8 action-execution subsystem out + of the dispatcher: `domains/actions.ts` (ADR-0066 D4 permission gate + + ADR-0104 param contract) and `domains/mcp.ts` (JSON-RPC transport, + `/mcp/skill` download, OAuth resource-metadata, the principal-bound tool + bridge). Env-resolution state stays behind two new deps seams — + `getDefaultEnvironmentId` and `resolveProjectKernelObjectQL` (the ADR-0006 + direct-caller kernel swap, side effect dispatcher-owned). The legacy + `/mcp/skill`-before-`/mcp` precedence is reproduced with ordered registry + entries incl. the `?` forms; the actions redundant trailing-slash regex + (the CodeQL polynomial-redos twin) is dropped for split+filter. The authz + identity pin for `buildMcpBridge(context)` follows the body to + `domains/mcp.ts`. Zero behavior change — runtime 649, http-conformance 41, + dogfood 351 green. + +- 6163393: feat(runtime): extract the /auth and /ai dispatcher domain bodies — ADR-0076 D11 step ③, PR-7 (#2462) + + `/auth` (better-auth service bridge + the browser-safe mock fallback for + MSW/test environments, with the local `randomUUID` wrapper moving alongside + its only consumer) and `/ai` (dispatch to the AI plugin's kernel-cached + route table with per-route auth-contract enforcement and actor threading) + move to `domains/`. `DomainHandlerDeps` grows two lazily-read members: + `isAuthRequired()` (the deployment's requireAuth posture — + construction-order safe) and `getRegisteredAiRoutes()`. `/mcp` was + deliberately excluded: `buildMcpBridge` couples to the action-execution + family (callData / actionPermissionError / invokeBusinessAction), so it + goes with the /actions /meta /data deep-coupling batch. Zero behavior + change — http-conformance (41) plus 5 new seam tests. + +- 688e9df: feat(runtime): extract the /automation dispatcher domain body — ADR-0076 D11 step ③, PR-6 (#2462) + + The automation bridge (flow CRUD, trigger/execute, runs history, + pause/resume — the ADR-0018/0019/0022 surfaces, ~260 lines) moves to + `domains/automation.ts` with zero new deps-contract growth. The route-order + subtlety is preserved verbatim: `/actions`, `/connectors` and `/_status` + keep their guard positions before the `/:name → getFlow` catch-all. Zero + behavior change — http-conformance (41) plus 3 new seam tests. + +- 8f124a7: feat(runtime): extract the first four dispatcher domain bodies into `domains/` modules — ADR-0076 D11 step ③, PR-2 (#2462) + + The `/analytics`, `/i18n`, `/notifications` and `/security` handler bodies + move out of the `HttpDispatcher` god class into per-domain modules under + `packages/runtime/src/domains/`, running against an explicit + `DomainHandlerDeps` contract (resolveService / getService / success / error — + the WHOLE dispatcher surface a domain may touch). The dispatcher keeps thin + `handleXxx` delegates for direct callers, and `/notifications` + `/security` + leave the legacy if-chain for the domain registry (new `match: 'segment'` + preserves their `=== p || startsWith(p + '/')` branch shape exactly). + + Route registration stays dispatcher-owned on purpose: most service slots are + multi-provider (i18n = I18nServicePlugin OR the AppPlugin in-memory fallback; + analytics = service-analytics OR the ObjectQLPlugin fallback), so a route is + the bridge to a SLOT, not the property of any one providing package. Zero + behavior change — http-conformance (41 cross-adapter assertions) and the + seam suite (18 tests) lock it. + +- 21ca1d5: feat(runtime): extract /keys, /storage and /ui dispatcher domain bodies — ADR-0076 D11 step ③, PR-3 (#2462) + + Continues the per-domain decomposition: three more handler bodies move out + of `HttpDispatcher` into `domains/keys.ts` (incl. the zero-tolerance + API-key-mint security contract), `domains/storage.ts` and `domains/ui.ts`, + running on the explicit `DomainHandlerDeps` contract (extended with + `getObjectQL` for the data-plane domains). The `/keys` legacy branch's + `'/keys?'` query-string form is reproduced with a second registry entry; + storage drops its strictly-redundant `kernel.services` index-access fallback + (dead under Map-shaped services, duplicate under object-shaped ones). Thin + `handleXxx` delegates remain for direct callers. Zero behavior change — + locked by the 41-assertion http-conformance suite and 6 new seam tests. + +- 03b11e8: feat(runtime): thin domain-handler registry seam in the HTTP dispatcher — ADR-0076 D11 step ③, PR-1 (#2462) + + `dispatch()` routed every domain through one hand-written + `if (cleanPath.startsWith('/xxx'))` chain — the "god implementation on a clean + port" shape ADR-0076 D11 calls out. This lands the decomposition seam: a + first-match `DomainHandlerRegistry` consulted before the legacy chain, plus a + public `HttpDispatcher.registerDomainHandler()` so follow-up PRs can hand each + domain's normalized handler to its owning service package. + + Migration discipline is "registry first, code moves later, ownership last": + this PR only wraps four existing branches (`/health`, `/ready`, `/analytics`, + `/i18n` — three shapes: no-service probe, service bridge, optional-service 501) into registry entries with faithful legacy matching semantics. Zero + behavior change, locked by the 41-assertion http-conformance cross-adapter + suite and 11 new seam tests. + +- 8891f93: feat(runtime): extract the /meta and /data dispatcher domain bodies — ADR-0076 D11 step ③, PR-10, the terminal cut (#2462) + + The last two domains leave the dispatcher: `domains/meta.ts` (metadata + read/write incl. ADR-0033 draft-aware protocol paths, ADR-0046 doc slimming + riding along with its exclusive `slimDocList` helper) and `domains/data.ts` + (CRUD/query over the action-execution `callData` bridge; the multi-tenant + unresolved-environment 428 now keys off a semantic `isMultiTenantHost()` + deps member instead of poking `kernelResolver`). **The dispatch() if-chain + is now EMPTY of domains** — 18 domains resolve through the registry, and + `createHonoApp`'s catch-all is ready for retirement (step ① of #2462). + Zero behavior change — runtime 649, http-conformance 41, dogfood 351 green. + +- d729a31: feat(runtime): extract the /packages dispatcher domain body — ADR-0076 D11 step ③, PR-5 (#2462) + + The largest domain so far (~680 lines: the handler plus its two exclusive + helpers `assemblePackageManifest` and `applyPublishedSeeds`) moves to + `domains/packages.ts` — list/install/enable/disable, ADR-0033 draft + publish/discard, ADR-0067 commit history & rollback, ADR-0070 export / + orphan adoption / duplicate, delete. `DomainHandlerDeps` grows the shared + facilities the body needs: `errorFromThrown` (field-anchored 422s), + `resolveActiveOrganizationId` (session org), `announceKernelEvent` + (`metadata:reloaded` after publish), and an optional `logger`. The step-② + (#3142) single-pipeline behavior is preserved. Zero behavior change — + http-conformance (41) plus 4 new seam tests (incl. the 409 + duplicate-install guard). + +- cb8322e: feat(runtime): extract the /share-links dispatcher domain body — ADR-0076 D11 step ③, PR-4 (#2462) + + The share-link capability-token surface (ADR-0047) moves out of + `HttpDispatcher` into `domains/share-links.ts`. This is cloud's designed + primary surface for per-env kernels (`registerShareLinkRoutes: false`, host + dispatcher serves after kernel swap — the #2462 step-① re-scope finding), so + the handler keeps working from the registry exactly as from the if-chain. + `DomainHandlerDeps` grows `getRequestKernelService` (reads off the + per-request RESOLVED kernel — the engine the shareLinks service is bound to) + and `routeNotFound` (the shared 404 envelope). Zero behavior change — locked + by http-conformance (41) and 5 new seam tests incl. token-resolve redaction. + +### Patch Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 6877e9a: test(client,runtime): the last wildcard was wrong evidence, not weak — AI ratchet 3 → 0 (#3718) + + The capstone (#3642) ratcheted "matched only by a `**` family" as weaker + evidence, to be driven down by enumerating each dynamic family. 60 → 3 after + #3656. The last 3 were `ai.nlq` / `ai.suggest` / `ai.insights` on `* /ai/**`. + + Enumerating that family (in `cloud`, where `service-ai` lives) showed the + wildcard had not been weak evidence but **wrong** evidence. `buildAIRoutes()` + mounts 12 routes — `chat`, `chat/stream`, `complete`, `models`, `status`, + `effective-model`, six `conversations` — and **none** is `/nlq`, `/suggest` or + `/insights`. The SDK's entire AI namespace is dead, the entire real AI surface + is unexpressed by the SDK, and the two sets are disjoint (#3718). + + The old row's note even claimed the client "expresses nlq/suggest/insights + against the REST AI routes". That was never verified and is false: + `DEFAULT_AI_ROUTES` declares them but has no runtime consumer (only the spec's + own test reads it), and `aiNlq?`/`aiSuggest?`/`aiInsights?` are optional + protocol methods nothing implements. + + `/api/v1/ai/` becomes a bounded prefix exemption alongside the control plane — + two cross-repo surfaces, both ledgered in `cloud` — and the wildcard-only + assertion becomes `toBe(0)`, not a ratchet: every matched call now rests on an + exact enumerated route. Mutation-checked in both directions (removing the + exemption re-exposes exactly the 3, and the pre-change count was verified to be + exactly those 3 and nothing else). + + Test-and-comment changes only; no runtime behaviour is affected. + +- 0bab8bb: fix(client,runtime): analytics.meta/explain now call routes that actually exist (#3584) + + The route audit (#3563) ledgered four dispatcher↔client shape mismatches. + Re-verification showed the two analytics shapes the client spoke — + `GET /analytics/meta/:cube` and `POST /analytics/explain` — were served by + **nothing**: not the dispatcher, not `@objectstack/rest`, not + `service-analytics`. Both methods 404ed against every deployment. + + - `analytics.meta(cube?)` — FROM `GET /analytics/meta/:cube` TO + `GET /analytics/meta[?cube=]`. The cube argument is now optional; when + given, the dispatcher threads it into `AnalyticsService.getMeta(cubeName?)`, + which always supported the filter. Responses now use the dispatcher envelope + (`{ success, data }`). + - `analytics.explain(payload)` — FROM `POST /analytics/explain` TO + `POST /analytics/sql` (the dispatcher's SQL dry-run route, backed by + `generateSql`). Method name unchanged. + + No migration is expected in practice: a method that unconditionally 404ed can + have no working callers (none exist in objectstack or objectui). Anyone who + had hand-rolled fetches against the imaginary shapes should switch to the + routes above. + + The two storage rows from the same audit are deliberately NOT reshaped: the + presigned/chunked protocol the SDK speaks is registered autonomously by + `service-storage` on any http-server and stays canonical; the dispatcher's + bare `POST /storage/upload` / `GET /storage/file/:id` are reclassified in the + route ledger as a `server-only` low-level compat surface. + +- 3c8cfd1: fix(rest): make the API-exposure gate's metadata fail-open observable (#3545, #3391 follow-up) + + The object API-exposure gate (`apiEnabled` / `apiMethods`) fails OPEN when object + metadata can't be resolved, so a transient metadata outage doesn't 405 every + request. #3545 evaluated the residual risk of that path and confirmed it is + acceptable — the gate is a **surface-area control, not the authorization + boundary**: every request still passes auth and the ObjectQL security middleware + (CRUD / FLS / RLS) on the data call regardless of the gate's outcome, so a + fail-open can never bypass data authorization. + + The one gap was that the fail-open was **silent** — a persistent metadata fault + (store down / corrupt schema doc), during which the gate allows every operation + unchecked, looked identical to healthy operation. + + - **rest** `loadObjectItems` now LOGS a _thrown_ metadata read (a real fault) + while leaving a legitimately-empty registry (a cold-start `[]`) silent — so a + genuine outage is diagnosable without false alarms during normal startup. The + behavior is unchanged (still returns `[]` → gate abstains → data path + security + enforce). + - **runtime** `api-exposure.ts` records the #3545 tiered decision in its + contract doc: keep fail-open when the whole metadata service is unavailable + (failing closed would break the cold-start window for no security gain); the + narrow "object resolvable but its `enable` policy is present-yet-unreadable" + widen (unreachable through Zod-validated registration) is deferred to the + exposure-semantics window (#3543). + + No contract or behavior change to the gate itself — observability + decision + record only. + +- d3f2ff6: feat(client): `actions` surface — the SDK path to server-registered actions (#3563 PR-2) + + `client.actions.invoke(object, action, { recordId, params })` and + `client.actions.invokeGlobal(action, opts)` dispatch handlers registered via + `engine.registerAction` (`POST /api/v1/actions/...`). This closes the largest + gap in the #3563 route audit: the whole `/actions` domain — the documented way + to expose custom server-side operations — was unreachable from the SDK, and + every console hand-rolled `fetch` for it. The record id travels in the body, + which both server URL shapes honor; the handler's own business failure comes + back as `{ success: false, error }` rather than a thrown exception. + + The route ledger flips all three `/actions` rows to `sdk` and the gap ratchet + drops 27 → 24. Also takes the documentation-drift findings from the audit: + the client README no longer documents six methods that do not exist, + `CLIENT_SPEC_COMPLIANCE.md` is retired to a tombstone pointing at the + CI-enforced ledger (its "FULLY COMPLIANT" verdict was measured against a + route table nothing consumes), and the docs-site SDK page documents the new + surface. + +- b7550d6: feat(client): `keys`, `shareLinks`, and `security` surfaces (#3563 PR-3) + + Three more domains the route audit found with zero SDK expression: + + - `client.keys.create({ name?, expiresAt? })` — mints a `sys_api_key` + (`POST /api/v1/keys`). The raw secret comes back exactly once; `user_id` + is pinned server-side. There was previously no SDK path to create an API + key at all. + - `client.shareLinks.create / list / revoke` — authenticated management of + record share links. Listing is server-constrained to the caller's own + links; the public token-consumption routes stay browser-only by design. + - `client.security.suggestedBindings.list / confirm / dismiss` — the + ADR-0090 admin surface for package audience-binding suggestions. + + The route ledger flips all seven rows to `sdk` and the gap ratchet drops + 24 → 17. + +- 0164f40: feat(client): the final six route-audit gaps — meta drafts/published/FSM + automation descriptors (#3563 PR-5) + + - `meta.getPublished(type, name)` — the published version of a metadata item + (ADR-0033; compound names pass through unencoded, matching `getItem`). + - `meta.listDrafts({ packageId?, type? })` — pending drafts the active-only + lists hide. + - `meta.getLegalNextStates(object, field, from?)` — ADR-0020 FSM + introspection ("from here, where can this record go?"). + - `automation.listActions({ paradigm?, source?, category? })` / + `automation.listConnectors({ type? })` — the ADR-0018/0022 descriptor + registries backing the Studio designer's pickers. + - `automation.getRuntimeStatus()` — per-flow enabled/bound engine state. + + With these, the #3563 gap ratchet reaches **0** (from 27): every dispatcher + route that should be SDK-expressible is, and the conformance guard keeps it + that way. + +- e295ad1: feat(client): the eleven package-lifecycle methods (#3563 PR-4) + + `client.packages` grows from install/enable to the full lifecycle the server + has shipped for three ADR generations: `update` (manifest edit), + `publish`, `publishDrafts` / `discardDrafts` (ADR-0033 whole-app draft + promotion), `listCommits` / `revertCommit` / `rollback` (ADR-0067 commit + timeline), `revert`, `export`, `adoptOrphans`, `duplicate` (ADR-0070 + portability). All eleven routes existed with no SDK expression — Studio + reached them via raw fetch. + + The route ledger flips all eleven rows to `sdk` and the gap ratchet drops + 17 → 6 (from 27 at the start of the audit). + +- 7180ed5: fix(security): fail closed when an object's security posture can't be resolved + (#3545) + + #3545 accepted the API-exposure gate's fail-open on unresolvable metadata on one + load-bearing premise: that gate is a SURFACE-AREA control, while the real + authorization boundary — auth + the ObjectQL security middleware (CRUD/FLS/RLS) + — enforces unconditionally on the data call whatever the gate answers. + + Verifying that premise rather than assuming it shows it did not hold. The + middleware does run unconditionally, but two of its INPUTS were read from the + same object metadata and defaulted permissively when it could not be resolved, + so the very trigger the issue is about reached one layer PAST the gate, into the + boundary itself: an unresolved `access.default` read as PUBLIC (so a plain `'*'` + wildcard covered an object ADR-0066 D2 excludes from it) and an unresolved + `requiredPermissions` read as NO CONTRACT (so the D3 capability AND-gate was + skipped entirely). + + `getObjectSecurityMeta` now flags `unresolved`, and the three consumers that turn + posture into an access decision fail closed on it: the middleware denies (with an + error log, so a persistent metadata outage is observable rather than a silent + blanket-allow), `canExport` denies, and `getReadableFields` exposes no columns — + the same stance already taken for a permission-resolution failure and a dangling + delegator. `computeLayeredRlsFilter` keeps consuming the defaults deliberately: + there the permissive value WITHHOLDS the cross-tenant exemption, so it is already + the closed direction. + + Blast radius is bounded to the risky case. System/boot writes (`isSystem`) and + principal-less/anonymous contexts short-circuit earlier in the middleware, so + reaching the new check means an authenticated principal with resolved grants + asking for an object whose declaration is missing; the cold-start window is + served by those short-circuits, not by the permissive default. The exposure + gate's own tiered decision (transient unavailability → fail open) is therefore + unchanged — it now rests on a boundary that actually holds. + + The explain engine reports the denial on its existing `object_crud` layer naming + the real cause, so the "why am I denied?" surface cannot drift from enforcement. + +- 083c414: fix(runtime): replace the polynomial-redos trailing-slash regex in the notifications domain with split+filter (CodeQL high, surfaced by #3507) + + The legacy `path.replace(/\/+$/, '')` in the notifications handler had + carried a polynomial-backtracking regex over request-controlled input since + ADR-0030; the domain extraction (#3507) made the line "changed code" and + CodeQL flagged it. Same split+filter treatment the security domain already + uses for the identical pattern. Redundant slashes in the sub-path now + collapse (`//read//` → `read`), matching the security domain's semantics. + +- 3d5f726: feat(rest): route audit tranche 2 — the REST surface gets its own ledger + + conformance guard (#3587, follow-up to #3563) + + The dispatcher tranche closed its 27 gaps and guards them (#3569…#3579), but + `@objectstack/rest` mounts a second, larger surface the client also reaches — + 89 routes, never audited. `rest-route-ledger.ts` now records a reviewed + disposition for every one of them (38 sdk, 43 gap, 3 server-only, 3 public, + 2 mismatch), and the guard is real enumeration on both sources: RouteManager + routes via the `getRoutes()` introspection seam, and the two + RouteManager-bypassing registrars (`package-routes.ts`, + `external-datasource-routes.ts`) via captured mock-server registrations — no + pinned-by-hand list. The client half + (`rest-route-ledger-coverage.test.ts`) verifies every claimed method exists; + a 43-gap ratchet is wired into CI. Every guard direction was negative-tested. + + Notable dispositions the audit surfaced: `POST /api/v1/packages` is a + publish/install shape collision between REST and the dispatcher (REST + registers first and wins) — ledgered `mismatch`; the REST + `GET /ui/view/:object/:type` path dialect is unreachable by the SDK's + query-param dialect — ledgered `mismatch`; `service-storage` / + `service-i18n` mount a third route surface outside `@objectstack/rest`, + explicitly out of scope here and tracked under #3587. + + No behavior change — data + tests only, plus a scope-note refresh in the + runtime ledger pointing at the new REST ledger. + +- 48d5a1c: Route ledger + conformance guard for the dispatcher↔client surface (#3563) + + #3528's root-cause class — a route that exists and works while + `@objectstack/client` has no way to express it — now has an inventory and a + ratchet. `route-ledger.ts` records the audited disposition of every dispatcher + route (sdk / gap / server-only / public / dynamic / mismatch); + The guard is split along the package boundary (a runtime→client edge is a + build cycle): runtime's `route-ledger.conformance.test.ts` fails when a + dispatcher domain lands with no ledger entry and ratchets the audited gap + count (27 at PR-1); client's `route-ledger-coverage.test.ts` fails when a + ledger entry claims a client method that doesn't exist. Findings and follow-up slicing live + in `docs/audits/2026-07-dispatcher-client-route-coverage.md`. No runtime + behavior change. + +- 810a3a2: fix(runtime,cloud-connection): multi-tenant seed replay covers every source, not just the first (#3453) + + In multi-tenant deployments (enterprise `@objectstack/organizations`) a brand-new org + gets its own private copy of demo data by replaying the kernel's `seed-datasets` list + on the `sys_organization` insert. That list is meant to hold the union of every seed + source — every config-declared app AND every marketplace package — but two framework + traps (the same pair #3444 fixed for seed-summary) shrank it to just the first source: + + - The standard `PluginContext` exposes `getService`/`registerService` but has NO + `.kernel` handle, so `(ctx as any).kernel?.getService('seed-datasets')` always read + `undefined`. Each source then saw "nothing registered" and overwrote the list with + only its own datasets instead of extending it. + - `registerService` throws on a duplicate name, so the second source's re-register was + swallowed by the surrounding try/catch — its datasets (and, for a config app, its + replayer) silently lost. + + Net effect: with two config apps, or a config app plus marketplace packages, a new org + replayed only the first app's seeds. + + The fix mirrors #3444's seed-summary hardening: `seed-datasets` is now a single shared + array, registered once and mutated in place by every source through a new + `mergeSeedDatasets` helper that reads via the context's own resolver first. AppPlugin's + per-org replayer reads that live list at invoke time instead of a captured snapshot, so + it replays the full union — including datasets merged after its closure was built — and + the replayer itself is registered once and reused by later config apps. + + Covered by seam-level unit tests (accumulation across app + marketplace sources; the + replayer reads the live union). True multi-tenant end-to-end coverage requires the + enterprise `@objectstack/organizations` plugin, which lives in the cloud repo. + +- 9981c1d: Surface seed outcomes in the `os dev` / `os serve` boot banner (#3415). Seeds run inside the boot-quiet stdout window and SeedLoader's logs sit under the default warn level, so a fixture could silently lose most of its rows — the showcase shipped 1 of 5 projects with zero terminal signal. AppPlugin now stashes the per-boot seed counters on the kernel (`seed-summary` service) and the banner prints `Seeds: X inserted · Y updated · Z skipped`, escalating to a yellow `⚠ … N REJECTED` line when records were dropped. +- d60968c: Surface marketplace rehydrate/heal seed outcomes in the `os dev` / `os serve` boot banner (#3430), extending the config-app Seeds line from #3415. + + The seed pipeline's most useful result lines are all `logger.info`, but `os dev` forwards a default `warn` level and the serve boot-quiet window swallows stdout — so "marketplace package rehydrated onto a fresh DB with 0 rows", a fresh-DB self-heal, and row-level seed failures were all invisible unless you queried the database directly. + + The `seed-summary` kernel service is now a per-source list. AppPlugin (config apps) and the marketplace rehydrate/heal path each contribute a labelled entry, and the banner prints one combined line that ignores the log level: + + ``` + Seeds: showcase 162 rows · hotcrm(marketplace) 157 ok / 5 errors ⚠ + ``` + + Fresh-DB heals are marked `(healed on fresh db)`; a marketplace package that installed with seed datasets but landed 0 rows, and any run that dropped records, escalate to a yellow `⚠` line instead of passing silently. + +- e231abb: feat(objectql,metadata-protocol)!: single-source the protocol assembly; drop objectql's protocol re-exports — ADR-0076 Step 2 PR-C (#2462) + + The ONE assembly now lives in `@objectstack/metadata-protocol` as + `assembleMetadataProtocol()` — `createMetadataProtocolPlugin()` (delegated + mode, cloud) and `ObjectQLPlugin`'s built-in convenience mode + (`registerProtocol !== false`, single-kernel/dev boots) both mount the same + code path (~112 inline lines deleted from the engine plugin). objectql's six + protocol re-exports (`ObjectStackProtocolImplementation`, + `SysMetadataRepository`, `SeedLoaderService`, `runBuildProbes` + types) are + removed — import them from `@objectstack/metadata-protocol` directly + (breaking, shipped as minor per the launch-window convention; the only known + importers were five test files, repointed). Scope note vs the original Step-2 + recipe: the objectql→metadata-protocol dependency is deliberately KEPT for + the convenience mount — `@objectstack/objectql/core` was already + protocol-free, and forcing 20 framework boot sites to mount two plugins buys + no runtime win. "Zero protocol dependency" lands as "zero assembly ownership, + single source". + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [735f850] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [96242ef] +- Updated dependencies [984396b] +- Updated dependencies [d0fea33] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [ce1f100] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [307e0fe] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [ef5e72d] +- Updated dependencies [dac6a08] +- Updated dependencies [313d7be] +- Updated dependencies [5faeac6] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [7180ed5] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [4e9e184] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [8e08bc3] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [bbd902d] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [f1a8114] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [4c5a584] +- Updated dependencies [0c302a7] +- Updated dependencies [bd68f08] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [a629074] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/rest@17.0.0-rc.0 + - @objectstack/driver-sql@17.0.0-rc.0 + - @objectstack/plugin-auth@17.0.0-rc.0 + - @objectstack/plugin-security@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + - @objectstack/metadata-protocol@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + - @objectstack/metadata@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + - @objectstack/service-i18n@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + - @objectstack/driver-memory@17.0.0-rc.0 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.0 + - @objectstack/service-cluster@17.0.0-rc.0 + - @objectstack/service-datasource@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 56b7e62a6e..0976430609 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 8ba5341b1b..52a9f19282 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.0.0-rc.0 + ## 16.1.0 ## 16.0.0 diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 4c09de1c65..c46dbb71c7 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 0b641c7d70..987be99850 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,429 @@ # Changelog — @objectstack/service-analytics +## 17.0.0-rc.0 + +### Minor Changes + +- 587fc91: feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt + + The analytics→engine bridge now forwards the request's `ExecutionContext` to + `engine.aggregate`, so the engine's own middleware chain scopes analytics reads + independently of the analytics layer's `getReadScope`. + + **Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing + forced the bridge to pass it — and it did not. An authenticated aggregate + reached the engine with no principal, plugin-security's principal-less fall-open + skipped its RLS injection, and the only thing left scoping the query was the + strategy remembering to call `getReadScope`. #3597 was a strategy that did not, + and both belts were off at once. + + `getReadScope` stays: the two resolve scope through different paths (engine + middleware vs `security.getReadFilter`), and a deployment without + plugin-security has only the analytics layer. This is depth, not a replacement. + + - `StrategyContext` gains `context?: ExecutionContext`, bound per call by + `AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`. + - `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` / + `AnalyticsService` `executeAggregate` config options gain `context?: +ExecutionContext`. **Custom bridges should forward it** to their engine; the + built-in auto-bridge does. Purely additive — an existing bridge that ignores + it keeps working exactly as before. + - `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain + an optional trailing `context`, beside the `scope` / `resolveScope` that + #3639 added — the same two-belt split as the aggregate path. + - `BootOptions.analytics` (`@objectstack/verify`) overrides the + AnalyticsServicePlugin instance, so a gate can boot with the analytics belt + off and assert the engine-side belt alone still scopes. + + **Also fixed on the same seam:** + + - `fetchRecordLabels` — the dimension display-label lookup — is row-granular + (one row per record, real display names). #3639 gave it the analytics-layer + belt (the referenced object's own read scope); it now also carries the + context, so the engine scopes the same read independently. + - `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the + `/analytics/sql` preview read as an unscoped table scan while the real + aggregate was scoped. It now renders the caller's filters and the read scope. + The preview never executed, so this was misleading output rather than a leak. + +- fc5f126: feat(analytics): serve in-envelope cross-object grouping on the ObjectQL path by FK-expand (#3654) + + `engine.aggregate()` cannot join, so the ObjectQL fallback path (date-granularity + bucketing, in-memory driver, federated objects) previously REJECTED any + cross-object grouping like `revenue by account.region` (#3664 stopgap — a loud + error instead of the earlier silent `(null)` mis-bucket). It now SERVES the + common case directly. + + For a single-hop cross-object DIMENSION with recombinable measures, the strategy: + + 1. groups the base aggregate on the lookup FK column (`account`) — which the + engine can do — scoped to the base object; + 2. resolves each FK id to the related attribute (`region`) with a read of the + referenced object **scoped to that object's own RLS**; then + 3. re-buckets by the resolved attribute in memory, recombining the measures + (sum/count add; min/max take the extremum). + + A base row whose referenced record the caller cannot read buckets under an + explicit `(restricted)` group: its measure still counts (grand totals are + preserved) but the hidden record's attribute never appears — no leak (ADR-0021 + D-C, the #3602 class). `/analytics/sql` renders the equivalent `LEFT JOIN`. + + Deliberately bounded — still REJECTED (loud, never silently wrong): cross-object + references in a MEASURE or FILTER (need a real join to evaluate), multi-hop + dimensions (`a.b.c`), and non-recombinable measures (`avg`, `count_distinct`) + with a cross-object dimension. Cross-object queries on `NativeSQLStrategy` (the + normal SQL path) are unchanged — it hand-compiles the joins. + +### Patch Changes + +- 7101ca2: fix(analytics): apply the EFFECTIVE date granularity to bucket labels and drill ranges (#3588 follow-up) + + `selection.dateGranularity` (shipped in #3652) reached the `GROUP BY` but not the + post-processing: the bucket-label formatter and the drill-range inverter both + kept reading the DATASET dimension's default. A query was grouped one way and + described another. Found by driving a real dashboard query in a browser against + a dataset whose dimension declares `dateGranularity: 'month'`: + + - selection `year` → the row came back labelled **`1970-01`** — a year bucket + re-formatted with the dataset's month granularity, its `"2026"` key re-read as + 2026 _milliseconds_ past the epoch; + - selection `day` → day buckets were re-labelled as months, so ten distinct days + collapsed into two duplicated keys; + - selection `quarter` / `year` / `day` / `week` → `drillRanges` came back empty, + silently removing drill-through from every bucketed chart. + + Granularity precedence now lives in one exported function, + `resolveDimensionGranularity`, called from all three sites that must agree — the + query's `GROUP BY`, the label formatter, and the range inverter. The drift was + possible only because each site resolved it independently. + + Two consequences beyond the override case: + + - A dataset dimension that declares **no** granularity but is bucketed by the + widget now gets drill ranges too. Previously the range sidecar keyed off the + dataset's own `dateGranularity`, so this case — the one #3588 is actually + about — could never drill. + - `formatDateBucket` no longer mistakes a bare year key for an epoch timestamp. + A year bucket's canonical key IS `"2026"`, which is the only bucket key that + collides with the pure-digit epoch heuristic (`"2026-Q2"`, `"2026-07"` and + `"2026-07-15"` all fail it). Being idempotent over already-formatted keys is + that function's stated contract; the year case just never held. + +- 415254c: fix(analytics): scope the dimension-label lookup to the referenced object's RLS (#3602) + + When a dataset groups by a `lookup`/`master_detail` dimension, analytics resolves + the grouped FK ids to the related record's display name via a per-record read + (`group by id`) dressed as an aggregate. That read carried **no read scope**, so + it revealed related-record display names whenever the referenced object's RLS is + stricter than the base object whose rows carry the id — a user could see a name + the referenced object's own RLS would hide. (Same-object and looser-referenced + cases were already safe because the ids come from the post-#3597 scoped + aggregate; this closes the stricter-referenced case.) + + The label lookup now applies the **referenced object's own** read scope — bound + to the request via the same `getReadScope` provider the aggregate path uses, + composed with `$and` (never key-merge) so it can't be displaced by the id + predicate. Fail-closed: if that object's scope can't be resolved, the dimension's + labels are skipped (the raw id renders) rather than fetched unscoped. No behaviour + change when no read-scope provider is configured. + + Internal `DimensionLabelDeps.fetchRecordLabels` gains an optional `scope` argument + and `resolveDimensionLabels` an optional `resolveScope` resolver; both are + service-analytics-internal (no spec/contract change). + +- 1f8390b: fix(analytics): ObjectQLStrategy now enforces the read scope (RLS + tenant) (#3597) + + `ObjectQLStrategy` never consumed `getReadScope`, so any analytics query served by + that path ran with **no RLS or tenant predicate** — an authenticated caller + received aggregates computed over every tenant's rows. + + Both belts were off at once. The strategy dropped the pre-resolved read scope, and + the engine could not compensate: the `executeAggregate` bridge passes no + `ExecutionContext`, so plugin-security's principal-less fall-open skipped its own + RLS injection. Only `NativeSQLStrategy` was ever wired for ADR-0021 D-C. + + The exposure was **not** limited to exotic drivers. `NativeSQLStrategy` declines — + handing the query to this path — on any date-bucketed query + (`timeDimensions[].granularity`, the most common dashboard shape, on Postgres and + SQLite too), on `RAW_SQL_UNSUPPORTED` (in-memory driver), and on federated objects. + + The scope is composed with `$and`, never by key merge, so a caller filter naming + the same field (e.g. `organization_id`) cannot displace the security predicate. + + **Behaviour change to be aware of:** a query that references a **joined** object + carrying its own read scope is now REJECTED on this path rather than run + partially-scoped. `engine.aggregate`'s `where` addresses the base object, so a + per-join predicate cannot be expressed there; failing closed matches the posture + already taken by `resolveReadScopes` and `compileScopedFilterToSql`. Such a query + previously returned results that omitted the joined object's tenant predicate. + Run it on a native-SQL driver (`NativeSQLStrategy` scopes each join), or drop the + cross-object dimension/measure. + + Deployments with no read-scope provider configured are unaffected — that path + stays unscoped by documented contract. + +- 3167e29: fix(analytics): sort dataset selections by the display label for select/lookup dimensions (#3680) + + `DatasetSelection.order` (what a widget's `options.sortBy` lowers to) sorted a + `select` or `lookup`/`master_detail` dimension by its STORED value — the option + value or the foreign-key id — while the response rows carry the resolved display + label. A "sort by Account" therefore ordered by opaque ids and read as arbitrary; + a localized select sorted by its ASCII value while showing a non-ASCII label. + + Order keys naming a label-bearing dimension now sort by the display label the + user reads. The executor receives an injected sort-key hook (`OrderLabelResolver`, + built by `queryDataset` over the same label-resolution capabilities and #3602 + read scoping as the display pass); only the COMPARISON substitutes the label — + rows keep their raw values until the display pass, so drill metadata still + snapshots stored values, and ordering + windowing stay one adjacent step (a + "top 10 by account name" truncates the right ten). + + Cost model: sorting by a measure or a plain/date dimension is unchanged (SQL + pushdown included). A label-ordered `select` resolves from field metadata (no + query). A label-ordered `lookup` costs one batched id→name read over the + pre-window grouped ids (chunked, and reused by the display pass via a + per-request cache), and its window can no longer be pushed into SQL — the + inherent price of ordering by a value the database doesn't store. + +- 0a6fb1e: fix(analytics): the read-scope auto-bridge no longer depends on plugin order (#3618) + + `getReadScope` was only wired when the `security` service already existed at this + plugin's `init()`. The closure itself resolved lazily, but the ASSIGNMENT was + gated on an init-time probe — so a kernel that registers `AnalyticsServicePlugin` + before the security plugin got **no read-scope provider at all**, and every + analytics strategy ran unscoped with only a WARN to show for it. + + Both sibling bridges (`executeAggregate`, `executeRawSql`) are wired + unconditionally and resolve at call time, and this one's own comment claimed the + same. Now it actually does: the probe only decides the log wording. + + The CLI (`os serve`) registers security before analytics, so that path was + already correct. The exposure was for embedders composing their own kernel — and + for this repo's own `bootStack` harness, which registers analytics first, meaning + the entire dogfood/verify suite had analytics RLS silently disabled and any RLS + assertion written there passed vacuously. + + Also corrects the WARN text: with no provider, scoping is absent on ALL paths and + ALL objects, not just "the raw-SQL path" and "joined objects" as it claimed. + + Adds `analytics-rls.dogfood.test.ts`: an owner-scoped RLS fixture driven over real + HTTP as a real non-admin, asserting the rows a member's aggregate actually + returns. Reverting either this fix or the #3597 strategy fix turns it red. + +- 1986594: feat(analytics): honour widget `dateGranularity`, `sortBy`/`sortOrder`, and `limit` in the dataset query (#3588) + + Three presentation options were accepted by the metadata layer and then dropped + by the analytics query builder. They reached no SQL, produced no error, and the + only way to notice was to read the `sql` a dataset response echoes — so a + dashboard could declare `dateGranularity: 'month'` and quietly render one bar + per record. + + - **`dateGranularity` now buckets.** `DatasetSelection` gained an optional + `dateGranularity`, applied to every selected `date` dimension. Precedence per + dimension: an explicit `timeDimensions` granularity, then the selection's, + then the dataset dimension's own default. A widget can bucket a trend by month + without the dataset committing every other consumer to that granularity. + - **`order` / `limit` / `offset` now apply on every path.** They are applied to + the ASSEMBLED grid — after measure-scoped sub-queries merge, after `compareTo` + columns attach, and after derived measures are computed — so a derived measure + is a valid sort key and the ObjectQL aggregate path (which has no ordering + grammar, and which native SQL hands every date-bucketed query to) orders + identically to native SQL. A single-query selection still pushes the window + down into the statement. An `order` key that names nothing the selection + projects is now rejected (400) rather than silently ignored. + - **`limit` is deterministic.** Without an `order`, a limit orders by the + selected dimensions first, so it truncates a reproducible window instead of an + arbitrary subset. + - **Widget `options` is a contract again.** The four query-affecting keys + (`dateGranularity`, `sortBy`, `sortOrder`, `limit`) plus `stageOrder` are + declared on `DashboardWidgetOptionsSchema`, so a typo like `sortDirection` is + an author-time error. The bag stays open — renderer extras (`icon`, `columns`, + `striped`, …) pass through untouched. + + Two latent bugs surfaced while fixing the above and are fixed here too: + + - `order`/`limit` were forwarded to EVERY sub-query. A measure-scoped + supplementary query selects one measure, so an inherited `ORDER BY` named a + column it never selected, and an inherited `LIMIT` truncated it before the + merge — dropping rows from the assembled grid. Nothing hit this only because + nothing passed `order`. + - The `compareTo` pass built its query by hand and skipped granularity + resolution, so a month-bucketed primary grid was merged against raw-timestamp + comparison rows. No dimension key matched and every `__compare` + column came back empty. + + `ObjectQLStrategy` now also echoes a representative `sql` (with `date_trunc`, + `WHERE`, `ORDER BY`, and `LIMIT`; filter values parameterized, never inlined). + Previously the `sql` field simply vanished from the response whenever a query + was date-bucketed, leaving an author unable to tell "not implemented" from "this + strategy doesn't report". + +- adabaa8: fix(analytics): fail closed on cross-object aggregation the ObjectQL path cannot join (#3654) + + `engine.aggregate()` has no join — it never expands a lookup and the SQL driver's + aggregate emits no `JOIN`. So a dotted dimension/measure like `account.region` + reaching `ObjectQLStrategy` (the fallback NativeSQL declines: date-granularity + bucketing, in-memory driver, federated objects) failed SILENTLY: the in-memory + path bucketed every row under one `(null)` group and summed the whole table into + it (a plausible number that is actually a mislabelled full-table total), and the + native path errored on the unresolved column. + + `ObjectQLStrategy` now rejects any cross-object reference outright, with a clear + message, before the query reaches the engine. This generalizes the #3597 guard + (which only rejected when the joined object carried a read scope, and skipped the + check entirely when no read-scope provider was configured — so the silent + `(null)` bucket still shipped on unsecured/in-memory setups) into an + unconditional one, and subsumes it: a rejected query never loads the joined + object, so there is nothing left unscoped. + + Cross-object datasets are unaffected on `NativeSQLStrategy`, which hand-compiles + the LEFT JOINs (and scopes each). This only changes the fallback path, turning a + silent wrong answer into a loud, actionable error. Full lookup-traversal support + in the aggregate path is left as follow-up (see #3654). + +- 605c23f: fix(analytics): ObjectQLStrategy applies `timeDimensions[].dateRange` — the predicate every date-bucketed chart was missing (#3650) + + `ObjectQLStrategy.execute()` built its engine filter purely from + `normalizeAnalyticsFilters(query)`, which reads only `query.where`. But + `dateRange` is a **sibling** of `where`, never folded into it — so the window + was dropped on the floor. No error, no warning: the chart rendered, and the + numbers were for all of history. + + This was not a "some drivers only" corner. `NativeSQLStrategy.canHandle` + declines any query carrying a `granularity`, so a **date-bucketed trend lands on + the ObjectQL path on every driver**, Postgres and SQLite included — and a + bucketed trend is precisely the shape that also carries a range ("last 12 + months", "this quarter"). The other two paths always applied it + (`NativeSQLStrategy` as `BETWEEN`, `preview-evaluator` row-wise); only this one + did not. + + **Two visible symptoms:** + + - A trend chart with a time filter plotted **every row ever recorded** instead + of the selected window. + - `compareTo` (period-over-period) was **structurally dead**. `runCompare` + builds the comparison pass by shifting `dateRange` and changing nothing else, + so with the window ignored both passes issued a byte-identical aggregate: + every `__compare` column equalled its primary and the delta was a + flat 0%. And since `compareTo` requires a time dimension, it always took this + path. + + The window now lowers to an inclusive `{$gte, $lte}` on the resolved field — the + same shape `NativeSQLStrategy` binds as `BETWEEN` and the memory driver builds + as a `$match` — so one dashboard reads the same on every driver. No storage + coercion is applied here on purpose: unlike the raw-SQL path (which had to learn + about SQLite's INTEGER epoch in #2034), this path goes through + `engine.aggregate()`, where the driver's own CRUD filter coercion already + handles a `where` bound on that same column. + + **Same-field composition was fixed alongside it**, because the window makes it + routine. Operands merged into one field entry by spreading, which silently kept + whichever came last: a `where` bound and a window bound on `close_date` would + have had one erase the other, and a `where` that names one field twice through + `$and` (`{$and: [{stage: 'won'}, {stage: {$ne: 'lost'}}]}`) already lost its + first operand today. Operands that name **different** operators still share one + entry; colliding ones become their own `$and` conjunct, so the engine + intersects them instead of the strategy picking a winner. + + `generateSql()` renders the window as a parameterised `BETWEEN` to match — its + comment previously explained why a `BETWEEN` was deliberately absent, which was + correct only while `execute()` dropped the window. Bounds bind as `$n` + placeholders, never inlined: the echoed statement travels to the browser. + + A window on a **cross-object** time dimension is still rejected, and is now + reported as the bucketing error it is rather than as the "cross-object filter" + its lowered predicate would otherwise resemble. `execute()` and + `/analytics/sql` continue to accept and reject the same set. + + Relative-phrase ranges ("Last 7 days") are still not resolved on this path, and + a bare-string `dateRange` degenerates to a single point — both matching + `NativeSQLStrategy` exactly, rather than inventing a second interpretation for + the driver-independent path. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index d309746333..16a6946136 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 0a48b05b3e..8eb1a95c74 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,318 @@ # @objectstack/service-automation +## 17.0.0-rc.0 + +### Minor Changes + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +- 5524f84: feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475) + + A record-change flow can now declare `expand: ['', …]` on its start + node config so node templates resolve `{record..}` (e.g. + `{record.account.name}` in a notify title, closing the #3426 gap for lookups). + + The engine re-reads the declared relations AFTER identity resolution, as the + run's OWN principal — `resolveRunDataContext` honors `runAs`, so a `runAs:'user'` + run reads the referenced object as the **triggering user** (its RLS/FLS enforced) + rather than system-elevated. This is what made expansion unsafe to do in the + trigger's re-read (which has no resolved grants) and is why it lives in the + engine (new `AutomationEngine.setRecordExpander`, bridged by the plugin to the + same data engine the CRUD nodes use). + + Only the declared relation keys are grafted onto the run record, so bare lookup + ids and `multiple` lookup arrays (#1872) on other relations — and the formula + fields the trigger already hydrated — are untouched. Opt-in ⇒ zero cost when + unused; best-effort ⇒ a re-read failure leaves the record unexpanded and never + breaks the flow. + + The `os validate` lint rule `flow-template-lookup-traversal` (#3426/#3472) is now + suppressed for a relation once the flow declares it in `config.expand`. + +- b95577a: feat(automation): surface silently-stripped write fields as step warnings (#3407) + + `update_record` used to report an unconditional `success` even when the data + layer legally stripped the requested write fields — static `readonly` (#2948) + or a TRUE `readonlyWhen` predicate (#3042). The only trace was a server-side + logger warn, invisible in the flow run trace: an author saw a clean 3ms + `success` while the DB truth never changed (how #3356's approval stage + write-backs failed unnoticed). + + - **spec**: new `DroppedFieldsEventSchema` / `DroppedFieldsEvent` + (`{ object, fields, reason: 'readonly' | 'readonly_when' }`) in + `data/data-engine.zod.ts`, and a `WriteObservabilityOptions` + (`onFieldsDropped` listener) mixin on `IDataEngine.insert/update` option + params in `contracts/data-engine.ts`. The listener is a TS-contract-level, + in-process-only channel — deliberately NOT part of the serializable Zod + options schemas or the RPC boundary. + - **objectql**: `engine.update()` reports each strip pass's dropped keys + + reason through `options.onFieldsDropped` (all four strip sites: single-id + + bulk × readonly + readonlyWhen). A throwing listener never breaks the write. + System-context writes skip the readonly strip and therefore report nothing, + as before. `insert()` accepts the option for symmetry but strips nothing + today (INSERT is readonly-exempt; FLS write denial throws). + - **service-automation**: `NodeExecutionResult` and `StepLogEntry` gain + advisory `warnings?: string[]`; `update_record` / `create_record` attach one + warning per strip event naming the dropped fields, plus a structured + `droppedFields` output (`{.droppedFields}`) for downstream nodes. + `success` semantics are unchanged — stripping stays legal, it just is no + longer silent. + +### Patch Changes + +- b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456) + + The record lock is keyed on a **pending** `sys_approval_request`, and it could + not tell _the run that owns that request_ from _an unrelated user editing the + record_. So a flow that touched its own target record while its own approval was + still pending — a manual `resume` with no decision, or a node that writes the + record between opening the approval and the decision — died on its own + `RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery + existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it + self-healing. + + Both halves are now closed. + + **Prevention — the owning run may write its own record.** The automation engine + stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it + travels with every data node's ObjectQL context into `ctx.provenance`. The lock + hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`. + It is keyed on run identity rather than elevation on purpose: a `runAs:'user'` + run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance — + server-constructed like `isSystem`, never client-supplied, evaluated by no + security middleware, and the only write it permits is to the one record its own + run already holds a pending request against. + + **Recovery — a sweep releases records held by runs that died anyway.** A pending + request whose owning run has reached a terminal state (`completed`, `failed`, + `cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled` + — releasing the lock — and audited under the reserved actor `system:dead-run` + with the run and its status in the comment, so it is never mistaken for a + submitter's withdrawal. It runs on the existing approvals sweep clock, which also + covers the case no in-band handler can: a run killed by a process crash. + + The sweep is fail-safe by construction. It acts only on an explicit terminal + status from a closed set; `paused` (the normal state of a live approval), + `running`, an unrecognised status, an unknown run, a `getRun` that throws, and a + deployment with no automation engine are all read as "still alive". The failure + mode is "a dead run's lock survives until an admin recalls it" — today's + behaviour — never "a live approval is destroyed". + + Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for + a run id rather than the latest. A run that pauses and later finishes records two + entries under one id, so every suspend-then-finish run — every approval, screen + and wait flow — reported itself as `paused` forever, both on the Runs + observability surface and to this sweep. + + One shape was left out here and closed separately in #3712: a `runAs:'user'` run + with no trigger user (a schedule) resolved no ObjectQL context at all, so it + carried no `flowRunId` and stayed subject to the lock. It now passes a + provenance-only context — the run id and nothing the security middleware keys on + — so it is attributable without acquiring a principal, and its documented + unscoped posture (#1888) is unchanged. + +- c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712) + + #3456 let the run that opened a pending approval write its own target record, + keyed on `flowRunId`. It worked for every run that resolves an identity and + missed the one that doesn't: an effective `runAs:'user'` run with **no trigger + user** — a schedule being the canonical case — passed no ObjectQL context at + all, so nothing carried the run id and the run still died on its own + `RECORD_LOCKED`. + + The blocker was never the lock. It was that "no identity" and "no context" were + the same thing on the wire, so a run could not say _who it was_ without also + claiming _what it was allowed to do_. + + **A run with no principal now passes provenance alone.** + `resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`, + no `permissions`, not even `isSystem: false`. Every principal gate keys on one + of those fields (the elevation short-circuit on `isSystem`, the ADR-0103 + engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`, + the empty-principal fall-open on all three), so this context authorizes + **identically to no context at all**. The run keeps the documented #1888 + unscoped posture, its loud `[runAs]` warning, and the + `flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch + changed — only that it can now be attributed. + + **Provenance moved out of the hook session, into `ctx.provenance`.** `session` + answers _who is calling_ and is absent when no identity envelope was supplied — + a distinction real gates depend on (the attachment access gate skips bare-kernel + writes on exactly that test). Folding a run id into `session` would have forced + an identity-less run to present an empty session, silently turning "no caller" + into "an anonymous caller" and narrowing the #1888 fail-open for attachments + alone. `HookContext.provenance.flowRunId` says what produced the write; the + approvals lock reads it there. + + Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope + (`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time + defaults, which made them _required_ on a caller-supplied option and asserted + something untrue — that every data-engine context carries a principal. Callers + have always passed slices (`{ isSystem: true }` for a system read); the type now + says so. + + Migration: nothing to change unless you read the run id inside a hook. If you + wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the + field never shipped under the old name. + +- 9dcc0ae: fix(automation): array-form flow `triggerType` fails loudly instead of silently never firing (#3481) + + An array `triggerType` on a flow start node — the shape an author (or an AI + authoring pass) naturally reaches for to fire on more than one event, e.g. + + ```ts + config: { objectName: 'app_task', triggerType: ['record-after-create', 'record-after-delete'] } + ``` + + was accepted everywhere and armed nowhere. Multi-event unions are deliberately + unsupported (only the single tokens plus the `record-after-write` create-OR-update + union exist — see #3457), but nothing said so: `defineFlow` passed the array + (start-node `config` is an open record), the engine's `typeof === 'string'` check + folded it to no trigger and misclassified the flow as **manual**, so it never + entered the trigger-binding audit, and the flow-trigger-readiness lint used the + same `typeof` narrowing and produced no finding. The flow bound to nothing and + never fired, with zero output at any layer — the same silent-never-fire class as + #3427 / #3472, and the last authoring shape still slipping past every guard. + + This is a **defensive** fix — arrays remain unsupported; they now fail loudly: + + - **lint** (`validate-flow-trigger-readiness`): an array `triggerType` containing + any `record-*` element now yields a `flow-trigger-unknown-event` warning at + `os validate` time, steering to `record-after-write` (for created-or-updated) or + one flow per event. + - **engine** (`resolveTriggerBinding`): such an array is routed to the + `record_change` trigger — exactly as an unmappable single token is — instead of + being folded to a manual flow, so it reaches the trigger's bind-time rejection. + - **trigger** (`record-change`): the bind-time rejection detects the array shape + and emits a targeted warning (naming the flow, pointing at `record-after-write` + and #3457) rather than the generic unknown-token line. + +- c88eeda: fix(automation): flow string templates serialize object tokens readably, never `[object Object]` (#3450) + + A flow string field that embeds an object-valued token — most notably the + engine's `$error` (`{nodeId, message, ...}`, set on a failed step) in a fault + handler's notify body — rendered as the useless `[object Object]`. The + multi-token branch of `interpolateString` coerced every value with `String()`, + and `notify-node` did the same for a sole `{$error}` token. + + - New shared `stringifyForTemplate` helper (`builtin/template.ts`): objects and + arrays are JSON-serialized (so the text stays legible and still carries the + message), primitives pass through, `null`/`undefined` render as ''. + - `interpolateString`'s embedded-substitution branch and `notify-node`'s + title/body coercion use it. The sole-token branch still returns the raw value + (typed config fields keep their type), and `{$error.message}` still resolves + to just the message string — the documented, cleanest author form. + + Split from #3425 (the readonly-strip half shipped in #3465). + +- 9bf4588: fix(service-automation): bind `previous` (as null) on the create leg so start conditions can discriminate create vs update (#3427) + + The engine bound `previous` into the flow condition scope only when it was + truthy, so on a record insert (`record-after-create`, and the create leg of + `record-after-write`) `previous` was an **unknown** CEL variable. Any reference to + it — including the documented `previous == null` create-discrimination — threw + `condition failed to evaluate as CEL: Unknown variable: previous`, failing the + whole start condition and dropping the run. + + `previous` is now always bound, to `null` when there is no prior row. So + `previous == null` is the create leg and `previous != null` / `previous.` + the update leg — the pattern the `record-after-write` docs and the Studio flow + designer advertise. Update-triggered flows are unaffected (`previous` was, and + stays, the prior row there). + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/formula@17.0.0-rc.0 + ## 16.1.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 4c8360399e..64da506ce8 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 e47021c9de..ab5910b79b 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/service-cache +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index df78436465..09d648bfc3 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 e347846f9f..4f9a873382 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-cluster-redis +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/service-cluster@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 962b91d21a..d232d2b61e 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": "16.1.0", + "version": "17.0.0-rc.0", "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 d5ce2fb44f..1c4879a822 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-cluster +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index fae0e0b0d0..0e7ab190f5 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 930ea76d7b..4b67768b9e 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-external-datasource +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 6b5e2e2492..3c963c3699 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 cf8873c3ab..daa75b3d49 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,185 @@ # @objectstack/service-i18n +## 17.0.0-rc.0 + +### Patch Changes + +- f1a8114: fix(client,service-i18n): ledger the autonomously-mounted service routes, and repair the two i18n calls that reached nothing (#3636) + + Tranche 3 of the #3563 route audit — the last un-audited server surface. The + dispatcher ledger (#3563) and the REST ledger (#3587) each stop at their own + package boundary, and two services mount routes outside both: they reach for + the `http-server` service and register straight on `IHttpServer`, so neither + `RouteManager` nor `RestServer.getRoutes()` has ever seen them. That left the + SDK's entire storage surface, plus all of i18n, in the pre-#3563 posture: + expressed, working, guarded by nothing. + + **Ledgers + guards.** `storage-route-ledger.ts` (10 routes) and + `i18n-route-ledger.ts` (3) sit next to the registrars that mount them, each + enumerated for real — the registrar runs against a capturing mock + `IHttpServer` and its registration calls _are_ the route set, so a new route + lands with a reviewed disposition or fails CI. The client half is + `packages/client/src/service-route-ledger-coverage.test.ts`; ledgers cross the + boundary as relative source imports, never a service→client package edge. + + **Two wire-level 404s fixed.** `i18n.getTranslations` sent + `/i18n/translations?locale=xx` and `i18n.getFieldLabels` sent + `/i18n/labels/:object?locale=xx`, while every serving surface — service-i18n's + mounts, the dispatcher's HTTP mounts, and the `plugin-rest-api.zod.ts` + contract — mounts only the path form. Neither call could ever be answered. + Both had carried a green `sdk` row in the dispatcher ledger since tranche 1, + because that guard asks whether the client _method_ exists, not whether it + speaks a URL anything mounts. The client now sends the path dialect, the same + resolution #3611 gave `meta.getView`, and a new suite drives the real client + at a real router so a revert cannot pass quietly. + + **One response-shape fix.** service-i18n's success bodies omitted the + `success` flag that `ObjectStackClient.unwrapResponse` keys on, so the SDK + returned the raw `{ data: … }` wrapper against that provider while returning + the declared unwrapped shape against the dispatcher — one method, two shapes, + decided by which plugin mounted the route. Its three handlers now emit the + `{ success: true, data }` envelope the `i18n` route group declares. `data` did + not move, so direct body readers are unaffected. + + Storage audited clean: 7 routes SDK-expressed, 3 reviewed `server-only` (the + browser capability URL objectql stamps into file-field payloads, and the two + local-driver loopbacks). The chunked-upload family, flagged for triage, turned + out fully expressed. Both ledgers ratchet `gap` and `mismatch` at zero. + + Filed, not fixed: `GET {base}/_local/file/:key` is built by three call sites + and mounted by none (#3641); the cross-surface URL conformance guard that would + have caught all of the above mechanically is the capstone (#3642). + +- bd68f08: fix(service-storage,service-i18n): emit the declared error envelope, not a bare `{ error }` (#3675) + + #3636 aligned the **success** bodies of the autonomously-mounted service + routes because those were the ones breaking `ObjectStackClient.unwrapResponse`. + The error bodies were left alone and stayed a bare `{ error: '' }` — + with the code, where one existed at all, as a _sibling_ of `error` rather than + a field of it — against a contract (`BaseResponseSchema` + `ApiErrorSchema`) + that declares `{ success: false, error: { code, message } }`. + + So the same SDK method returned two different error shapes depending on which + provider mounted the route: a caller reading `body.error.message` got the real + message from the dispatcher and `undefined` from these services. All 32 sites + (27 in `storage-routes.ts`, 5 in `i18n-service-plugin.ts`) now go through a + single `sendError` helper per module — the nested-`error` shape the sibling + services already use (`settings-routes.ts`, `share-link-routes.ts`), plus the + `success` flag those two still omit and the contract requires. + + **Codes moved, and that is the breaking part.** `AUTH_REQUIRED`, + `ATTACHMENT_DOWNLOAD_DENIED` and `FILE_DOWNLOAD_DENIED` used to sit at + `body.code`; they now sit at `body.error.code`. The SDK is unaffected — it + already reads `errorBody?.code || errorBody?.error?.code`, one of the four + shapes its error path sniffs for, which is the consumer-side shim Prime + Directive #12 says to cure at the producer. The console's attachment panel + was NOT: it read the top level only, so every gated download would have + degraded from "You don't have access to download this attachment." to + "Download failed (403)". Fixed in objectui to read both dialects, since a + console build ships independently of the server it talks to. + + **Guarded both ways.** New `error-envelope.conformance.test.ts` in each + service drives every distinct error branch through the real registrar and + parses the body against the real `BaseResponseSchema` imported from + `packages/spec` — not a local restatement of it — and scans the module source + so a new route cannot quietly reintroduce the bare shape. The route ledgers + (#3563 → #3656) could never have caught this: they audit which routes exist + and whether the SDK can address them, not what comes back. + + Measured and left alone: the dispatcher does not conform either — it puts the + HTTP status in `error.code`, where the contract declares a semantic string, + and parks the real code in `details` to work around its own occupied field. + That deviation is now pinned to exactly one field by a test in + `http-dispatcher.test.ts` rather than described in prose. Also unchanged: + service-storage's success bodies are still three shapes of their own + (`{ data }`, bare `{ url }`, `{ ok, key }`, none with `success: true`) — a + non-additive change that needs its own issue, not a quiet ride along with this + one. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index c875ee0c80..b33f1a1f80 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 75df24ca68..4f60bfcfa1 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,130 @@ # @objectstack/service-job +## 17.0.0-rc.0 + +### Minor Changes + +- 394b7a1: feat(job): honor the authored `retryPolicy` / `timeout` in the job scheduler (#3494) + + `JobSchema.retryPolicy` and `JobSchema.timeout` used to be parsed-but-ignored + (the 2026-06 liveness audit's aspirational-config cluster). They are now + enforced end to end — built rather than pruned, since retry/backoff and + per-run time limits are semantics job authors reasonably expect: + + - **spec**: `IJobService.schedule` gains an optional 4th `options` argument + (`JobScheduleOptions` with `retryPolicy` / `timeout`, mirroring the + authorable schema); new `JobRetryPolicy` type. Backward compatible — + existing 3-arg implementations and callers are unaffected. + - **service-job**: new `runWithPolicy` helper (exported, with + `JobTimeoutError`) wraps every handler invocation in `CronJobAdapter` and + `IntervalJobAdapter`; `DbJobAdapter` threads options through to its inner + adapters. Failed attempts (including timeouts) retry with exponential + backoff `backoffMs * backoffMultiplier^(retry-1)` up to `maxRetries`; + an attempt exceeding `timeout` is recorded with execution status + `'timeout'`. No `options` → exactly the legacy single-attempt behavior. + - **runtime**: declarative-jobs registration in AppPlugin forwards the + authored `retryPolicy` / `timeout` to the scheduler. + + Note: JavaScript cannot forcibly cancel an in-flight handler — a timed-out + attempt is abandoned, not killed. The retry delay caps only via the + multiplier arithmetic (no maxDelay knob yet). + + Refs #3494, #1878, #1893. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 1db773e0dc..f1cd0ced01 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 bfe3557cf4..3f2f7a15fb 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-knowledge +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index bc69828290..699b941545 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 428b03e69f..aaa96f1291 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-messaging +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index e273c33d80..518b817cf3 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 010634da38..849e3282ad 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,96 @@ # @objectstack/service-package +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [c073b8c] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/metadata-core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 3522d1e649..4a4fb6bb62 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 c81ca56d65..fad5ed6d39 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,101 @@ # @objectstack/service-queue +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index 53b5b28809..6efd62b9de 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 861a395c04..e9dd216b79 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,101 @@ # @objectstack/service-realtime +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index f9f6cee0c1..ebe84eebb3 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 c591f3eca0..1c04be2e08 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,141 @@ # @objectstack/service-settings +## 17.0.0-rc.0 + +### Patch Changes + +- a629074: fix(auth): the second factor now obeys the operator's lockout policy instead of better-auth's defaults (#3690) + + `auth-manager.ts` constructed `twoFactor()` with a schema and nothing else, so + better-auth's built-in `accountLockout` defaults — on, 10 attempts, 15 minutes — + governed two-factor verification no matter what the admin configured. An operator + who tightened **Setup → Authentication → Account lockout threshold** to 3 got a + password stage that locked at 3 and a second factor that still locked at 10: the + stricter door was the looser one, with nothing in the UI saying so. + + `lockout_threshold` / `lockout_duration_minutes` are now projected onto + better-auth's own `accountLockout` shape (`enabled` / `maxFailedAttempts` / + `durationSeconds`, minutes converted to seconds) rather than growing a parallel + `two_factor_lockout_*` pair — one policy, one mental model, and a future upstream + field arrives as a new option instead of a conflict. The projection goes through + `applyConfigPatch`, which resets the cached better-auth instance, so a settings + change takes effect without a restart. + + Threshold `0` is deliberately **not** forwarded as `enabled: false`. It is the + password stage's "off", and a deployment may leave that stage unlocked because + rate limiting or an IdP covers it; the second factor is the last check before a + session is issued, so it keeps better-auth's default rather than being switched + off by a setting that never mentioned it. + + The threshold field is also no longer hidden behind `email_password_enabled` — + two-factor verification exists in passwordless deployments, where the setting was + previously unreachable. + + The admin **Unlock Account** action now clears both stages. It only ever reset + `sys_user`, so a user locked at the second factor had no admin escape hatch and + had to wait the duration out — survivable while that lock needed 10 failures, + routine once an operator can set the threshold to 3. The second-factor clear is + best-effort and runs after the primary write, so an account with no enrolment + still unlocks normally. + + Note the plugin caps attempts at 5 per challenge (`beginAttempt(5)`), which no + option reaches; a threshold above 5 forces a fresh challenge rather than raising + that cap. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [030125b] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/types@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 2f1a6369b3..81fd2a347f 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 ed1e2b3cf6..02e4eecf7b 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/service-sms +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 1a36658f53..e09e9ac4b2 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 ac182f1b13..0b074b82e3 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,493 @@ # @objectstack/service-storage +## 17.0.0-rc.0 + +### Minor Changes + +- 99736a0: feat(storage): exclusive field-reference file ownership — ADR-0104 D3 wave 2 (PR-3) + + A `file`/`image`/`avatar`/`video`/`audio` field that holds a `sys_file` id now + records its owner on the file: `sys_file.ref_object` / `ref_id` / `ref_field` + name the single `(object, record, field)` slot that references it, maintained on + the engine write path — claimed on insert, reconciled on update, released when + the owning record is deleted. + + **Field references are exclusive, unlike attachments.** The attachments surface + deliberately shares one file across many `sys_attachment` join rows; a field + reference is owned by at most one slot, and writing an already-owned id into a + second slot **copies the bytes into a fresh `sys_file`** rather than sharing the + row. That keeps a file's read authorisation derived from exactly one parent + record instead of the union of every referrer's — so copying a private record's + file id into a world-readable one cannot silently widen access — and it removes + reference counting from the lifecycle entirely: a file is released because its + one owner let go, never because a count came back zero. + + **Deletes nothing.** This records and releases ownership; it never tombstones, + and the `scope === 'attachments'` guardrail that keeps field-referenced files + out of the reap is untouched. Collection is a separate, gated change that must + also extend the reap guard's sweep-time re-verify in the same commit. + + Also exports `isFileIdToken` from `@objectstack/spec/data` as the single arbiter + of "is this stored string an opaque file id, or a legacy/external URL?", now + shared by the read resolver and the write claimer so the two cannot drift. + + Dormant until a field actually holds an id token: objects without file-class + fields, inline-blob values and URL-shaped values all exit before any I/O. + +- 134df4f: feat(storage): governed download for field-owned files — ADR-0104 D3 wave 2 (PR-4) + + A file owned by a record's field (`sys_file.ref_object` / `ref_id`, set by + PR-3) is now authorized on download the same way an attachment is: the caller + must be able to READ the file's parent record, or be its uploader. Previously + only `attachments`-scope files were gated and every field file kept an + anonymous capability URL. + + **Parent resolution differs by surface, and that asymmetry is the point.** An + attachment may hang off many records, so its readable-by set is the union over + its `sys_attachment` join rows. A field-owned file belongs to exactly one + record, so its readable-by set is that one record's — nothing more. Under a + shared reference model the field case would have had to union too, which is + what makes copying a file id into a more public record silently widen access. + + Denials are reported as `FILE_DOWNLOAD_DENIED` (403), distinct from the + attachments path's `ATTACHMENT_DOWNLOAD_DENIED`, since the file _belongs to_ one + record rather than being _attached to_ several. + + **`acl: 'public_read'` is the opt-out**, and now an explicit declaration rather + than the silent default every field file used to get. Genuinely public images — + anything embedded in an ``, which cannot carry a bearer token — must + declare it. + + **Dual-mode safe, gates nothing that is open today.** A pre-cutover field holds + an inline blob or an external URL, never a `sys_file` id, so no existing file + has an owner recorded and none of them start being gated. The gate engages only + for files a record's field has actually claimed, and disengages again when + ownership is released. + + *** + + Also adds `verifyFileReferences()` — the executable form of ADR-0104's R4 + acceptance gate. It compares ground truth (what records' file fields actually + hold) against recorded ownership, and classifies disagreements by whether they + could cause data loss once collection is enabled: + + - **blocking** — `unowned_reference` (a held file nothing owns), `foreign_owner` + (a record holds a file owned by another slot), `shared_reference` (one file + held by two slots, i.e. exclusivity was violated). Each would let a later reap + delete bytes a record still points at. + - **advisory** — `stale_owner` (owned but no longer held; fails toward + retention) and `unreferenced_file` (storage cost, not a correctness problem). + + The scan is read-only — it never writes, tombstones, or deletes. A ledger may + not be given authority over irreversible deletes until it has been shown to + agree with reality, so this must report zero blocking discrepancies on real + tenant data, on consecutive runs, before the gated collection change may merge. + +- fe67e34: feat(spec)!: media fields declare accept/maxSize, and the stored form is a file reference — ADR-0104 D3 wave 2 (PR-5a) + + **`accept` and `maxSize` are now declared on `FieldSchema`, and enforced on the + server.** Both were already read by the upload widgets — `field.accept`, + `field.maxSize` — while the spec did not declare them, so an author who wrote + them had the keys silently stripped at parse and the constraint simply never + existed. That is exactly the ADR-0104 failure class (a declaration accepted in + source, dropped from the contract, with no feedback). + + Now that the platform owns the file, `sys_file` carries the authoritative MIME + type and byte size, so a record write is re-checked against the declaration + where it actually binds rather than only in the browser — a client-side check is + a convenience, not a control, since any caller talking to the API directly + bypasses it. Violations raise `FileConstraintError` and fail the write. An entry + is only judged against metadata the file actually reports: a file with no + recorded MIME type cannot fail an `accept` test, and one with no recorded size + cannot fail `maxSize` — "we don't know" must not become "not permitted". + + **The stored form of a media field narrows to an opaque `sys_file` id.** + `valueSchemaFor(field, 'stored')` now yields an id for `file`/`image`/`avatar`/ + `video`/`audio`; the inline `{url, name, size, …}` blob becomes the `'expanded'` + read form, which also still admits an unresolved id (storage service absent, + file not committed) exactly as an unexpanded lookup id stays valid. + + Two legacy forms therefore stop conforming, both deliberately: + + - the **inline blob**, which is no longer stored but derived; + - an **external URL**, which was never a managed file — ADR-0104 R7 retires it + toward an explicit `url` field, and under AI authoring that is the point: it + stops "managed file" and "external link" being the same declaration. + + **Not a breaking change today.** Value-shape checking is warn-first + (ADR-0104 R1/R2): a not-yet-backfilled row still writes and the author gets a + warning naming the field. Hard rejection arrives only when a deployment opts + into `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` — which it should do after running the + backfill and confirming reconciliation. The `!` marks the contract change for + the v17 window, not a runtime break on upgrade. + +- 3d3fddf: feat(storage): legacy file-value backfill — ADR-0104 D3 wave 2 (PR-6) + + `backfillFileReferences()` converts the pre-reference forms a `file`/`image`/ + `avatar`/`video`/`audio` field may hold — an inline metadata blob + (`{url, name, size, …}`) or a bare URL string — into the reference form: an + opaque `sys_file` id, owned by the record's field. + + What it will and will not convert: + + - **A URL naming this platform's own resolver** (`…/storage/files/:id`) already + identifies a `sys_file`; the field is rewritten to the bare id and no bytes + move. + - **A `data:` URI** carries its bytes inline; they are uploaded, a `sys_file` is + registered, and the field is rewritten to its id. + - **An external URL** is reported, never converted. Re-hosting third-party + content is a bandwidth, licensing and privacy decision that is not a + migration's to make — ADR-0104 R7 retires these toward an explicit `url` + field, which under AI authoring is the point: it stops "managed file" and + "external link" being the same declaration. + + **Dry run by default** — nothing is written unless `apply` is set, and the + dry-run report has the same shape as the applied one so the plan can be reviewed + and diffed. **Idempotent** — a value already in reference form is recorded and + left alone, so a partially-completed run is safe to repeat. + + The backfill never writes the ownership columns itself: it rewrites the record, + and the claim hooks observe that write and record ownership. One claiming path, + so there is nothing that can disagree with itself. Run + `verifyFileReferences()` afterwards to confirm the two agree — that + reconciliation is the gate the irreversible collection change must pass. + +- fdb4f50: feat(migrate): `os migrate files-to-references` — a data migration with a self-check, gated per deployment (#3617) + + The ADR-0104 file-as-reference migration ships as a command a deployment runs + against its own database, and the deployment-level flag it records is what may + later authorise irreversible behaviour — never the platform version. + + ```bash + os migrate files-to-references # dry run: reports, writes nothing + os migrate files-to-references --apply # converts, verifies, records the flag + ``` + + The run backfills legacy file-field values (inline metadata blobs, own-resolver + URLs, `data:` URIs) into owned `sys_file` references, reconciles the ownership + ledger against what records actually hold, and — only on an `--apply` run whose + reconciliation reports **zero blocking discrepancies** — records + `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. + + **Why a flag rather than a release note.** ObjectStack is a development + platform: third-party deployments upgrade on their own schedule and their data + is not observable by anyone else, so no release-side soak can vouch for them. + The evidence has to be produced where the data is. Consequences: + + - Installing a new version never starts deleting bytes. Running the migration + and passing its self-check is the consent. + - Not run, or not passed → files are retained forever. Wasted storage, zero + data loss. + - A later failing run **clears** `verified_at`: a deployment whose data has + drifted closes its own gate. + - A dry run writes nothing at all — not the conversions, and not the flag, + even when the self-check would pass. + - External URLs stay advisory. They are not `sys_file`s, so they can never + enter collection; whether to remodel them as a `url` field is the app + author's decision (ADR-0104 R7), not a gate. + + Ships alongside: + + - `@objectstack/spec` — `DataMigrationFlagSchema`, `FILE_REFERENCES_MIGRATION_ID`, + and the single `isDataMigrationFlagVerified` predicate both future consumers + (collection #3459, strict value-shape #3438) read, so the two gates cannot + disagree about the same fact. + - `@objectstack/platform-objects` — the `sys_migration` object plus + `readDataMigrationFlag` / `isDataMigrationVerified` / `recordDataMigrationRun`. + Reads fail toward "not verified": a gate that cannot read its evidence stays + closed. + - `@objectstack/objectql` — a read may now opt out of file-reference expansion + via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`, and the storage service's + bookkeeping/scan reads do. Without it the read resolver rewrites stored ids to + their expanded form before the reconciliation sees them, which reports held + references as absent — noisy `stale_owner` findings, and a missed + `unowned_reference` would have been a false pass of the collection gate. + +### Patch Changes + +- 37b1346: feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1) + + `POST /api/v1/storage/upload/complete` now returns the opaque `sys_file` id + (`data.fileId`), and `client.storage.upload()` surfaces it on the returned + `FileMetadata`. Previously the commit response omitted the id — the caller + could not learn which id to persist after committing an upload, so a file + field could never store a reference. + + Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the + client falls back to the presigned id when talking to an older server). This is + the enabling foundation for file-as-reference; the storage model itself is + unchanged in this PR. + +- deb538f: fix(storage): let an object delegate file-read authorization to its service + + Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a + **legitimate approver could see a decision attachment's filename but got 403 + opening it**, found by driving app-showcase in a browser as a real non-admin + approver. + + Cause: a field-owned file's download was authorized by testing whether the + caller can READ the owning row. For an ordinary business object that is right — + row readability _is_ the access rule. For `sys_approval_action` it is the wrong + authority: the audit table is deliberately closed to ordinary approver + positions (`operation 'find' … is not permitted for positions [auditor, +everyone]`), so the test denied the very approver the attachment was filed for. + The approvals _service_ has always had the real rule, which is why the timeline + listing the attachment returned 200 while the bytes returned 403. + + An object may now name a service to answer the question instead: + + - `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes + downloads of files owned by that object's media fields. + - `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract. + - `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead` + reuses the _same_ gate `listActions` applies (visibility of the parent + request) rather than inventing a second, looser rule for the bytes. + + **Fails closed**: a declared delegate that is missing or does not implement the + method denies, rather than silently reverting to the raw read it was declared to + replace. Objects without the declaration are unchanged. + + Verified in the browser against app-showcase, both sides of the gate: the + approver now downloads the real PDF (200), and an anonymous request is still + refused (401) — the anonymous capability URL the original change closed stays + closed. A decision attachment ends up exactly as readable as the decision it + hangs off: never more, and no longer less. + +- 2c19383: fix(service-storage): stop handing out `_local/file/:key`, a URL nothing mounts (#3641) + + Three call sites built `${basePath}/_local/file/`. No registrar has ever + mounted it, so anyone who followed one got a 404. Found by the tranche-3 + storage ledger (#3636), which recorded the URL as deliberately absent and filed + this; now nothing builds it either. + + Each site is fixed according to what it could honestly do: + + - **`LocalStorageAdapter.getPresignedUpload()`** simply omits `downloadUrl` + (optional on the descriptor). It cannot construct the real capability URL — + that is keyed by `sys_file.id`, and an adapter only ever sees the storage + key. Nothing read the field anyway, which is how it survived: the + presigned-upload route builds its own `downloadUrl` + (`${basePath}/files/:fileId/url`) and ignores this one, while all three real + readers of `desc.downloadUrl` take it from `getPresignedDownload`, whose URL + _is_ mounted (`_local/raw/`). + + - **`GET /files/:fileId/url` and `GET /files/:fileId`** answer **501 + `NOT_IMPLEMENTED`** when the adapter has neither `getPresignedDownload` nor + `getSignedUrl`, instead of returning (or redirecting to) the unmounted URL. + The caller now learns the adapter is the limitation rather than chasing a + broken link. + + Behaviour change is confined to adapters implementing neither capability — + `LocalStorageAdapter` and the S3 adapter both implement `getPresignedDownload`, + so no shipped path changes. A 200/302 pointing at a 404 becomes a 501 that says + why. + + Two conformance cases added for the new branches, and mutation-checked: + restoring either dead URL fails them. + +- f1a8114: fix(client,service-i18n): ledger the autonomously-mounted service routes, and repair the two i18n calls that reached nothing (#3636) + + Tranche 3 of the #3563 route audit — the last un-audited server surface. The + dispatcher ledger (#3563) and the REST ledger (#3587) each stop at their own + package boundary, and two services mount routes outside both: they reach for + the `http-server` service and register straight on `IHttpServer`, so neither + `RouteManager` nor `RestServer.getRoutes()` has ever seen them. That left the + SDK's entire storage surface, plus all of i18n, in the pre-#3563 posture: + expressed, working, guarded by nothing. + + **Ledgers + guards.** `storage-route-ledger.ts` (10 routes) and + `i18n-route-ledger.ts` (3) sit next to the registrars that mount them, each + enumerated for real — the registrar runs against a capturing mock + `IHttpServer` and its registration calls _are_ the route set, so a new route + lands with a reviewed disposition or fails CI. The client half is + `packages/client/src/service-route-ledger-coverage.test.ts`; ledgers cross the + boundary as relative source imports, never a service→client package edge. + + **Two wire-level 404s fixed.** `i18n.getTranslations` sent + `/i18n/translations?locale=xx` and `i18n.getFieldLabels` sent + `/i18n/labels/:object?locale=xx`, while every serving surface — service-i18n's + mounts, the dispatcher's HTTP mounts, and the `plugin-rest-api.zod.ts` + contract — mounts only the path form. Neither call could ever be answered. + Both had carried a green `sdk` row in the dispatcher ledger since tranche 1, + because that guard asks whether the client _method_ exists, not whether it + speaks a URL anything mounts. The client now sends the path dialect, the same + resolution #3611 gave `meta.getView`, and a new suite drives the real client + at a real router so a revert cannot pass quietly. + + **One response-shape fix.** service-i18n's success bodies omitted the + `success` flag that `ObjectStackClient.unwrapResponse` keys on, so the SDK + returned the raw `{ data: … }` wrapper against that provider while returning + the declared unwrapped shape against the dispatcher — one method, two shapes, + decided by which plugin mounted the route. Its three handlers now emit the + `{ success: true, data }` envelope the `i18n` route group declares. `data` did + not move, so direct body readers are unaffected. + + Storage audited clean: 7 routes SDK-expressed, 3 reviewed `server-only` (the + browser capability URL objectql stamps into file-field payloads, and the two + local-driver loopbacks). The chunked-upload family, flagged for triage, turned + out fully expressed. Both ledgers ratchet `gap` and `mismatch` at zero. + + Filed, not fixed: `GET {base}/_local/file/:key` is built by three call sites + and mounted by none (#3641); the cross-surface URL conformance guard that would + have caught all of the above mechanically is the capstone (#3642). + +- bd68f08: fix(service-storage,service-i18n): emit the declared error envelope, not a bare `{ error }` (#3675) + + #3636 aligned the **success** bodies of the autonomously-mounted service + routes because those were the ones breaking `ObjectStackClient.unwrapResponse`. + The error bodies were left alone and stayed a bare `{ error: '' }` — + with the code, where one existed at all, as a _sibling_ of `error` rather than + a field of it — against a contract (`BaseResponseSchema` + `ApiErrorSchema`) + that declares `{ success: false, error: { code, message } }`. + + So the same SDK method returned two different error shapes depending on which + provider mounted the route: a caller reading `body.error.message` got the real + message from the dispatcher and `undefined` from these services. All 32 sites + (27 in `storage-routes.ts`, 5 in `i18n-service-plugin.ts`) now go through a + single `sendError` helper per module — the nested-`error` shape the sibling + services already use (`settings-routes.ts`, `share-link-routes.ts`), plus the + `success` flag those two still omit and the contract requires. + + **Codes moved, and that is the breaking part.** `AUTH_REQUIRED`, + `ATTACHMENT_DOWNLOAD_DENIED` and `FILE_DOWNLOAD_DENIED` used to sit at + `body.code`; they now sit at `body.error.code`. The SDK is unaffected — it + already reads `errorBody?.code || errorBody?.error?.code`, one of the four + shapes its error path sniffs for, which is the consumer-side shim Prime + Directive #12 says to cure at the producer. The console's attachment panel + was NOT: it read the top level only, so every gated download would have + degraded from "You don't have access to download this attachment." to + "Download failed (403)". Fixed in objectui to read both dialects, since a + console build ships independently of the server it talks to. + + **Guarded both ways.** New `error-envelope.conformance.test.ts` in each + service drives every distinct error branch through the real registrar and + parses the body against the real `BaseResponseSchema` imported from + `packages/spec` — not a local restatement of it — and scans the module source + so a new route cannot quietly reintroduce the bare shape. The route ledgers + (#3563 → #3656) could never have caught this: they audit which routes exist + and whether the SDK can address them, not what comes back. + + Measured and left alone: the dispatcher does not conform either — it puts the + HTTP status in `error.code`, where the contract declares a semantic string, + and parks the real code in `details` to work around its own occupied field. + That deviation is now pinned to exactly one field by a test in + `http-dispatcher.test.ts` rather than described in prose. Also unchanged: + service-storage's success bodies are still three shapes of their own + (`{ data }`, bare `{ url }`, `{ ok, key }`, none with `success: true`) — a + non-additive change that needs its own issue, not a quiet ride along with this + one. + +- 0bc685a: fix(storage): downloads carry the real filename + content-type, not the URL token (#3504) + + A presigned download served the bytes as `application/octet-stream` with no + `Content-Disposition`, so a browser saved the file under the opaque URL token + (e.g. `eyJrIjoiYXR0YWNo…`) instead of its real name — an approval's + `signed-contract.pdf` downloaded as a nameless blob. + + - `IStorageService.getSignedUrl` / `getPresignedDownload` take an optional + `PresignedDownloadOptions` (`filename`, `contentType`, `disposition`). + - The REST download routes (`GET /storage/files/:id/url` and `/:id`) pass the + `sys_file` record's `name` + `mime_type`. + - The local adapter carries them in the signed token; the `_local/raw` route + emits `Content-Type` + an RFC 5987 `Content-Disposition` (ASCII fallback + + `filename*=UTF-8''…` for non-ASCII names). The S3 adapter bakes the same into + the signed URL via `ResponseContentType` / `ResponseContentDisposition`. + - Default disposition is `inline`, so previewable types (PDF, images) still open + in the browser — now with the correct name when saved. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [524151c] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [5487c20] +- Updated dependencies [aa8b847] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [9aa5510] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/platform-objects@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/observability@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index c11de60036..b258091b76 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 3a4cc45298..9a2e31336c 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,2823 @@ # @objectstack/spec +## 17.0.0-rc.0 + +### Major Changes + +- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710) + + **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a + record and taking a bulk machine-readable copy of the whole table are different + privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite + "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so. + + ### Migration — FROM → TO + + | | before | after | + | -------------------- | ----------------------------------- | -------------------------- | + | `allowExport` unset | export **allowed** (inherited read) | export **denied** | + | `allowExport: false` | export denied | export denied (unchanged) | + | `allowExport: true` | export allowed | export allowed (unchanged) | + + **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'` + wildcard) of every permission set whose holders should keep exporting. + + ```ts + objects: { + deal: { allowRead: true, allowExport: true }, // ← add the grant + } + ``` + + Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set + that never exported is unaffected. + + **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the + built-ins are handled for you — `admin_full_access` and `organization_admin` now + carry `allowExport: true` explicitly. **Environment-authored sets are not**: any + custom set whose users export must be edited. `member_default` deliberately does + NOT carry the grant, so ordinary authenticated users lose export until an admin + grants it — that is the point of the flip, not an oversight. + + **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set + granting `true` grants export. `false` and unset are the same outcome; `false` + is authoring intent, not a veto, because permission sets are additive capability + containers (ADR-0090). + + **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no + longer confer export. Separating "may see all data" from "may take a bulk copy" + is the segregation-of-duties case the axis exists for. + + ### Also in this change + + - **spec** — a set carrying `allowExport` is now **high-privilege** + (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` / + `guest` audience anchors. Without this the opt-in was defeatable by binding an + export-granting set to `everyone`. One predicate, so the runtime anchor gate, + the `@objectstack/lint` security-posture rule and the install-time suggestion + surface all pick it up together. + - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so + `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It + explains as `read ∧ the export grant`: `object_crud` reports the conjunction + and attributes the granting set, while every data-shaped layer + (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed + as the `find` the export actually performs — asking the RLS compiler about an + `export` operation would match no policy and wrongly report "no RLS applies". + `readFilter` is surfaced for `export` as it is for `read`. + - **plugin-reports** — closes the reports side door (#3710). A report rendered + as `csv`/`json` is the same bulk copy of the same object, so it is gated by + the same `ISecurityService.canExport`. Enforced in `executeReport`, which the + interactive run, the ad-hoc run and the scheduled dispatch all funnel through; + `scheduleReport` additionally refuses at create time so an author is not told + at 3am. A schedule created while granted stops delivering once the grant is + revoked. `html_table` stays a read — it is a rendered view, not a bulk copy. + Deployments without `plugin-security` are unaffected (no permission sets + exist, so the axis does not apply). + +- f24cb83: feat(spec)!: dissolve the ObjectStackProtocol composition alias — ADR-0076 D9 end-state (v17, #3606) + + The transitional union of the twelve per-domain contracts (and its parallel + `ObjectStackProtocolSchema` Zod object + `ObjectStackProtocolZod` inferred + type, 171 schema lines) is removed. Capability availability comes from the + runtime discovery `services` registry — a static union was its degraded + snapshot (ADR-0076 rev.7 verdict). Depend on the narrowest per-domain slice + (`DataProtocol`, `MetadataProtocol`, …; composition precedent: REST's + `DataProtocol & MetadataProtocol`, A1.5/#3028). + `ObjectStackProtocolImplementation` now declares exactly the four domains it + actually provides (Data/Metadata/Analytics/Package) — the D10 "facade never + implemented the other domains" reality, now enforced by the type system. + BREAKING for anything importing the alias or the Zod schema; no runtime + behavior change. + +- 5dbbb92: release!: promote the accumulated launch-window train to v17.0.0 (RC cycle) + + Anchor changeset for the v17 major. The lockstep group applies the highest + bump across all pending changesets to every package, so this single `major` + promotes the whole train — every other pending changeset keeps its own + `minor`/`patch` declaration and its own narrative. + + **Why a major, when the launch-window policy ships breaking changes as + `minor`:** this train's breaking density is the highest since the policy was + adopted — the `ApiMethod` enum shrink (#3543, compile-time breaking for TS + authors), the GraphQL surface removal, the ADR-0104 field value-shape write + cutover, and the retirement of several dead spec clusters all ride together. + Publishing that set as a bare minor would auto-upgrade every `^16.x` consumer + into it on their next install. A major puts the version-number signal back: + caret ranges hold at 16.x until a consumer opts in. + + **RC cycle:** this lands inside Changesets pre-mode (`rc` tag), so the train + publishes as `17.0.0-rc.N` — nothing reaches `latest` until `changeset pre +exit`. Downstream validation during the RC window: cloud / objectui / + examples upgrade against the RC, the dogfood gate and the third-party + consumer gate (#2035) run against it, and legacy `apiMethods` strip warnings + are watched for the deny-all cliff. + + Migration: each breaking change's own changeset carries its FROM → TO guide + (grep the CHANGELOG for `!:` entries); the ApiMethod shrink additionally + ships a reporter codemod (`scripts/codemod/apimethods-legacy-to-primitives.mjs`). + +### Minor Changes + +- 08b5a3d: fix(action): one precedence for `target` vs the deprecated `execute` — lower the alias, then drop it (#3713) + + `execute` is the deprecated alias of `target`, and three readers resolved "the + author declared both" in **two opposite directions**: + + | Reader | Preferred | + | ------------------------------------- | --------- | + | `ActionSchema` transform (spec) | `target` | + | objectui `ActionRunner.executeScript` | `execute` | + | CLI compile step (`lowerCallables`) | `execute` | + + So `defineAction({ type: 'script', target: 'preferredHandler', execute: 'legacyHandler' })` + ran `preferredHandler` server-side and `legacyHandler` client-side — two + different scripts for one button, silently, with no error anywhere. Low + frequency (it needs an author to set both, which happens mid-migration or by + copy-paste), but the failure mode is "the wrong code ran". + + **`target` now wins everywhere, and the alias is removed from the parsed + output** — the same "canonical wins, alias disappears" shape as + `agent.knowledge.topics` → `sources`. The conflict is now _unrepresentable_ + rather than merely agreed-upon: no renderer can see a second slot to disagree + about. Worth noting the server runtime never read `execute` at all + (`isHeadlessInvokableAction` gates on `target || body`; dispatch probes + `target`/`name`), so authoring `execute` worked _solely_ because it was lowered + at parse time — dropping it costs the server nothing. + + The CLI's inline-handler lowering had the same bug in compile-time form: with a + function in both slots it bundled the `execute` one and then overwrote + `action.target` with that ref, silently discarding the function the author + declared on `target`. It now probes `target` first and drops the alias. + + **Authoring is unchanged** — `execute` is still accepted on input (`ActionInput`), + still lowered to `target`, and still listed in the reference docs. Nothing to + migrate in your app metadata. + + **Consumers of the parsed metadata**, however, must read the canonical slot: + + - FROM: `parsedAction.execute` → TO: `parsedAction.target` + - One-line fix: delete the alias fallback, e.g. `action.execute || action.target` + becomes `action.target`. + + `z.infer` no longer carries `execute`, so any such reader + fails to compile rather than silently reading `undefined`. The objectui + `ActionRunner` counterpart ships separately. + +- 4727eb8: feat(spec): reject unknown keys on an action param instead of stripping them (#3405) + + `ActionParamSchema` was zod-default `.strip`: any key it does not declare was + **discarded silently** and the param went on parsing. That is the mechanism + behind the `reference` bug — an author wrote a correct, clearly intended + `reference: 'sys_user'`, the key was eaten, and the param dialog rendered a text + box asking a human to paste a UUID. Adding `reference` fixed that one key; the + mechanism that swallowed it stayed, so the next mis-spelled key would fail the + same way, with the same zero feedback (ADR-0078 no-silently-inert-metadata, + ADR-0049 enforce-or-remove). + + An action param is now `.strict()`. An undeclared key is a parse error naming the + offending key, and — when the key is a recognisable spelling of a declared one — + the canonical key to use instead: + + ``` + Unrecognized key(s) on this action param: `reference_to`. Until #3405 these were + dropped silently — the param still parsed, so a mis-spelled config shipped as a + control that quietly ignored it. Did you mean `reference_to` → `reference`? + ``` + + **Migration.** A param that previously carried an extra key now fails to parse. + The fix is to correct or remove that key; the error names it. Common mappings — + case/underscore slips are matched automatically, these are the ones that need a + different word: + + | Wrote | Use | + | ----------------------------------------------- | -------------- | + | `reference_to` / `referenceTo` / `targetObject` | `reference` | + | `visibleWhen` / `visibleOn` / `visibility` | `visible` | + | `description` / `help` | `helpText` | + | `default` | `defaultValue` | + + Declared keys are unchanged: `name`, `field`, `objectOverride`, `label`, `type`, + `required`, `options`, `placeholder`, `helpText`, `defaultValue`, `multiple`, + `accept`, `maxSize`, `reference`, `defaultFromRow`, `visible`, `requiresFeature`. + +- fa3d0cf: feat(spec): field runtime value-shape contract — ADR-0104 phase 1 (D1) + + `@objectstack/spec/data` now owns the runtime VALUE shape of every field type + (`field-value.zod.ts`): semantic type classes (`STRING_VALUE_TYPES`, + `NUMERIC_VALUE_TYPES`, `REFERENCE_VALUE_TYPES`, `FILE_REFERENCE_TYPES`, + `STRUCTURED_JSON_TYPES`, `MULTI_CAPABLE_TYPES`, …), the shared + `isMultiValueField`, and `valueSchemaFor(field, 'stored' | 'expanded')`. The + four consumers that each hand-copied this knowledge (objectql record-validator, + rest import-coerce, driver-sql column classification, qa conformance) now + derive from the spec, and the field-zoo round-trip MATRIX is asserted against + the contract so the two cannot drift. + + **Write-path change (objectql, warn-first):** previously-unvalidated types — + single `lookup`/`master_detail`/`user`/`tree`, `file`/`image`/`avatar`/ + `video`/`audio`, `location`, `address`, `composite`, `repeater`, `record`, + `vector` — are now checked against the contract. A violation **logs a warning + and passes** in this release (legacy rows must not strand their records); + set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` to enforce as a + `400 VALIDATION_FAILED`. The flip to strict-by-default rides a later minor + (ADR-0104 R1/R2). + + **Deprecations (removal rides the next spec major), FROM → TO:** + + - `CurrencyValueSchema` (`{value, currency}`) → none. A `currency` field's + value is a **bare number** everywhere in the runtime (validator, SQL `float` + column, import coercion, field-zoo oracle); the currency code lives in field + config. Use `valueSchemaFor({type: 'currency'})`. + - `LocationCoordinatesSchema` (`{latitude, longitude}`) → `LocationValueSchema` + (`{lat, lng}`) — the shape the platform actually stores. + - `AddressSchema` is **adopted** (unchanged) as the enforced `address` value + contract via `AddressValueSchema`. + + No stored data changes shape; the contract codifies deployed reality + ("reality wins", ADR-0104 D1). + +- af5a224: feat: enforce declared action-param contract at dispatch — ADR-0104 phase 2 (D2) + + An action's declared `params[]` (`type` / `required` / `multiple` / `options` / + `reference`) was a complete value contract that only ever informed the client + dialog — the server passed `reqBody.params` straight to the handler unvalidated + (REST `handleActions` and the MCP `invokeBusinessAction` path), and handlers + read an untyped bag. D2 makes the declaration enforced and typed. + + - **`@objectstack/spec/ui`** now exports `validateActionParams` (+ + `ResolvedActionParam`, `ActionParamIssue`, `ACTION_PARAM_BUILTIN_KEYS`): a + pure check that validates a params bag against resolved param declarations, + reusing the D1 `valueSchemaFor` so option membership, `multiple` arrays and + reference-id shape all ride the one value contract. Also exports the typed + authoring surface `ActionHandler` / `ActionHandlerContext` / + `ActionEngineFacade` — annotate a handler with `ActionHandler` instead of + `(ctx: any)`. + - **Dispatch (runtime)**: both the REST and MCP action paths resolve the + action's declared params (field-backed params resolved through the referenced + object field) and validate the request bag **before the handler runs** — + required presence, per-type value shape, and unknown keys (the dispatcher's + own `recordId` / `objectName` are allowlisted). + + **Warn-first rollout (ADR-0104 R3).** A violation is **logged and passes** by + default — params that were silently wrong before keep working while the drift + becomes visible. Set `OS_ACTION_PARAMS_STRICT_ENABLED=1` to reject with a + `400 VALIDATION` (REST) / an error (MCP). Actions that declare no `params` are + untouched (nothing to validate against). The flip to strict-by-default rides a + later minor once telemetry is quiet. + + Not included: file/image params becoming `sys_file` references — that depends + on file-as-reference (ADR-0104 D3). Per-name static typing of `ctx.params` from + the literal `params` array is a deferred DX nicety; the runtime guarantee holds + regardless. + +- 71f76e1: feat(spec): declared media value shape — ADR-0104 D3 wave 1 (file/image/avatar/video/audio) + + `@objectstack/spec/data` now exports `FileValueSchema` — the declared inline + form the platform stores today for the whole `FILE_REFERENCE_TYPES` class + (`file` / `image` / `avatar` / `video` / `audio`): `{ url, name?, size?, +mimeType?, alt?, duration? }` with `url` required. It replaces D1's loose + transitional union, so `valueSchemaFor(fileField, 'stored')` now catches a + malformed media value (a number, an empty object, a url-less `{ name }` + fragment) that was previously waved through as an opaque payload — while still + admitting the opaque id/url string form for import compatibility. + + This is **wave 1** of ADR-0104 D3 (see the 2026-07-24 addendum): the value-shape + contract only. It is single-repo, additive, and carries no migration — the + enforcement rides D1's existing warn-first write-path posture, so deployed + records with a legacy media value are not stranded. `accept` / `maxSize` field + config, the `sys_file` reference storage model, GC, and governed download are + **wave 2** (a protocol-major migration), deliberately not in this change. + +- 99736a0: feat(storage): exclusive field-reference file ownership — ADR-0104 D3 wave 2 (PR-3) + + A `file`/`image`/`avatar`/`video`/`audio` field that holds a `sys_file` id now + records its owner on the file: `sys_file.ref_object` / `ref_id` / `ref_field` + name the single `(object, record, field)` slot that references it, maintained on + the engine write path — claimed on insert, reconciled on update, released when + the owning record is deleted. + + **Field references are exclusive, unlike attachments.** The attachments surface + deliberately shares one file across many `sys_attachment` join rows; a field + reference is owned by at most one slot, and writing an already-owned id into a + second slot **copies the bytes into a fresh `sys_file`** rather than sharing the + row. That keeps a file's read authorisation derived from exactly one parent + record instead of the union of every referrer's — so copying a private record's + file id into a world-readable one cannot silently widen access — and it removes + reference counting from the lifecycle entirely: a file is released because its + one owner let go, never because a count came back zero. + + **Deletes nothing.** This records and releases ownership; it never tombstones, + and the `scope === 'attachments'` guardrail that keeps field-referenced files + out of the reap is untouched. Collection is a separate, gated change that must + also extend the reap guard's sweep-time re-verify in the same commit. + + Also exports `isFileIdToken` from `@objectstack/spec/data` as the single arbiter + of "is this stored string an opaque file id, or a legacy/external URL?", now + shared by the read resolver and the write claimer so the two cannot drift. + + Dormant until a field actually holds an id token: objects without file-class + fields, inline-blob values and URL-shaped values all exit before any I/O. + +- fe67e34: feat(spec)!: media fields declare accept/maxSize, and the stored form is a file reference — ADR-0104 D3 wave 2 (PR-5a) + + **`accept` and `maxSize` are now declared on `FieldSchema`, and enforced on the + server.** Both were already read by the upload widgets — `field.accept`, + `field.maxSize` — while the spec did not declare them, so an author who wrote + them had the keys silently stripped at parse and the constraint simply never + existed. That is exactly the ADR-0104 failure class (a declaration accepted in + source, dropped from the contract, with no feedback). + + Now that the platform owns the file, `sys_file` carries the authoritative MIME + type and byte size, so a record write is re-checked against the declaration + where it actually binds rather than only in the browser — a client-side check is + a convenience, not a control, since any caller talking to the API directly + bypasses it. Violations raise `FileConstraintError` and fail the write. An entry + is only judged against metadata the file actually reports: a file with no + recorded MIME type cannot fail an `accept` test, and one with no recorded size + cannot fail `maxSize` — "we don't know" must not become "not permitted". + + **The stored form of a media field narrows to an opaque `sys_file` id.** + `valueSchemaFor(field, 'stored')` now yields an id for `file`/`image`/`avatar`/ + `video`/`audio`; the inline `{url, name, size, …}` blob becomes the `'expanded'` + read form, which also still admits an unresolved id (storage service absent, + file not committed) exactly as an unexpanded lookup id stays valid. + + Two legacy forms therefore stop conforming, both deliberately: + + - the **inline blob**, which is no longer stored but derived; + - an **external URL**, which was never a managed file — ADR-0104 R7 retires it + toward an explicit `url` field, and under AI authoring that is the point: it + stops "managed file" and "external link" being the same declaration. + + **Not a breaking change today.** Value-shape checking is warn-first + (ADR-0104 R1/R2): a not-yet-backfilled row still writes and the author gets a + warning naming the field. Hard rejection arrives only when a deployment opts + into `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` — which it should do after running the + backfill and confirming reconciliation. The `!` marks the contract change for + the v17 window, not a runtime break on upgrade. + +- fdb4f50: feat(migrate): `os migrate files-to-references` — a data migration with a self-check, gated per deployment (#3617) + + The ADR-0104 file-as-reference migration ships as a command a deployment runs + against its own database, and the deployment-level flag it records is what may + later authorise irreversible behaviour — never the platform version. + + ```bash + os migrate files-to-references # dry run: reports, writes nothing + os migrate files-to-references --apply # converts, verifies, records the flag + ``` + + The run backfills legacy file-field values (inline metadata blobs, own-resolver + URLs, `data:` URIs) into owned `sys_file` references, reconciles the ownership + ledger against what records actually hold, and — only on an `--apply` run whose + reconciliation reports **zero blocking discrepancies** — records + `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. + + **Why a flag rather than a release note.** ObjectStack is a development + platform: third-party deployments upgrade on their own schedule and their data + is not observable by anyone else, so no release-side soak can vouch for them. + The evidence has to be produced where the data is. Consequences: + + - Installing a new version never starts deleting bytes. Running the migration + and passing its self-check is the consent. + - Not run, or not passed → files are retained forever. Wasted storage, zero + data loss. + - A later failing run **clears** `verified_at`: a deployment whose data has + drifted closes its own gate. + - A dry run writes nothing at all — not the conversions, and not the flag, + even when the self-check would pass. + - External URLs stay advisory. They are not `sys_file`s, so they can never + enter collection; whether to remodel them as a `url` field is the app + author's decision (ADR-0104 R7), not a gate. + + Ships alongside: + + - `@objectstack/spec` — `DataMigrationFlagSchema`, `FILE_REFERENCES_MIGRATION_ID`, + and the single `isDataMigrationFlagVerified` predicate both future consumers + (collection #3459, strict value-shape #3438) read, so the two gates cannot + disagree about the same fact. + - `@objectstack/platform-objects` — the `sys_migration` object plus + `readDataMigrationFlag` / `isDataMigrationVerified` / `recordDataMigrationRun`. + Reads fail toward "not verified": a gate that cannot read its evidence stays + closed. + - `@objectstack/objectql` — a read may now opt out of file-reference expansion + via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`, and the storage service's + bookkeeping/scan reads do. Without it the read resolver rewrites stored ids to + their expanded form before the reconciliation sees them, which reports held + references as absent — noisy `stale_owner` findings, and a missed + `unowned_reference` would have been a false pass of the collection gate. + +- 1bd5652: feat(auth): give ADR-0105 D8's scope-bounded issuance a caller — the + `delegated_admin` org role, capped so it cannot mint authority (#3697) + + D8 authorizes invitation _placement_ against the issuer's `adminScope` + (ADR-0090 D12), so a delegated plant admin may invite only into their own + subtree. That gate is implemented, unit-proven and reachable — but no principal + could reach it in a state where it did anything: + + - better-auth grants `invitation: ["create"]` to `owner` and `admin` only + (`memberAc` holds `invitation: []`, and roles registered through + `additionalOrgRoles` inherited that empty statement); + - under a wall-enforcing posture, owners and admins are auto-elevated to + `organization_admin` (`auto-org-admin-grant.ts`), which carries the wildcard + `modifyAllRecords` that makes `isTenantAdmin()` true — and the gate + short-circuits on tenant admins. + + The two sets were disjoint. Issuance placement was bounded by the Layer 0 org + wall (real, and correct) but never by `adminScope`, so D8's motivating story — + "a plant admin invites into their own subtree without a platform admin + finishing the job" — could not happen. + + **Two pieces, and they only ship together.** + + **1. The role.** `delegated_admin` is now registered with the organization + plugin as `memberAc.statements` plus `invitation: ["create"]` — the one + membership grade that may reach `/organization/invite-member` without being an + org admin. Deliberately _not_ `invitation: ["cancel"]`: better-auth's cancel + route checks the permission with no inviterId attribution, so it would mean + "cancel anyone's pending invitation in the org". + + The role carries no ObjectStack authority by construction — `mapMembershipRole` + passes it through as a position name, and with no `sys_position_permission_set` + binding that name resolves to nothing. Role = _can reach the endpoint_; + `adminScope` = _what the endpoint permits_. + + `sys_member.role` and `sys_invitation.role` each gain `delegated_admin` as a + fourth option. Those selects are **enforced on write** — better-auth's own + invitation and membership inserts are validated like any other row — so + registering the role with the org plugin without listing it in both would have + produced a role nobody could hold and nobody could hand out + (`ValidationError: role must be one of: owner, admin, member`). That is exactly + how the end-to-end regression caught it, twice; neither unit test could. The + three non-English translation bundles carry the English label for the new option + until localized. + + **2. The role cap**, in the framework's own `beforeCreateInvitation` hook, + beside the D8 placement gate. Registering the role alone would have been a + four-step privilege escalation: better-auth's only role-level cap on _what role + you may invite someone as_ is its `creatorRole` check (default `owner`), which + blocks inviting an **owner** but not an **admin** — and an accepted `admin` + membership is auto-elevated to `organization_admin` → `isTenantAdmin()`. A + subtree-scoped delegate could have manufactured a tenant admin, with every + existing defense off the path (`sys_member` is not a `GOVERNED_OBJECT`, and the + acceptance-time membership write runs under better-auth's context, not the + issuer's). + + The cap refuses an invitation whose role outranks the issuer's own, and + restricts a below-admin issuer to plain `member` — not merely "not admin/owner", + because an app-registered role projects into `current_user.positions` and may be + bound to permission sets, making it a capability channel too. A delegate's + channel for capability is the invitation's _placement_ intent, which the D12 + gate allowlists position-by-position. The cap applies to every invitation, + placement-carrying or not (the escalation is independent of placement), and + fails closed: an issuer role that cannot be resolved confers nothing above a + plain member. + + **What changes for deployments.** One new class of principal exists: members + holding the `delegated_admin` org role, who can invite into the org — as + `member` only, into the subtree their `adminScope` allows. It is opt-in twice + over (someone must set the membership role _and_ grant an adminScope set), so a + default deployment changes not at all. Org owners and admins are unaffected. + + Also exported: `MEMBERSHIP_ROLE_DELEGATED_ADMIN` from `@objectstack/spec`, so + console and control-plane surfaces name the role from one place. + +- 7fb436c: Multi-organization operation is an ENTITLEMENT again: the `group` posture no + longer activates without the enterprise runtime (ADR-0105 D12 correction). + + The first ADR-0105 wave read D12 as "the `group` wall ships open" and made the + posture self-activating — it never probed for `@objectstack/organizations`. That + turned `group` into a free multi-org path around the `isolated` gate (ADR-0081 + D2), and made the weaker isolation the free one, which is not a boundary anyone + would draw on purpose. + + The distinction that was missed: **open code is not free activation.** The wall's + implementation has always lived in the open packages — that is equally true of + `isolated`, whose Layer 0 wall sits in `plugin-security` and is gated on a + service the enterprise package registers. Cloud ADR-0016's 铁律 + (强制免费、治理收费) guarantees that a deployment RUNNING a multi-org shape is + safe; it is satisfied by REFUSING to run one unwalled, not by giving the posture + away. + + ## Changes + + - **`tenancy-service`**: `group` probes `org-scoping` exactly like `isolated`. + Without it the posture resolves to `single` and reports `degraded`. + - **`os serve`**: the ADR-0093 D5 boot guard keys off the resolved POSTURE + instead of `OS_MULTI_ORG_ENABLED`. Previously `OS_TENANCY_POSTURE=group` skipped + both the enterprise package load AND the fail-fast, silently degrading to an + unwalled deployment — the exact ADR-0049 class that guard exists to close. A + `group` request without the runtime now refuses to boot unless + `OS_ALLOW_DEGRADED_TENANCY=1`. + - **New seam — the runtime declares what it entitles.** `org-scoping` may expose + `supportedPostures` (`OrgScopingEntitlement`, `@objectstack/spec/security`); + the open side honours it and fails closed on anything not listed. Whether + `group` and `isolated` are one commercial tier or two is packaging policy, and + packaging policy belongs to the commercial runtime rather than hard-coded in + open core. Omitting the field entitles every walled posture, so existing + runtimes are unaffected. + - **`organization_id` stamping returns to the enterprise runtime.** The previous + wave moved auto-stamping into the open engine; that removed the closed + package's only load-bearing runtime duty, so a five-line forged `org-scoping` + registration would have produced a fully working multi-org deployment. With + stamping back where it was, a forged registration yields NULL-org rows the wall + hides — a broken deployment, not an unlicensed working one. + + **Write-side VALIDATION stays open and is unchanged**, including the + bulk-insert coverage: rejecting a forged `organization_id` is a security + property, not a packaging one. Only filling an ABSENT value moved back. + + - Default-organization bootstrap returns to `single`-only; every walled posture + keeps its existing owner (ADR-0081 D1). + + ## Note for operators + + `OS_TENANCY_POSTURE=group` without `@objectstack/organizations` installed now + **refuses to boot** rather than running single-org. This only affects + deployments that adopted `group` between the two waves. + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 587fc91: feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt + + The analytics→engine bridge now forwards the request's `ExecutionContext` to + `engine.aggregate`, so the engine's own middleware chain scopes analytics reads + independently of the analytics layer's `getReadScope`. + + **Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing + forced the bridge to pass it — and it did not. An authenticated aggregate + reached the engine with no principal, plugin-security's principal-less fall-open + skipped its RLS injection, and the only thing left scoping the query was the + strategy remembering to call `getReadScope`. #3597 was a strategy that did not, + and both belts were off at once. + + `getReadScope` stays: the two resolve scope through different paths (engine + middleware vs `security.getReadFilter`), and a deployment without + plugin-security has only the analytics layer. This is depth, not a replacement. + + - `StrategyContext` gains `context?: ExecutionContext`, bound per call by + `AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`. + - `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` / + `AnalyticsService` `executeAggregate` config options gain `context?: +ExecutionContext`. **Custom bridges should forward it** to their engine; the + built-in auto-bridge does. Purely additive — an existing bridge that ignores + it keeps working exactly as before. + - `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain + an optional trailing `context`, beside the `scope` / `resolveScope` that + #3639 added — the same two-belt split as the aggregate path. + - `BootOptions.analytics` (`@objectstack/verify`) overrides the + AnalyticsServicePlugin instance, so a gate can boot with the analytics belt + off and assert the engine-side belt alone still scopes. + + **Also fixed on the same seam:** + + - `fetchRecordLabels` — the dimension display-label lookup — is row-granular + (one row per record, real display names). #3639 gave it the analytics-layer + belt (the referenced object's own read scope); it now also carries the + context, so the engine scopes the same read independently. + - `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the + `/analytics/sql` preview read as an unscoped table scan while the real + aggregate was scoped. It now renders the caller's filters and the read scope. + The preview never executed, so this was misleading output rather than a leak. + +- ad4af62: feat: single-source API-method derivation — the server is the only adjudicator (#3391) + + An object's effective API surface is now resolved from **six primitives** + (`get/list/create/update/delete/bulk`) by ONE derivation table in + `@objectstack/spec/data` (`resolveEffectiveApiMethods` / `isApiOperationAllowed` + / `effectiveOperationsArray` / `API_METHOD_DERIVATION`). Every gate consumes it: + the REST data surface, the runtime HTTP/MCP dispatcher, and the + `/me/permissions` annotation. The `apiMethods` whitelist is three-state — + `undefined` = unrestricted, `[]` = deny-all, a subset = the derived closure — and + the legacy 8 verbs (`upsert/aggregate/history/search/restore/purge/import/ +export`) are DERIVED from the primitives, never declared standalone. (This + release also ships the enum shrink — see the `#3543` changeset: the authored + enum IS the six primitives, and a stored legacy value is stripped at parse + with a warning rather than honored.) + + **Derivation:** `import` ⊆ create∨update (writeMode-precise: insert→create, + update→update, upsert→create∧update); `export` ⊆ list (reserved user-export slot, + always on this phase); `aggregate`/`search` ⊆ list (search also needs + `searchable`); `history` ⊆ get ∧ `trackHistory`; `upsert` ⊆ create∧update; + bulk sub-ops ⊆ bulk ∧ derived(child). `restore`/`purge` do not derive (the + `enable.trash` flag was retired, #2377). + + **New response-side contract:** `EffectiveObjectPermissionSchema` extends + `ObjectPermissionSchema` with an optional `apiOperations` array; + `GetEffectivePermissionsResponse.objects` uses it, and `/me/permissions` now + hands down the per-object effective operation set. The authoring + `ObjectPermissionSchema` is deliberately NOT extended — the frontend consumes + the effective set the server resolves, never the raw whitelist. + + **Behavior changes (tightening — a `declared ≠ enforced` gap closed):** + + 1. `apiMethods: []` + `apiEnabled: true` now denies every operation (405), + matching the documented three-state contract instead of the prior fail-open + "no restriction". In-repo impact is zero (every `[]` object also sets + `apiEnabled: false`, so 404 precedes 405). + 2. The runtime dispatcher / MCP whitelist is now live. It previously read the + flat shape while `getObject()` returns the flags nested under `.enable`, so + the gate never fired — a silent dead gate now enforced (nested-first, + flat-compatible). + 3. `import`/`export` reverse-derive: an object with a plain CRUD whitelist (no + explicit `import`/`export`) now admits import (⊆ create∨update) and export + (⊆ list). Row-level FLS is shared with list; the export column header is now + projected to the FLS-readable set so it can never expose a wider column set + than list (previously a masked column leaked its name as an empty column). + 4. The bulk surfaces (`createMany`/`updateMany`/`deleteMany`, per-object + `/batch`, cross-object `/batch`) now require the `bulk` primitive AND the + child write (`bulk ∧ child`). The four in-repo explicit-whitelist objects + (`sys_user`, `sys_user_preference`, `sys_business_unit`, + `sys_business_unit_member`) gained `bulk`; a third-party object with an + explicit write whitelist that omits `bulk` will now 405 on the Many/batch + routes. + 5. The 405 body's `allowed` array is now the derived EFFECTIVE operation set + (enum-ordered), not the raw whitelist. + +- d44dbfa: feat(spec)!: shrink the `ApiMethod` enum to the six primitives — legacy values are stripped at parse, never honored (#3543, P2 of #3391) + + **BREAKING** (the `!` marker and this changeset are the breaking-change + record; the train ships as the v17 major — see the `v17-rc-anchor` changeset): + the authored `enable.apiMethods` enum is now exactly the six + primitives (`get`, `list`, `create`, `update`, `delete`, `bulk`). The eight + legacy values (`upsert`, `aggregate`, `history`, `search`, `restore`, `purge`, + `import`, `export`) are no longer authorable — they are DERIVED effective + operations, resolved by the server's single derivation table. + + **Migration (FROM → TO).** Replace each legacy value with the primitives it + derives from, then de-duplicate; if the result names all six primitives, delete + the `apiMethods` key entirely (equivalent to default-open, and it tracks future + primitives): + + | FROM (legacy) | TO (primitives) | why | + | ------------- | -------------------- | ---------------------------------------------- | + | `upsert` | `create`, `update` | upsert ⊆ create ∧ update | + | `import` | `create`, `update` | import ⊆ create ∨ update (writeMode-precise) | + | `export` | `list` | export ⊆ list | + | `aggregate` | `list` | aggregate ⊆ list | + | `search` | `list` | search ⊆ list ∧ `searchable` | + | `history` | `get` | history ⊆ get ∧ `trackHistory` | + | `restore` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + | `purge` | _(delete the value)_ | never derives — `enable.trash` retired (#2377) | + + Reporter codemod: `node scripts/codemod/apimethods-legacy-to-primitives.mjs` + (scans, reports the exact replacement per site, and flags whitelists the + mapping would WIDEN so the edit stays reviewable). + + **Stored metadata keeps parsing — permanent tolerance, narrowing only.** Real + metadata does not upgrade in lockstep with the spec, so a stored legacy value + is NOT a parse error: `stripLegacyApiMethods` (new export) strips it with a + FROM→TO warning (canonicalize-and-warn). Stripping only ever NARROWS exposure — + the derivation table still grants every legacy verb that derives from the + primitives you declared. Two cliffs to know: + + 1. A whitelist of ONLY legacy values (e.g. `['upsert']`) strips to `[]` = + **deny-all** — the object's API closes instead of widening. The strip + warning and the objectql registration diagnostic both call this out. + 2. A legacy value NOT derivable from your declared primitives (e.g. + `['get', 'export']` — export needs `list`) was honored by the P1 + "explicit wins" path and is now denied. Declare the underlying primitive. + + **Type split — authored vs effective vocabulary.** `ApiMethod` (authored) is + now six values; the NEW `ApiOperation` type / `ApiOperationSchema` / + `API_OPERATION_ORDER` (fourteen values, byte-stable pre-shrink wire order) + carry the EFFECTIVE vocabulary. The wire contract is unchanged: the 405 + `allowed` array and `/me/permissions` `apiOperations` still serialize derived + verbs (`export`, `search`, …), and `EffectiveObjectPermissionSchema.apiOperations` + now validates against `ApiOperationSchema`. `EffectiveApiMethods.explicitLegacy` + is removed (nothing is honored verbatim anymore); `API_METHOD_ORDER` remains as + a deprecated alias of `API_OPERATION_ORDER`. + + **Fail-closed tightening (#3545):** a PRESENT but non-array `apiMethods` (only + producible by a raw/out-of-band metadata write) now resolves to `deny-all` + instead of unrestricted — a policy that exists but cannot be read fails CLOSED. + + **Published JSON Schema diverges deliberately:** `data/ApiMethod.json` is the + strict six-value enum (a `z.preprocess` is not representable in JSON Schema), + so external JSON-Schema validators reject legacy values that the zod parse + would strip-and-warn. Treat the JSON Schema as the authored contract; the zod + tolerance exists for stored metadata. + + **objectql:** the P1 "explicit wins" transition is reclaimed — + `warnDeprecatedExplicitApiMethods` is replaced by `warnStrippedLegacyApiMethods` + (a permanent per-object diagnostic for schemas that reach the registry without + passing through Zod; the parse-time strip warning carries no object name). + + **platform-objects:** whitelist audit — `sys_business_unit`, + `sys_business_unit_member` (P1's explicit `import`/`export` reclaimed) and + `sys_user_preference` dropped their `apiMethods` entirely (each named all six + primitives = default-open). Read-only and deny-all whitelists are unchanged; + the seven `[]` declarations are deliberately KEPT as defense-in-depth alongside + `apiEnabled: false`. + +- 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508) + + - `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a + designer must source each approver row's `value`: `user`/`team`/`department`/`position` + are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` / + `sys_business_unit` / `sys_position`; `position` commits the machine **name**, the + others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`), + `manager` is auto-resolved, `field` names a trigger-object field, and `queue` is + unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`. + - `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep + loading and rendering) but is published in `xEnumDeprecated`, so designers stop + offering it — the runtime has no queue resolution and the slot routes to nobody. The + approver `value` xRef now also maps `manager`, so designers can render its + auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries + `{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete + `user`) until a real ownership-queue implementation lands. + - `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue` + approver is skipped. + - `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver + types that are declared but not implemented by the runtime. + +- db02d47: **BREAKING** `ChartInteraction` drops `zoom` and `clickAction`; `stepSize` / `description` / `height` are delivered (issue #3752) + + The tail of the declared-≠-delivered sweep from #3729. Five `ChartConfig` props + reached the renderer and did nothing; each got the ADR-0078 call — honor it, or + remove it. Three were honored (objectui#2885), two are removed here. + + **Removed — `ChartInteraction.zoom` and `ChartInteraction.clickAction`.** Both + were redundant against something the platform already delivers, which is why + neither had a consumer anywhere in the framework, the console, the showcase, or + the skill corpus: + + - `zoom` had no renderer primitive behind it, and `brush` already narrows a + range. **Migration:** `interaction: { brush: true }`. + - `clickAction` competed with two click owners that _do_ work — `drillDown` + (opens the filtered records, which is what a segment click is almost always + for) and, in the react tier, the host's own `onSegmentClick`. A third, silent + owner only invited authors to wire a click that never fired. + **Migration:** `drillDown`, or handle the click in React. + + `ChartInteraction` is now `{ tooltips, brush }` — both honored. This follows the + #1475 precedent: trim what cannot be cleanly delivered, implement the rest, and + leave nothing declared-but-inert in between. + + **Delivered — `ChartAxis.stepSize`, `ChartConfig.description`, `ChartConfig.height`** + (objectui#2885). `description` and `height` join ``'s published + `dataProps` now that they do something; `stepSize` rides along inside + `xAxis`/`yAxis`. Their schema descriptions say what they actually do rather than + restating their names. + + Breaking, but shipped as `minor` per the launch-window convention (see + `scripts/check-changeset-no-major.mjs`). Off-spec `zoom`/`clickAction` keys are + stripped by Zod rather than rejected, so no stored metadata fails to parse — the + break is at the TypeScript type level for anyone constructing a + `ChartInteraction` in code. + +- 7c7e246: feat(authz): expose the caller's delegable scope — the read half of the + delegated-administration gate (ADR-0090 D12 / ADR-0105 D8) + + `adminScope` decided writes but could not be READ: `assignablePermissionSets` + lived only inside `delegated-admin-gate.ts`, so a UI offering "place this + person in a unit, with these positions" (the D8 scoped-invitation form) had no + way to narrow its pickers. It would list the whole tree and let the user + discover the boundary by being refused — which turns an authorization gate into + a validator and makes the boundary invisible until it bites. + + `ISecurityService.describeDelegableScope(callerContext)` answers it, exposed as + `GET /api/v1/security/my-delegable-scope` and `client.security.describeDelegableScope()`: + + - `placeableBusinessUnitIds` — union of the subtrees where the caller may place + people (scopes granting `manageAssignments`); + - `assignablePositions` — positions whose every distributed permission set the + caller may hand out (containment check included); + - `scopes` — the held `adminScope`s with subtrees resolved, for attribution; + - `isTenantAdmin` — unconstrained, with everything enumerated so a consumer + renders ONE uniform picker instead of special-casing. + + Computed by the same helpers the write gate enforces with, so an option this + reports is one `assert()` accepts — a test asserts that agreement directly. It + NARROWS; the gate still decides. + + Strictly self-scoped: no target-user parameter, so it discloses nothing beyond + the authority the caller already holds (unlike `explain`, which has one and + gates it). Fail-closed — unresolvable scopes contribute nothing, a caller with + no delegated authority gets empty lists, and a deployment without + `@objectstack/plugin-security` gets 501. + +- c2d9098: feat(rest/protocol): extend droppedFields write-observability to the bulk paths + client SDK (#3455) + + Follow-up to #3448 (#3431 D2): the single-write PATCH/POST `/data` paths already + surface LEGALLY-stripped write fields (static `readonly` #2948 / `readonlyWhen` + #3042 / #3043 create ingress) as `droppedFields`. The **bulk** write paths did + not — the same strips happened silently on every batched row — and the typed + client warning + CORS mirror were deferred. This closes those out. + + **Bulk passthrough (metadata-protocol).** + + - `updateManyData` and `batchData` (update/upsert rows) now register a per-row + `onFieldsDropped` collector and attach the events to that row's result. + - `createManyData` diffs each supplied row against its #3043-stripped form and + returns an **aggregated** top-level `droppedFields` (one event per + object/reason with the union of field names) — its `{ records, count }` + response has no per-row slot, and the insert-time strip is static-`readonly` + only, so it is schema-uniform across rows and the aggregate is faithful. + - `insertManyData` keeps per-row precision, attaching `droppedFields` to each + outcome. + - **Correctness fix bundled in:** `updateManyData` and `batchData` never threaded + the caller's execution `context` to the engine — bulk writes ran context-less, + so RLS/FLS and `readonlyWhen` evaluated without the caller's principal, and the + batch create-ingress strip was hard-coded to a non-system context. All engine + calls in both methods now run under the resolved `context`. + + **Contract (spec).** `BatchOperationResultSchema` gains an optional per-row + `droppedFields` (covers `updateMany` + `batch`, which alias + `BatchUpdateResponseSchema`); `CreateManyDataResponseSchema` gains the optional + aggregated `droppedFields`. Both are omit-when-empty, so existing clients are + unaffected. `X-ObjectStack-Dropped-Fields` is deliberately **not** emitted for + batches — one response header cannot express per-row drops, so the per-row body + field is the canonical bulk channel. + + **Typed client warnings (@objectstack/client).** `CreateDataResult` / + `UpdateDataResult` gain `droppedFields?: DroppedFieldsEvent[]`, giving the body + channel a type instead of an untyped property. + + **CORS (@objectstack/hono, @objectstack/plugin-hono-server).** + `x-objectstack-dropped-fields` is added to the default `Access-Control-Expose-Headers` + allow-list (kept in lockstep across both Hono CORS sites) so a cross-origin + browser can read the single-write drop header. The body `droppedFields` remains + the primary, cross-origin-safe surface — this is a convenience mirror. + + **GraphQL — not applicable (documented).** #3455 lists a GraphQL mutation item, + but GraphQL has no runtime: `kernel.graphql` is unassigned everywhere and + `handleGraphQL` returns `501`, and discovery never advertises `/graphql`. There + is no schema generator or mutation resolver to expose a typed payload field on, + so there is nothing to wire until a GraphQL engine lands — at which point the + protocol-layer `droppedFields` is already present and only the GraphQL schema + projection would remain. + +- 9613396: feat(security): ENFORCE the user-level export axis on the server (#3544) + + `allowExport` landed as a spec bit plus a `/me/permissions` annotation, which + hid the client's Export button — and nothing else. Because `export ⊆ list`, the + REST export route streams through `findData` and the engine middleware sees an + ordinary `find` gated by `allowRead`, so no code path ever read the bit: a caller + holding `allowExport: false` could still `curl +/api/v1/data/:object/export` and drain the whole table. Declared, not enforced. + + - **plugin-security** `PermissionEvaluator.checkObjectPermission('export', …)` is + now a real decision: `export` = read granted ∧ not explicitly denied. + `allowExport` stays out of `OPERATION_TO_PERMISSION` on purpose — that map + means "the bit must be truthy", which would have denied export to every + permission set authored before the axis existed. The new exported + `resolveUserExportAllowed()` folds the tri-state across sets (`true` beats + `false` beats unset) exactly as the `/me/permissions` merge does. + - **spec** `ISecurityService` gains `canExport(object, context)` — the question a + bulk-egress door outside the engine middleware has to ask before it reads. + Fails CLOSED; `isSystem` and an empty set resolution bypass, mirroring the + middleware. + - **rest** `GET /data/:object/export` calls it and answers **403 + `EXPORT_NOT_PERMITTED`** before the first chunk is fetched. Distinct from the + object-level 405 `OBJECT_API_METHOD_NOT_ALLOWED`, which still runs first: 405 + says the object exposes no export, 403 says this caller may not use it. No + security service (no `plugin-security` ⇒ no permission sets) → allowed, the + same fail-open posture as every other permission gate in that layer; service + present but unable to answer → denied. + - **plugin-hono-server** the `/me/permissions` annotation now falls back to the + `'*'` entry's export bit when a per-object entry declares none, matching the + evaluator's own wildcard fallback — so a set that denies export wholesale via + `'*'` no longer offers a button the server refuses. + + Backward-compatible: `allowExport` is still an opt-out with no default, so an + unset bit inherits read and existing permission sets behave exactly as before. + Only a permission set that explicitly sets `allowExport: false` changes — and it + now changes on the server, which is the point. + + Implementers of `ISecurityService` outside this repo must add `canExport`; the + interface member is required, matching how `getReadableFields` was added. + Consumers still feature-detect (`typeof svc.canExport === 'function'`), so a + partial implementation degrades rather than throwing. + +- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities: + + **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate. + + **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals. + + **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `.` variables — a later approval node's expression can read `vars..picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot). + + Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time. + +- f5a2320: fix(field): fold the deprecated `conditionalRequired` alias into `requiredWhen` and drop it from the parsed output (#3754) + + Second instance of the alias-drift shape #3713/#3742 fixed for `action.execute`. + `requiredWhen` is canonical and `conditionalRequired` is its documented deprecated + alias, but `FieldSchema` had **no canonicalization at all** — both keys stayed live + in the parsed output, so every consumer had to re-implement the precedence. That is + exactly the condition that produced #3713, where the server kept `target` while + objectui's renderer preferred the alias and one button ran two different scripts. + + Worse, the alias surviving parse was **test-pinned**, including a case literally + named _"requiredWhen and its alias conditionalRequired can coexist"_ — the inverse + of the contract #3742 had just established one field over. + + `FieldSchema` now lowers `conditionalRequired` into `requiredWhen` at parse time and + removes the alias from its output; `requiredWhen` wins when both are declared. The + pinning tests are inverted accordingly, and a new case asserts the alias is gone + from a field parsed through `ObjectSchema` — the path a renderer actually receives, + not just a bare `FieldSchema.parse()`. + + No live bug is being fixed here: every reader we can see already prefers the + canonical key (`rule-validator.ts` reads `requiredWhen ?? conditionalRequired`). The + point is that nothing in the contract _made_ that right. This is hardening — it + removes the chance rather than a defect. + + `objectql`'s `requiredWhen ?? conditionalRequired` fallback is kept on purpose: + `evaluateValidationRules` is also handed raw, unparsed field definitions, which still + carry the alias. + + **Authoring is unchanged.** `conditionalRequired` is still accepted on input, still + lowered, still listed in the reference docs and JSON Schema. Nothing to migrate in + app metadata. + + **Consumers of the parsed metadata** must read the canonical slot: + + - FROM `parsedField.conditionalRequired` → TO `parsedField.requiredWhen` + - One-line fix: `field.conditionalRequired || field.requiredWhen` becomes + `field.requiredWhen` + + `z.infer` no longer carries `conditionalRequired`, so a stale + reader fails to compile rather than silently reading `undefined`. A new + `FieldParseInput` (`z.input`) names the author-facing shape that + still accepts the alias — distinct from the pre-existing `FieldInput` factory-helper + type, which is `Partial` and unrelated. + +- deb538f: fix(storage): let an object delegate file-read authorization to its service + + Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a + **legitimate approver could see a decision attachment's filename but got 403 + opening it**, found by driving app-showcase in a browser as a real non-admin + approver. + + Cause: a field-owned file's download was authorized by testing whether the + caller can READ the owning row. For an ordinary business object that is right — + row readability _is_ the access rule. For `sys_approval_action` it is the wrong + authority: the audit table is deliberately closed to ordinary approver + positions (`operation 'find' … is not permitted for positions [auditor, +everyone]`), so the test denied the very approver the attachment was filed for. + The approvals _service_ has always had the real rule, which is why the timeline + listing the attachment returned 200 while the bytes returned 403. + + An object may now name a service to answer the question instead: + + - `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes + downloads of files owned by that object's media fields. + - `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract. + - `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead` + reuses the _same_ gate `listActions` applies (visibility of the parent + request) rather than inventing a second, looser rule for the bytes. + + **Fails closed**: a declared delegate that is missing or does not implement the + method denies, rather than silently reverting to the raw read it was declared to + replace. Objects without the declaration are unchanged. + + Verified in the browser against app-showcase, both sides of the gate: the + approver now downloads the real PDF (200), and an anonymous request is still + refused (401) — the anonymous capability URL the original change closed stays + closed. A decision attachment ends up exactly as readable as the decision it + hangs off: never more, and no longer less. + +- 9e2caf3: feat(spec): codify the IHttpServer soft extensions and unmatched-request semantics (#3607, ADR-0076 OQ#10 follow-up) + + Three behaviors every adapter already implements — locked until now only by + the `@objectstack/http-conformance` cross-adapter suite — become formal + contract on the interface: `IHttpResponse.write`/`end` (SSE/chunked + streaming: headers flush on first write, no buffering until end, `end` + required wherever `write` exists), `IHttpServer.getPort()` (real bound port + after `listen()`, incl. ephemeral `listen(0)`), and the unmatched-request + semantics (path-miss → 404 with the shared not-found body; method-miss → + 405 with an `Allow` header). All members optional with feature-detect + guidance — zero behavior change, both adapters already conform; the + conformance assertions now cite the contract instead of merely observing + parity. + +- 394b7a1: feat(job): honor the authored `retryPolicy` / `timeout` in the job scheduler (#3494) + + `JobSchema.retryPolicy` and `JobSchema.timeout` used to be parsed-but-ignored + (the 2026-06 liveness audit's aspirational-config cluster). They are now + enforced end to end — built rather than pruned, since retry/backoff and + per-run time limits are semantics job authors reasonably expect: + + - **spec**: `IJobService.schedule` gains an optional 4th `options` argument + (`JobScheduleOptions` with `retryPolicy` / `timeout`, mirroring the + authorable schema); new `JobRetryPolicy` type. Backward compatible — + existing 3-arg implementations and callers are unaffected. + - **service-job**: new `runWithPolicy` helper (exported, with + `JobTimeoutError`) wraps every handler invocation in `CronJobAdapter` and + `IntervalJobAdapter`; `DbJobAdapter` threads options through to its inner + adapters. Failed attempts (including timeouts) retry with exponential + backoff `backoffMs * backoffMultiplier^(retry-1)` up to `maxRetries`; + an attempt exceeding `timeout` is recorded with execution status + `'timeout'`. No `options` → exactly the legacy single-attempt behavior. + - **runtime**: declarative-jobs registration in AppPlugin forwards the + authored `retryPolicy` / `timeout` to the scheduler. + + Note: JavaScript cannot forcibly cancel an in-flight handler — a timed-out + attempt is abandoned, not killed. The retry delay caps only via the + multiplier arithmetic (no maxDelay knob yet). + + Refs #3494, #1878, #1893. + +- 677b591: feat(spec): `ListColumn` gains `prefix` and the `{ type, field }` `summary` form (objectui#2231) + + Two list-column capabilities the ObjectUI grid renderer has shipped for a while + were missing from the protocol, so they lived on as a local `.extend()` in + `@object-ui/types` — the exact fork-shaped drift objectui#2231 is closing. Both + are now spec-owned: + + - **`prefix`** (`ColumnPrefixSchema`) — Airtable-style compound cells: render a + second field inline before the cell value (e.g. a status badge in front of the + record name), so a list carries two signals in one column. + `{ field, type?: 'badge' | 'text' }`, `type` defaulting to `'text'`. + - **`summary` object form** (`ColumnSummaryConfigSchema`) — `{ type, field? }`, + for a footer that aggregates a field OTHER than the column's own (an `amount` + column summing `amount_in_base_currency`). The shorthand `summary: 'sum'` is + unchanged and remains the common case. `type` reuses `ColumnSummarySchema`, so + both forms share one aggregation vocabulary and cannot drift apart. + + Additive and backward compatible: every previously valid `ListColumn` still + parses. New exports: `ColumnPrefixSchema` / `ColumnPrefix` and + `ColumnSummaryConfigSchema` / `ColumnSummaryConfig`. + +- 2a5f04a: `` aggregate result-column naming is now a contract, and its axis bindings are validated (issue #3701) + + Split out of #3583 Phase 2 (#3684), which extended ADR-0021 axis checking to + report charts, list-view charts, and dataset-bound page chart components but had + to leave the react `` block out: it is OBJECT-bound (`objectName` + + an inline `aggregate`), `aggregate` existed in the contract only as the + description string `'{ field, function, groupBy }'`, and nothing in the repo said + what the aggregated result columns were called. Without that, `xAxis`/`yAxis` had + nothing to resolve against, and guessing a convention would have manufactured + false positives (ADR-0072 D1). + + **The convention, recorded rather than invented.** Every path that can serve an + object-bound chart already agreed — the engine's structured-`groupBy` aggregate + (whose alias objectui sets to `field || function`), the legacy analytics query + (which remaps its measure key back to `field`), the client-side fallback, and the + console's own chart-view wiring (`xAxisKey: groupBy`, `series[].dataKey: field`). + `packages/spec/src/ui/chart-aggregate.ts` writes it down and exports it: + + - an object-bound aggregate returns rows keyed by the **raw field names** — + `groupBy` for the category column, `field` for the value column, the literal + `count` for a fieldless count, plus `__comparison` under a comparison + overlay; + - `chartAggregateCategoryKey` / `chartAggregateValueKey` / `chartAggregateResultKeys` + derive those columns so producers and checkers cannot re-derive them apart; + - `ChartAggregateSchema` replaces the description string with a real Zod schema + and rejects a non-`count` function with no `field` (which used to reach the + renderer as `sum(undefined)` and render blank). + + This is the deliberate opposite of the dataset path, whose rows are keyed by the + declared measure `name` (`sum_amount`) — the trap `chart-measure-unknown` catches. + Only the dataset path has an author-chosen name to key by. + + **``'s contract now names the props it actually reads.** The block + consumes `xAxisKey` and `series[].dataKey`; `ChartConfig`'s `xAxis`/`yAxis`/`series` + shapes reached it and were silently dropped, which ADR-0078 forbids. They are + removed from the block's `dataProps`; `chartType`, `xAxisKey`, and `series` are + declared in the React overlay where the other bindings live. + + **`validate-react-page-props` now reads attribute VALUES**, not just names, for + ``: + + - `react-chart-field-unknown` (error) — `aggregate.field` / `aggregate.groupBy` + naming a field the bound object does not declare; + - `react-chart-aggregate-invalid` (error) — an unimplemented aggregation + function, or a non-`count` function with nothing to aggregate; + - `react-chart-axis-unknown` (error) — `xAxisKey` / `series[].dataKey` naming a + column the aggregate does not return (including a dataset-style `sum_total`), + or a category axis bound to the value column; + - `react-chart-axis-inert` (warning) — the `xAxis` / `yAxis` shapes this block + never reads. + + Value reading is opt-in per block and evaluates only static literals: a prop + driven by React state or a variable, a usage carrying a `{...spread}`, a chart + given inline `data`, and objects another package defines are all skipped + silently — an unresolvable binding is not a wrong one. + +- 4f740b0: ``'s author contract is the spec `ChartConfig` shape again (issue #3729) + + #3701 trimmed `xAxis`/`yAxis`/`series` out of the `` contract + because the renderer read `xAxisKey`/`series[].dataKey` and silently dropped the + ChartConfig shapes — an honest record of the runtime gap, not the target state. + objectui#2880 closed the gap the other way round (the renderer now honors + `ChartConfig` through one normalization boundary), so the contract follows the + protocol again (ADR-0082 D1: the spec schema IS the protocol). + + **Contract.** `type`, `xAxis`, `yAxis`, `series`, `subtitle`, `showDataLabels`, + `annotations` and `interaction` are published from `ChartConfigSchema`; the + internal `chartType`/`xAxisKey`/`series[].dataKey` spellings leave the author + contract. `annotations` and `interaction` gained the `.describe()` they never + had, so the generated contract stops publishing bare `object[]` with no meaning. + + **The `type` exception.** `ChartConfig.type` is the chart family, but on any + surface that flattens chart config into a props bag `type` is already the SDUI + envelope's component discriminator — an author writing `type="bar"` used to + replace `object-chart` and the block stopped resolving. The collision is created + by the flattening and is resolved there (objectui's react-page wrapper), so the + contract can publish `type` as the spec spells it. The contract generator's + blanket `type` skip is now overridable by an explicit `dataProps` allow-list, + since for this one block `type` is a real author prop. + + **Lint.** `validate-react-page-props` reads the axes in the spec spelling — + `xAxis.field`, `yAxis[].field`, `series[].name` — and keeps accepting the + internal spellings silently, because dashboards and the console's own chart-view + wiring emit them. `react-chart-axis-inert` is retired: the props it warned about + are honored now, so the warning would be false. The three binding-integrity + rules from #3701 are unchanged. + + **Spec.** `chart-aggregate.ts` records the constraint the whole result-column + convention rests on: an inline `aggregate` is SINGLE-MEASURE. Keying rows by the + raw field name only works because there is exactly one measure to key; two + measures over one field would collide, and resolving that needs an author-chosen + name per measure — which is what a dataset is. Widening `ChartAggregateSchema` + into a measures array would silently invalidate every axis binding these rules + validate, so the boundary is now written down rather than left to be rediscovered. + + The chart taxonomy note is corrected too: grouped/stacked bar and stacked area + are absent from `ChartTypeSchema` not because they render as their base chart, + but because stacking is a property of the SERIES (`ChartSeries.stack`), not a + chart family — one `bar` family plus a series stack group expresses all three. + `ChartInteraction.zoom` is now marked declared-not-delivered in its own + description rather than reading as shipped. + +- 67452d1: feat(spec): resolve page metadata i18n — `page:header` title/subtitle (#3589) + + Custom system pages authored as metadata (Installed Apps, Cloud Connection, + Connect an Agent) hard-code their `page:header` copy in + `properties.title` / `properties.subtitle`. Every other metadata type is + localized at the REST boundary, but `page` was not: the `pages` namespace + existed only on `AppTranslationBundleSchema` — a schema no runtime reads — + with no resolver behind it, so those headers stayed English in every locale + while the matching nav labels translated correctly. + + - `TranslationDataSchema` (the shape the i18n service actually serves) gains a + `pages` namespace: `pages..{label,description,title,subtitle}`. + - New `translatePage` in `@objectstack/spec/system` translates a page's own + `label` / `description` and overlays `title` / `subtitle` onto every + `page:header` in the page's regions. Registered in + `translateMetadataDocument`, so it rides the existing read path. + - `page` added to the REST boundary's `TRANSLATABLE_META_TYPES`. Locale + extraction, the locale-keyed ETag, and `Vary: Accept-Language` already + covered every metadata type — no new plumbing. + - `objectstack i18n extract` now emits page entries, including the + `page:header` copy, so the new namespace is not invisible to the tooling. + - zh-CN / ja-JP / es-ES translations shipped for the three Setup pages, plus + the missing `nav_cloud_connection` / `nav_connect_agent` nav labels (these + existed only in zh-CN). + + Header copy is keyed by **page name**, not by component id: `page:header` + instances carry no stable id. `title` falls back to `pages..label`, since + a page's header title and its nav label are normally the same string. + + Authoring is unchanged and English literals stay in metadata as the fallback — + a page with no `pages` entry renders exactly as before. Consumers of + `@object-ui` need no change: pages arrive already localized from the server. + +- 605e190: feat(spec)!: prune the still-dead aspirational config from Theme / Translation / Webhook (#3494) + + Removes the authorable-but-never-consumed props confirmed dead by the 2026-06 + liveness audit (follow-up to #1878/#1893; same treatment as the #2377 and + #3464 prunes). Authoring any of these was a silent no-op. + + ## Removed + + **Theme** (`ThemeSchema`) — the theme engine (objectui `generateThemeVars`) + never emitted or consumed them: + + - Props: `spacing`, `breakpoints`, `logo`, `density`, `wcagContrast`, `rtl`, + `touchTarget`, `keyboardNavigation` + - Exports: `SpacingSchema`, `BreakpointsSchema`, `DensityModeSchema` (+ + deprecated `DensityMode` alias), `WcagContrastLevelSchema` (+ deprecated + `WcagContrastLevel` alias), and the `Spacing` / `Breakpoints` / + `DensityMode` / `WcagContrastLevel` types + + **Translation** (`TranslationConfigSchema`) — no runtime reader; there is no + ICU engine and interpolation is always simple `{variable}` substitution: + + - Props: `fileOrganization`, `messageFormat`, `lazyLoad`, `cache` + - Exports: `MessageFormatSchema`, `TranslationFileOrganizationSchema`, and the + `MessageFormat` / `TranslationFileOrganization` types + + **Webhook** (`WebhookSchema`) — the delivery path always sends its own fixed + envelope and only applies HMAC signing via `secret`; delivery retries are owned + by the messaging outbox's fixed schedule: + + - Props: `body`, `payloadFields`, `includeSession`, `authentication` + (bearer/basic/api-key were never attached; HMAC via `secret` stays), + `retryPolicy`, `tags` + - Exports: the entire inbound `WebhookReceiverSchema` + `WebhookReceiver` type + (never consumed by any runtime) + + ## Migration + + Delete these keys from your configs — they never did anything, so removing + them changes no behavior. Parsed output no longer contains the previously + defaulted keys (`includeSession: false`, `fileOrganization: 'per_locale'`, + `messageFormat: 'simple'`, `lazyLoad: false`, `cache: true`). Webhook HMAC + signing (`secret`), `headers`, and `timeoutMs` are unaffected. File layout for + translations remains a pure authoring convention — no config knob needed. + + ## Deliberately NOT removed + + - Translation `supportedLocales` — it has a live reader (pinyin-search + capability toggle in `serve.ts`). + - Job `retryPolicy` / `timeout` — being implemented (built, not pruned) in the + #3494 follow-up PR. + - The materialized webhook props (`name`, `object`, `triggers`, `url`, + `method`, `headers`, `timeoutMs`, `secret`, `isActive`, `description`, + `label`) — live via the #3489 bridge; ledger flip tracked in #3490. + + Refs #3494, #1878, #1893. + +- c6c59f1: feat(spec)!: remove the dead `AuditConfig` cluster from `@objectstack/spec/system` (#1878 recheck loose-end) + + The entire `system/audit.zod.ts` module — `AuditConfigSchema`, + `AuditStorageConfigSchema`, `AuditRetentionPolicySchema`, + `AuditEventFilterSchema`, `SuspiciousActivityRuleSchema`, + `DEFAULT_SUSPICIOUS_ACTIVITY_RULES`, and the `AuditEvent` / + `AuditEventActor` / `AuditEventTarget` / `AuditEventChange` / + `AuditEventType` / `AuditEventSeverity` shape schemas (plus all their + type exports) — is removed. Verified zero consumers repo-wide: the live + audit path (`plugin-audit`) imports none of it, defines its own + `sys_audit_log` row shape, and captures **unconditionally** via engine + hooks, so `AuditConfigSchema.enabled: false` advertised a semantic + (turning the compliance ledger off) the platform deliberately rejects. + Same ADR-0056 D8 family as the earlier `compliance.zod` / `masking.zod` / + `RLSAuditConfig` / `PolicySchema` removals: security/compliance-shaped + config must never merely look live. + + **Migration — every dead knob maps to a live surface (or is deliberately + not configurable):** + + | Removed (never enforced) | Live replacement | + | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | + | `AuditConfigSchema.enabled` | none — audit capture is **always on** (compliance ledger; `object.zod` `trackHistory` contract) | + | `eventTypes` / `excludeEventTypes` / `minimumSeverity` / `AuditEventFilterSchema` | none today — if event filtering ships it lands as an `audit` **settings** namespace (ADR-0069 pattern), not app metadata | + | which fields/objects are summarized + History tab UI | object-level + field-level **`trackHistory`** (live, enforced by plugin-audit) | + | `AuditRetentionPolicySchema` / `storage` | object **`lifecycle`** `audit` category (retain → archive → delete) + per-org settings overrides (ADR-0057) | + | `SuspiciousActivityRuleSchema` / `DEFAULT_SUSPICIOUS_ACTIVITY_RULES` | none — no detection engine exists; security monitoring is org-operations tooling, not app-package metadata | + | `AuditEvent*` shape schemas | the `sys_audit_log` object definition in `plugin-audit` is the row-shape source of truth | + + No first-party, example, or downstream-contract code imported any of + these symbols; `defineStack` never accepted an `audit` key, so no stack + config changes. Docs page `references/system/audit.mdx` is removed by + regeneration; the security-context module doc now marks audit alongside + the previously removed compliance/masking subsystems. + +- b0e78a8: feat(spec)!: remove the dead static capabilities-descriptor cluster (`ObjectQL`/`ObjectUI`/`Kernel`/`ObjectStack`/`ObjectOS CapabilitiesSchema`) (#1878 family) + + The "RUNTIME CAPABILITIES PROTOCOL" tail of `stack.zod.ts` — `ObjectQLCapabilitiesSchema`, + `ObjectUICapabilitiesSchema`, `KernelCapabilitiesSchema`, `ObjectStackCapabilitiesSchema`, + the deprecated `ObjectOSCapabilitiesSchema` alias, and all five inferred types — is + removed. Verified zero consumers repo-wide (framework, objectui apart from bare + re-exports, cloud, downstream-contract): it was never authorable (`defineStack` has + no such key), never registered, and never fed any endpoint. + + Worse than dead, it **lied**: the fixed-boolean self-portrait defaulted + `fieldLevelSecurity` / `rowLevelSecurity` / `auditLogging` / `backgroundJobs` to + `false` while every one of those is live and enforced on the platform, and + advertised `odataApi` which has never existed. An AI reading the schema would + build a systematically wrong model of the platform. + + **Migration — runtime capability discovery is dynamic, not a static schema:** + + | Removed | Live replacement | + | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `KernelCapabilitiesSchema` booleans (`restApi`/`websockets`/`auditLogging`/…) | `GET /api/v1/discovery` — dynamic `capabilities` record with **declared === enforced** discipline (#3298: a capability is advertised only when the route is actually mounted AND the engine supports it) | + | `WellKnown`-style backend feature probes | `WellKnownCapabilitiesSchema` (`@objectstack/spec/api` discovery contract: `comments`/`automation`/`cron`/`search`/…) | + | `ObjectQLCapabilitiesSchema` driver/query booleans | driver-level `DriverCapabilities` / `DatasourceCapabilities` (`@objectstack/spec/data`) — per-connection, resolved at runtime | + | `ObjectStackCapabilitiesSchema` layer roll-up + `ObjectOS*` aliases | none — no replacement import | + + The objectui `@object-ui/types` re-exports of these symbols are dropped in the + companion objectui change. `ClusterCapabilityConfigSchema` / `FeatureFlagSchema` / + `ApiEndpointSchema` (referenced by the dead cluster) are untouched — they live in + their own modules and `ApiEndpointSchema` remains consumed by the stack `apis` key. + +- f31cc8d: refactor(spec)!: finish the 2026-06 field prune — drop both orphaned value schemas, `DataQualityRulesSchema` and `ComputedFieldCacheSchema` (#3726, #3733) + + **BREAKING** (the `!` marker and this changeset are the breaking-change record; + the train ships as the v17 major — see the `v17-rc-anchor` changeset), though + nothing in-tree or out could have depended on either meaningfully. Removed from + the `@objectstack/spec` public surface: + + - `DataQualityRulesSchema` (const), `DataQualityRules` (type), `DataQualityRulesInput` (type) — #3726 + - `ComputedFieldCacheSchema` (const), `ComputedFieldCache` (type) — #3733 + + and the published `data/DataQualityRules.json` / `data/ComputedFieldCache.json` + JSON Schemas. + + **Why.** Five field keys were pruned in 2026-06 — `encryptionConfig`, + `maskingRule`, `auditTrail`, `cached` and `dataQuality` — as "dead in both + layers, aspirational governance with no runtime consumer" (see + `docs/audits/2026-06-dead-surface-disposition-plan.md`, P0/P2 field prune). + Three of the five took their value schemas with them. Two did not: `dataQuality` + and `cached` each lost their key from `FieldSchema` while + `DataQualityRulesSchema` / `ComputedFieldCacheSchema` stayed on the published API + surface and in the generated reference docs, with zero consumers anywhere in the + tree. The tombstone claimed "dead in both layers"; for these two it was true of + only one. + + That middle state is the worst of the three available (key + schema + consumer / + none of them / schema only), and it failed quietly rather than loudly. + `FieldSchema` is **not** `.strict()`, so an author who found either type in the + reference docs and wrote `dataQuality: { uniqueness: true }` or + `cached: { enabled: true, ttl: 3600 }` got no error at all — the field parsed + clean and the key was silently stripped, leaving a rule that was declared in + source, absent from the contract, and enforced by nothing. That is the ADR-0104 + failure class, the same one the `accept` / `maxSize` declarations were added to + close. + + Each had its own sharp edge. `DataQualityRules.uniqueness`, described as "Enforce + unique values across all records", reads exactly like the platform-wide scope + that `unique: 'global'` actually provides (#3696), making it the option an author + was most likely to reach for by mistake. `ComputedFieldCache` was quieter and + therefore harder to catch: an author writing `ttl: 3600` on a formula field would + believe results were cached for an hour, get no error, and never see a signal + that nothing had happened. + + **Migration.** There is no runtime behavior to migrate — neither schema was ever + reachable from `FieldSchema`, and neither had a consumer in-tree. For per-field + uniqueness use `unique` (`true` = unique within the tenant, `'global'` = unique + platform-wide; see #3696). `completeness`, `accuracy`, and computed-field caching + (`enabled` / `ttl` / `invalidateOn`) have no replacement; none was ever + implemented. + + If field-level data-quality governance or computed-field caching is built for + real later, re-add the field key and its schema **together, with a consumer** — + the enforce side of enforce-or-remove (ADR-0049). A tombstone in `field.zod.ts` + records this so neither schema is restored on its own again. + +- f343dc4: feat(spec)!: remove the orphaned `FeatureFlagSchema` module (`@objectstack/spec/kernel` feature.zod) + + Follow-through of the capabilities-descriptor prune (#3605). `kernel/feature.zod.ts` + (`FeatureStrategy`, `FeatureFlagSchema`, the `FeatureFlag` factory and its + `FeatureFlag` / `FeatureFlagInput` types) had **zero runtime consumers**, and its + only protocol home — the static `ObjectStackCapabilities.system.features` + descriptor — was itself removed as dead in #3605 (no endpoint ever served it). + The module was a compile-checkable shape with nowhere to go: not authorable + (`defineStack` has no `features` key; strict parsing strips it), not registered, + not read by any engine. + + **Migration — flags are runtime configuration, not authored metadata:** + + | Removed | Live replacement | + | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `FeatureFlagSchema` / `FeatureFlag.create()` rollout documents (strategies, percentage/group conditions) | the `feature_flags` **settings manifest** (`@objectstack/service-settings`, ADR-0007) — org-tunable `ai_enabled` / `beta_*` toggles, env-overridable via `OS_FEATURE_FLAGS_*` | + | deployment-level capability gating | `PUBLIC_AUTH_FEATURES` registry (`kernel/public-auth-features.ts`) + `requiresFeature` sugar on actions/params (unchanged, live) | + | runtime capability discovery | `GET /api/v1/discovery` (dynamic, declared === enforced) | + + Docs regenerated (`references/kernel/feature.mdx` removed); the platform skill's + Feature Flags section and the hand-written quick-reference row now point at the + settings surface; `PROTOCOL_MAP.md` row dropped. + +- 8269e32: feat(spec)!: remove the dead PortalSchema (portal metadata was never enforced) + + `PortalSchema` and its top-level `portals` collection on `StackSchema` were a + forward-looking design that was **never wired to a runtime** — no metadata-type + registration, no dispatcher route family, no auth scope, and no + LayoutDispatcher / NavigationBuilder / ThemeProvider ever consumed it. Authoring + a portal was already documented as a no-op and marked + `[EXPERIMENTAL — not enforced]`. This removes the dead schema rather than + building a portal runtime (issue #3464, disposition **A — prune**). + + **Removed exports** (`@objectstack/spec`, from `ui/portal.zod`): + `PortalSchema`, `Portal`, `definePortal`, and the `PortalInput` / + `PortalTheme` / `PortalNavItem` (+ `PortalViewNavItem`, `PortalActionNavItem`, + `PortalDashboardNavItem`, `PortalUrlNavItem`) / `PortalAnonymousEntry` / + `PortalAnonymousRoute` / `PortalRateLimit` / `PortalSeo` / `PortalAuthMode` / + `PortalLayout` schemas and inferred types. The `portals` key is removed from + `StackSchema` / `defineStack()`. + + **Migration**: none required for behavior — authoring a portal had no runtime + effect. Any `portals: [...]` entry in a `defineStack()` config was already + ignored at runtime and should be deleted (with the schema gone it is an + excess-property type error). To project a scoped UI to external users today, + compose the existing `apps` / `views` surfaces and gate admission with + `positions` + permission sets (`externalSharingModel` on the objects you + expose). + + Refs #3464, #1893, #1878. + +- 74f7339: feat(spec)!: prune the dead `aria` / `performance` props from ReportSchema (report-liveness close-out) + + Follow-up to the #3463 report cleanup. The 2026-06 ReportSchema liveness audit + flagged `aria` and `performance` as dead — declared on `ReportSchema` (and + editable in the Studio report form) but read by **no renderer**. This removes + them. Every other finding from that audit is now closed too: `chart` turned out + to be **live** (`DatasetReportRenderer` plots `chart.xAxis`/`yAxis` via + `DatasetReportChart`), and the obsolete sub-schemas / naming-drift / joined-preview + items were resolved by #3463 and earlier work. + + - Removed `ReportSchema.aria` (`AriaPropsSchema`) and `ReportSchema.performance` + (`PerformanceConfigSchema`), dropping the now-orphan imports. Both schemas + remain exported and are still used by other metadata types (views, pages, + charts) — only the report's use of them is removed. `ReportChart` keeps its + own `aria` (inherited from `ChartConfigSchema`). + - No manifest key or public export changes (`aria`/`performance` were properties, + not schemas); `report.mdx` regenerated. + + **Migration**: nothing an author writes changes — no first-party or example + report set `aria`/`performance`. Reports carry no ARIA/performance overrides; + use the dataset/view surface for those concerns. Ships as `minor` per the + launch-window breaking-as-minor policy. + +- a6c35a2: feat(spec)!: prune the dead `ReportColumnSchema`/`ReportGroupingSchema` exports + the unread report chart `groupBy` (#3463, #1878/#1890) + + Deep-cleanup close-out of the report-chart disposition (follow-up to #3441). + After the ADR-0021 single-form cutover a dataset-bound report expresses its + columns/grouping as dataset **measure/dimension name arrays** — `values`, + `rows` and `columns` are `z.array(z.string())`, not object literals — so + `ReportColumnSchema` / `ReportGroupingSchema` were referenced by **no schema + body**. They survived only as public type exports and were marked + `@deprecated` in #3441; this removes them. + + - Deleted `ReportColumnSchema` / `ReportGroupingSchema` and their type + exports `ReportColumn` / `ReportGrouping` / `ReportColumnInput` / + `ReportGroupingInput` from `@objectstack/spec/ui`. The manifest ratchet + keys `ui/ReportColumn` / `ui/ReportGrouping` are dropped in the same PR. + - Deleted `ReportChart.groupBy` — the `[EXPERIMENTAL — not enforced]` + series-split field flagged in #3441. The dataset-bound `DatasetReportRenderer` + plots a single `xAxis`×`yAxis` series and never read it; only the retired + legacy `ReportViewer` fallback ever consumed a top-level `groupBy`. + `ReportChartSchema` is non-strict, so any residual `chart.groupBy` in stored + metadata is silently stripped on parse — no tombstone needed. + - Regenerated `content/docs/references/ui/report.mdx` and the spec API-surface + snapshot. + + **Migration**: nothing an author writes changes. + + - No first-party or example report authored `ReportColumn` / `ReportGrouping` + objects or `chart.groupBy` — a dataset-bound report already expresses + columns as `values` (measure names) and grouping as `rows` / `columns` + (dimension names). + - TypeScript consumers importing `ReportColumn` / `ReportGrouping` / + `ReportColumnInput` / `ReportGroupingInput` (or the `*Schema` values) from + `@objectstack/spec/ui` have no replacement type — model report columns as + the dataset's measure names and grouping as its dimension names. objectui's + `SpecReportColumn*` / `SpecReportGrouping*` re-exports are removed in the + companion objectui change. + +- c2f1002: feat(spec)!: remove `SkillSchema.permissions` — it never gated anything (#3686) + + Owner decision on the enforce-or-prune call filed in #3686: **prune**. + + `skill.permissions` was declared, surfaced in the Studio authoring form under a + section labelled _"Access — Required permissions to use this skill"_, and echoed + by the objectui preview — but **no runtime ever read it**. The cloud + `SkillRegistry` selects skills by `active` / `triggerConditions` / `tools` only. + A security-shaped field that enforces nothing is worse than no field: it invites + an author (or an AI) to believe a skill is gated when it is not. Same disposition + as agent `visibility` (#1901) and the `PolicySchema` tree (#2387). + + Removed: the schema property, the form's whole `Access` section (it existed only + for this field), its generated i18n keys, the liveness-ledger entry, and the + `permissions` line from the objectstack-ai skill doc's `os:check` example. The + objectui preview's "Required Permissions" panel is removed in the companion + objectui change. + + **Migration** — gate access where it is actually enforced: + + - **Agent level** — `access` / `permissions` on `defineAgent` ARE enforced at the + chat route (403 for a caller missing any of them, #1884). Bind the restricted + skill only to a restricted agent. + - **Action level** — gate the underlying actions the skill's tools invoke via + permission sets (ADR-0066). + + `SkillSchema` is non-strict, so an existing `permissions:` key is silently + stripped on parse rather than rejected — no boot break, but it stops appearing + anywhere. + +- f163028: Reference-integrity validation for object and action names (issue #3583) + + A HotCRM audit found ~20 shipped instances of one bug class — metadata naming + something that does not exist — all passing `objectstack validate` / `lint` + cleanly and failing silently at runtime. This closes the object-name and + action-name half of that class. + + **New — `@objectstack/spec`:** `PLATFORM_PROVIDED_OBJECT_NAMES`, a curated + registry of every object name contributed by a platform package, official + plugin, or the cloud runtime, plus `isPlatformProvidedObjectName()` and + `hasPlatformObjectPrefix()`. This replaces the `startsWith('sys_')` prefix guess + that could not tell `sys_user` (real) from `sys_approval_process` (fictional — + removed by ADR-0019, registered by nothing), which is why every fictional + platform-prefixed reference shipped. A conformance test scans each package's + `*.object.ts` declarations and fails if the registry drifts. + + **New lint rules** (wired into both `os validate` and `os lint`): + + - `validate-object-references` — action-param `reference` / `objectOverride`, + dashboard `globalFilters[].optionsFrom.object`, and navigation + `requiresObject` gates. Severity follows resolvability: an unresolved + _unprefixed_ name is a typo (**error** — `object: 'user'` where the platform + object is `sys_user`); an unresolved _platform-prefixed_ name is **advisory**, + since a third-party package may still provide it. + - `validate-action-name-refs` — the surfaces that bind an action BY NAME: + list-view `bulkActions` / `rowActions`, page `record:quick_actions` + `actionNames`, and nav action items. A name matching no defined action is an + **error** (the button renders and does nothing), matching the existing + dashboard-action-target rule. + + **Fixes:** + + - `defineStack` cross-reference validation now walks `app.areas[].navigation` — + an areas-based app previously got no navigation checking at all — and recurses + into `children` on `object` nav items, not only `group` ones. + - `os lint` i18n coverage now reads field `options` in the canonical + `{value,label}[]` array shape; it only handled the record map, so option-label + coverage silently never fired for canonically-shaped select fields. + - Hook `condition` expressions are now field-checked when `object` is an ARRAY + of targets (previously only a single string target was checked, so a + multi-target hook filtering on a nonexistent field passed clean). Per-target + diagnostics are de-duplicated. + - A dashboard widget binding no `dataset` at all is now reported instead of + silently bypassing every binding and chart check on the raw-config + (`lint`/`doctor`) paths. `dataset` is schema-required, so this matches what + the parsed paths already enforce. + +- 7ffc3d3: feat(client,spec)!: delete the 21 dead SDK methods and the four ghost route + tables that underwrote them (#3612, #3587 finding) + + Five client surface families built URLs that exist on NO server surface — + not the dispatcher, not `@objectstack/rest`, not the autonomous service + mounts — so every call was a guaranteed 404: + + - `permissions` (check, getObjectPermissions, getEffectivePermissions) + - `realtime` (connect, disconnect, subscribe, unsubscribe, setPresence, + getPresence) — `service-realtime` registers zero HTTP routes and the + dispatcher deliberately never advertises `/realtime` + - `workflow` (getConfig, getState, transition) + - `views` CRUD (list, get, create, update, delete) — no `/ui/views` route + anywhere + - `notifications` device/preference helpers (registerDevice, + unregisterDevice, getPreferences, updatePreferences) — the ADR-0012 + server side was never built + + Each family was underwritten only by an unconsumed spec `DEFAULT_*_ROUTES` + table — the same disease `DEFAULT_DISPATCHER_ROUTES` had (#3586) — so + `DEFAULT_PERMISSION_ROUTES`, `DEFAULT_VIEW_ROUTES`, `DEFAULT_WORKFLOW_ROUTES`, + and `DEFAULT_REALTIME_ROUTES` are deleted with them; + `getDefaultRouteRegistrations()` now returns 9 registrations. + `ApiRouteType` loses its client-only `'views' | 'permissions'` extras. + + Kept: `client.events` (explicitly local in-memory buffer, no HTTP), + `notifications.list/markRead/markAllRead` (dispatcher-served), + `approvals.*` (ADR-0019 — the real approval decision API), and + `meta.getLegalNextStates` (the real FSM read). + + Breaking for anyone calling the removed methods — a repo-wide and + objectui-wide sweep found one consumer (`useClientNotifications`'s dead + device/preference delegates, trimmed in the objectui companion change); + shipped as minor per the launch-window convention (cf. #3562/#3581/#3595). + Re-adding any of these surfaces requires the server route to exist and a + route-ledger row proving it (#3569/#3609 guards). + +- 88346ba: feat(spec)!: remove the dead `object.enable.trash` / `enable.mru` capability flags (#2377, ADR-0049 enforce-or-remove — close-out) + + Both flags parsed and defaulted to `true` but had **no runtime consumer**: + every delete has always been a hard delete (no recycle bin), and no MRU + tracking was ever implemented. A default-true flag promising recoverability + is the worst kind of false affordance — first-party objects were authoring + `trash: false // Never soft-delete audit logs` in the belief that a + soft-delete existed to opt out of. + + - `ObjectCapabilities` is now **`.strict()`** (pattern of the tenancy block, + #2763): an unknown `enable` key — the retired `trash`/`mru` or a typo like + `feedEnabled` — fails parse with upgrade guidance instead of stripping + silently (#1535). The retired-key tombstones live in + `CAPABILITIES_RETIRED_KEY_GUIDANCE`. + - ~45 first-party object definitions (platform-objects, plugin-security, + plugin-audit, plugin-approvals, plugin-sharing, metadata-core, + service-realtime, examples) dropped their inert `trash:`/`mru:` lines. + - Liveness ledger: both entries deleted (removal precedent: `tags`/ + `recordName`); object row in the README count table now shows **0 dead**. + - Docs + skills no longer advertise a recycle bin / MRU tracking; the API + skill's "DELETE is soft-delete when `trash: true`" claim is corrected to + the real contract (hard delete; use per-field `trackHistory` or a + `lifecycle` policy for recoverability). + + **Migration**: delete any `enable.trash` / `enable.mru` keys from object + metadata — they never changed behavior. `ObjectSchema.create()` / + `ObjectCapabilities.parse()` now reject them with this prescription. A real + recycle bin or MRU feature, if built, returns as a live enforced flag + (#1893 prune-or-build). + +- 4631592: feat(spec)!: remove the never-implemented GraphQL surface from the product plan (#2462 follow-on) + + GraphQL was schema-only from day one: the spec shipped 20+ config schemas + (`GraphQLTypeConfig`, federation, persisted queries, …), the dispatcher's + `handleGraphQL` answered 501 unconditionally (`kernel.graphql` was never + assigned in the monorepo), and THREE separate mounts advertised the dead + endpoint. Per the product decision, the surface is deleted rather than + maintained: + + - **spec**: `api/graphql.zod.ts` + `contracts/graphql-service.ts` deleted; + `graphql` removed from `CoreServiceName`, `ApiProtocolType`, the + query-adapter dialects, `graphql-playground` from testing-UI types; the + `graphqlApi`/network capability booleans, discovery/router route fields + dropped. Breaking for consumers referencing those exports/enum members (shipped as minor per the launch-window convention, cf. #3486/#2377). + - **runtime**: `handleGraphQL`, the if-chain branch, the dispatcher-plugin + and hono-adapter mounts, discovery advertisement, and the now-dead + `resolveRequestExecutionContext` helper removed. + - **plugin-dev**: the graphql stub family removed. + - **qa**: authz-conformance matrix rows, ratchet high-risk id, discover + patterns and identity pins for the GraphQL surface retired; expression + ledger covers updated. + - **NOT removed**: the `'graphql'` protocol option on external datasource + lookups (third-party systems may speak GraphQL) and cloud's reserved + slug — those are not our API surface. + + `/graphql` now 404s (was an unconditional 501); the anonymous-deny posture + matrix shrinks by the two GraphQL rows. + +- 5ac93d4: feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431) + + #3413 (closes #3407) built the engine-level strip-observability channel + (`WriteObservabilityOptions.onFieldsDropped`) and wired the flow side + (`update_record` / `create_record` emit a step warning + `droppedFields`). The + **REST write path was never wired**, so an external API caller writing N fields + still got a bare `200 + record` when `readonly` (#2948) / `readonlyWhen` (#3042) + stripping meant `< N` actually landed — the same silent-success class #3407 + fixed flow-side, just on HTTP. The only way to notice was a per-field diff of + the returned row (which need not echo every field). This wires the channel + through the protocol → REST, on both write verbs. + + **Passthrough (metadata-protocol).** `updateData` now registers an + `onFieldsDropped` collector on `engine.update` and returns the events on the + response as `droppedFields`. `createData` surfaces the #3043 static-`readonly` + INGRESS strip too — that strip runs at the protocol ingress + (`stripReadonlyForInsert`), _before_ the engine, so it is recovered by diffing + the supplied payload against the stripped one (the engine's `onFieldsDropped` is + also wired for a future insert-side engine strip). A faulty listener never + breaks the write — the engine catches and logs. + + **Contract (spec).** `UpdateDataResponseSchema` / `CreateDataResponseSchema` + gain an **optional** `droppedFields: DroppedFieldsEvent[]` — present only when + ≥1 field was dropped. Optional + omit-when-empty keeps the response shape + backward-compatible for clients that only read `record`. + + **REST surface.** PATCH `/data/:object/:id` and POST `/data/:object` echo the + drops as an `X-ObjectStack-Dropped-Fields` response header + (`field;reason=` tokens, comma-joined — e.g. + `approval_status;reason=readonly`) and keep the structured `droppedFields` on + the body. **Status/success semantics are unchanged** (200 update / 201 create) — + a strip is legitimate semantics, not a failure (same principle as #3413). The + FLS write gate is untouched (it already fails closed with 403). + + Out of scope (issue #3431 D2 open questions, deferred): bulk + (`updateManyData` / `createManyData` / `batchData`) and GraphQL mutation wiring, + typed `@objectstack/client` warnings, and adding the header to the Hono CORS + `exposeHeaders` allow-list for cross-origin browser reads (the body + `droppedFields` is the cross-origin-safe channel meanwhile). + +- 0024abf: feat(spec)!: delete `DEFAULT_DISPATCHER_ROUTES` — the dead route table that + underwrote a false compliance verdict (#3586, #3563 follow-up) + + The const was consumed by nothing in the runtime — only its own tests and + `api-surface.json`. It listed dispatcher branches that never existed + (`/workflow`, `/realtime`) while omitting eight real prefixes (`/keys`, + `/mcp`, `/mcp/skill`, `/actions`, `/security`, `/share-links`, `/ready`, + `/openapi.json`), and `CLIENT_SPEC_COMPLIANCE.md` anchored a "FULLY + COMPLIANT" verdict on it while 27 real routes had no SDK expression. + + The audited, guard-enforced source of truth for the dispatcher's route + surface is `packages/runtime/src/route-ledger.ts` (#3569): the conformance + suite fails when the registry and the ledger drift, which the dead table + never could. + + Also swept the last GraphQL fixture debris that #3562's surface removal + left behind: registry test fixtures renamed to honest OData naming, the + tautological `config.graphql` assertions dropped, and the stale + `"type": "graphql"` JSDoc example in `registry.zod.ts` corrected. + + Breaking for anyone importing `DEFAULT_DISPATCHER_ROUTES` (a repo-wide and + objectui-wide grep shows zero consumers); shipped as minor per the + launch-window convention, cf. #3562/#3581. + +- 1659072: feat(spec): publish `ISecurityService` — the `security` service surface becomes an enforced contract + + The `security` service registers seven cross-package methods (`getReadFilter`, + `getReadableFields`, `resolvePermissionSetNames`, `explain`, and the three + audience-binding suggestion calls) but had no contract in + `@objectstack/spec/contracts`. Consumers duck-typed it, and each one invented its + own fallback for a missing method or an "empty" answer — with more consumers + arriving, that is a drift surface. + + `ISecurityService` now documents the surface, and both ends are typed against it + so it is **enforced rather than declared**: `plugin-security` assigns its + registration to `ISecurityService` (a renamed, dropped, or re-typed method fails + that build), and the REST layer resolves the service as a `Partial` + (so call sites must keep feature-detecting instead of assuming the full surface). + + The contract makes explicit the one thing consumers cannot guess — that the + methods do **not** share a failure convention: + + - `getReadFilter` fails **CLOSED**: a resolution failure yields a deny filter + matching zero rows, never `undefined`. `undefined` means "no row restriction", + and nothing else. + - `getReadableFields` fails **SOFT**: `undefined` means "no answer, use your own + projection", while `[]` is authoritative and means "no field is readable" — + opposite instructions that a consumer must not conflate. + + Typing the producer immediately caught one real discrepancy, fixed here: + `getReadFilter` declared `Promise | null | undefined>` + while every return path yields a filter or `undefined` (`filter ?? undefined` + normalizes the null away). The dead `| null` is removed, so "no restriction" has + exactly one representation. Type-level only — no runtime behaviour changes. + +- 503be86: feat(security)!: reconcile the SharingRule authoring surface with the enforced runtime — rename `group` → `team`, add `business_unit`, prune `guest` + owner-type rules (#1878) + + The authoring `ShareRecipientType` enum had drifted behind the ADR-0090 D3 + rename and the enforced runtime: the runtime expands `team` (via + `sys_team`/`sys_team_member`) and `business_unit`, but the authoring enum + still offered the pre-rename `group` (silently skipped at seed time) and + omitted the two live recipients. After this change **every authorable + recipient and rule type is enforced** — nothing on the SharingRule surface + validates and then silently does nothing (ADR-0078). + + - **`sharedWith.type: 'group'` → `'team'`** (wire-rename): the enum member is + renamed to match the runtime vocabulary and now maps through the seed + bootstrap to the live `TeamGraphService` expansion. Flat `sys_team` + membership; enforced. + - **`business_unit` added** to the authoring enum — exactly one business + unit's members (no subtree; use `unit_and_subordinates` for the subtree). + The runtime + bootstrap already enforced it; only the enum omitted it. + - **`guest` removed** — it had no runtime recipient mapping. Anonymous access + is served by the public-form grant and share links, not sharing rules. + - **Owner-type rules removed** (`type: 'owner'`, `ownedBy`, + `OwnerSharingRuleSchema` + its type export): they depend on live + team/position membership, which the static materialiser cannot track, so + they validated but never materialised a share. They return as an enforced + form if membership-reactive re-materialisation is designed. + `SharingRuleSchema` is now the criteria form; the `queue` recipient stays + runtime-reserved (no `sys_queue` yet) and deliberately non-authorable. + + **Migration** (stale definitions now fail parse with the valid options listed): + + - `sharedWith: { type: 'group', … }` → `sharedWith: { type: 'team', … }`. + - `sharedWith: { type: 'guest', … }` → delete the rule; expose the records + via a public form or share link instead. + - `type: 'owner'` rules → rewrite as a `type: 'criteria'` rule scoping the + rows by field values (see the migrated examples: + `share_open_tasks_with_manager` in app-showcase, + `share_active_leads_with_manager` in app-crm), or use a scope-depth grant. + +- 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list. +- b90086a: fix(driver-sql)!: `unique` materializes per tenant, ending its contradiction with the per-tenant autonumber sequence (#3696) + + `unique: true` became a **single-column global index that ignored `tenancy` + entirely**, while the autonumber sequence table is keyed by + `(object, tenant_id, field, scope)` and hands every tenant its own counter + starting at 1. Two subsystems of the same platform contradicted each other: + tenant B's `PROD-00001` was rejected by an index it could not see — **no user + did anything wrong**, the platform's left hand refused what its right hand + issued. + + The rejection also doubled as a **cross-tenant existence oracle**: a UNIQUE + violation told tenant B that some _other_ tenant held the value, enumerable by + probing emails / codes / names. + + **The contract now:** + + | Declaration | Materializes as | + | -------------------------------- | --------------------------------------------------------------- | + | `unique: true` + tenant column | composite `(tenantField, field)` — unique **within** the tenant | + | `unique: true`, no tenant column | single-column — single-tenant DDL is byte-identical to before | + | `unique: 'global'` | single-column, always platform-wide | + + The tenant column comes first in the composite, so the index also serves the + `WHERE tenant = ?` prefix scans every tenant-scoped read issues. + + **Declared `indexes[]` are deliberately unchanged.** They are materialized over + exactly the columns listed — no tenant column is injected. The author already + spells them out, per-tenant ones have always been written explicitly + (`fields: ['organization_id', 'code']`), and many are legitimately platform-wide + (a DNS hostname, a reserved slug, an external provider id). `'global'` is + accepted there as a synonym of `true` so one vocabulary covers both spellings. + + **Migration is automatic and cannot fail.** Legacy indexes + (`
__unique` from knex, `uniq_
_` from the drift-rebuild + path) are retired inline at schema-sync time. The old global constraint is + strictly stronger than the new per-tenant one, so existing rows satisfy the + replacement by construction — no dedup, no cleanup, no data touched. It + converges at sync rather than waiting for a deliberate `os migrate` run because + a deployment that never ran migrate would otherwise stay broken. + + **Upgrading — audit your `unique: true` fields.** On a tenant-scoped object the + constraint is now per tenant. Anything that must stay platform-wide has to say + so: + + ```ts + hostname: Field.text({ unique: "global" }); // no two tenants may claim it + ``` + + Note the reach: `applySystemFields` injects `organization_id` into every + registered object unless it opts out, and the driver falls back to that column + when no `tenancy.tenantField` is declared — so most objects are tenant-scoped. + Typical candidates for `'global'`: DNS hostnames, reserved slugs, external + provider ids (Stripe customer/subscription), device identities. + + Postgres materializes `col.unique()` as a table CONSTRAINT rather than a bare + index, so the retirement tries `DROP CONSTRAINT` before `DROP INDEX` — + `DROP INDEX` alone would have made the migration a no-op on exactly the + deployments that matter most. + + `@objectstack/driver-mongodb` accepts the new declaration but keeps single-field + indexes: it implements no row-level tenancy at all (no tenant predicate on read, + no tenant stamp on write), so a `(tenant, field)` index would advertise an + isolation it does not deliver. Tracked separately. + +- b95577a: feat(automation): surface silently-stripped write fields as step warnings (#3407) + + `update_record` used to report an unconditional `success` even when the data + layer legally stripped the requested write fields — static `readonly` (#2948) + or a TRUE `readonlyWhen` predicate (#3042). The only trace was a server-side + logger warn, invisible in the flow run trace: an author saw a clean 3ms + `success` while the DB truth never changed (how #3356's approval stage + write-backs failed unnoticed). + + - **spec**: new `DroppedFieldsEventSchema` / `DroppedFieldsEvent` + (`{ object, fields, reason: 'readonly' | 'readonly_when' }`) in + `data/data-engine.zod.ts`, and a `WriteObservabilityOptions` + (`onFieldsDropped` listener) mixin on `IDataEngine.insert/update` option + params in `contracts/data-engine.ts`. The listener is a TS-contract-level, + in-process-only channel — deliberately NOT part of the serializable Zod + options schemas or the RPC boundary. + - **objectql**: `engine.update()` reports each strip pass's dropped keys + + reason through `options.onFieldsDropped` (all four strip sites: single-id + + bulk × readonly + readonlyWhen). A throwing listener never breaks the write. + System-context writes skip the readonly strip and therefore report nothing, + as before. `insert()` accepts the option for symmetry but strips nothing + today (INSERT is readonly-exempt; FLS write denial throws). + - **service-automation**: `NodeExecutionResult` and `StepLogEntry` gain + advisory `warnings?: string[]`; `update_record` / `create_record` attach one + warning per strip event naming the dropped fields, plus a structured + `droppedFields` output (`{.droppedFields}`) for downstream nodes. + `success` semantics are unchanged — stripping stays legal, it just is no + longer silent. + +- d8c4957: feat: user-level export permission axis (#3544, #3391 follow-up) + + `export` is a user-gated operation, not just "anyone who can list". A permission + set can now deny export on an object while keeping read — matching Salesforce + "Export Reports" / Dynamics "Export to Excel" / NetSuite "Export Lists" / SAP + S_GUI 61. + + - **spec** `ObjectPermissionSchema` gains an optional `allowExport` bit. It is + deliberately OPTIONAL with **no default** so it is a backward-compatible + opt-out: unset → inherits read (today's "can-list ⇒ can-export"), `false` → + export denied while read is kept, `true` → granted. + - **plugin-hono-server** `annotateEffectiveApiOperations` derives + `userExportAllowed = allowExport !== false` from the resolved per-object + permission and threads it into `resolveEffectiveApiMethods` — so `export` + derives from `list ∧ userExportAllowed`. When the axis removes `export` from + an otherwise-open object, the object is now annotated (the effective set minus + `export`) so the client hides the Export button; an unrestricted object with + export still allowed stays unannotated (client default-allow). + + Wires the `userExportAllowed` slot reserved in #3391 P1 — zero contract change + to the derivation table or the frontend (it already consumes the effective + `apiOperations`). Backward-compatible: existing permission sets (no + `allowExport`) keep today's behavior everywhere. + +### Patch Changes + +- d99aeb3: feat(spec): let an inline `lookup` action param declare its reference target (#3405) + + `ActionParamSchema` had no way to name the object an inline record-picker param + should search. Authors reasonably wrote the same key the field schema uses — + `{ name: 'inspector', type: 'lookup', reference: 'sys_user' }` — and the schema + stripped it as an unknown key, without an error. Downstream, the param dialog + saw a picker with no target and degraded it to a "paste the record id (UUID)" + text input. The authored intent was dropped silently and the user was handed a + control that a human cannot reasonably operate. + + - Added `reference` to `ActionParamSchema`, spelled to match + `FieldSchema.reference` so one spelling works in both places. It sits with the + existing inline widget config (`multiple` / `accept` / `maxSize`), which had + covered the file/image params but not the picker ones. + - A `lookup` / `master_detail` param declared **inline** with no `reference` is + now a parse-time error pointing at the missing key, instead of degrading at + render time. Field-backed params are unaffected: they inherit the target from + the referenced field's metadata, which is not visible at parse time. + +- 37b1346: feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1) + + `POST /api/v1/storage/upload/complete` now returns the opaque `sys_file` id + (`data.fileId`), and `client.storage.upload()` surfaces it on the returned + `FileMetadata`. Previously the commit response omitted the id — the caller + could not learn which id to persist after committing an upload, so a file + field could never store a reference. + + Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the + client falls back to the presigned id when talking to an older server). This is + the enabling foundation for file-as-reference; the storage model itself is + unchanged in this PR. + +- 201b31f: fix(spec): fold agent `knowledge.topics` into `sources` at parse; mark unenforced AI config experimental (#1891, #1893) + + Two liveness-audit closeouts (umbrella #1878): + + - **`AIKnowledgeSchema`** now folds the deprecated `topics` alias into the + canonical `sources` at parse time (canonical wins; alias dropped from the + output — mirrors the `visibleWhen` normalization, ADR-0089 D2). Authoring + `topics` was a silent no-op: the renderer only reads `sources`. The schema's + JSDoc example now shows `sources`. + - **Author-facing experimental markers** added to config that is parsed but has + no runtime consumer, matching the liveness ledger (ADR-0078): agent + `memory` / `guardrails` / `structuredOutput` / `lifecycle`, and tool + `outputSchema` (keys folded into the LLM-facing description only — no output + validation). + + Reference docs regenerated. No parse-acceptance change; `Agent`'s inferred + output type no longer carries `knowledge.topics` (input still accepts it). + +- 1986594: feat(analytics): honour widget `dateGranularity`, `sortBy`/`sortOrder`, and `limit` in the dataset query (#3588) + + Three presentation options were accepted by the metadata layer and then dropped + by the analytics query builder. They reached no SQL, produced no error, and the + only way to notice was to read the `sql` a dataset response echoes — so a + dashboard could declare `dateGranularity: 'month'` and quietly render one bar + per record. + + - **`dateGranularity` now buckets.** `DatasetSelection` gained an optional + `dateGranularity`, applied to every selected `date` dimension. Precedence per + dimension: an explicit `timeDimensions` granularity, then the selection's, + then the dataset dimension's own default. A widget can bucket a trend by month + without the dataset committing every other consumer to that granularity. + - **`order` / `limit` / `offset` now apply on every path.** They are applied to + the ASSEMBLED grid — after measure-scoped sub-queries merge, after `compareTo` + columns attach, and after derived measures are computed — so a derived measure + is a valid sort key and the ObjectQL aggregate path (which has no ordering + grammar, and which native SQL hands every date-bucketed query to) orders + identically to native SQL. A single-query selection still pushes the window + down into the statement. An `order` key that names nothing the selection + projects is now rejected (400) rather than silently ignored. + - **`limit` is deterministic.** Without an `order`, a limit orders by the + selected dimensions first, so it truncates a reproducible window instead of an + arbitrary subset. + - **Widget `options` is a contract again.** The four query-affecting keys + (`dateGranularity`, `sortBy`, `sortOrder`, `limit`) plus `stageOrder` are + declared on `DashboardWidgetOptionsSchema`, so a typo like `sortDirection` is + an author-time error. The bag stays open — renderer extras (`icon`, `columns`, + `striped`, …) pass through untouched. + + Two latent bugs surfaced while fixing the above and are fixed here too: + + - `order`/`limit` were forwarded to EVERY sub-query. A measure-scoped + supplementary query selects one measure, so an inherited `ORDER BY` named a + column it never selected, and an inherited `LIMIT` truncated it before the + merge — dropping rows from the assembled grid. Nothing hit this only because + nothing passed `order`. + - The `compareTo` pass built its query by hand and skipped granularity + resolution, so a month-bucketed primary grid was merged against raw-timestamp + comparison rows. No dimension key matched and every `__compare` + column came back empty. + + `ObjectQLStrategy` now also echoes a representative `sql` (with `date_trunc`, + `WHERE`, `ORDER BY`, and `LIMIT`; filter values parameterized, never inlined). + Previously the `sql` field simply vanished from the response whenever a query + was date-bucketed, leaving an author unable to tell "not implemented" from "this + strategy doesn't report". + +- e9b11df: fix(auth): app-declared organization roles are now storable, not just registerable (#3723) + + `AuthManagerOptions.additionalOrgRoles` registered every `permission` / + `position` name a stack declared with better-auth's organization plugin, so + `POST /organization/invite-member { role: 'sales_rep' }` passed the role check — + and then the write failed, because `sys_invitation.role` and `sys_member.role` + were closed selects listing `owner|admin|member` only: + + ``` + ValidationError: role must be one of: owner, admin, member + { field: 'role', code: 'invalid_option' } + ``` + + A select is enforced on write and better-auth's own inserts are not exempt (they + run through the ordinary ObjectQL validator), so any stack declaring role names + was registering roles that could be requested and never stored. + + Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single + normalizer; its output feeds better-auth's role map **and** the two `select` + option lists, so neither side can accept a name the other rejects. The built-in + roles (`owner`, `admin`, `delegated_admin`, `member`) live in + `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the + platform objects declare statically — app roles are appended at boot. + + New exports: + + - `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to + `identity/membership-role`; the package-level export path is unchanged). + - `@objectstack/plugin-auth` — `collectStackOrgRoles`, + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, + `withMembershipRoleOptions`. + + Hosts that boot `AuthPlugin` from a loaded stack should derive + `additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the + stack themselves — `objectstack serve`, the `@objectstack/verify` harness and + `DevPlugin` now all do. The harness previously passed none, which is why a + dogfood proof could boot a stack whose declared roles better-auth had never + heard of; `DevPlugin` documents itself as equivalent to the full stack and + silently excluded app roles from that equivalence. + + `additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and + `collectStackOrgRoles` now returns those descriptors. The label is what the + declaring `position` / `permission` metadata already says, so the role picker + shows `Executive` for a position declared as such instead of title-casing the + machine name into `Exec` — a third source of truth for one string. Presentation + only: better-auth sees just the name, and the stored value is always the name. + Passing `string[]` keeps working unchanged. + + Behaviour change worth noting: a declared role name that is not a valid machine + name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a + boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a + name would be registered verbatim and stored mangled — the same mismatch with + extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. + +- 0bc685a: fix(approvals): return decision attachments as file values, not "[object Object]" (#3504) + + `sys_approval_action.attachments` is a `Field.file`, so the column **stores an + opaque `sys_file` id** (ADR-0104 D3 — the stored form of every media field). The + ObjectQL read path resolves that id into its expanded + `{ id, name, size, mimeType, url }` form on the way out. But `rowFromAction` + mapped the column with `.map(String)`, collapsing each expanded value to the + literal string `"[object Object]"`. Every `listActions` consumer (the approval + inbox timeline) then received garbage: the attachment chip had no filename and + its id was `"[object Object]"`, so opening it 404'd. + + - `ApprovalActionRow.attachments` is now `ApprovalActionAttachment[]` — the + expanded file value plus its id, so a consumer can label and open an + attachment without needing read access to the system `sys_file` object (which + regular approvers do not have). + - Three read forms are accepted: the expanded value (the normal case), a bare id + (nothing to expand it into — storage service absent, file not committed), and + a legacy inline blob written before file-as-reference (`file_id` / + `mime_type`), until the backfill converts it. The id test reuses the + platform's `isFileIdToken`, so this and the engine's read resolver cannot + disagree about what counts as an id. + - The decision _input_ (`ApprovalDecisionInput.attachments`) is unchanged — it + still takes fileId strings, which is also exactly what the column stores. Only + the read shape changed. + +- b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456) + + The record lock is keyed on a **pending** `sys_approval_request`, and it could + not tell _the run that owns that request_ from _an unrelated user editing the + record_. So a flow that touched its own target record while its own approval was + still pending — a manual `resume` with no decision, or a node that writes the + record between opening the approval and the decision — died on its own + `RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery + existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it + self-healing. + + Both halves are now closed. + + **Prevention — the owning run may write its own record.** The automation engine + stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it + travels with every data node's ObjectQL context into `ctx.provenance`. The lock + hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`. + It is keyed on run identity rather than elevation on purpose: a `runAs:'user'` + run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance — + server-constructed like `isSystem`, never client-supplied, evaluated by no + security middleware, and the only write it permits is to the one record its own + run already holds a pending request against. + + **Recovery — a sweep releases records held by runs that died anyway.** A pending + request whose owning run has reached a terminal state (`completed`, `failed`, + `cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled` + — releasing the lock — and audited under the reserved actor `system:dead-run` + with the run and its status in the comment, so it is never mistaken for a + submitter's withdrawal. It runs on the existing approvals sweep clock, which also + covers the case no in-band handler can: a run killed by a process crash. + + The sweep is fail-safe by construction. It acts only on an explicit terminal + status from a closed set; `paused` (the normal state of a live approval), + `running`, an unrecognised status, an unknown run, a `getRun` that throws, and a + deployment with no automation engine are all read as "still alive". The failure + mode is "a dead run's lock survives until an admin recalls it" — today's + behaviour — never "a live approval is destroyed". + + Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for + a run id rather than the latest. A run that pauses and later finishes records two + entries under one id, so every suspend-then-finish run — every approval, screen + and wait flow — reported itself as `paused` forever, both on the Runs + observability surface and to this sweep. + + One shape was left out here and closed separately in #3712: a `runAs:'user'` run + with no trigger user (a schedule) resolved no ObjectQL context at all, so it + carried no `flowRunId` and stayed subject to the lock. It now passes a + provenance-only context — the run id and nothing the security middleware keys on + — so it is attributable without acquiring a principal, and its documented + unscoped posture (#1888) is unchanged. + +- be1c52c: fix(approvals): admin override for a request routed to an unstaffed approver (#3424) + + An `approval` node routed to a `position` (or `team`/`department`) with **no + holders** resolved to only the unresolvable `position:` literal in + `pending_approvers` — no concrete user was in the slate. Every normal + `decide` / `reassign` / `recall` then returned `FORBIDDEN` (not a pending + approver) and, with `lockRecord`, the target record stayed `RECORD_LOCKED` + forever: a data-availability dead-end with no in-product recovery (the only exit + was editing the DB by hand). Very easy to hit in fresh/demo orgs (positions + seeded, holders not) and whenever a role is vacated in production. + + A **platform or tenant admin** — the same posture the engine's superuser bypass + already trusts — may now act on any _pending_ request to release it: **approve, + reject, reassign** it to a real approver, or **recall** it. The override finalizes + the request (which releases the record lock, keyed on a pending request); a + tenant admin's authority is org-scoped, a platform admin's is not, and the + decision is audited under the admin's own id. An admin approval is authoritative, + finalizing the node even under `unanimous` / `quorum` / `per_group` rather than + counting as one vote among the (empty) slate. + + - `sys_approval_request.viewer` gains `can_override` (server-computed): true for a + privileged admin on a pending request. The `approve` / `reject` / `reassign` + declared actions OR it into their `visible` gate, so the console surfaces the + recovery path without a hand-wired button. Existing approver/submitter gating is + unchanged. + - `openNodeRequest` now logs a loud warning when a node resolves to **no concrete + approver**, so the misconfiguration is visible instead of silently locking the + record. The literal-fallback behavior (kept for 15.x slot back-compat) is + otherwise unchanged. + +- c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712) + + #3456 let the run that opened a pending approval write its own target record, + keyed on `flowRunId`. It worked for every run that resolves an identity and + missed the one that doesn't: an effective `runAs:'user'` run with **no trigger + user** — a schedule being the canonical case — passed no ObjectQL context at + all, so nothing carried the run id and the run still died on its own + `RECORD_LOCKED`. + + The blocker was never the lock. It was that "no identity" and "no context" were + the same thing on the wire, so a run could not say _who it was_ without also + claiming _what it was allowed to do_. + + **A run with no principal now passes provenance alone.** + `resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`, + no `permissions`, not even `isSystem: false`. Every principal gate keys on one + of those fields (the elevation short-circuit on `isSystem`, the ADR-0103 + engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`, + the empty-principal fall-open on all three), so this context authorizes + **identically to no context at all**. The run keeps the documented #1888 + unscoped posture, its loud `[runAs]` warning, and the + `flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch + changed — only that it can now be attributed. + + **Provenance moved out of the hook session, into `ctx.provenance`.** `session` + answers _who is calling_ and is absent when no identity envelope was supplied — + a distinction real gates depend on (the attachment access gate skips bare-kernel + writes on exactly that test). Folding a run id into `session` would have forced + an identity-less run to present an empty session, silently turning "no caller" + into "an anonymous caller" and narrowing the #1888 fail-open for attachments + alone. `HookContext.provenance.flowRunId` says what produced the write; the + approvals lock reads it there. + + Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope + (`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time + defaults, which made them _required_ on a caller-supplied option and asserted + something untrue — that every data-engine context carries a principal. Callers + have always passed slices (`{ isSystem: true }` for a system read); the type now + says so. + + Migration: nothing to change unless you read the run id inside a hook. If you + wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the + field never shipped under the old name. + +- 84e7be9: feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807) + + `per_group` (会签) requests now carry `pending_approver_groups` on the + enriched row — a map from each still-pending approver id to the group key(s) + it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label + each "waiting on" chip with the group it represents instead of showing + duplicate, context-free names. + + - Resolved in `attachDecisionProgress` from the same open-time + `__approverGroups` snapshot the `decision_progress` groups already use, so + the two never disagree. + - Only the **pending** slots are mapped (a resolved approver has left + `pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are + dropped — a `· #0` sub-tag would be noise. + - Absent for non-`per_group` behaviors. Display-only; the engine's + finalization tally stays authoritative. + - Added to the `ApprovalRequestRow` contract in `@objectstack/spec`. + +- debc23a: feat(approvals): enrich inbox rows with `payload_labels` (snapshot field labels) + + The approvals inbox summary title-cased raw snapshot machine keys + (`assessment_status` → "Assessment Status") because the API sent no field + labels. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot + field key → the target object's field label), symmetric with the existing + `payload_display` (which resolves the values), and `ApprovalRequestRow` gains + the field. For a single-locale project the schema label is already the + localized string, so a client can render the human field name (e.g. "考核状态") + instead of a prettified English key. + +- 376a061: Surface the approval node's author-declared `decisionOutputs` keys on the request read as `ApprovalRequestRow.decision_outputs` (#3447 P2 UI enablement). The set varies per request (each node declares its own), so it rides the row rather than the object's static action params — a decision UI renders one input per key and POSTs `outputs` with the decision. +- 5b89711: feat(spec,lint): freeze the `{current_user_id}` filter vocabulary and fail the build on unresolvable placeholders (#3574) + + A dashboard widget filtered on `{current_user}` rendered `0`. Not an error — a + zero, indistinguishable from a metric that is legitimately empty, with nothing + in the console or the server log. `service_dashboard.my_open_cases_by_priority` + in the HotCRM template had shipped broken this way since the day it was + written. + + The token had never been part of the contract. Date macros were frozen in + `date-macros.zod.ts` with a spec vocabulary, a lint-usable predicate, and a + single client resolver; `{current_user_id}` had only prose in an `app.zod.ts` + JSDoc and three ad-hoc client implementations that each handled one surface's + filter shape. Nothing could tell an author their token was wrong. + + - **`@objectstack/spec`** — new `data/context-tokens.zod.ts` freezing + `CONTEXT_TOKENS` (`current_user_id`, `current_org_id`) as the sibling of + `DATE_MACRO_TOKENS`, with `isContextToken` / `isKnownFilterToken` / + `classifyFilterToken` and a `CONTEXT_TOKEN_SUGGESTIONS` near-miss table. The + module documents what the tokens are _not_: presentation scope, never an + access boundary — that is RLS, which uses the unrelated `current_user.id` + expression root. + - **`@objectstack/lint`** — new `validateFilterTokens` (rule + `filter-token-unknown`, severity `error`). It walks `filter` / `filters` / + `runtimeFilter` subtrees across dashboards, objects, views, reports, + datasets, pages and apps, and reports any placeholder that resolves in + neither vocabulary. It scans for filter _keys_ rather than enumerating known + surfaces, so a new surface following the convention is covered the day it + ships — enumerating surfaces is how the dashboard was missed in the first + place. Navigation `recordId` / `params` are deliberately out of scope: they + resolve `AppContextSelector` ids, which are meaningless in a filter. + - **`@objectstack/cli`** — the gate runs in `os validate` and `os compile`. + + It is an error rather than a warning because of who authors this metadata. An + AI reads a query returning `0` as a correct answer and builds on it; its + correction loop is author → validate → fix, so a diagnostic only reaches it if + it can fail the build. The three spellings the suggestion table covers — + `{current_user}`, `{user_id}`, `{organization_id}` — are each correct + _somewhere else_ in the platform, which is exactly why authors reach for them. + + Also fixes a `ViewSchema` JSDoc example that documented `{user_id}`, a token + that resolves nowhere. + +- c4df271: chore(spec): mark FormView `buttons`/`defaults` live now the ObjectUI renderer folds them (#1894) + + The structured `FormViewSchema.buttons` (per-button `submit`/`cancel`/`reset` + visibility + label) and `defaults` (create-mode initial values) shipped under + the ADR-0078 escape hatch — declared, but carrying an `[EXPERIMENTAL — NOT +ENFORCED]` marker because no consumer read them yet. The ObjectUI `ObjectForm` + renderer now folds both onto the flat props it reads + (`showSubmit`/`submitText`/`showCancel`/`cancelText`/`showReset`/ + `initialValues`), so the escape-hatch marker is dropped and the two spec + liveness-ledger entries (`view.form.buttons`, `view.form.defaults`) flip + `experimental → live`. + + No shape or parse-behavior change — both keys were already accepted. This + closes the `view` half of the inverse-drift cleanup (renderers reading + undeclared props), umbrella #1878. + +- a41ba5c: chore(spec): enroll `report` and `dashboard` in the liveness GOVERNED set (#3462) + + Closes the systemic anti-drift gap for two more authorable UI types (umbrella + #1878). Both were registered/round-trippable but ungoverned, so their property + liveness wasn't CI-checked — the reason drifts like dashboard `title`↔`label` + and stale report `chart` config survived until an audit caught them. + + - Added `packages/spec/liveness/report.json` (20 live / 2 dead) and + `dashboard.json` (18 live / 2 dead), each property classified with an + objectui consumer reference. + - Re-verification corrected several stale 2026-06 audit findings against current + code: report `chart` is **live** (DatasetReportChart plots `chart.xAxis`/ + `yAxis` via `useDatasetRows`, #1890/#3441); dashboard `globalFilters`/ + `dateRange` are **live** (framework#2501); `title`↔`label` fixed (objectui#2806); + the ADR-0021 widget migration shipped (#3251). Only `aria`/`performance` remain + dead on each (perf `authorWarn`'d). + - Added both to `GOVERNED` in `check-liveness.mts`; the gate is green. Future + drift on these types is now a CI failure, not an audit finding. + + `webhook` (the third type in #3462) is deferred — it isn't a registered + metadata type; its enrollment rides with the disconnect decision in #3461. + + No spec shape/behavior change (ledger + gate config only). + +- 189854c: chore(spec,cli): enroll `webhook` in the liveness GOVERNED set (#3462) + + Closes the final third of #3462 (umbrella #1878) — `report` and `dashboard` + landed in #3474; `webhook` was deferred for two reasons, both handled here. + + - **Not a registered metadata type.** `webhook` is absent from the metadata-type + registry, so the gate can't resolve it via `getMetadataTypeSchema`. Registering + it would switch on Studio webhook CRUD, `saveMetaItem` overlay acceptance, and + diagnostics sweeping — the wrong move while the authoring surface is still + disconnected (below). Instead the gate resolves it through a small + `SPEC_ONLY_SCHEMAS` override in `check-liveness.mts` (consulted before the + registry): the gate only needs to **walk** the schema, not register it. + - **The whole authoring surface is dead (#3461).** Nothing materializes an + authored `webhooks:` entry (stack/connector) into a `sys_webhook` dispatcher + row — the runtime reads only admin-authored `sys_webhook` rows. So + `packages/spec/liveness/webhook.json` classifies all 16 authorable props + **dead** and `authentication` **experimental** (HMAC-`secret`-only, its + existing marker). Per-prop notes record which props a future materializer + (#3461 option A) could remap (e.g. `object`→`object_name`, `isActive`→`active`) + vs which have no sink anywhere — doubling as that mapping table. + - **Author-warning wired (`@objectstack/cli`).** Added + `{ type: 'webhook', key: 'webhooks' }` to `TYPE_COLLECTIONS` in + `lint-liveness-properties.ts`, so `os compile` now advises authors that + `webhooks:` is a silent no-op. The required `url` prop carries the single + warning per webhook (one heads-up per artifact, not one per dead prop); + `isActive` is left unmarked (default(true) boolean). + + This is enrollment only — it does **not** decide #3461's build-the-bridge vs + retire-the-surface question. When that lands, the mapped props flip to live (cite + the materializer) or the ledger is removed with the schema. No spec shape/behavior + change (ledger + gate/lint config only). + +- 0e3a226: fix(authz): widen the driver's native tenant scope to the membership union + under the `group` posture — ADR-0105 D2 finally reaches the wire (#3623) + + The Layer 0 wall correctly compiled `organization_id IN accessible_org_ids` + under `group`, but the ObjectQL engine also propagated the active-org + `tenantId` into `DriverOptions` unconditionally, and the SQL driver's native + scoping ANDed `organization_id = tenantId` under the union — collapsing every + group read back to active-org (isolated) reach. Found by the cloud-side + `ee-group-showcase` dogfood (cloud#880), the first end-to-end boot of `group` + against a real driver. + + - `DriverOptions.tenantIds` (spec): the union tenant access set. Drivers with + native scoping widen reads/updates/deletes/aggregates to `IN (...)`, + keeping the NULL-tenant global-row carve-out; inserts still stamp from + `tenantId` (the active organization is the write target, D5). Absent or + empty ⇒ equality fallback — fail toward isolation, never toward exposure. + - ObjectQL engine threads `ExecutionContext.accessible_org_ids` as + `tenantIds` when the tenancy posture is `group`, reported by a new + `setTenancyPostureProvider` seam. + - SecurityPlugin wires that provider at start — deliberately from the + enforcement layer, so the driver wall only widens while the Layer 0 union + wall enforces above it. Embeddings without plugin-security keep active-org + equality. + +- a8d1e24: feat(cli,spec): gate the whole declared surface for i18n, and translate inline object actions server-side (#3370) + + In a zh-CN workspace the platform chrome was localized while author-declared + labels leaked English — the approval drawer rendered **Approve / Reject / + Reassign** right beside the inbox's own 通过 / 拒绝. Two independent holes, both + closed here. + + **The lint gate could not see them.** `os lint`'s i18n coverage kept its own + walk of the metadata, separate from the one `os i18n extract` uses to scaffold + bundles, and the two had drifted: coverage only ever walked the _top-level_ + `actions` array, while `sys_approval_request` declares its decision actions + **inline on the object**. Those labels were extractable but ungated, so an + untranslated one could ship and no lint run would notice. Coverage now derives + its expected keys from `collectExpectedEntries()` — the extractor's walker — so + the gated surface and the scaffolded surface cannot disagree again. Newly gated + as a result: inline object actions, action `params` and `resultDialog` copy, + object-nested `listViews` (label / description / `emptyState`), object + `description`, field `help` / `placeholder`, and the `apps` / `dashboards` / + `pages` surfaces. Extract output is byte-identical — verified against the + committed plugin bundles. + + **It stays silent for projects that do not translate.** Which locales get + checked is the project's declaration, never an assumption: `os lint`, + `os i18n check` and `os i18n extract` now read the stack's own + `i18n.defaultLocale` / `i18n.supportedLocales`, falling back to the locales a + bundle already exists for, and finally to `en`. A project with neither is + checked against its default locale alone — which its inline labels already + satisfy — so it reports zero i18n issues. That also fixes a monolingual + _non-English_ project being told it owed `en` translations it never claimed to + speak. Locked by regression tests; the three bundled examples stay at 0 errors. + + **The server sent English regardless of locale.** `translateObject` walked an + object's `label` / `pluralLabel` / `description` / `fields` but never its inline + `actions`, so `GET /api/v1/meta/object/:name` returned the authored English + literals even though `@objectstack/plugin-approvals` ships `_actions` + translations for all eight decision actions in zh-CN / ja-JP / es-ES. The + Console compensated by re-resolving labels client-side against a separately + fetched bundle; every other consumer — mobile, plain HTTP, SDUI — rendered the + source language. It now runs inline actions through `translateAction`, without + stamping a synthetic `objectName` onto the response. + + Adds `os i18n extract --no-metadata-forms`. Whether the companion + `.metadata-forms.generated.ts` file is written was previously implicit: + every run emitted it, so `--check` demanded that file in packages that + deliberately do not commit one. The Studio metadata-form baseline is + registry-driven and identical for every stack, so exactly one package owns it + (`platform-objects`); a plugin translating only its own objects now opts out, + and its `--check` stops failing on a tree that is in sync. Defaults to emitting, + so `pnpm check:i18n` keeps covering all 8 platform bundles. + +- 81ce41a: feat(rest): `treatAsHistorical` import also preserves the original audit timeline (#3493) + + Follow-up to #3479/#3483. `treatAsHistorical` solved the FSM half — mid-lifecycle + rows are no longer rejected by `initialStates` — but the OTHER half of a historical + migration, preserving the original timeline, still didn't hold: an imported ticket + that closed in 2021 stored `updated_at` = the import day (and `updated_by` = the + importer), and a `writeMode: 'upsert'` refresh silently dropped business `readonly` + fields (`closed_at`, `resolved_by`). Reports, audit, and "recently modified" + sorting all came out wrong. + + Three layers were force-overwriting the timeline; all three now respect a single + new opt-in flag, `ExecutionContext.preserveAudit`, which `treatAsHistorical` sets + alongside `skipStateMachine`: + + - **spec**: `ExecutionContext.preserveAudit` (server-set only, never client-supplied) + and `DriverOptions.preserveAudit` (threaded to the driver's update stamp). + - **objectql** — the built-in audit hook (`plugin.ts`) now treats `updated_at` / + `updated_by` as CLIENT-PREFERRED (`?? now` / `?? userId`) under `preserveAudit`, + symmetric with how `created_at` / `created_by` already behave on insert; and the + static-`readonly` write strip (`stripReadonlyFields`) admits a WHITELIST — the + audit/timestamp family plus author-declared business `readonly` fields — so an + upsert refresh no longer drops them. + - **driver-sql** — the SQL `update` path keeps a supplied `updated_at` instead of + force-advancing it to `now` when `DriverOptions.preserveAudit` is set (fills-only- + empty, mirroring the insert stamp). + - **rest** — the import runner sets `preserveAudit` on the write context iff the + request opts into `treatAsHistorical`. + + Deliberately a WHITELIST, not the blanket `isSystem` exemption: platform-managed + `system` columns OUTSIDE the audit family (`organization_id` / tenancy, generated + columns) STAY stripped, so a historical import reinstates established facts without + becoming a backdoor to forge tenancy. Permissions / RLS / field-level security are + unaffected — this changes only which audit/readonly values the runtime overwrites, + never who may write the record. Fully opt-in: a normal write still auto-stamps + `updated_at`/`updated_by` and strips `readonly` exactly as before. The objectui + "Import as historical data" checkbox (objectui#2815) now drives both halves — no new + UI. + +- 85e1e4e: feat(rest): `treatAsHistorical` import option — skip the state machine for historical-data migration (#3479) + + Sibling of #3433 (seed exemption), one entry point over. #3165's `initialStates` enforced + the FSM entry point on every INSERT, so importing established historical facts — + a batch of already-`closed` tickets, `closed_won` deals, `completed` projects — + was rejected row-by-row with `invalid_initial_state`, blocking the core + data-migration path. Unlike the seed case it was visible (per-row errors), but it + still functionally blocked a legitimate use. + + - **spec**: `ExecutionContext.skipStateMachine` — a general, server-set flag (the + seed-specific `seedReplay`'s sibling) that skips the `state_machine` rule for a + write; `ImportRequestSchema.treatAsHistorical` (default `false`) — the user-facing + import option. + - **objectql**: the engine now skips the state machine for `seedReplay` OR + `skipStateMachine` (one helper), covering both seed replay and historical import. + - **rest**: the import runner sets `skipStateMachine` on the write context iff the + request opts into `treatAsHistorical`; default off, so a normal import still walks + the FSM (the strict behavior is the default). Import **undo** now also carries + `skipStateMachine`, since restoring a prior snapshot re-writes an earlier state + that need not be a legal transition from where the row is now. + - **platform-objects**: `sys_import_job.treat_as_historical` audit column (additive). + + Scope is identical to the seed exemption: ONLY the `state_machine` rule is skipped; + field shape, `format`, `cross_field`, `script` all still run. The objectui import + wizard checkbox is a separate follow-up. + +- dac6a08: feat(driver-sql)!: make index drift visible to `os migrate plan` — no more silent DDL at boot (#3728) + + The #3696 unique-scope migration converged **in place**: `syncTableIndexes` ran a + `DROP` + `CREATE UNIQUE INDEX` during `initObjects`, in every environment, + leaving one log line behind. `os migrate plan` showed nothing, because + `detectManagedDrift` was column-only — `ManagedDriftOp` had no index dimension at + all. An operator who wanted to review the DDL before it reached their database + had no way to, and a managed schema was being auto-altered in production, which + the #2186 contract explicitly forbids. + + Index drift is now a first-class dimension, reconciled through the same path as + column drift: + + - **`syncTableIndexes` is additive only.** It creates indexes; it never drops or + rewrites one. `dropLegacyGlobalUniques` is gone. + - **New `DriftOp` variants** — `replace_unique_index` (safe: retire the legacy + platform-wide unique in favour of the tenant composite), `create_index` (safe), + `recreate_index` (needs-confirm; destructive when it tightens to `UNIQUE`), and + `drop_index` (destructive). + - **`detectManagedDrift` reports them**, `os migrate plan` renders them (index + ops display as `table [index_name]`), and `os migrate apply` executes them. + Index DDL is portable, so it applies directly on every dialect — no SQLite + table rebuild. + - **`replace_unique_index` creates before it drops**, so uniqueness is never + unenforced mid-migration and a failed create leaves the schema untouched. + - **Declared `indexes[]` drift is covered too**: an index metadata declares but + the database lacks, and one whose definition no longer matches the declaration + (the additive sync skips those by name, so they could never self-heal). + - **Orphan detection is limited to ObjectStack's own generated naming** + (`uniq_…` / `idx_…`, plus the pre-#3696 `
__unique` knex + spelling). A hand-rolled operational index is never reported as drift and + `--allow-destructive` will not delete it. + + **Behaviour change.** Boot no longer rewrites the index unconditionally. Dev + (`autoMigrate: 'safe'`, what `os dev` / `os serve` use) still self-heals on + restart, so local workflows are unchanged. Production now **warns** with an + actionable `os migrate` hint and leaves the schema alone — the deployment stays + on the legacy global unique (multi-tenant inserts still collide) until someone + runs `os migrate apply`. That is the deliberate trade: a visible, pre-inspectable + migration instead of an invisible one. + + Also fixed: `managedObjectIndexes` was never cleared when an object dropped its + `indexes[]`, so drift detection kept expecting an index nobody declared. + + `SchemaDiffEntryKind` gains `index_mismatch` and `unmapped_index`. + +- f07808c: feat(spec): reject a `body` on a non-script action — it would never run (#3530) + + `Action.body` is documented as "only meaningful when `type === 'script'`", but + nothing enforced it. A `type: 'modal'` action authored with `params` and a + `body` — expecting the modal to collect the input and the body to write the + record on submit — passed validation, passed shape tests, and shipped a button + that opened a modal and silently wrote nothing. Non-script types all dispatch on + `target` (the page to open, the URL, the flow, the endpoint); there is no point + at which a renderer would invoke the body. + + This is the same invisible-failure shape as the existing rule that rejects a + `script` action with neither `body` nor `target` (#2169), so it is enforced the + same way: a parse-time error that names the fix — `type: 'script'` collects the + same `params` and does run the body, and a modal that only opens a page should + drop the `body` and keep `target` naming the page. + +- 32ff033: docs(spec): correct ReportChart `xAxis`/`yAxis` semantics; mark dead report surface (#1890) + + Closes the report residual of the ADR-0021 analytics migration (#1890). The + dataset-bound report chart already renders — objectui's `DatasetReportRenderer` + plots `chart.xAxis`/`yAxis` as the bound dataset's **dimension**/**measure** via + `useDatasetRows`, and the Studio `ReportDefaultInspector` picks them from the + dataset's dimension/measure catalogs — but the spec `.describe()` still called + them raw "Grouping field" / "Summary field", misleading an author (or AI) into + naming object fields instead of dataset dimension/measure names. + + - `ReportChart.xAxis`/`yAxis` describe now states they are dataset + dimension/measure names (matching the live renderer + inspector). + - `ReportChart.groupBy` marked `[EXPERIMENTAL — not enforced]` — the + dataset-bound renderer plots a single `xAxis`×`yAxis` series and never reads + it; only the legacy `ReportViewer` fallback did. + - `ReportColumnSchema` / `ReportGroupingSchema` marked `@deprecated` — the + single-form report shape expresses columns/grouping as dataset + measure/dimension name arrays, so these objects are unreferenced; they remain + only as public type exports (objectui re-exports them) pending a governed + prune. + + Docs regenerated (`ui/report.mdx`). No shape or parse-behavior change; no + export removed. + +- abceb0d: fix(seed-loader): support a composite `externalId` so join-table seeds dedupe on replay (#3434) + + A junction / join table has no single-field natural key — the PAIR of its + foreign keys is what's unique — so its seed could only run `mode: 'insert'`, + which re-inserts every row on each replay boot with no existing-row check + (`decideWriteAction`'s `insert` case returns `insert` unconditionally). The + table duplicated on every restart: the showcase `showcase_project_membership` + fixture (3 rows) grew 3 → 6 → 9. It was masked until #3415 let the master-detail + parents seed at all. + + - `SeedSchema.externalId` now accepts a **list** of field names + (`externalId: ['team', 'project']`) in addition to a single field name, + declaring a composite natural key. Default stays `'name'`. + - `SeedLoaderService` builds the uniqueness key from all listed fields (joined + with a `\u0000` separator that can't occur in a natural-key value). Reference + key fields are compared by their RESOLVED parent ids — which the existing DB + row already stores — so a composite of foreign keys matches across restarts. + A partial key (any component absent) is treated as no key, falling back to + insert, exactly as a missing single-field key already did. + - A composite-key target does not participate in single-value reference + resolution (a reference is one natural-key string), so such objects keep the + `'name'` default when referenced by another dataset. + + The showcase membership fixture switches to `mode: 'ignore'` + + `externalId: ['team', 'project']`, so replay boots leave the three rows + untouched instead of duplicating them. + +- 0c302a7: Exempt curated seed writes from `state_machine` validation (#3433). + + A seed is a snapshot of established facts — a project already `completed`, an + opportunity already `closed_won` — not a record walking its lifecycle. But once + an object declared `state_machine.initialStates` (#3165), the write path enforced + the FSM entry point on **every** insert, so seed replay silently rejected every + mid-lifecycle row and cascaded its master-detail children. That is the "installed + but no data" failure for the showcase board (1 of 5 projects), and it would hit + every marketplace template (a `closed_won` opportunity, a `closed` case) plus the + rehydrate-heal and per-org replay paths. + + `SeedLoaderService` now marks its writes with a server-set `ExecutionContext.seedReplay` + flag; the engine passes `skipStateMachine` to the rule evaluator for those writes, + which skips the `state_machine` rule on both insert (`initialStates`) and update + (transitions). The exemption is scoped to `state_machine` only — a seed must still + satisfy every other validation (`format`, `cross_field`, `script`, `json_schema`, + `conditional`). Because all seed paths funnel through `SeedLoaderService.SEED_OPTIONS`, + the fix covers boot inline seed, marketplace install/heal, and per-org replay at once. + + The showcase project seed drops its three-phase FSM-walk workaround (#3415) and + seeds each project directly at its real status again. + +- cde1975: fix(dev): eliminate three fixed startup log warnings so official examples boot clean (#3420) + + `os dev` on the stock showcase printed three fixed noise sources on every boot, + with zero example-side changes — training users to ignore warnings. + + - **spec** — add a field-level `ackPlaintextMasking: true` opt-out for the + generic `password` author-time warning (ADR-0100). A deliberately-masked + field (like field-zoo's `f_password`) can now affirm intent instead of + printing an un-actionable "safe to ignore" on every boot; the warning text + points authors at the flag. + - **plugin-auth** — pass better-auth's documented + `silenceWarnings.oauthAuthServerConfig` to `oauthProvider(...)`. We already + mount the `/.well-known/oauth-authorization-server` documents ourselves at + the issuer root, so the plugin's "please ensure it exists" reminder was a + false positive (printed twice); silencing it removes both. + - **objectql** — route the Registry's re-register / package-overwrite lines + (normal rebuild / HMR / seed-replay paths) through a new debug-only + `SchemaRegistry.debug()` so they stay out of the default `info` boot log. Adds + a `logLevel` construction option (and matching `OS_REGISTRY_LOG` env var) so + the debug-gated housekeeping is discoverable for troubleshooting. + +- 0bc685a: fix(storage): downloads carry the real filename + content-type, not the URL token (#3504) + + A presigned download served the bytes as `application/octet-stream` with no + `Content-Disposition`, so a browser saved the file under the opaque URL token + (e.g. `eyJrIjoiYXR0YWNo…`) instead of its real name — an approval's + `signed-contract.pdf` downloaded as a nameless blob. + + - `IStorageService.getSignedUrl` / `getPresignedDownload` take an optional + `PresignedDownloadOptions` (`filename`, `contentType`, `disposition`). + - The REST download routes (`GET /storage/files/:id/url` and `/:id`) pass the + `sys_file` record's `name` + `mime_type`. + - The local adapter carries them in the signed token; the `_local/raw` route + emits `Content-Type` + an RFC 5987 `Content-Disposition` (ASCII fallback + + `filename*=UTF-8''…` for non-ASCII names). The S3 adapter bakes the same into + the signed URL via `ResponseContentType` / `ResponseContentDisposition`. + - Default disposition is `inline`, so previewable types (PDF, images) still open + in the browser — now with the correct name when saved. + +- b098b0e: docs(ai): stop `tool.requiresConfirmation` promising a gate it does not provide (#3715) + + The flag is read by **no execution path** — not the LLM tool set, not + `ToolRegistry.execute`, not `POST /ai/tools/:name/execute`, not the MCP bridge. + Yet the authoring surface actively taught reliance on it: the Studio form + section was titled _"Access & safety"_ with helpText _"Ask user to approve + before executing (for destructive actions)"_, and the AI skill doc, MCP guide + and spec README all recommended it for destructive operations. + + The prune-or-wire decision is deliberately **deferred** (#3715 — the field's + shape is likely needed once side-effect tools exist, which `ToolCategory` + already anticipates with `action` / `integration` / `flow`). What changes now + is only the promise: + + - spec `.describe()` carries `[EXPERIMENTAL — not enforced]` + a pointer to the + real gate; + - the form section is renamed _"Declarative metadata (not enforced)"_ and both + its fields (this and the already-dead `permissions`) say so, with the enforced + alternative spelled out; + - `skills/objectstack-ai/SKILL.md`, `MCP_GUIDE.md` and `README.md` now point at + the action-level `ai.requiresConfirmation` + approval queue (and note that AI + metadata edits are already gated by draft/publish, ADR-0033). + + No behaviour change: nothing read the flag before and nothing reads it now. + +- 69f1dfd: fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461) + + A webhook authored declaratively — `defineStack({ webhooks })` / `defineWebhook()`, + validated against the spec `WebhookSchema` — was a **silent no-op**. The runtime + dispatcher (`AutoEnqueuer`) fans out off `sys_webhook` DATA rows (`object_name` / + `active`), which until now were only ever written by hand through the object's + CRUD UI. Nothing turned a declared webhook (`object` / `isActive`) into a + dispatchable row, so authoring `webhooks:` on a stack produced `webhook` metadata + that never fired (ADR-0078). The showcase app itself shipped a `webhooks:` entry + that did nothing. + + `@objectstack/plugin-webhooks` now bridges the two on boot: + + - **`bootstrapDeclaredWebhooks`** reads declared `webhook` metadata from the + ObjectQL registry (where the manifest decomposition already parks + `stack.webhooks`), validates each through `WebhookSchema.parse()` — the spec + schema finally has a real consumer — and materializes it into a `sys_webhook` + row, mapping `object → object_name`, `isActive → active`, and stashing the full + envelope (headers / secret / retry / timeout) in `definition_json`. The + auto-enqueuer's first cache refresh then picks the row up and dispatches it. + - **Seed-not-clobber provenance** (mirrors `sys_sharing_rule`, #2909): `sys_webhook` + gains `managed_by` / `customized` columns. Declared webhooks re-seed every boot + as `managed_by: 'package'`, but a row an admin created (`managed_by: 'admin'`) or + edited in Setup (`customized: true`, stamped by a `beforeUpdate` hook) is never + overwritten — a deactivated noisy webhook survives redeploys. + + Connector-declared `webhooks` remain not-yet-enforced (that is a separate seam, + #3197). Registering `webhook` as a first-class metadata type + enrolling it in the + liveness `GOVERNED` set is a tracked follow-up. + + Migration: none required. Existing hand-authored `sys_webhook` rows default to + `managed_by: 'admin'` and are never touched by the seeder. Anyone who authored + `webhooks:` on a stack expecting it to fire will find it now does — review those + declarations (especially `url` / `isActive`) before upgrading. + ## 16.1.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index b633345d6c..fdfacc5931 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "16.1.0", + "version": "17.0.0-rc.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/spec/src/kernel/protocol-version.ts b/packages/spec/src/kernel/protocol-version.ts index e986a83160..e35b717486 100644 --- a/packages/spec/src/kernel/protocol-version.ts +++ b/packages/spec/src/kernel/protocol-version.ts @@ -15,7 +15,7 @@ * Kept in lockstep with the package's own major; `protocol-version.test.ts` * asserts it against `package.json` so the two cannot drift. */ -export const PROTOCOL_VERSION = '16.0.0'; +export const PROTOCOL_VERSION = '17.0.0'; /** The protocol major as an integer — the value the handshake compares. */ export const PROTOCOL_MAJOR: number = Number.parseInt(PROTOCOL_VERSION.split('.')[0]!, 10); diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 43dde3534b..1f2e14ffbd 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/trigger-api +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 1492f1d30a..705c5fd3dc 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 86b564cf60..e49031587c 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,213 @@ # @objectstack/plugin-trigger-record-change +## 17.0.0-rc.0 + +### Minor Changes + +- 6f55c63: feat(trigger-record-change): `record-after-write` fires one flow on create OR update (#3427) + + A `record_change` flow's `start` node bound to exactly one lifecycle event via + `triggerType`, so a rule meant to run on both insert and update ("recompute the + SLA whenever a case is created or its priority changes") forced authors to + duplicate the whole flow — two near-identical definitions that drift. + + Adds `record-after-write` and `record-before-write` as the **create-OR-update + union** trigger tokens. One `start` node binds both lifecycle hooks + (`afterInsert` + `afterUpdate`) under the same flow; exactly one fires per + mutation (a write is an insert _xor_ an update), so it is not a double run. + `delete` is deliberately excluded — a write persists field data, a delete + removes the row. To branch on which event fired inside the flow, test + `previous` (empty on create, populated on update). + + - `triggerTypeToHookEvents(triggerType)` (new, plural) is the canonical mapper: + it returns the list of hook events a token binds, expanding `write` to both. + `triggerTypeToHookEvent` (singular) is kept for back-compat and now returns + `null` for the multi-event `write` tokens rather than silently dropping a + binding. + - The engine already forwards any `record-*` token through to this trigger, so + no engine, lint, or spec change is needed — the trigger owns the vocabulary. + + Documented under Automation › Flows (Create-or-update flow) and the trigger's + README. + +### Patch Changes + +- 9dcc0ae: fix(automation): array-form flow `triggerType` fails loudly instead of silently never firing (#3481) + + An array `triggerType` on a flow start node — the shape an author (or an AI + authoring pass) naturally reaches for to fire on more than one event, e.g. + + ```ts + config: { objectName: 'app_task', triggerType: ['record-after-create', 'record-after-delete'] } + ``` + + was accepted everywhere and armed nowhere. Multi-event unions are deliberately + unsupported (only the single tokens plus the `record-after-write` create-OR-update + union exist — see #3457), but nothing said so: `defineFlow` passed the array + (start-node `config` is an open record), the engine's `typeof === 'string'` check + folded it to no trigger and misclassified the flow as **manual**, so it never + entered the trigger-binding audit, and the flow-trigger-readiness lint used the + same `typeof` narrowing and produced no finding. The flow bound to nothing and + never fired, with zero output at any layer — the same silent-never-fire class as + #3427 / #3472, and the last authoring shape still slipping past every guard. + + This is a **defensive** fix — arrays remain unsupported; they now fail loudly: + + - **lint** (`validate-flow-trigger-readiness`): an array `triggerType` containing + any `record-*` element now yields a `flow-trigger-unknown-event` warning at + `os validate` time, steering to `record-after-write` (for created-or-updated) or + one flow per event. + - **engine** (`resolveTriggerBinding`): such an array is routed to the + `record_change` trigger — exactly as an unmappable single token is — instead of + being folded to a manual flow, so it reaches the trigger's bind-time rejection. + - **trigger** (`record-change`): the bind-time rejection detects the array shape + and emits a targeted warning (naming the flow, pointing at `record-after-write` + and #3457) rather than the generic unknown-token line. + +- 169b58a: fix(#3426): build-time warning for unresolvable flow template paths + guard the formula re-read + + Two follow-ups to #3426 (the formula/lookup `{record.}` template gap that #3445 began closing). + + **Build-time signal (the issue's fallback ask).** `os validate` now flags a + record-change flow node whose `{record.}` template cannot resolve — + turning the previous SILENT blank into an advisory warning. Two cases, via the + new `@objectstack/lint` rule `validateFlowTemplatePaths`: + + - `flow-template-unknown-field` — `{record.}` where `` is neither a + declared field nor a system column (a typo like `{record.full_naem}`). + - `flow-template-lookup-traversal` — `{record..}`, a cross-object + hop the seeded record carries only as a scalar id (still unsupported; tracked + on #3426). + + Deliberately quiet: formula fields, bare lookup ids, numeric indexes into + `multiple` lookups (#1872), `json` sub-paths, and system columns are NOT flagged, + and flows bound to an object this stack does not define are skipped (no schema to + compare against). + + **Hydration re-read guards.** The `trigger-record-change` computed-field re-read + (#3445) is now (a) skipped when the object declares no `formula` field — the only + thing it adds — via the engine's optional `getObjectConfig`, and (b) memoized per + write on the shared HookContext, so N flows on one written record share ONE + re-read instead of N. Any uncertainty falls back to the prior unconditional + re-read (correctness over the optimization). + +- 1dc94f0: fix(trigger-record-change): hydrate read-time formula fields onto the seeded flow record (#3426) + + A `formula` field is a read-time virtual — the engine evaluates it post-fetch on + `find`/`findOne`, never on the write path — so it was absent from the raw + after-create/after-update row a record-change flow is seeded with. A notify + node template like `{record.full_name}` (or a start condition on the same field) + therefore resolved to an empty string, silently emitting notifications such as + `"New lead to assign: "` with the name missing. + + The record-change trigger now re-reads the just-written record through the data + engine, so the seeded `record` carries the same computed fields a data-API read + returns. The fix is at the trigger (the producer of the flow's `record`), so it + benefits the whole flow — start condition, every node, and notify `title`/`body` + templates — not just the notify node. + + Deliberately conservative: + + - Runs only for `afterInsert` / `afterUpdate` (the row exists in its post-write + state); `before*` and `afterDelete` keep the raw hook record untouched. + - Reads as an elevated system principal, so it can only ADD computed fields, + never let RLS/FLS on the re-read shrink the snapshot the flow already saw. + - Raw hook fields win on merge, preserving trigger-time scalar values and the + #1872 multi-lookup input overlay; the re-read only fills in keys the raw row + lacks (the formula virtuals). + - Any failure (no read surface, no id, a throw, an empty read) falls back to the + raw record — hydration never breaks the flow it feeds. + + Lookup **traversal** (`{record.account.name}`) is intentionally not hydrated: a + default data-API read does not expand relations either, and expanding would turn + `record.account` from its scalar FK id into an object, breaking templates and + conditions that use the bare id (e.g. #1872's `{record.target_channels.0}`). + That traversal remains tracked on #3426. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index bfc7cf5e55..875bc5ff2f 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": "16.1.0", + "version": "17.0.0-rc.0", "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 8017411e4d..554cd430ed 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/plugin-trigger-schedule +## 17.0.0-rc.0 + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index ae7fbfe05e..aff5ff79c3 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 e0da4b5816..790a4a7c82 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,259 @@ # @objectstack/types +## 17.0.0-rc.0 + +### Minor Changes + +- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a + first-class authorization dimension. + + > This release carries BREAKING spec removals (see "Enforce-or-remove" below) + > but is recorded as `minor`: every publishable package is in the Changesets + > lockstep group, so one `major` would promote the whole monorepo. Breaking + > changes ship as `minor` during the launch window — the migration notes below + > are what reach consumers in `CHANGELOG.md`. + + ## Tenancy is now a spectrum (D1) + + `single | group | isolated`, resolved by the `tenancy` service and selected with + the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: + `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` + (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot + rather than silently landing in a posture with no organization wall. + + - `single` — no wall (unchanged). + - `group` — **new.** Organizations are membership boundaries over one shared + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC + semantics). Enforced by the OPEN engine. + - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` + probe and degraded-boot handling all unchanged. + + ## Organization scope is a first-class context field (D2) + + `ExecutionContext.accessible_org_ids` — every organization the caller holds a + currently-valid membership in (ADR-0091 validity windows) — is resolved once by + `resolveAuthzContext` and carried by every transport. The `group` wall reads it + directly; RLS policies may reference it as + `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set + fails the wall closed. + + Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still + computed independently of the RLS compiler, AND-composed outermost, and + crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so + ADR-0095's W1/W2 invariants hold in every posture. + + ## Two P0 correctness fixes (D3, D4) — behavior changes + + **D3 — app-authored org-scoped RLS policies are no longer silently dropped** + (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose + `using` contained the substring `current_user.organization_id` when isolation was + inactive, which swallowed app-authored policies as well as the platform's own. + Stripping is now decided by PROVENANCE (identity against the shipped + declaration). **Upgrade impact:** in a deployment with no organization wall, an + app-authored policy referencing the active organization is now RETAINED and + fails closed (zero rows) with a one-time warning, where it previously vanished + and the object read unscoped. `getReadFilter` shared the defect, so analytics and + raw-SQL consumers were affected too. If a policy was only ever meant for + multi-org, delete it or install `@objectstack/organizations`. + + **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization + boundary** (finding F2, framework#3540). Under a wall-less posture nothing + bounded the wildcard superuser bits `organization_admin` carries, so a + deployment that accumulated organizations (personal orgs on signup) made every + owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a + de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and + revokes the superseded variant whenever the posture changes. **Upgrade impact:** + in `single` posture an org owner/admin keeps full CRUD but loses the blanket + ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains + available through `admin_full_access` or an explicitly authored permission set — + it just stops being a side effect of a better-auth membership role. + + ## Engine-owned organization stamping (D5) + + Under any wall-enforcing posture the engine stamps `organization_id` from the + caller's active organization on an insert that omits it, and validates every + supplied value against the wall. Idempotent with the enterprise auto-stamp + (neither overwrites a supplied value). This also closes a real hole: the + pre-existing post-image check required a non-array payload, so a BULK insert + could carry a forged `organization_id` per row. One forged row now denies the + whole write. + + ## Group structure, extension fields and red-line lints (D6, D7) + + - `sys_organization` gains `parent_organization_id` and `sort_order` — a + **reporting dimension only**. + - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / + `os validate`: an RLS policy or sharing rule that walks the org tree is an + error, as is a business-unit grant on a platform-global object. + - Extension fields on better-auth-managed objects ride the existing ADR-0092 + whitelist. A new guard derives better-auth's real field surface from + `getAuthTables()` at the pinned version and fails the build on any name + collision, so a library upgrade cannot silently take ownership of a column. + + ## Enforce-or-remove (D11) — BREAKING + + Both removals are of surface that had **zero runtime consumers**, so no + behavior changes; authoring them is now a no-op instead of a lint warning. + + - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read + it. FROM → TO: a set a policy needs as `field IN (current_user.)` is now + supplied by a registered membership resolver (below); a constant belongs in + the policy itself as a literal (`status = 'published'`). + - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: + matrix requirements are served by multi-position × business-unit anchoring; a + generalized dimension-security module will arrive with its own ADR. + - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has + merged since ADR-0056 finally has a producer: register an + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by + construction — an unresolved key makes its policies drop out. Kernel-owned + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be + overwritten from this seam. + + ## Edition boundary (D12) + + The `group` posture's enforcement primitives ship OPEN — the union wall, + `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness + fixes and the D6 lints — because the correctness of a wall is never a paid + feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing + enterprise `org-scoping` probe, so the current commercial boundary for + legal-entity isolation is unchanged by this release. + +- 030125b: feat(objectql)!: `init()` refuses to boot when a data driver fails to connect (#3741) + + `ObjectQLEngine.init()` wrapped every driver's `connect()` in a try/catch, logged + one error line, and carried on. A server whose database was unreachable therefore + "started successfully" — health endpoints could even stay green — and then failed + every request with an error that reads nothing like _the database is down_. The + warning it printed (`Operations may recover via lazy reconnection or fail at query +time`) was half fiction: grep the repo and no reconnection exists in `driver-sql` + or `driver-mongodb`, so only the "fail at query time" half was ever real. The + caller made it worse — `ObjectQLPlugin.start()` runs `syncRegisteredSchemas()` + immediately after `init()`, issuing DDL against a driver that isn't there. + + The structural half of the bug was worse than the operational one: the catch + removed a driver's ability to **refuse startup at all**. Any fatal startup check — + licence, server version, incompatible configuration, missing capability, not just + an unreachable socket — is expressed by throwing from `connect()`, and every one + of them was silently downgraded to a runtime error. That is why driver-mongodb's + multi-tenancy guard (#3724 / #3734) had to be hoisted into its constructor. + + - `init()` now **throws** `DriverConnectError` (`code: 'ERR_DRIVER_CONNECT'`) + when any boot-registered driver's `connect()` rejects, aborting kernel + bootstrap. It still attempts every driver first, so one failed boot names all + of them. The message is self-contained — each failed driver and its cause — + because the CLI prints `error.message` alone; the first cause is also attached + as `error.cause`. Exported from both `@objectstack/objectql` and + `@objectstack/objectql/core`. + - `connect()` is now a supported place for a driver to veto boot. Startup + validation that needs a live connection (server version, capability probes) + no longer has to be forced into a constructor. + - The misleading "lazy reconnection" warning is gone. + - New escape hatch `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` + (`resolveAllowDriverConnectFailure()` in `@objectstack/types`) restores the old + lenient boot, but loudly: a `DEGRADED BOOT` banner names the failed drivers and + states that they are never retried or reconnected and that every query and + schema sync routed to them will fail for the process lifetime. The banner goes + to stderr as well as the logger, because `os serve` swallows all of stdout + during boot and `Logger` routes `warn` there — logger-only, the one message + that matters would be invisible in exactly the deployment the flag is for. + Defaults off. + + **Migration.** No code or config change is needed for a correctly configured + deployment — a driver that connected before still connects. A deployment that was + _silently_ booting without its database now fails the boot instead, with the + driver name and cause in the error; fix the datasource configuration (typically + `OS_DATABASE_URL`, credentials, or network reachability). To keep booting without + it — deliberately, and knowing every request that touches it will fail — set + `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`. + +### Patch Changes + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [587fc91] +- Updated dependencies [1986594] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [db02d47] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [dac6a08] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [67452d1] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [5ac93d4] +- Updated dependencies [0024abf] +- Updated dependencies [1659072] +- Updated dependencies [abceb0d] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [cde1975] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 47e2d59871..854dd7efe9 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 46121f01df..003d2777a4 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,317 @@ # @objectstack/verify +## 17.0.0-rc.0 + +### Minor Changes + +- 587fc91: feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt + + The analytics→engine bridge now forwards the request's `ExecutionContext` to + `engine.aggregate`, so the engine's own middleware chain scopes analytics reads + independently of the analytics layer's `getReadScope`. + + **Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing + forced the bridge to pass it — and it did not. An authenticated aggregate + reached the engine with no principal, plugin-security's principal-less fall-open + skipped its RLS injection, and the only thing left scoping the query was the + strategy remembering to call `getReadScope`. #3597 was a strategy that did not, + and both belts were off at once. + + `getReadScope` stays: the two resolve scope through different paths (engine + middleware vs `security.getReadFilter`), and a deployment without + plugin-security has only the analytics layer. This is depth, not a replacement. + + - `StrategyContext` gains `context?: ExecutionContext`, bound per call by + `AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`. + - `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` / + `AnalyticsService` `executeAggregate` config options gain `context?: +ExecutionContext`. **Custom bridges should forward it** to their engine; the + built-in auto-bridge does. Purely additive — an existing bridge that ignores + it keeps working exactly as before. + - `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain + an optional trailing `context`, beside the `scope` / `resolveScope` that + #3639 added — the same two-belt split as the aggregate path. + - `BootOptions.analytics` (`@objectstack/verify`) overrides the + AnalyticsServicePlugin instance, so a gate can boot with the analytics belt + off and assert the engine-side belt alone still scopes. + + **Also fixed on the same seam:** + + - `fetchRecordLabels` — the dimension display-label lookup — is row-granular + (one row per record, real display names). #3639 gave it the analytics-layer + belt (the referenced object's own read scope); it now also carries the + context, so the engine scopes the same read independently. + - `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the + `/analytics/sql` preview read as an unscoped table scan while the real + aggregate was scoped. It now renders the caller's filters and the read scope. + The preview never executed, so this was misleading output rather than a leak. + +### Patch Changes + +- e9b11df: fix(auth): app-declared organization roles are now storable, not just registerable (#3723) + + `AuthManagerOptions.additionalOrgRoles` registered every `permission` / + `position` name a stack declared with better-auth's organization plugin, so + `POST /organization/invite-member { role: 'sales_rep' }` passed the role check — + and then the write failed, because `sys_invitation.role` and `sys_member.role` + were closed selects listing `owner|admin|member` only: + + ``` + ValidationError: role must be one of: owner, admin, member + { field: 'role', code: 'invalid_option' } + ``` + + A select is enforced on write and better-auth's own inserts are not exempt (they + run through the ordinary ObjectQL validator), so any stack declaring role names + was registering roles that could be requested and never stored. + + Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single + normalizer; its output feeds better-auth's role map **and** the two `select` + option lists, so neither side can accept a name the other rejects. The built-in + roles (`owner`, `admin`, `delegated_admin`, `member`) live in + `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the + platform objects declare statically — app roles are appended at boot. + + New exports: + + - `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to + `identity/membership-role`; the package-level export path is unchanged). + - `@objectstack/plugin-auth` — `collectStackOrgRoles`, + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, + `withMembershipRoleOptions`. + + Hosts that boot `AuthPlugin` from a loaded stack should derive + `additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the + stack themselves — `objectstack serve`, the `@objectstack/verify` harness and + `DevPlugin` now all do. The harness previously passed none, which is why a + dogfood proof could boot a stack whose declared roles better-auth had never + heard of; `DevPlugin` documents itself as equivalent to the full stack and + silently excluded app roles from that equivalence. + + `additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and + `collectStackOrgRoles` now returns those descriptors. The label is what the + declaring `position` / `permission` metadata already says, so the role picker + shows `Executive` for a position declared as such instead of title-casing the + machine name into `Exec` — a third source of truth for one string. Presentation + only: better-auth sees just the name, and the stored value is always the name. + Passing `string[]` keeps working unchanged. + + Behaviour change worth noting: a declared role name that is not a valid machine + name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a + boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a + name would be registered verbatim and stored mangled — the same mismatch with + extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. + +- 96242ef: feat(auth): AuthPlugin derives app-declared organization roles itself — hosts pass nothing (#3723 follow-up, cloud#897) + + Five hosts boot `AuthPlugin` from a stack, and per-host `additionalOrgRoles` + wiring proved to be the defect pattern: three of them (the verify harness, + `DevPlugin`, cloud's `ArtifactKernelFactory`) at some point forgot it, and the + failure is silent — app-declared roles are simply absent. One host (cloud) + mounts `AuthPlugin` before the app metadata even exists, so no init-time walk + could ever cover it. + + `AuthPlugin` now derives the roles in its own `kernel:ready` hook — the one + point that fires after all metadata is registered in every host — via the new + `collectRegisteredOrgRoles(engine, metadataService?)` (the late-bound twin of + `collectStackOrgRoles`). Both consumers are updated from the derived union: + better-auth's org-plugin roles map (`applyConfigPatch`; the instance builds + lazily) and the `sys_invitation.role` / `sys_member.role` select options + (re-registration under the same package id — a supported registry path; no + DDL, options are validator/picker metadata). + + `objectstack serve`, the `@objectstack/verify` harness and `DevPlugin` no + longer pass `additionalOrgRoles` — deliberately, so the dogfood invite gate + only stays green if the auto-derivation works. The option remains for roles + declared OUTSIDE stack metadata; explicit entries are unioned with the derived + set. `collectStackOrgRoles` stays exported for hosts that want an init-time + walk of a raw stack object. + +- e889386: `bootStack({ multiTenant: true })` now REQUESTS the `isolated` tenancy posture + for the boot (ADR-0105 D1), restoring the request on `stop()` and respecting an + explicit caller-provided `OS_TENANCY_POSTURE`. + + Since #3559 a walled posture is an explicit operator request resolved from env + when AuthPlugin registers the `tenancy` service — mounting the enterprise + organizations plugin only ENTITLES it. The harness's multi-tenant opt-in + predates that split and only mounted the plugin, so multi-org fixtures silently + booted `single`: no Layer 0 wall, D3 default-org write stamping, and every + cross-tenant proof asserting against the wrong posture (first surfaced by + cloud's security-enterprise multi-org integration test, which runs the licensed + path open-core CI cannot). + + The verify package also gains a `test` script so its suite actually runs under + `turbo run test`, including the new regression pin for this contract. + +- Updated dependencies [08b5a3d] +- Updated dependencies [d99aeb3] +- Updated dependencies [4727eb8] +- Updated dependencies [6169615] +- Updated dependencies [fa3d0cf] +- Updated dependencies [af5a224] +- Updated dependencies [71f76e1] +- Updated dependencies [37b1346] +- Updated dependencies [a749273] +- Updated dependencies [99736a0] +- Updated dependencies [fe67e34] +- Updated dependencies [fdb4f50] +- Updated dependencies [1bd5652] +- Updated dependencies [735f850] +- Updated dependencies [7fb436c] +- Updated dependencies [879ea13] +- Updated dependencies [201b31f] +- Updated dependencies [6877e9a] +- Updated dependencies [0bab8bb] +- Updated dependencies [7101ca2] +- Updated dependencies [587fc91] +- Updated dependencies [415254c] +- Updated dependencies [1f8390b] +- Updated dependencies [3167e29] +- Updated dependencies [0a6fb1e] +- Updated dependencies [1986594] +- Updated dependencies [3c8cfd1] +- Updated dependencies [ad4af62] +- Updated dependencies [d44dbfa] +- Updated dependencies [e9b11df] +- Updated dependencies [474fe39] +- Updated dependencies [0bc685a] +- Updated dependencies [b949059] +- Updated dependencies [be1c52c] +- Updated dependencies [c5ff96d] +- Updated dependencies [84e7be9] +- Updated dependencies [debc23a] +- Updated dependencies [9dcc0ae] +- Updated dependencies [96242ef] +- Updated dependencies [984396b] +- Updated dependencies [d0fea33] +- Updated dependencies [9f060e5] +- Updated dependencies [bc17d39] +- Updated dependencies [db02d47] +- Updated dependencies [d3f2ff6] +- Updated dependencies [b7550d6] +- Updated dependencies [0164f40] +- Updated dependencies [e295ad1] +- Updated dependencies [1003125] +- Updated dependencies [6e62a93] +- Updated dependencies [ecda20c] +- Updated dependencies [6e62a93] +- Updated dependencies [fc968af] +- Updated dependencies [376a061] +- Updated dependencies [7c7e246] +- Updated dependencies [c2d9098] +- Updated dependencies [9613396] +- Updated dependencies [4ed7ed4] +- Updated dependencies [ce1f100] +- Updated dependencies [2fa4ca1] +- Updated dependencies [f5a2320] +- Updated dependencies [deb538f] +- Updated dependencies [5b89711] +- Updated dependencies [c88eeda] +- Updated dependencies [5524f84] +- Updated dependencies [c4df271] +- Updated dependencies [a41ba5c] +- Updated dependencies [307e0fe] +- Updated dependencies [189854c] +- Updated dependencies [0e3a226] +- Updated dependencies [a8d1e24] +- Updated dependencies [d1cabaa] +- Updated dependencies [9e2caf3] +- Updated dependencies [81ce41a] +- Updated dependencies [85e1e4e] +- Updated dependencies [65ac468] +- Updated dependencies [ef5e72d] +- Updated dependencies [dac6a08] +- Updated dependencies [313d7be] +- Updated dependencies [5faeac6] +- Updated dependencies [394b7a1] +- Updated dependencies [677b591] +- Updated dependencies [e1fa8d5] +- Updated dependencies [402f534] +- Updated dependencies [7180ed5] +- Updated dependencies [083c414] +- Updated dependencies [2a5f04a] +- Updated dependencies [4f740b0] +- Updated dependencies [fc5f126] +- Updated dependencies [adabaa8] +- Updated dependencies [030125b] +- Updated dependencies [605c23f] +- Updated dependencies [67452d1] +- Updated dependencies [9bf4588] +- Updated dependencies [605e190] +- Updated dependencies [c6c59f1] +- Updated dependencies [b0e78a8] +- Updated dependencies [f31cc8d] +- Updated dependencies [f343dc4] +- Updated dependencies [8269e32] +- Updated dependencies [74f7339] +- Updated dependencies [a6c35a2] +- Updated dependencies [c2f1002] +- Updated dependencies [db48ad5] +- Updated dependencies [8e08bc3] +- Updated dependencies [16adb3c] +- Updated dependencies [f163028] +- Updated dependencies [f07808c] +- Updated dependencies [7ffc3d3] +- Updated dependencies [88346ba] +- Updated dependencies [4631592] +- Updated dependencies [32ff033] +- Updated dependencies [bbd902d] +- Updated dependencies [5ac93d4] +- Updated dependencies [3d5f726] +- Updated dependencies [0024abf] +- Updated dependencies [48d5a1c] +- Updated dependencies [3216344] +- Updated dependencies [f5bfac8] +- Updated dependencies [6163393] +- Updated dependencies [688e9df] +- Updated dependencies [8f124a7] +- Updated dependencies [21ca1d5] +- Updated dependencies [03b11e8] +- Updated dependencies [8891f93] +- Updated dependencies [d729a31] +- Updated dependencies [cb8322e] +- Updated dependencies [aa8b847] +- Updated dependencies [d318b24] +- Updated dependencies [1659072] +- Updated dependencies [810a3a2] +- Updated dependencies [abceb0d] +- Updated dependencies [9981c1d] +- Updated dependencies [d60968c] +- Updated dependencies [0c302a7] +- Updated dependencies [503be86] +- Updated dependencies [5f0852f] +- Updated dependencies [cde1975] +- Updated dependencies [20cb232] +- Updated dependencies [e231abb] +- Updated dependencies [0bc685a] +- Updated dependencies [b098b0e] +- Updated dependencies [a629074] +- Updated dependencies [57bab76] +- Updated dependencies [b90086a] +- Updated dependencies [b95577a] +- Updated dependencies [54f479a] +- Updated dependencies [d8c4957] +- Updated dependencies [f24cb83] +- Updated dependencies [5dbbb92] +- Updated dependencies [69f1dfd] + - @objectstack/spec@17.0.0-rc.0 + - @objectstack/objectql@17.0.0-rc.0 + - @objectstack/rest@17.0.0-rc.0 + - @objectstack/runtime@17.0.0-rc.0 + - @objectstack/plugin-auth@17.0.0-rc.0 + - @objectstack/plugin-security@17.0.0-rc.0 + - @objectstack/core@17.0.0-rc.0 + - @objectstack/plugin-hono-server@17.0.0-rc.0 + - @objectstack/service-analytics@17.0.0-rc.0 + - @objectstack/service-automation@17.0.0-rc.0 + - @objectstack/plugin-sharing@17.0.0-rc.0 + - @objectstack/service-settings@17.0.0-rc.0 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.0 + - @objectstack/service-datasource@17.0.0-rc.0 + ## 16.1.0 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 6d7306c196..f57bfa170d 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "16.1.0", + "version": "17.0.0-rc.0", "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 8c7c079ee5..99f22fb719 100644 --- a/packages/vscode-objectstack/CHANGELOG.md +++ b/packages/vscode-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # objectstack-vscode +## 17.0.0-rc.0 + ## 16.1.0 ## 16.0.0 diff --git a/packages/vscode-objectstack/package.json b/packages/vscode-objectstack/package.json index 50c147208d..0da0220c87 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": "16.1.0", + "version": "17.0.0-rc.0", "publisher": "objectstack", "license": "Apache-2.0", "repository": {