diff --git a/.changeset/15204-7a-permission-set-projection-retired.md b/.changeset/pre/15204-7a-permission-set-projection-retired.md similarity index 100% rename from .changeset/15204-7a-permission-set-projection-retired.md rename to .changeset/pre/15204-7a-permission-set-projection-retired.md diff --git a/.changeset/15204-7b-position-write-through-retired.md b/.changeset/pre/15204-7b-position-write-through-retired.md similarity index 100% rename from .changeset/15204-7b-position-write-through-retired.md rename to .changeset/pre/15204-7b-position-write-through-retired.md diff --git a/.changeset/15204-s8-catalog-row-objects-retired.md b/.changeset/pre/15204-s8-catalog-row-objects-retired.md similarity index 100% rename from .changeset/15204-s8-catalog-row-objects-retired.md rename to .changeset/pre/15204-s8-catalog-row-objects-retired.md diff --git a/.changeset/15953-verify-dashboard-window-now-removed.md b/.changeset/pre/15953-verify-dashboard-window-now-removed.md similarity index 100% rename from .changeset/15953-verify-dashboard-window-now-removed.md rename to .changeset/pre/15953-verify-dashboard-window-now-removed.md diff --git a/.changeset/19939-flow-single-brace-resolver-deleted.md b/.changeset/pre/19939-flow-single-brace-resolver-deleted.md similarity index 100% rename from .changeset/19939-flow-single-brace-resolver-deleted.md rename to .changeset/pre/19939-flow-single-brace-resolver-deleted.md diff --git a/.changeset/22161-lint-slice-16-one-line-fix.md b/.changeset/pre/22161-lint-slice-16-one-line-fix.md similarity index 100% rename from .changeset/22161-lint-slice-16-one-line-fix.md rename to .changeset/pre/22161-lint-slice-16-one-line-fix.md diff --git a/.changeset/22585-automation-skill-value-slots.md b/.changeset/pre/22585-automation-skill-value-slots.md similarity index 100% rename from .changeset/22585-automation-skill-value-slots.md rename to .changeset/pre/22585-automation-skill-value-slots.md diff --git a/.changeset/22659-scaffold-skills-from-package.md b/.changeset/pre/22659-scaffold-skills-from-package.md similarity index 100% rename from .changeset/22659-scaffold-skills-from-package.md rename to .changeset/pre/22659-scaffold-skills-from-package.md diff --git a/.changeset/22668-skills-readme-install.md b/.changeset/pre/22668-skills-readme-install.md similarity index 100% rename from .changeset/22668-skills-readme-install.md rename to .changeset/pre/22668-skills-readme-install.md diff --git a/.changeset/22861-readable-ids-composition.md b/.changeset/pre/22861-readable-ids-composition.md similarity index 100% rename from .changeset/22861-readable-ids-composition.md rename to .changeset/pre/22861-readable-ids-composition.md diff --git a/.changeset/22871-imap-mailbox-sync.md b/.changeset/pre/22871-imap-mailbox-sync.md similarity index 100% rename from .changeset/22871-imap-mailbox-sync.md rename to .changeset/pre/22871-imap-mailbox-sync.md diff --git a/.changeset/22871-mail-association-live.md b/.changeset/pre/22871-mail-association-live.md similarity index 100% rename from .changeset/22871-mail-association-live.md rename to .changeset/pre/22871-mail-association-live.md diff --git a/.changeset/22871-mail-ingest-seam.md b/.changeset/pre/22871-mail-ingest-seam.md similarity index 100% rename from .changeset/22871-mail-ingest-seam.md rename to .changeset/pre/22871-mail-ingest-seam.md diff --git a/.changeset/22937-cli-ceremony-promotions.md b/.changeset/pre/22937-cli-ceremony-promotions.md similarity index 100% rename from .changeset/22937-cli-ceremony-promotions.md rename to .changeset/pre/22937-cli-ceremony-promotions.md diff --git a/.changeset/22937-cli-organization-ownership-apply.md b/.changeset/pre/22937-cli-organization-ownership-apply.md similarity index 100% rename from .changeset/22937-cli-organization-ownership-apply.md rename to .changeset/pre/22937-cli-organization-ownership-apply.md diff --git a/.changeset/22937-core-email-template-row-definition.md b/.changeset/pre/22937-core-email-template-row-definition.md similarity index 100% rename from .changeset/22937-core-email-template-row-definition.md rename to .changeset/pre/22937-core-email-template-row-definition.md diff --git a/.changeset/23015-one-default-record-page-per-object.md b/.changeset/pre/23015-one-default-record-page-per-object.md similarity index 100% rename from .changeset/23015-one-default-record-page-per-object.md rename to .changeset/pre/23015-one-default-record-page-per-object.md diff --git a/.changeset/23072-dispatcher-meta-query-decision.md b/.changeset/pre/23072-dispatcher-meta-query-decision.md similarity index 100% rename from .changeset/23072-dispatcher-meta-query-decision.md rename to .changeset/pre/23072-dispatcher-meta-query-decision.md diff --git a/.changeset/23082-view-data-api-provider-retired.md b/.changeset/pre/23082-view-data-api-provider-retired.md similarity index 100% rename from .changeset/23082-view-data-api-provider-retired.md rename to .changeset/pre/23082-view-data-api-provider-retired.md diff --git a/.changeset/23100-cbp-one-required-master.md b/.changeset/pre/23100-cbp-one-required-master.md similarity index 100% rename from .changeset/23100-cbp-one-required-master.md rename to .changeset/pre/23100-cbp-one-required-master.md diff --git a/.changeset/23103-page-action-row-on-success-and-executor.md b/.changeset/pre/23103-page-action-row-on-success-and-executor.md similarity index 100% rename from .changeset/23103-page-action-row-on-success-and-executor.md rename to .changeset/pre/23103-page-action-row-on-success-and-executor.md diff --git a/.changeset/23120-server-composed-protection-drop.md b/.changeset/pre/23120-server-composed-protection-drop.md similarity index 100% rename from .changeset/23120-server-composed-protection-drop.md rename to .changeset/pre/23120-server-composed-protection-drop.md diff --git a/.changeset/23134-related-list-parent-data-source.md b/.changeset/pre/23134-related-list-parent-data-source.md similarity index 100% rename from .changeset/23134-related-list-parent-data-source.md rename to .changeset/pre/23134-related-list-parent-data-source.md diff --git a/.changeset/23145-tombstone-removal-version.md b/.changeset/pre/23145-tombstone-removal-version.md similarity index 100% rename from .changeset/23145-tombstone-removal-version.md rename to .changeset/pre/23145-tombstone-removal-version.md diff --git a/.changeset/23147-component-props-every-finding-refused.md b/.changeset/pre/23147-component-props-every-finding-refused.md similarity index 100% rename from .changeset/23147-component-props-every-finding-refused.md rename to .changeset/pre/23147-component-props-every-finding-refused.md diff --git a/.changeset/23149-view-container-reset-heals-expansions.md b/.changeset/pre/23149-view-container-reset-heals-expansions.md similarity index 100% rename from .changeset/23149-view-container-reset-heals-expansions.md rename to .changeset/pre/23149-view-container-reset-heals-expansions.md diff --git a/.changeset/23152-import-invite-redirect-to.md b/.changeset/pre/23152-import-invite-redirect-to.md similarity index 100% rename from .changeset/23152-import-invite-redirect-to.md rename to .changeset/pre/23152-import-invite-redirect-to.md diff --git a/.changeset/23157-server-authored-ui-text-names-what-and-who.md b/.changeset/pre/23157-server-authored-ui-text-names-what-and-who.md similarity index 100% rename from .changeset/23157-server-authored-ui-text-names-what-and-who.md rename to .changeset/pre/23157-server-authored-ui-text-names-what-and-who.md diff --git a/.changeset/23158-ai-store-caller-context.md b/.changeset/pre/23158-ai-store-caller-context.md similarity index 100% rename from .changeset/23158-ai-store-caller-context.md rename to .changeset/pre/23158-ai-store-caller-context.md diff --git a/.changeset/23165-direct-save-resolves-package.md b/.changeset/pre/23165-direct-save-resolves-package.md similarity index 100% rename from .changeset/23165-direct-save-resolves-package.md rename to .changeset/pre/23165-direct-save-resolves-package.md diff --git a/.changeset/23168-page-action-row-name-leg-url-target.md b/.changeset/pre/23168-page-action-row-name-leg-url-target.md similarity index 100% rename from .changeset/23168-page-action-row-name-leg-url-target.md rename to .changeset/pre/23168-page-action-row-name-leg-url-target.md diff --git a/.changeset/23168-page-action-row-on-success-beside-executor.md b/.changeset/pre/23168-page-action-row-on-success-beside-executor.md similarity index 100% rename from .changeset/23168-page-action-row-on-success-beside-executor.md rename to .changeset/pre/23168-page-action-row-on-success-beside-executor.md diff --git a/.changeset/23170-rolling-presets-close-on-today.md b/.changeset/pre/23170-rolling-presets-close-on-today.md similarity index 100% rename from .changeset/23170-rolling-presets-close-on-today.md rename to .changeset/pre/23170-rolling-presets-close-on-today.md diff --git a/.changeset/23174-nested-fault-writer.md b/.changeset/pre/23174-nested-fault-writer.md similarity index 100% rename from .changeset/23174-nested-fault-writer.md rename to .changeset/pre/23174-nested-fault-writer.md diff --git a/.changeset/23175-dev-seed-workspace-timezone.md b/.changeset/pre/23175-dev-seed-workspace-timezone.md similarity index 100% rename from .changeset/23175-dev-seed-workspace-timezone.md rename to .changeset/pre/23175-dev-seed-workspace-timezone.md diff --git a/.changeset/23178-auth-gate-raw-host-families.md b/.changeset/pre/23178-auth-gate-raw-host-families.md similarity index 100% rename from .changeset/23178-auth-gate-raw-host-families.md rename to .changeset/pre/23178-auth-gate-raw-host-families.md diff --git a/.changeset/23178-settings-auth-gate.md b/.changeset/pre/23178-settings-auth-gate.md similarity index 100% rename from .changeset/23178-settings-auth-gate.md rename to .changeset/pre/23178-settings-auth-gate.md diff --git a/.changeset/23189-identity-objects-default-organization.md b/.changeset/pre/23189-identity-objects-default-organization.md similarity index 100% rename from .changeset/23189-identity-objects-default-organization.md rename to .changeset/pre/23189-identity-objects-default-organization.md diff --git a/.changeset/23190-auth-gate-cli-sites.md b/.changeset/pre/23190-auth-gate-cli-sites.md similarity index 100% rename from .changeset/23190-auth-gate-cli-sites.md rename to .changeset/pre/23190-auth-gate-cli-sites.md diff --git a/.changeset/23198-container-expansion-reconcile.md b/.changeset/pre/23198-container-expansion-reconcile.md similarity index 100% rename from .changeset/23198-container-expansion-reconcile.md rename to .changeset/pre/23198-container-expansion-reconcile.md diff --git a/.changeset/23198-orphan-adoption-keeps-overlays.md b/.changeset/pre/23198-orphan-adoption-keeps-overlays.md similarity index 100% rename from .changeset/23198-orphan-adoption-keeps-overlays.md rename to .changeset/pre/23198-orphan-adoption-keeps-overlays.md diff --git a/.changeset/23214-dispatcher-query-arity.md b/.changeset/pre/23214-dispatcher-query-arity.md similarity index 100% rename from .changeset/23214-dispatcher-query-arity.md rename to .changeset/pre/23214-dispatcher-query-arity.md diff --git a/.changeset/23224-guest-grants-channel.md b/.changeset/pre/23224-guest-grants-channel.md similarity index 100% rename from .changeset/23224-guest-grants-channel.md rename to .changeset/pre/23224-guest-grants-channel.md diff --git a/.changeset/23225-d2b-anonymous-elevated-flow-refused.md b/.changeset/pre/23225-d2b-anonymous-elevated-flow-refused.md similarity index 100% rename from .changeset/23225-d2b-anonymous-elevated-flow-refused.md rename to .changeset/pre/23225-d2b-anonymous-elevated-flow-refused.md diff --git a/.changeset/23225-domain-floor-anonymous-deny.md b/.changeset/pre/23225-domain-floor-anonymous-deny.md similarity index 100% rename from .changeset/23225-domain-floor-anonymous-deny.md rename to .changeset/pre/23225-domain-floor-anonymous-deny.md diff --git a/.changeset/23225-ui-data-domain-floor.md b/.changeset/pre/23225-ui-data-domain-floor.md similarity index 100% rename from .changeset/23225-ui-data-domain-floor.md rename to .changeset/pre/23225-ui-data-domain-floor.md diff --git a/.changeset/23226-guest-owner-stamp.md b/.changeset/pre/23226-guest-owner-stamp.md similarity index 100% rename from .changeset/23226-guest-owner-stamp.md rename to .changeset/pre/23226-guest-owner-stamp.md diff --git a/.changeset/23228-record-share-guest-recipient-retired.md b/.changeset/pre/23228-record-share-guest-recipient-retired.md similarity index 100% rename from .changeset/23228-record-share-guest-recipient-retired.md rename to .changeset/pre/23228-record-share-guest-recipient-retired.md diff --git a/.changeset/23231-walled-environment-metadata-writes.md b/.changeset/pre/23231-walled-environment-metadata-writes.md similarity index 100% rename from .changeset/23231-walled-environment-metadata-writes.md rename to .changeset/pre/23231-walled-environment-metadata-writes.md diff --git a/.changeset/23233-permission-echo-names-runtime-package.md b/.changeset/pre/23233-permission-echo-names-runtime-package.md similarity index 100% rename from .changeset/23233-permission-echo-names-runtime-package.md rename to .changeset/pre/23233-permission-echo-names-runtime-package.md diff --git a/.changeset/23245-dispatcher-auth-gate-outage.md b/.changeset/pre/23245-dispatcher-auth-gate-outage.md similarity index 100% rename from .changeset/23245-dispatcher-auth-gate-outage.md rename to .changeset/pre/23245-dispatcher-auth-gate-outage.md diff --git a/.changeset/23254-auth-org-record-page-option.md b/.changeset/pre/23254-auth-org-record-page-option.md similarity index 100% rename from .changeset/23254-auth-org-record-page-option.md rename to .changeset/pre/23254-auth-org-record-page-option.md diff --git a/.changeset/23259-permission-set-owner-mismatch-refused.md b/.changeset/pre/23259-permission-set-owner-mismatch-refused.md similarity index 100% rename from .changeset/23259-permission-set-owner-mismatch-refused.md rename to .changeset/pre/23259-permission-set-owner-mismatch-refused.md diff --git a/.changeset/23260-builtin-token-value-judged.md b/.changeset/pre/23260-builtin-token-value-judged.md similarity index 100% rename from .changeset/23260-builtin-token-value-judged.md rename to .changeset/pre/23260-builtin-token-value-judged.md diff --git a/.changeset/23269-v18-stored-row-fates.md b/.changeset/pre/23269-v18-stored-row-fates.md similarity index 100% rename from .changeset/23269-v18-stored-row-fates.md rename to .changeset/pre/23269-v18-stored-row-fates.md diff --git a/.changeset/23276-upgrade-skill-skills-step.md b/.changeset/pre/23276-upgrade-skill-skills-step.md similarity index 100% rename from .changeset/23276-upgrade-skill-skills-step.md rename to .changeset/pre/23276-upgrade-skill-skills-step.md diff --git a/.changeset/23280-host-fate-supplement.md b/.changeset/pre/23280-host-fate-supplement.md similarity index 100% rename from .changeset/23280-host-fate-supplement.md rename to .changeset/pre/23280-host-fate-supplement.md diff --git a/.changeset/23281-comment-reactions-upgrade-conversion.md b/.changeset/pre/23281-comment-reactions-upgrade-conversion.md similarity index 100% rename from .changeset/23281-comment-reactions-upgrade-conversion.md rename to .changeset/pre/23281-comment-reactions-upgrade-conversion.md diff --git a/.changeset/23284-notification-takes-record-organization.md b/.changeset/pre/23284-notification-takes-record-organization.md similarity index 100% rename from .changeset/23284-notification-takes-record-organization.md rename to .changeset/pre/23284-notification-takes-record-organization.md diff --git a/.changeset/23293-client-delete-package.md b/.changeset/pre/23293-client-delete-package.md similarity index 100% rename from .changeset/23293-client-delete-package.md rename to .changeset/pre/23293-client-delete-package.md diff --git a/.changeset/23297-package-axis-delete-draft.md b/.changeset/pre/23297-package-axis-delete-draft.md similarity index 100% rename from .changeset/23297-package-axis-delete-draft.md rename to .changeset/pre/23297-package-axis-delete-draft.md diff --git a/.changeset/23298-env-kernel-registry-write-through.md b/.changeset/pre/23298-env-kernel-registry-write-through.md similarity index 100% rename from .changeset/23298-env-kernel-registry-write-through.md rename to .changeset/pre/23298-env-kernel-registry-write-through.md diff --git a/.changeset/23306-storage-attach-verb.md b/.changeset/pre/23306-storage-attach-verb.md similarity index 100% rename from .changeset/23306-storage-attach-verb.md rename to .changeset/pre/23306-storage-attach-verb.md diff --git a/.changeset/23308-security-catalog-echo.md b/.changeset/pre/23308-security-catalog-echo.md similarity index 100% rename from .changeset/23308-security-catalog-echo.md rename to .changeset/pre/23308-security-catalog-echo.md diff --git a/.changeset/23312-permission-set-owner-binding-build-and-registration.md b/.changeset/pre/23312-permission-set-owner-binding-build-and-registration.md similarity index 100% rename from .changeset/23312-permission-set-owner-binding-build-and-registration.md rename to .changeset/pre/23312-permission-set-owner-binding-build-and-registration.md diff --git a/.changeset/23316-refused-flow-registration-verdict.md b/.changeset/pre/23316-refused-flow-registration-verdict.md similarity index 100% rename from .changeset/23316-refused-flow-registration-verdict.md rename to .changeset/pre/23316-refused-flow-registration-verdict.md diff --git a/.changeset/23317-stored-preview-gaps.md b/.changeset/pre/23317-stored-preview-gaps.md similarity index 100% rename from .changeset/23317-stored-preview-gaps.md rename to .changeset/pre/23317-stored-preview-gaps.md diff --git a/.changeset/23328-share-link-refusal-order.md b/.changeset/pre/23328-share-link-refusal-order.md similarity index 100% rename from .changeset/23328-share-link-refusal-order.md rename to .changeset/pre/23328-share-link-refusal-order.md diff --git a/.changeset/23332-page-action-row-operation.md b/.changeset/pre/23332-page-action-row-operation.md similarity index 100% rename from .changeset/23332-page-action-row-operation.md rename to .changeset/pre/23332-page-action-row-operation.md diff --git a/.changeset/23335-held-platform-admin-refused.md b/.changeset/pre/23335-held-platform-admin-refused.md similarity index 100% rename from .changeset/23335-held-platform-admin-refused.md rename to .changeset/pre/23335-held-platform-admin-refused.md diff --git a/.changeset/23338-flow-binding-d3-replacement-reads.md b/.changeset/pre/23338-flow-binding-d3-replacement-reads.md similarity index 100% rename from .changeset/23338-flow-binding-d3-replacement-reads.md rename to .changeset/pre/23338-flow-binding-d3-replacement-reads.md diff --git a/.changeset/23374-automation-references-envelopes.md b/.changeset/pre/23374-automation-references-envelopes.md similarity index 100% rename from .changeset/23374-automation-references-envelopes.md rename to .changeset/pre/23374-automation-references-envelopes.md diff --git a/.changeset/23403-audience-anchor-binding-gate-retired.md b/.changeset/pre/23403-audience-anchor-binding-gate-retired.md similarity index 100% rename from .changeset/23403-audience-anchor-binding-gate-retired.md rename to .changeset/pre/23403-audience-anchor-binding-gate-retired.md diff --git a/.changeset/console-b9814b334d8a.md b/.changeset/pre/console-b9814b334d8a.md similarity index 100% rename from .changeset/console-b9814b334d8a.md rename to .changeset/pre/console-b9814b334d8a.md diff --git a/.changeset/objectui-pin-citations-b9814b334d8a.md b/.changeset/pre/objectui-pin-citations-b9814b334d8a.md similarity index 100% rename from .changeset/objectui-pin-citations-b9814b334d8a.md rename to .changeset/pre/objectui-pin-citations-b9814b334d8a.md diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 73f6ff69b56..d19d956f0ab 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/example-crm +## 4.0.100-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [5d3018d] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/service-i18n@18.0.0-next.1 + ## 4.0.100-next.0 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index 8a4b3ae053a..6a09eae55f3 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.100-next.0", + "version": "4.0.100-next.1", "description": "Minimal CRM example \u2014 a smoke-test workspace that exercises the metadata loading pipeline (objects \u2192 views \u2192 app \u2192 dashboard \u2192 hook \u2192 flow \u2192 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-multi-package/CHANGELOG.md b/examples/app-multi-package/CHANGELOG.md index 5a477c10099..75564356e58 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/example-multi-package +## 0.0.7-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 0.0.7-next.0 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index a511f779574..45fdfd350d5 100644 --- a/examples/app-multi-package/package.json +++ b/examples/app-multi-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-multi-package", - "version": "0.0.7-next.0", + "version": "0.0.7-next.1", "description": "One release artifact carrying TWO packages that share a namespace (ADR-0130 D4) — the producer-side fixture for `packages[]`", "license": "Apache-2.0", "private": true, diff --git a/examples/app-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index dbe37252915..c0c6f17eb22 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,63 @@ # @objectstack/example-showcase +## 0.3.22-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [08b38cb] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [5d3018d] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/service-i18n@18.0.0-next.1 + - @objectstack/service-datasource@18.0.0-next.1 + - @objectstack/cloud-connection@18.0.0-next.1 + - @objectstack/connector-mcp@18.0.0-next.1 + - @objectstack/connector-openapi@18.0.0-next.1 + - @objectstack/connector-rest@18.0.0-next.1 + - @objectstack/connector-slack@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + ## 0.3.22-next.0 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index f50e796a0db..3c82983c10e 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.22-next.0", + "version": "0.3.22-next.1", "description": "Kitchen-sink showcase workspace — exercises every metadata type, every view type, every chart type, and the major end-to-end capability chains (security, automation, analytics). Built for demonstration, debugging, and coverage-driven verification.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-todo/CHANGELOG.md b/examples/app-todo/CHANGELOG.md index bf282a77549..fce2786c0b6 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,65 @@ # @objectstack/example-todo +## 4.0.100-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [5d3018d] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [4f1e435] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/service-i18n@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/client@18.0.0-next.1 + - @objectstack/driver-sqlite-wasm@18.0.0-next.1 + - @objectstack/mcp@18.0.0-next.1 + - @objectstack/knowledge-memory@18.0.0-next.1 + - @objectstack/service-knowledge@18.0.0-next.1 + ## 4.0.100-next.0 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 864352c8902..170d7882a3a 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.100-next.0", + "version": "4.0.100-next.1", "description": "Example Todo App using ObjectStack Protocol", "license": "Apache-2.0", "private": true, diff --git a/examples/embed-objectql/CHANGELOG.md b/examples/embed-objectql/CHANGELOG.md index 9282306db87..bde58f45835 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/example-embed-objectql +## 0.0.40-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/driver-memory@18.0.0-next.1 + ## 0.0.40-next.0 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 19764d9dd00..89f694b5610 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.40-next.0", + "version": "0.0.40-next.1", "private": true, "description": "Embed the ObjectQL engine as a plain library via @objectstack/objectql/core — no kernel, no plugins, no metadata protocol (ADR-0076).", "type": "module", diff --git a/packages/adapters/hono/CHANGELOG.md b/packages/adapters/hono/CHANGELOG.md index c530388220b..74aed1cd428 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,25 @@ # @objectstack/hono +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [8024e12] +- Updated dependencies [d4aaeaf] +- Updated dependencies [3c1a046] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [430e5f5] +- Updated dependencies [5d3018d] +- Updated dependencies [28e294f] + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/plugin-hono-server@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 83cd9497e5f..c0ace21b988 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index 6f3dfec2fe5..8b11c9f141c 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/account +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 6a5c3ea4af5..316080107e6 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index ad5fd1709af..a09cd72b3dc 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/setup +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 4ebae39b69c..b16d6c7b067 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index c5d9d25b302..aca1e426633 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/studio +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 2c54772068b..467dca48757 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 6de10763436..f36016d29ba 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,240 @@ # @objectstack/cli +## 18.0.0-next.1 + +### Minor Changes + +- ed03834: feat(cli): `os migrate organization-ownership --apply` promotes customized email templates and moves the settings global rung (ADR-0131 D10) + + Clause-②: yes (widening) + + Two steps join the ceremony, after the authored catalog conversion and before the verified id→name rewrite. Each is checkpointed and resumable like the others. + + - **`template-promotion`** (ruling C on ADR-0131 §6 Q1). Every `sys_email_template` row the mirror deletion keeps for it (customized, or with `customized` unknown, and every row no seeder wrote: `managed_by` NULL or other than package / platform / system, with or without an organization) becomes an environment `email_template` item through the metadata door, one per `(name, locale)`, before mirror deletion removes the rows. It runs under every posture. Seeded, uncustomized copies are not promoted. A promotion that already landed is skipped. The step stops, keeping every row of the template, in four cases: + - a conflict: rows of one `(name, locale)` that differ. Each conflict is listed in the plan and in the `--apply` report, with every candidate's row id and organization. It counts as remaining and holds back `verified_at` until the operator chooses one row with the new repeatable flag `--template-choice ROW_ID`. A choice that names no candidate, or two candidates of one conflict, is refused before anything is written. The chosen body is promoted, and the other candidates are deleted only after that promotion is verified; + - a customized or authored row with no name; + - an environment template that already exists and differs, since the environment's edit is never overwritten; + - a door refusal. + - **`settings-move`** (ADR-0131 D7). Every `sys_setting` row with `scope = 'global'` and no organization is copied into `sys_platform_setting` by the database itself (`INSERT … SELECT`), so `value` and the `value_enc` secret handle move verbatim. The destination row is read back, and only when it equals the source does the row leave `sys_setting`. A key `sys_platform_setting` already holds with the same value is only removed from `sys_setting`. The step stops, moving nothing for that key, in two cases: + - `sys_platform_setting` already holds the key with a different value (the one the runtime serves); + - `sys_setting` holds differing rows for the key. + + The `global-rung-move` departure is now executed, so it reads zero after the run and no longer holds back `verified_at`. The `declared-rule-mirrors` departure still counts until the declared-rule seeder is retired. + + Mirror deletion now also deletes the promoted template rows, and only those. Before its first template delete it runs the promotion pass again, so a row customized or authored between runs (on a resumed ceremony) is promoted, not deleted. It then deletes the seeded, uncustomized mirrors and, by id, the rows whose environment copy that pass verified. Any other promotion row stays and blocks. The apply report gains `movedOut` / `movedIn` per table, and row counts reconcile as before − deleted − moved out + moved in. +- 2808069: feat(cli): `os migrate organization-ownership --apply` — the v18 organization-ownership ceremony's apply step and post-check (ADR-0131 D10, ceremony items 2 to 4) + + Clause-②: yes (widening) + + The command that planned the ceremony now performs it. Without `--apply` nothing changes: the read-only plan, written to a file. With `--apply`: + + - **The backup comes first.** It is the operator's act: pass `--backup-taken`, or answer yes at the prompt. A non-interactive run without the flag is refused before the database is opened. + - **The fates run in order**, each step stopping the run where it meets a blocker: + 1. **Attribution** — every NULL `organization_id` row the plan derives an owner for takes it (a parent, subject or holder anchor; under `single`, the Default Organization). A row with no derivable owner is never assigned one: it stays NULL and is listed by id. + 2. **Authored catalog rows** — permission sets and positions an organization authored in Setup become environment metadata through the metadata door (under `single`; under a wall they are reported), with each position's binding rows carried into its `permissionSets`. + 3. **The verified id→name rewrite** — every `sys_user_permission_set` grant names its set in `permission_set`, then every grant is verified to name a set the security catalog resolves. `permission_set_id` stays. + 4. **Mirror deletion** — only after that verification, read again just before the first delete: the seeded catalog copies (per organization and NULL residue), the binding rows, seeded capabilities and seeded templates. Authored, customized and organization-stamped rows stay. + 5. **Column drops** — `organization_id` leaves the deployment-level objects through the schema-drift ceremony (`os migrate apply`'s machinery), never a hand `ALTER`. A table whose rows still need another step keeps its column, and the run says why. + - **Idempotent and resumable.** Every write is conditioned on the state it changes, and each finished step is checkpointed in `sys_migration` (row `adr-0131-organization-ownership`); a run interrupted mid-step resumes at that step and never re-derives an owner already written. + - **The post-check** re-reads the plan and reports, per table, what remains. The ledger row records `applied_at` once every step has finished and `verified_at` when the post-check also reads zero discrepancies. Exit code 1 while the ceremony is unfinished or the post-check finds a discrepancy. + - A JSON report is written beside the plan (`organization-ownership-apply-TIMESTAMP.json`, or `--out`); an existing file is never overwritten. + + ```bash + os migrate organization-ownership --out plan.json # read the plan first + os migrate organization-ownership --apply --backup-taken # then perform it + ``` +- 707b3cd: feat(cli,spec): `os migrate organization-ownership --fate-supplement FILE`. A host declares the ADR-0131 D10 fates of the objects it provides, and the ceremony plans and applies them as it does the platform's own rows + + Clause-②: yes (widening) + + The v18 organization-ownership ceremony refuses any platform-prefixed table that carries `organization_id` when its inventory gives that table no fate. That is right for the platform's own objects, but a host's objects have no row in the inventory, so every database a host runs was refused. The host now declares those fates itself, in one JSON file, and passes the file to the command. **Adoption: a host passes its supplement through the flag; with no flag, the ceremony behaves as before.** + + - **The file is declared in the spec.** `OrganizationOwnershipSupplementSchema` (`@objectstack/spec/system`) is a JSON array of `OrganizationOwnershipSupplementEntrySchema` rows. Each row gives `object`, `fate` (`column-drop`, `mirror-deletion`, `attribution` or `report`), `citation`, and only the fields its fate reads: `anchors` (required for `attribution`), `mirror`, `departures`, `categories`, `notNullExempt` and `note`. Every shape refuses an unknown key, and a key written on the wrong fate is refused rather than ignored. + - **Both modes read it.** The plan plans each row on the same terms as a platform row, and `--apply` performs it the same way. A row whose table is not on the database being planned is listed `absent`, so one supplement serves both a control-plane database and a tenant database. + - **It is refused, never half-read.** The command refuses a missing file, a file that is not JSON, a row the schema refuses, two rows for one object, or a row naming an object the platform inventory already gives a fate. The refusal comes before the database is opened, inside the `--json` envelope (`plan_refused` / `apply_refused`), and names the file, the row index and the object. A row naming one of the cloud-provided objects that the inventory lists with no fate (`cloudCarried`) replaces that placeholder row. + - **Nothing is read without the flag:** no environment variable and no conventional path. A host table with no declared fate is still refused by name, and the refusal now names `--fate-supplement FILE` as the remedy. + - The supplement's rows are part of the plan's `inventoryDigest`, so the apply's checkpoint is keyed on the supplement as well. + + ```bash + os migrate organization-ownership --fate-supplement host-fates.json --out plan.json + os migrate organization-ownership --fate-supplement host-fates.json --apply --backup-taken + ``` +- 6673c80: feat(cli)!: the v18 upgrade carries stored comment reactions — `os migrate comment-reactions` turns each reaction stored in the retired `sys_comment.reactions` column into a `sys_comment_reaction` record, and `os migrate apply --allow-destructive` refuses to drop that column until the conversion is verified + + Clause-②: no (narrowing) — `os migrate apply --allow-destructive` now refuses the `sys_comment.reactions` drop it accepted before, until a verified conversion is recorded; the conversion itself only writes records of an existing object, so nothing widens. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + **What the upgrade now carries.** A database upgraded from 17.x keeps `sys_comment.reactions` and every emoji reaction stored in it (schema sync is additive). Those reactions are no longer lost. `os migrate comment-reactions` converts them into records: + + - **One record per stored reaction.** Each (comment, emoji, user) in a stored map becomes one `sys_comment_reaction` row with the same comment, emoji and user, in the comment's organization, owned by the user who reacted. That user can remove it like any reaction made on v18. A user id listed twice under one emoji is one reaction. + - **Nothing is guessed.** A stored value that is not JSON or not a map of emoji to user-id lists, an entry that is not a user id, an emoji longer than a reaction record holds, and a user id that names no `sys_user` row are each listed in the report with their comment id. Nothing is written for them. The readable entries of the same comment still convert. + - **Idempotent.** A reaction that already has its record is skipped, so a second run writes nothing, and re-running finishes an interrupted run. + - **Dry run by default.** `--apply` writes the records, reads the column back, and records the run in `sys_migration`. The run is recorded as verified only when every readable reaction has its record and no stored value is left unreadable. `--json` gives the full report. A run that lists an unreadable value or fails a write exits `1`, and so does an `--apply` run whose read-back finds anything unconverted. + + **The column drop waits for the conversion (the narrowing).** Until a verified run is recorded, `os migrate apply --allow-destructive` refuses to drop `sys_comment.reactions` and exits `1` (`--json`: `message: "refused_comment_reactions_unconverted"`, with the recorded run under `conversion`). Nothing else is applied on that run. This also covers the boot drift line, which names that command as the remedy for the orphaned column. What to do when it refuses: run the conversion below until it reports the run verified, then re-run the apply. + + **`os migrate apply --json` exits `1` when the `sys_account.issuer` retirement pre-flight refuses.** Its two refusals, `refused_account_issuer_collisions` and `refused_account_issuer_preflight_unreadable`, emitted their document with exit code `0`, so a caller gating on the status read a refused apply as a successful one. They now exit `1`, as the same refusals do without `--json`. The document is unchanged. + + **Upgrade order:** take a backup, run `os migrate comment-reactions`, then `os migrate comment-reactions --apply`. If it lists unreadable values, correct each one in the database so it reads as a map of emoji to user ids, or clear it if it holds nothing to keep, and run `--apply` again. Once the run is verified, run `os migrate apply --allow-destructive` to drop the column. Run these with the flags and environment the deployment boots with (an auth secret set), so the audit plugin's objects are composed. + + This supersedes the "stored reactions are not migrated" sentence in the entry that retired the column. `@objectstack/spec`: the protocol-18 semantic entry `sys-comment-reactions-column-retired` now names the conversion in its replacement, reason and acceptance criteria, and orders it before the drop. `os migrate meta` and the upgrade guide carry that text. Its id and surface are unchanged. + +### Patch Changes + +- eeb95f7: fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door: + + - **The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it. + - **Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key. + + The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted. + + **Readers.** `@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report. + + **Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` / `objectstack build` | `defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm` | refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 | + | `ObjectQL.registerApp` | the same manifest registered the set under `com.acme.crm`, silently | `400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed | + | an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key | + + **Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again. +- Updated dependencies [dac99c2] +- Updated dependencies [5c9bfb0] +- Updated dependencies [66949b7] +- Updated dependencies [06dbc4e] +- Updated dependencies [7958bdf] +- Updated dependencies [9fbabd7] +- Updated dependencies [5da2c0f] +- Updated dependencies [87e902d] +- Updated dependencies [3d830b7] +- Updated dependencies [777eed2] +- Updated dependencies [8258181] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [c6139f2] +- Updated dependencies [3490bf5] +- Updated dependencies [920bc12] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [08b38cb] +- Updated dependencies [bae5841] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [78c13bd] +- Updated dependencies [9543e9c] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [e790cfc] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [09ef77c] +- Updated dependencies [5d3018d] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [4f1e435] +- Updated dependencies [2de9c03] +- Updated dependencies [5b68524] +- Updated dependencies [5e725b8] +- Updated dependencies [6d96721] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [be50633] +- Updated dependencies [325a92e] +- Updated dependencies [325a92e] + - @objectstack/plugin-security@18.0.0-next.1 + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-protocol@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/plugin-sharing@18.0.0-next.1 + - @objectstack/lint@18.0.0-next.1 + - @objectstack/service-settings@18.0.0-next.1 + - @objectstack/verify@18.0.0-next.1 + - @objectstack/service-automation@18.0.0-next.1 + - create-objectstack@18.0.0-next.1 + - @objectstack/service-mailbox@18.0.0-next.1 + - @objectstack/rest@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/plugin-approvals@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-storage@18.0.0-next.1 + - @objectstack/service-datasource@18.0.0-next.1 + - @objectstack/plugin-webhooks@18.0.0-next.1 + - @objectstack/cloud-connection@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/service-messaging@18.0.0-next.1 + - @objectstack/client@18.0.0-next.1 + - @objectstack/console@18.0.0-next.1 + - @objectstack/account@18.0.0-next.1 + - @objectstack/setup@18.0.0-next.1 + - @objectstack/driver-memory@18.0.0-next.1 + - @objectstack/driver-mongodb@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + - @objectstack/driver-sqlite-wasm@18.0.0-next.1 + - @objectstack/driver-turso@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + - @objectstack/mcp@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + - @objectstack/plugin-audit@18.0.0-next.1 + - @objectstack/plugin-email@18.0.0-next.1 + - @objectstack/plugin-hono-server@18.0.0-next.1 + - @objectstack/service-analytics@18.0.0-next.1 + - @objectstack/service-cache@18.0.0-next.1 + - @objectstack/service-job@18.0.0-next.1 + - @objectstack/service-package@18.0.0-next.1 + - @objectstack/service-queue@18.0.0-next.1 + - @objectstack/service-realtime@18.0.0-next.1 + - @objectstack/service-sms@18.0.0-next.1 + - @objectstack/trigger-api@18.0.0-next.1 + - @objectstack/trigger-record-change@18.0.0-next.1 + - @objectstack/trigger-schedule@18.0.0-next.1 + - @objectstack/plugin-pinyin-search@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index f232c4412fb..b013869d97a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/client-react/CHANGELOG.md b/packages/client-react/CHANGELOG.md index cb2071b6bff..cfa98cf4743 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/client-react +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [4f1e435] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/client@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index ce02336466a..9f83e9c87b5 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 6697e0f5b9c..d9dd44891f4 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/client +## 18.0.0-next.1 + +### Minor Changes + +- 4f1e435: feat(client): `meta.deleteItem` can name the package whose row it removes (`packageId`, sent as `?package=`), so a caller can follow the `409 RESOURCE_CONFLICT` remedy the reset door prescribes for an item two packages hold (#23293) + + Clause-②: yes (widening) + + **What is new.** `DeleteMetaItemOptions` gains `packageId?: string`. Both `deleteItem` declarations take it, the unscoped `ObjectStackClient.meta` and the environment-scoped `client.environment(id).meta`, and send it as `?package=` through the one query builder they share. The name, the query key and the omission rule are the ones `saveItem`'s `packageId` already uses: only a non-empty id reaches the wire, and `undefined` and `''` send nothing. + + **Why.** A metadata item can hold one row per package (ADR-0048). The REST reset door `DELETE /api/v1/meta/:type/:name` reads `?package=` and refuses a delete that names no package of an item two or more packages hold, with `409 RESOURCE_CONFLICT`. The refusal names the packages and the remedy, which is to name one with `?package=`. This client had no option for it, so an SDK caller (an agent, the CLI, an app) could not do what the refusal said. + + FROM → TO: + + ```ts + // FROM: refused 409 RESOURCE_CONFLICT when two packages hold the item, with no way to name one + await client.meta.deleteItem('view', 'shared_grid'); + + // TO: removes com.acme.crm's row of the item and no other package's row + await client.meta.deleteItem('view', 'shared_grid', { packageId: 'com.acme.crm' }); + ``` + + **Unchanged.** A delete without `packageId` sends the same request as before: no query string unless `state: 'draft'` is set, and no header unless `ifMatch` is set. `packageId` combines with `ifMatch` and `state` (`?state=draft&package=`). `?dropStorage` is still not an option. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index 2986467119b..1071abde810 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 8f73f2ba6ed..165ffc8cb32 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,111 @@ # @objectstack/cloud-connection +## 18.0.0-next.1 + +### Minor Changes + +- d9d7edd: fix(runtime,rest,cloud-connection)!: the declared-endpoint fallback, REST's direct-mounted publish and federation routes, and cloud-connection's session doors refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by doors that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST's `enforceAuth` and the runtime dispatcher already refuse such a session. Three groups of doors that resolve a session on their own never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/runtime`**: the declared-endpoint fallback (app-declared `apis:` endpoints, served by the dispatcher plugin's fallback seam). Its policy chain now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) before `authRequired` and before the endpoint's target runs, on every declared endpoint, an `authRequired: false` one included, because the target runs as the caller. While a gate feature is active the chain reads the session for this, and when an active gate cannot read the session the request is answered `503` (`SERVICE_UNAVAILABLE`), as REST answers it. No declared path is handed to the allow-list, so no endpoint can exempt itself through the prefix it sits under. + - **`@objectstack/rest`**: the direct-mounted package publish door (`registerPackageRoutes`) and the external-datasource federation family (`registerExternalDatasourceRoutes`). Each refuses on the `authGate` the REST resolver already puts on the execution context it hands them. + - **`@objectstack/cloud-connection`**: `CloudConnectionPlugin`'s seven session doors (bind, poll, unbind, install, and the three installed-state reads) and all five `MarketplaceInstallLocalPlugin` doors (install, list, uninstall, reseed, purge). Each plugin asks the gate once, where it resolves the session, and every door that resolves one refuses on it. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each door's declared error envelope, before the door does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, an API-key caller is unaffected because it has no session to hold, and an anonymous caller gets the same `401`. `CloudConnectionPlugin`'s status read and `MarketplaceProxyPlugin`'s catalog proxy resolve no session and answer every caller alike, so they are unchanged. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these doors answer them, as on every other protected route. Nothing needs configuring: the posture comes from the kernel's `auth` service, as it does for REST and the dispatcher. +- 430e5f5: fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs. + + **Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not. + + **FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner): + + - `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`). + - Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items. + - `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`. + - `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched. + - `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read. + - Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence. + - The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator. + + **Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before. + + **The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [c6139f2] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [78c13bd] +- Updated dependencies [9543e9c] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [5d3018d] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [2de9c03] +- Updated dependencies [5b68524] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-protocol@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 4dd4f5c755a..de28d859917 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (cloud ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-imap/CHANGELOG.md b/packages/connectors/connector-imap/CHANGELOG.md new file mode 100644 index 00000000000..0f2a4fafb69 --- /dev/null +++ b/packages/connectors/connector-imap/CHANGELOG.md @@ -0,0 +1,28 @@ +# @objectstack/connector-imap + +## 18.0.0-next.1 + +### Minor Changes + +- 3d830b7: Inbound mail has a mail source: the new `@objectstack/connector-imap` reads a mailbox over IMAP, and `@objectstack/service-mailbox` polls each mailbox's folder into its ingest seam, keeping a UID cursor on `sys_mailbox` + + Clause-②: yes (a new published package, `@objectstack/connector-imap`, and its exports; new exports and plugin options in `@objectstack/service-mailbox`, and four new fields on its published `sys_mailbox` object) + + - **`@objectstack/connector-imap` (new) — protocol and authentication only.** + - `openImapSession({ account, credentials, timeoutMs?, maxLiteralBytes?, maxCommandBytes? })` opens a read-only IMAP4rev1 session: `examine(folder)` (`EXAMINE`, never `SELECT`; answers `uidValidity`, `uidNext`, `exists`), `listMessages(fromUid)`, `fetchMessages(uids)` (`BODY.PEEK[]`, so no message is marked `\Seen`), `logout()` and `close()`. A folder opened without a UIDVALIDITY is refused. + - The account and credential abstraction is shaped for the per-user mailbox: a `MailAccount` (`{ id, username, imap: { host, port?, security? } }`) and a `MailCredentialProvider` that resolves the account's credential each time a session opens. `staticCredentials(credential)` answers one fixed credential. + - A `password` credential is sent with `AUTHENTICATE PLAIN` when the server offers it, otherwise `LOGIN`; an `oauth2` access token with `AUTHENTICATE XOAUTH2`. A server that refuses `LOGIN` and offers no `PLAIN` is refused before anything is sent. + - `security: 'tls'` (the default) is implicit TLS with the certificate verified. A plaintext connection (`security: 'none'`) is refused to any host but loopback, before a socket opens. + - Failures are `ImapError` with a `kind`: `configuration`, `connection`, `timeout`, `authentication`, `refused`, `protocol` or `oversized`. + - An IMAP server is treated as an untrusted peer. A line above 4 MiB, a literal above `maxLiteralBytes` (64 MiB by default), a response of more than `IMAP_MAX_RESPONSE_LITERALS` (32) literals, and more than `maxCommandBytes` of responses to one command (128 MiB by default) are refused as `oversized` before the excess is kept, and the connection is closed. + - `@objectstack/connector-imap/testing` exports `ImapServerDouble`, an in-process IMAP server on loopback for tests. + - No runtime dependencies. + - **`@objectstack/service-mailbox` — the IMAP sync.** + - `new MailboxServicePlugin({ imap: { accountFor, pollIntervalMs?, batchSize?, maxMessagesPerRun?, maxMessageBytes?, maxBatchBytes? } })` schedules the interval job `mailbox:imap-sync` on the composed job service. For each mailbox `accountFor` answers (asked per mailbox per run; `null` for a mailbox the host does not sync), it fetches the messages above the stored cursor, in UID order, and hands each one to `ingest` under the system context. Dedupe, association, the pool and auto-reply suppression are unchanged. + - Without `imap` — the capability boot constructs the plugin with no options — no mailbox is synced, and the boot says so once. With `imap` but no job service, the boot warns, and `plugin.syncService.syncAll({ isSystem: true })` runs a pass on the host's own schedule. A run in which a mailbox could not be synced reports `degraded` to the job service. + - `sys_mailbox` gains the cursor, written by the sync only: `imap_folder`, `imap_uid_validity`, `imap_last_uid` and `imap_synced_at`. A cursor taken under another UIDVALIDITY, or in another folder, is discarded and the folder is read from its start; the messages already stored answer `duplicate`. No credential is stored on the object: the account and its credential come from `accountFor`. + - The cursor moves after each batch past every message stored, already stored, suppressed or skipped. A store failure stops the run below the failed message, so the next run retries it. A message above `maxMessageBytes` (32 MiB by default) or one the seam refuses as unreadable is skipped and logged at `error` with its UID. + - A batch is bounded by count (`batchSize`, 25) and by listed bytes (`maxBatchBytes`, 64 MiB). The session's literal bound is `maxMessageBytes` plus a 64 KiB framing envelope, so a server that omits or understates a message's size cannot hand over more. That message is skipped as `too_large`, after the batch is read again one message at a time on a fresh session, and the rest of the mailbox keeps arriving. + - A forwarded `message/global` part (RFC 6532, the internationalized `message/rfc822`) is now read as an `attachment` source, the same as `message/rfc822`. + - New exports: `MailboxSyncService`, `MAILBOX_SYNC_JOB`, `DEFAULT_MAILBOX_POLL_INTERVAL_MS`, `DEFAULT_SYNC_FOLDER`, `DEFAULT_SYNC_BATCH_SIZE`, `DEFAULT_SYNC_MAX_MESSAGES_PER_RUN`, `DEFAULT_SYNC_MAX_MESSAGE_BYTES`, `DEFAULT_SYNC_MAX_BATCH_BYTES`, `SYNC_LITERAL_ENVELOPE_BYTES`, and the types `MailboxServicePluginOptions`, `MailboxImapSyncOptions`, `MailboxAccountResolver`, `MailboxImapAccount`, `MailboxSyncTarget`, `MailboxSyncResult`, `MailboxSyncAllResult`, `MailboxSyncSkipReason`, `MailboxSyncServiceOptions`, `MailSyncEngine` and `MailSyncLogger`. + - This supersedes the ingest seam entry's "no mail source feeds the seam": the IMAP sync does. Attachments are still not stored. diff --git a/packages/connectors/connector-imap/package.json b/packages/connectors/connector-imap/package.json index 02c22909498..0be21ebdaf7 100644 --- a/packages/connectors/connector-imap/package.json +++ b/packages/connectors/connector-imap/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-imap", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "IMAP connector for ObjectStack — a read-only IMAP4rev1 session (EXAMINE, UID FETCH with BODY.PEEK) and the per-user-shaped mail account and credential abstraction the mailbox sync connects through.", "main": "dist/index.js", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index e5750a81625..08aa1af74dd 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-mcp +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 05a738787c6..42aaa2d1419 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index b1bad218cff..0aad5473f93 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-openapi +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 67c05366ce7..b22cf66f627 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 1be7c4ffccf..29404e73095 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-rest +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 49e3fd92be7..c640a870293 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index 201fc6aff47..2d1dbc62355 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-slack +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 89a32f4d68e..de4246be87f 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index a4fd63b8a2f..833f4e6ffe5 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,91 @@ # @objectstack/console +## 18.0.0-next.1 + +### Minor Changes + +- 325a92e: Console (objectui) refreshed to `b9814b334d8a`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 31 releasing of 35 changesets added across 36 non-merge commits; omitted: 4 release-nothing changesets, 4 commits carrying no changeset (they ship no package code). + + - **minor** — Studio's first draft save after opening an item no longer replaces a draft that was saved elsewhere in the meantime (objectui#11773). (objectui `b9814b334`) + - **minor** — An empty list draws its declared link, and a list page's own empty state wins over its source view's (objectui#12270). (objectui `b434f5f47`) + - **minor** — A `record:details` section heading can show how many values a field holds (objectui#12192). (objectui `c08c9332a`) + - **minor** — The console and the runner bundles are built against `@objectstack/*` 18.0.0-next.0 (objectui#12030). In both, the `@objectstack/*` ranges are `devDependencies`, but each package… (objectui `c5406c201`) + - **minor** — The ten locale packs follow `@objectstack/spec` 18's platform capability set (objectui#12030): the key `capability.label.manage_org_presentation` is replaced by `capability.label.… (objectui `c5406c201`) + - **minor** — **BREAKING** — Depend on the `@objectstack/*` 18 prerelease line (objectui#12030). (objectui `c5406c201`) + - **minor** — A report the viewer may not read shows the no-access state, not the server's refusal (objectui#12330). (objectui `2b06a91e6`) + - **minor** — A flow action launched from a list toolbar now resolves the row selection by the same rule as a script action: `@object-ui/core`'s `resolveServerActionRecordId` (objectui#12331).… (objectui `2722d33c5`) + - **minor** — Console writes that landed in silence now say so, and controls disabled on a read-only package say why to every user, not only to a mouse (objectui#12303). (objectui `b64fda3f6`) + - **minor** — A record page draws one provenance footer for its record, whatever the number of `record:details` nodes it carries (objectui#12307). (objectui `28ebfb5f8`) + - **minor** — The marketplace package page offers the install modes a package permits, managed or an unmanaged copy, and reads what the install door answers (objectui#12288, the objectui stage… (objectui `cea429b98`) + - **minor** — Calendar quick-create no longer offers a dialog that cannot succeed. On an object with a required field it cannot fill, an empty-day click opens the full create form with the clic… (objectui `ee470719e`) + - **minor** — A grid's inline-edit save no longer overwrites a concurrent write without a word (objectui#12292). The grid's default row save and "Save All" now send each row's `updated_at` as `… (objectui `3092aca7a`) + - **minor** — The OAuth consent page names the requesting application by its registered name again, and never shows the raw `client_id` in the name's place (objectui#12301). (objectui `ca45c6d55`) + - **minor** — The strict authoring face accepts `dataSource` on `record:related_list` (objectui#12302). The block's renderer has honoured the spec's per-element binding since objectstack#6953:… (objectui `093d55bb6`) + - **minor** — `record:details`, `record:highlights` and `record:quick_actions` honour the per-element `dataSource` binding the spec already declares on every page component (objectui#12294). A… (objectui `82e5f5348`) + - **minor** — `AutoNumberFieldMetadata` declares `autonumberFormat`, typed by reference to `@objectstack/spec`'s `FieldSchema.autonumberFormat`, and keeps `format` as its shorthand (objectui#12… (objectui `fd4872f9e`) + - **minor** — **BREAKING** — `buildValidationRules` stops compiling a field-level `pattern` / `pattern_message` and `validate` (objectui#12242, maintainer ruling A). `@objectstack/spec`'s `FieldSchema` refuse… (objectui `5d3f437ab`) + - **minor** — **BREAKING** — The field metadata types stop declaring the keys a host writes at runtime and the field-level regex keys, and `geolocation` leaves the `FieldMetadata` union (objectui#12242, secon… (objectui `5d3f437ab`) + - **minor** — `@object-ui/fields` exports `fieldHelpText(field)`, the one read of the help line a field draws under its control (objectui#12213). (objectui `5b45c0d50`) + - **minor** — **BREAKING** — A dashboard or report route's breadcrumb no longer carries a 「Dashboards」 or 「Reports」 crumb (objectui#12273, applying the maintainer's ruling on objectstack-ai/cloud#2768, as obj… (objectui `9f282ef53`) + - **patch** — `data-table`: an inline edit made while a row save or Save All is in flight is no longer dropped when the save settles (objectui#12322). (objectui `72594f29e`) + - **patch** — A failed first `connect()` with `autoReconnect` now retries discovery and settles (objectui#12318). The retry used to call `connect()` from inside the attempt it was retrying. `co… (objectui `866763bda`) + - **patch** — Metadata admin detail pages no longer overprint their header on a narrow window (objectui#12306). (objectui `67ca3af91`) + - **patch** — An inline edit in a grid is no longer written to another record when the rows are re-read in a new order (objectui#12310). The table kept each pending edit under its row's positio… (objectui `a8051d654`) + - **patch** — Each app tile in Home's "Your apps" strip is now a link to the app, so an app can be opened in a new tab and its address can be copied (objectui#12305). (objectui `0ffa07605`) + - **patch** — The connection state follows the requests the console already makes, so the header chip now shows a mid-session outage (objectui#12295). (objectui `40cb13b2e`) + - **patch** — The console's Setup › Apps page reports what the server did with a write (objectui#12300). Before, a Delete and a Disable could report success for a write that changed nothing. (objectui `068a5407f`) + - **patch** — Saving a master-detail edit form no longer rewrites the position of lines the user did not touch (objectui#12286). (objectui `f0532d8a8`) + - **patch** — A dashboard header action of type `url`, or of no declared type, now goes through the action runner, like every other header action (objectui#12275). (objectui `74964ad68`) + - **patch** — A flow action whose flow runs straight through, with no screen, now closes with the flow's own `successMessage` when the action declares none (objectui#12274). (objectui `bfca2a2ce`) + + ⚠️ 4 of these carry a breaking change: 4 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 4 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs(ci): retire the four-shard leftovers in AGENTS.md section 9 and two comment sites (#12287) (objectui `157017967`) + - _(no changeset)_ docs(fields): the auto-number page teaches the spec's format tokens, with no quarter token and no hand-written renderer (objectui#12258) (#12280) (objectui `5595aabca`) + - _(no changeset)_ ci(spec-main-shape-gate): drop the merge_group trigger now the context is out of the required set (objectui#12252) (#12262) (objectui `a1a917a08`) + - _(no changeset)_ ci(type-check): the affected speed builds the packages the repo-root setup program imports (objectui#12272) (#12277) (objectui `48fb558d3`) + + Clause-②: no + + + + objectui range: `1fb1ced36243...b9814b334d8a` + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 96e29b6b819..f1828aa316c 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "description": "Prebuilt Console SPA pinned to this framework release, installed as a dependency of @objectstack/cli. 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 d9d610c34fc..a895e3a05dc 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,147 @@ # @objectstack/core +## 18.0.0-next.1 + +### Minor Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- ed03834: feat(core): `emailTemplateDefinitionFromRow` — a `sys_email_template` row as the `email_template` definition it carries (ADR-0131 D10, ruling C on §6 Q1) + + Clause-②: yes (widening) + + A new pure export beside `permissionSetDefinitionFromRow` and `positionDefinitionFromRow`: one row in, one body out, with no engine and no I/O. Its one import is the spec's floor-locale constant. The upgrade ceremony (`os migrate organization-ownership --apply`) uses it to promote a customized or admin-authored template row to an environment `email_template` item through the metadata door before the row is deleted. It runs only in that ceremony, never at boot. + + The mapping: `body_html` → `bodyHtml`, `body_text` → `bodyText`, `from_address` / `from_name` → `fromOverride`, `reply_to` → `replyTo`, `variables_json` → `variables`, and `active` keeps its stored meaning. `label` falls back to the name and `locale` to `EMAIL_TEMPLATE_FLOOR_LOCALE` (`en-US`). The retired `category` / `is_system` columns and every provenance or state column are left out. It does not validate; the door does. + + Nothing existing changes. +- 37510aa: feat(security): the guest's grants channel — an anonymous request at a guest-entry door holds the sets bound to the `guest` anchor, in the one organization the deployment resolves for it (ADR-0138 E1, D3/D4) + + ADR-0087 disposition: none. No authorable key, accept set or stored shape moves: no spec schema, metadata type, stored row or published response field is added, removed, renamed or re-shaped. A declared binding that was not enforced becomes enforced (ADR-0090 D9, as ADR-0138 D3 amends it). + + **The bindings that start granting.** On the reference apps, none: `app-showcase`, `app-crm`, `app-multi-package` and `app-todo` declare no definition of the `guest` anchor that names a set. `app-showcase` ships `showcase_guest_portal` as a capability an administrator may bind and binds it to no anchor (its `client_portal_user` position is an ordinary position), and `app-crm`'s `guest_portal` is the public form doors' set, which keeps its own posture. No reference app declares a permission set named `guest`, so no grant stops either. A set starts granting only where a deployment holds an environment definition of the `guest` position that names it. + + What changes: + + - **`@objectstack/core`** — the anonymous branch of `resolveAuthzContext` resolves the guest's grants on a face that takes the explicit guest entry (the new `guestEntry` input). It reads the sets the `guest` position's definition names, through the same position-bound read every position uses, and the activation ledger as for any held set. It resolves them only in the organization the guest entry's organization step derives (`resolveGuestOrganization`), which asks ADR-0131 D9's predicate and no second spelling of it. With nothing bound, the guest holds no set and no read is issued. The predicate itself, `resolveSystemWriteOrganization`, moves to `@objectstack/core` so both of its askers share it. New exports: `resolveGuestOrganization`, `resolveSystemWriteOrganization` (with `SystemWriteOrganizationDecision` and `SystemWriteRefusalReason`), and `GuestOrganizationUnresolvedError` with `isGuestOrganizationUnresolvedError` and its constants. + - **`@objectstack/objectql`** — `resolveSystemWriteOrganization` and its two types are re-exported from `@objectstack/core` as the same bindings. Nothing a caller imports changes. + - **`@objectstack/plugin-security`** — the set resolution treats the guest's sets as any requested set, and no longer folds the guest's position into the request. A permission set that merely shares the anchor's name no longer reaches an anonymous caller, and the baseline and `everyone` never did. At the CRUD gate, a guest that reaches a data operation acting in no organization, on a deployment where the organization step refuses, is refused before the operation runs. + - **`@objectstack/plugin-sharing`** — the read filter answers deny-all for the guest on every object that record sharing scopes, before the sharing-model branch: the guest reaches rows only through share links, never through the object's sharing model (ADR-0138 D3 item 6, as the maintainer's ruling C amends it). `externalSharingModel` is still not read for any caller. Share links are redeemed at their own door and are unchanged. + - **`@objectstack/runtime`** — the runtime face, which serves the guest, asks for the guest's grants. + + What an anonymous caller sees at an `authRequired: false` endpoint of `type: 'object_operation'`: + + - **One organization, a set bound to `guest`:** the operation runs as the guest, under that set, in that organization. A read the set grants is admitted and returns no row, whatever the object's sharing model (`private`, `public_read`, `public_read_write` or `controlled_by_parent`): the guest reaches a record only through a share link. A create the set grants lands in that organization with no owner. + - **Nothing bound:** `403 PERMISSION_DENIED`, as before. + - **A walled deployment, several organizations, or none:** `501 NOT_IMPLEMENTED`, naming the missing site binding (ADR-0138 D5), where it used to be `403 PERMISSION_DENIED` with nothing granted. Nothing that was served stops being served. The operator's log names the reason once. + + Unchanged: share links, which still serve an anonymous caller the one record they name, with `publicSharing.eligibility` checked at the mint and at every redemption; the public form doors, which keep their own posture and organization; the flow door, which runs under the flow's own declared posture; every surface that answers an anonymous request `401`; and every signed-in caller. + + Level: `minor` for `@objectstack/core`, because new exported symbols widen its published index; `patch` for the other four, whose exports do not move. + +### Patch Changes + +- 87e902d: feat(objectql): the engine composes a `ReadableIds` leaf at its executor boundary, on the final query immediately before the driver call, and refuses it by name wherever it cannot (ADR-0139 D2, D5) + + Clause-②: no + + - **Composed.** On `find`, `findOne`, `count` and `aggregate`, after the middleware chain and the `beforeFind` hooks, every `{ FIELD: { $in: ReadableIds.of(P) } }` in `where` is replaced by `ReadableIds.compose(leaf, scope)`, where `scope` is the registered security service's `getReadFilter(P, caller)`, marked `'policy'` (`undefined` composes with no row restriction). The comparand becomes `$in: []`, which matches no row, when: + - `canReadObject(P, caller)` is false or throws (a service without `canReadObject` is asked `explain`, and only `allowed: true` admits); + - `getReadFilter` answers `RLS_DENY_FILTER` (by identity) or throws; + - no security service is registered, or the registered one cannot answer both halves (the engine reads the kernel's `security` service through `ObjectQLPlugin` when a leaf is composed, so an engine used without that plugin has none); + - the registry holds no object `P`. + + Each parent object is asked once per read, and nothing is kept across reads. A leaf that a middleware or a `beforeFind` hook writes is composed the same way. + - **Refused, `INTERNAL_ERROR` / 500, before any driver is asked:** + - on `update` and `delete`: the caller's leaf at the comparand door, a middleware's at the executor's entry, before the prior-row reads; + - in `having` and in an aggregation's own `filter`; + - anywhere but a `$in` comparand, including a leaf the array form of `where` lowers into another position; + - a composed leaf the engine did not compose, written by a middleware or a hook (the caller's own, as a `$in` comparand, stays the comparand door's `INVALID_FILTER` / 400); + - ADR-0139 D5's three classes: a delegated (on-behalf-of) caller, a parent with an object-scoped read middleware, and a parent bound to another datasource than the object read. + - **Before:** the comparand door refused every `ReadableIds` leaf `INTERNAL_ERROR` / 500 ("this engine does not compose that leaf yet"), and a leaf that a middleware or a hook wrote reached the driver unrefused. + - **`@objectstack/core`:** `resolveFilterTokens` returns a `ReadableIds` leaf by reference. When the filter also held a placeholder, it used to rebuild the leaf as a plain `{ object }` and drop its brand. + - **Not yet:** no driver evaluates the composed leaf (ADR-0139 E3 and E4), and no code in the platform writes a `ReadableIds` leaf today. +- b4d573a: fix(spec,core): the rolling `last_7_days`, `last_30_days` and `last_90_days` presets close on today, at the spec's prescription table, the save door's refusal and the analytics preset arm + + Clause-②: no (no accept set widens: the three rolling windows gain the inclusive `{today}` end every calendar preset already has) + + The three rolling windows were the open arm of `DATE_RANGE_PRESET_MACRO_WINDOWS`: `['{30_days_ago}', null]`, prescribed as `{ $gte: '{30_days_ago}' }`. A `$gte`-only window has no upper bound, so on a column that carries future dates (a close date, a due date) "last 30 days" also counted every later day. Measured on a `date` field with one row dated today−5 and one dated today+5: that window counted 2, while the console's preset lowering and the analytics preset arm counted 1. + + - `DATE_RANGE_PRESET_MACRO_WINDOWS` now closes every rolling window on `{today}`: `last_7_days` is `['{7_days_ago}', '{today}']`, and `last_30_days` and `last_90_days` follow the same shape. That is the inclusive last-day end every calendar preset already uses, and the extent the console's lowering already selects. The table's declared type narrows to match, `readonly [start: string, end: string]` (it was `end: string | null`). A reader that compared `end` with `null` still compiles; that branch can no longer be taken. + - The save door's refusal of a bare preset comparand (`bareDateRangePresetComparandMessage`, and the `filter-preset-comparand` lint verdict that quotes it) now prescribes the closed window. For `last_30_days` it says: write `{ $between: ['{30_days_ago}', '{today}'] }`, where it used to say `{ $gte: '{30_days_ago}' }`. + - `@objectstack/core`'s `resolveAnalyticsDateRangePreset`, the preset arm behind `timeDimensions[].dateRange`, selects the same extent. A rolling window now stops before tomorrow begins in the reference timezone, exactly as `today` does, with `endExclusive: true`. It used to end at the reference instant, so it dropped rows stamped later today. Its ends keep this module's own "the day the window stops before" spelling (`tomorrow`). Every preset this resolver returns is now `endExclusive: true`, and a caller's explicit `[a, b]` window keeps its inclusive reading, as before. + - The semantic migration entry `filter-preset-ordering-comparand-refused`, which `spec-changes.json` and `os migrate meta` carry, now names `{ $between: ["{30_days_ago}", "{today}"] }` as the window for `last_30_days` in its replacement, where it named `{ $gte: "{30_days_ago}" }`. + + If you followed the old refusal and authored `{ $gte: '{N_days_ago}' }` for a rolling preset, add `$lte: '{today}'`, or write the `$between` pair above, to stop counting future-dated rows. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index b67263d1441..0fc25877da4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Microkernel Core for ObjectStack", "type": "module", diff --git a/packages/create-objectstack/CHANGELOG.md b/packages/create-objectstack/CHANGELOG.md index c8f11df7691..78297a81256 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,19 @@ # create-objectstack +## 18.0.0-next.1 + +### Minor Changes + +- 5da2c0f: feat(create-objectstack): a scaffolded project installs its AI skills from the `@objectstack/skills` package at its own `@objectstack/spec` version, never from the repository's `main` branch + + Clause-②: no + + - **The project depends on the catalog.** The blank template's `devDependencies` gain `@objectstack/skills`, at the same `^` as every other `@objectstack/*` dependency, so a new project's dependency set changes: the skills are the ones written for the `@objectstack/spec` it installs. + - **The install reads `node_modules`.** After ` install`, the scaffolder runs `npx -y skills@1.7.2 experimental_sync --agent claude-code --copy -y`, which copies the installed package's catalog into `.claude/skills/` as real files, once, beside a `skills-lock.json` that names the package as every skill's source. The GitHub `npx skills add objectstack-ai/objectstack/skills …` command is gone from the run, its summary and the `AGENTS.md` and `README.md` it writes into the project. + - **The summary says what happened.** It names the package version installed and prints the same command for re-installing after an upgrade of the `@objectstack/*` packages, and for any other agent runtime. When the install leaves no catalog to read, or a skill did not come from the package, the run says so instead of reporting the skills installed. + - **The flags keep their meaning.** `--skip-skills` skips the skills step. `--skip-install` installs no skills, because nothing is in `node_modules` yet; the summary names ` install` and then the sync command. + - **To move a project created earlier onto the package:** add `@objectstack/skills` to `devDependencies` at your `@objectstack/spec` range and run your package manager's install. Remove the skills `skills add` installed — `npx skills remove objectstack-data -y` and so on for each `objectstack-*` skill, or `npx skills remove --all` if you installed no others — because the sync skips a skill it did not install and leaves the old copy in place. Then run `npx skills@1.7.2 experimental_sync --agent claude-code --copy -y` (name your own runtime after `--agent`). + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 63d2cc78f91..fb0cfb64097 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index d90e468e49b..b5bec572604 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/driver-memory +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 2bcf18541cb..bbdb47f3992 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index a696204a438..de8de7b8677 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/driver-mongodb +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 7a9789bb05a..292601daf64 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index 8f206425199..d2e03c796fc 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/driver-sql +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index eb4d8d7e134..ba3b1311027 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index b693dd6aec5..693040393f1 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/driver-sqlite-wasm +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index d647c17e987..17cbcdd9948 100644 --- a/packages/drivers/driver-sqlite-wasm/package.json +++ b/packages/drivers/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/drivers/driver-turso/CHANGELOG.md b/packages/drivers/driver-turso/CHANGELOG.md index fa34ae9e934..4472cee2c5d 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/driver-turso +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index a170a852fa6..0dfd66a27a9 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Turso/libSQL Driver for ObjectStack — Edge-first SQLite with embedded replicas", "keywords": [ diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index 0d70e4b6dda..da350a53d0f 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/formula +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index be70af5b1ac..45c887b7bbd 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index 54f68aded5b..0b1b9c45ca8 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,227 @@ # @objectstack/lint +## 18.0.0-next.1 + +### Major Changes + +- 7958bdf: No flow position reads the single-brace `{…}` template dialect any more, and its resolver is deleted (ADR-0032 Sequencing 6: "delete the single-brace resolver"). `AutomationEngine.evaluateCondition` evaluates every condition as CEL: a condition carrying a `{var}` hole is refused as the CEL brace trap, naming its source, and a `template` or `flow` envelope is refused before evaluation, where the deleted path substituted the variables and compared the text. A literal in a value slot is handed on as written: a `Date` in a code-defined flow reaches the write as the `Date`, where the deleted interpolator wrote `{}`. + + Clause-②: no (narrowing) + + + + **BREAKING**: a narrowing of a published runtime method and of published lint exports, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`). + + **Why.** ADR-0032 Decision 3 keeps one template delimiter, `{{ }}`, and its Sequencing 6 deletes the single-brace resolver. The earlier changes on this line retired the dialect position by position — every value slot, the text slots, each CRUD filter operand — so the resolver's last readers were two calls that saw literals only and a condition path that only a direct call reached. + + **What changes.** + + - **Conditions.** `AutomationEngine.evaluateCondition(condition, variables, context?)` reads every condition as CEL — bare text, an envelope with `dialect: 'cel'`, or one naming no dialect. The template path it kept for a string carrying a `{var}` hole is deleted: such a string is refused as a CEL fault that names its source and says not to wrap field references in `{…}`, and an envelope with `dialect: 'template'` or `dialect: 'flow'` is refused before evaluation. `registerFlow` already refused both at every condition slot (a start node's `condition`, an edge `condition`, a `decision` branch's `expression`, a screen field's `visibleWhen`), so no registered flow changes. What changes is a direct caller: a plugin calling the method, or `@objectstack/verify`'s `stack.automation.evaluateCondition`, which hands the condition over as given. + - **Value-slot literals.** A literal in a value slot — a `fields`, `input` or `templateData` value, an `assignment` value in any of its three shapes, a `notify` recipient, an `http` `body`, a screen's `defaults`, `defaultValue` or `recordId` — is handed on as a copy of what was written (arrays and plain objects copied, every other value as it is). The deleted interpolator walked every object's own entries, so a `Date` written in a code-defined flow reached the write as `{}`; it reaches it as the `Date`. + - **`@objectstack/lint`.** `validateFlowTemplatePaths` reads the `{{ }}` holes of a node's strings and no single-brace token; a CRUD node's `filter` is not scanned, since each operand is a value slot that renders no template. So `flow-template-unknown-field` and `flow-template-lookup-traversal` report only `warning` now: their `error` at a filter operand, and every finding on a single-brace token, are gone, and the value-slot and text-slot judges refuse those tokens at the same doors as `expression-invalid`. Messages and hints spell a hole `{{ record.x }}`. `flow-double-brace-interpolation` and `flow-bare-dollar-reference`, at a string outside every value slot, text slot and declared literal (a plugin node's own config, a `connector_action` input), say the value is read as written, where they prescribed a `{var}` token nothing reads. The lint's mirror of the retired grammar is removed with its exports. + - **`@objectstack/spec`.** The `FlowSchema` example's `connector_action` input holds a literal, since nothing reads a token there. The upgrade guide's step-18 text-slot entry says no other flow string reads the single-brace dialect, where it said every other one kept it, and its value-slot entry names every value slot the retirement reached on this line, where it named only the `assignment` values and the CRUD `fields` map. + + ## FROM → TO + + | you wrote | write instead | what changes | + |:--|:--|:--| + | `engine.evaluateCondition('{amount} > 100', vars)` | `engine.evaluateCondition('amount > 100', vars)` | the template substituted text and compared it; CEL compares the values and fails the run on an absent variable | + | `"{status} == 'open'"`, `'{status} == open'` | `"status == 'open'"` | a string literal is quoted | + | `{ dialect: 'template', source: "{flag} == true" }`, `dialect: 'flow'` | `{ dialect: 'cel', source: 'flag == true' }` | — | + | `classifyFlowTemplateToken(inner)` (`@objectstack/lint`) | `valueSlotTemplateRefusals(value)` / `textSlotTemplateRefusal(text)` (`@objectstack/spec/automation`), which refuse a token with the spelling that replaces it | the grammar it classified is read by nothing | + | `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` | the CEL stdlib: `isoDate(today())`, `isoDatetime(now())`, `round`, `floor`, `ceil`, `abs`, `min`, `max` | — | + | `FlowTemplateTokenVerdict` | nothing: the judges' reading of the retired grammar is internal to `@objectstack/spec` | — | + + **The one-line fix: write a condition as CEL, without `{…}` around a reference.** + + **Who is affected, measured.** In this repository at the base: no braced condition and no `template` or `flow` condition envelope in `examples/`, the docs or any package source; the deleted path's own pins in `@objectstack/service-automation` (`engine.test.ts`, `nested-region-parity.test.ts`) and `@objectstack/verify`'s door test are re-pinned to the refusal here. No authored `Date` literal in a flow was found; a compiled artifact serialises one to text, so only a flow handed to `registerFlow` in code carried one. Neither repository imports the removed lint exports. hotcrm at `0f84c9c` carries no braced condition and no `connector_action` input; its flows still author the single-brace dialect at value and text positions earlier changes on this line refuse, which its own upgrade migrates. + + ### The kit + + - **The engine.** `AutomationEngine.evaluateCondition` (`@objectstack/service-automation`): the template path, its hole discriminator and its text comparison are deleted. The value-slot resolvers' literal arm and the `assignment` executor copy a literal (`copyLiteral`, `builtin/value-slot-map.ts`). The single-brace interpolator (`interpolate`, `interpolateString`, `resolveToken`, its function table and `FlowExpressionFunctionError`), which the package did not export, is deleted with its tests. + - **The lint.** `validateFlowTemplatePaths` and the two wrong-syntax hints (`lint-flow-patterns.ts`); `classifyFlowTemplateToken`, `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` and `FlowTemplateTokenVerdict` are removed. + - **The verify handle.** `stack.automation.evaluateCondition`'s documentation states the engine's reading: CEL, a `{var}` hole refused. + - **ADR-0087.** No new entry: the step-18 D3 entries `flow-value-slot-template-dialect-refused` and `flow-text-slot-single-brace-refused` record the retirement. +- 4f68ba1: Every finding of the component-props gate is now an error at `objectstack validate`, `objectstack build` and `objectstack lint`. A page component whose `properties` carry a key its `ComponentPropsMap` row does not declare, a value that row rejects, or no `dataSource.object` binding where the element reads only that binding drew a warning: the command printed "✓ Validation passed" and built the artifact, and the renderer ignored the key or drew nothing. Each is now a `component-props-unknown-key` or `component-props-invalid` error, and the stack does not pass until it is fixed. A retired prop was already refused, and still is. + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` and `component-props-unknown-key` rules of `objectstack validate`, `objectstack build` and `objectstack lint`). + + **What is refused now.** Every verdict of the gate: + + - `component-props-unknown-key`: a key the component's props row does not declare, at the bag's own level, on a strict object or union arm below it, or on a strip-mode object below it. + - `component-props-invalid`: a value the props row rejects (a wrong type, a missing required prop, a closed-set refusal such as a url action's `target` scheme or a `record:details` section's `count`, a retired enum value), and a missing `dataSource.object` on `element:record_picker`, `element:number` and `element:repeater`. + + **Not judged here.** The runtime publish door (the Studio, REST `/meta`, MCP) does not run this rule, so a page saved there is not refused for any of these. `defineStack` and the stack schema keep the open `properties` bag as written. A component `type` that `ComponentPropsMap` carries no row for (a `custom.*` or plugin type) is not judged. + + `ComponentPropsSeverity`, the type `@objectstack/lint` exports for these findings, narrows from `'error' | 'warning'` to `'error'`. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `page:header` `properties: { titel: 'Deals' }` | `properties: { title: 'Deals' }`, the declared spelling the error names | + | `record:related_list` `properties: { limit: 'five' }` | `properties: { limit: 5 }`, the type the error names | + | `element:number` with no `dataSource` | `dataSource: { object: 'deal' }` on the component node, beside `type` | + | a key the component does not read | nothing: delete the key | + + **The one-line fix: apply what the error names.** An unknown-key error names the declared spelling when one is close, a value error names what the row expects, and the binding error names where the object goes. If the component really does read the key or value, the props row is what is wrong: correct the declaration in `@objectstack/spec`, not the page. + + The `@objectstack/spec` change is text only: the reason of each D3 migration entry named above said the gate reported its refusal as an advisory finding, and now says it is an error. + + **Who is affected, measured.** At objectstack `8323cad97`, this rule ran at its own input tier over `examples/app-crm`, `app-todo`, `app-multi-package` and `app-showcase`, and over the eight shipped pages outside them (`@objectstack/platform-objects` ×3, `@objectstack/plugin-approvals`, `packages/qa/downstream-contract`, `@objectstack/cloud-connection` ×2, `@objectstack/mcp`): 37 pages, 177 components judged. It also ran over `objectstack-ai/hotcrm` at `0f84c9c72`: 8 pages, 50 components judged. Result: 0 findings of every class in both. A planted undeclared key and a planted wrong-typed prop each drew one finding on every population. The `create-objectstack` template authors no page. Deployed metadata and other repositories were not measured. + +### Minor Changes + +- 76815b9: feat(spec)!: a `controlled_by_parent` object derives its master from exactly one required `master_detail` — the parse refuses anything else, and the runtime's two fallback tiers retire (#23100) + + Clause-②: no (narrowing) + + ADR-0055 §1 says a `controlled_by_parent` object "must declare exactly one required `master_detail` field … Validation error otherwise (fail closed)". Until now neither the parse nor the runtime kept it. `ObjectSchema` accepted such an object with no master at all. The security runtime derived a master through a three-tier fallback the record never stated: a required `master_detail`, then any `master_detail`, then a required `lookup`, taking the first match by field order within a tier. The maintainer ruling (letter B) enforces the record, under ADR-0049 (enforce-or-remove). + + **BREAKING — what moves for consumers.** + + - **`@objectstack/spec`.** `ObjectSchema` (raw `.parse()` / `.safeParse()`, `ObjectSchema.create()`, `defineStack`, `os validate` and the metadata save door) refuses an object declaring `sharingModel: 'controlled_by_parent'` unless exactly one of its fields is a `master_detail` declared `required: true`. With none, the issue sits at `sharingModel` and names each field that relied on a retired tier. With two or more, it sits at `fields` and names them. A plain object literal is judged like a raw parse: an omitted `required` on its `master_detail` is refused, while `ObjectSchema.create()` keeps forcing an omitted `required` to `true`. The step-18 semantic migration entry `cbp-master-relation-tiers-retired` carries the prescription for `os migrate meta`. The step-18 conversion `cbp-master-detail-required-defaulted` covers the one lossless shape: a `controlled_by_parent` object whose one `master_detail` omits `required`, which Studio's field designer writes. It gets `required: true`, exactly what `ObjectSchema.create()` does. It runs on stored rows, built artifacts and `os migrate meta --from 17`. A live authoring parse still refuses the shape and names the fix. + - **`@objectstack/plugin-security`.** The relation resolver derives a master from that one field and nothing else. An object that declares a non-required `master_detail` only, a required `lookup` only, or two required `master_detail` fields resolves no master. Every read of it is denied, and every write and insert is refused with `422 INVALID_METADATA`. The plugin reports such an object once per object (`[security/controlled-by-parent] … resolves no master relation`), naming the shape it relied on. The `422` message now says "not exactly one required master_detail field". + - **`@objectstack/lint`.** `security-controlled-by-parent-no-relation` now refuses the non-required `master_detail` and required `lookup` shapes as well. `security-controlled-by-parent-ambiguous-relation` reports two or more required `master_detail` fields as "no master is derived", since the runtime no longer picks one by field order. Two required `lookup` fields are now a no-relation object, not a tie. `security-master-detail-ungranted` no longer treats a lookup-bound `controlled_by_parent` object as a detail. + + **FROM → TO — HotCRM's three objects.** These are the only producers measured on the retired third tier: the read-only HotCRM clone at `0f84c9c`, 8 `controlled_by_parent` objects, 5 of them already on one required `master_detail`. HotCRM's pinned engine is unaffected until it bumps past the release that carries this change. + + | object | FROM | TO | + |---|---|---| + | `crm_campaign_member` | `crm_campaign: Field.lookup('crm_campaign', { required: true })` | `crm_campaign: Field.masterDetail('crm_campaign', { required: true, deleteBehavior: 'restrict' })` | + | `crm_event_attendee` | `crm_event: Field.lookup('crm_event', { required: true })` | `crm_event: Field.masterDetail('crm_event', { required: true, deleteBehavior: 'restrict' })` | + | `crm_article_feedback` | `crm_knowledge_article: Field.lookup('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | `crm_knowledge_article: Field.masterDetail('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | + + `deleteBehavior: 'restrict'` keeps what the two lookups do today: a campaign or an event with members or attendees cannot be deleted. Without it a `master_detail` cascades, and a detail's rows are deleted with their master. An object whose rows must outlive their parent is not a detail. It leaves `controlled_by_parent` for `private`, `public_read` or `public_read_write`. + + **Remedy, in one line.** On every object with `sharingModel: 'controlled_by_parent'`, declare exactly one `master_detail` field with `required: true`, pointing at the master. Every other parent is a `lookup`. + + **Unchanged.** + + - An object with one required `master_detail` resolves and derives exactly as before, chain walk included. So does a required `master_detail` beside other `lookup` fields, or beside a `master_detail` that is not required. Every in-tree `controlled_by_parent` object is in that set: `examples/app-crm` (1), `examples/app-showcase` (2), the CLI's eval corpus (4) and the dogfood fixtures. + - Stored metadata whose one `master_detail` merely omits `required` keeps serving, converted as above. A stored row in any other retired shape is not rewritten: it still registers, under its `[metadata_spec_invalid]` diagnostic, and is denied until it is corrected and saved. Those shapes are an explicit `required: false`, two masters, or a required `lookup`. None of them is converted: turning a `lookup` into a `master_detail` changes what deleting the master does to every row, choosing between two masters is the pick ADR-0055 §1 refuses, and leaving `controlled_by_parent` is the author's decision. + - No export, signature or error code is added or removed. + + + +### Patch Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 9fbabd7: fix(lint): 17 more author-time rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain + + Clause-②: no + + - **Shorter fix lines.** Each finding of these 17 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after: + - approval nodes: `approval-approver-not-membership-tier` (313 → 168), `approval-expression-no-empty-policy` (294 → 121) and `approval-expression-invalid` (292 → 144); + - datasets: `dataset-include-unknown` (336 → 97) and `dataset-field-unknown` (206 → 139); + - record titles: `title-format-retired` (267 → 148) and `title-unresolvable` (237 → 122); + - list views: `list-view-field-unknown` (258 → 62) and `list-view-field-dotted` (233 → 158); + - translations: `translation-target-unknown` (317 → 164); + - seeds: `seed-insert-mode-duplicates-on-replay` (309 → 131) and `seed-value-outside-state-machine` (251 → 139); + - charts: `chart-measure-unknown` (285 → 115); + - object field-name lists: `object-field-ref-unknown` (284 → 186); + - stack views: `view-container-shape` (256 → 187); + - agents: `ai-skill-surface-mismatch` (235 → 171); + - source-tier pages: `page-source-className-tailwind` (231 → 167). + - **Lists are no longer echoed.** `dataset-include-unknown`, `dataset-field-unknown`, `list-view-field-unknown` and `object-field-ref-unknown` no longer append the object's whole field list, `chart-measure-unknown` no longer lists the dataset's measures, and `translation-target-unknown` no longer lists the declared fields, views, sections, tabs, rules, actions, apps, navigation ids, dashboards, widgets, header actions, flows, screens, screen fields, refusing end nodes, params, outcomes or result paths: the verdict's "Did you mean" (on `chart-measure-unknown`, the fix line's own) names the nearest one. Two facts a list used to carry stay on the line as one bounded clause: a stale `_views` key is told that a nameless default `list` is keyed `default`, and a `_tabs` key over an object with no preset bar is told to declare the tab in a page's `interfaceConfig.userFilters.tabs`, because a list view's own `tabs` has no renderer. `dataset-filter-field-unknown`, outside this change, keeps its field list. + - **One line, one remedy.** Where a fix line explained its remedy, the explanation moved: what an expression approver's three roots hold, why `record` and `previous` are unbound at an approval node, and what `resolveAs` does; what each `onEmptyApprovers` policy does; that a business role is always a position; that a dataset join is derived from the object graph, up to three hops, with `a.b` including `a`; why `titleFormat` is retired and how a record title resolves, with its fallback; what a denormalised field is; which presentation positions a chart measure lands on, and that none carries a `suppressWarnings` key; the composite `externalId` of a junction table; that the seed exemption never licenses an undeclared state; when a skill may declare `surface: 'both'`; and why a Tailwind class in page source produces no CSS, with the theme-token and overlay advice. + - **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Five ids gain an entry, so their `rule:` line now carries the `os explain` pointer: `approval-expression-invalid`, `approval-expression-no-empty-policy`, `title-format-retired`, `title-unresolvable` and `page-source-className-tailwind`. Ten existing entries gain text. The enumerations an entry spells out are held to their sources by the rules' tests: the empty-slate policies to the spec's `onEmptyApprovers` enum, the expression roots to `APPROVER_EXPRESSION_ROOTS`, and the title-eligible types to `TITLE_ELIGIBLE_TYPES`. + - **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), 9 of the 17 fire; the `hint` FROM → TO, measured at the door: + - a `flow` write with an approval node: `approval-approver-not-membership-tier` (2xx `advisories[].hint`, 313 → 138), `approval-expression-invalid` (422 `issues[].hint`: an empty expression 216 → 90, a `record` root 292 → 144; a parse failure 129, a bare root 110 and the `resolveAs` advisory 102 unchanged) and `approval-expression-no-empty-policy` (2xx advisory, 294 → 121); + - a `dataset` write: `dataset-include-unknown` (422, an entry naming nothing / an ordinary field 239 / 187 → 93 / 93) and `dataset-field-unknown` (422, a dimension / a measure 184 / 182 → 132 / 130); + - an `object` write: `object-field-ref-unknown` (422, 180 → 128; the same text is the `fix` of the publish receipt's `object_field_ref_unknown` runtime probe issue), and on its `listViews`, `list-view-field-unknown` (422, 110 → 57) and `list-view-field-dotted` (422, 231 → 157); + - a `view` write: `list-view-field-unknown` (422, 110 → 57) and `list-view-field-dotted` (422, a projection / a filter 231 / 143 → 157 / 95); + - a `report` write: `chart-measure-unknown` (422 at the query position, 117 → 106; 2xx advisory at a presentation position, 301 → 113). + + Studio's save-advisory toast prints an advisory's `hint` after its message, so the three approval advisories and the chart presentation advisory shorten there; Studio renders no 422 issue's `hint`. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead. + - **Never at the runtime gate:** `title-format-retired`, `title-unresolvable`, `translation-target-unknown`, both seed ids, `view-container-shape`, `ai-skill-surface-mismatch` and `page-source-className-tailwind` run on the CLI only. + - **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses. +- b4d573a: fix(spec,core): the rolling `last_7_days`, `last_30_days` and `last_90_days` presets close on today, at the spec's prescription table, the save door's refusal and the analytics preset arm + + Clause-②: no (no accept set widens: the three rolling windows gain the inclusive `{today}` end every calendar preset already has) + + The three rolling windows were the open arm of `DATE_RANGE_PRESET_MACRO_WINDOWS`: `['{30_days_ago}', null]`, prescribed as `{ $gte: '{30_days_ago}' }`. A `$gte`-only window has no upper bound, so on a column that carries future dates (a close date, a due date) "last 30 days" also counted every later day. Measured on a `date` field with one row dated today−5 and one dated today+5: that window counted 2, while the console's preset lowering and the analytics preset arm counted 1. + + - `DATE_RANGE_PRESET_MACRO_WINDOWS` now closes every rolling window on `{today}`: `last_7_days` is `['{7_days_ago}', '{today}']`, and `last_30_days` and `last_90_days` follow the same shape. That is the inclusive last-day end every calendar preset already uses, and the extent the console's lowering already selects. The table's declared type narrows to match, `readonly [start: string, end: string]` (it was `end: string | null`). A reader that compared `end` with `null` still compiles; that branch can no longer be taken. + - The save door's refusal of a bare preset comparand (`bareDateRangePresetComparandMessage`, and the `filter-preset-comparand` lint verdict that quotes it) now prescribes the closed window. For `last_30_days` it says: write `{ $between: ['{30_days_ago}', '{today}'] }`, where it used to say `{ $gte: '{30_days_ago}' }`. + - `@objectstack/core`'s `resolveAnalyticsDateRangePreset`, the preset arm behind `timeDimensions[].dateRange`, selects the same extent. A rolling window now stops before tomorrow begins in the reference timezone, exactly as `today` does, with `endExclusive: true`. It used to end at the reference instant, so it dropped rows stamped later today. Its ends keep this module's own "the day the window stops before" spelling (`tomorrow`). Every preset this resolver returns is now `endExclusive: true`, and a caller's explicit `[a, b]` window keeps its inclusive reading, as before. + - The semantic migration entry `filter-preset-ordering-comparand-refused`, which `spec-changes.json` and `os migrate meta` carry, now names `{ $between: ["{30_days_ago}", "{today}"] }` as the window for `last_30_days` in its replacement, where it named `{ $gte: "{30_days_ago}" }`. + + If you followed the old refusal and authored `{ $gte: '{N_days_ago}' }` for a rolling preset, add `$lte: '{today}'`, or write the `$between` pair above, to stop counting future-dated rows. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + - @objectstack/sdui-parser@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 1ed66cee1b5..c6b3d9884e7 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Static, build-time validation for an ObjectStack metadata graph — dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.", "type": "module", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index e8967b44be9..814c1bbebab 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/plugin-mcp-server +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 03c6496b01c..f367b159a47 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack as an MCP server — exposes your app's objects (and AI tools) over the Model Context Protocol (stdio + Streamable HTTP)", "type": "module", diff --git a/packages/metadata-core/CHANGELOG.md b/packages/metadata-core/CHANGELOG.md index fbf7018f3ce..335354ddb7a 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/metadata-core +## 18.0.0-next.1 + +### Minor Changes + +- 430e5f5: fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs. + + **Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not. + + **FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner): + + - `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`). + - Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items. + - `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`. + - `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched. + - `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read. + - Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence. + - The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator. + + **Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before. + + **The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index c9f73858497..d5a9c832a38 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Metadata Repository contracts: types, canonicalization, errors, interface (ADR-0008).", "type": "module", diff --git a/packages/metadata-fs/CHANGELOG.md b/packages/metadata-fs/CHANGELOG.md index bcf30d823cf..2c19b03007d 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/metadata-fs +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [430e5f5] + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 7463a113b62..1d1c891cc95 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).", "type": "module", diff --git a/packages/metadata-protocol/CHANGELOG.md b/packages/metadata-protocol/CHANGELOG.md index fbfe76e57cc..613b2486625 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,331 @@ # @objectstack/metadata-protocol +## 18.0.0-next.1 + +### Minor Changes + +- 5f37d36: feat(spec)!: at most one `isDefault: true` record page per object per deployment — a second is refused at `defineStack` / `os validate`, at package registration and at metadata save, naming both pages (#23015) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + `PageSchema.isDefault` now states the rule (maintainer ruling A on #23015): at most one `isDefault: true` record page may exist for an object in a deployment. The record view opens that page with no tie-break, so with two defaults for one object the page that opened used to be whichever plugin initialised first — an accident no author wrote. A record page is `type: 'record'` (the default when `type` is left out) bound to an `object`; `isDefault` on any other page is outside the rule. + + **What is refused, and where.** Every door that can add a second default refuses it, naming both pages: + + - **`defineStack`** (and so `objectstack validate`, `objectstack build` and a source boot) — two default record pages for one object inside one stack, `STACK_CROSS_REFERENCE_INVALID` (422), one finding per object naming the object, every page and its position (`stack.pages[0]`). **`composeStacks`** refuses the pair across the stacks it composes, in the same envelope; under `manifest: 'preserve'` one page name in two stacks is two packages' pages. + - **Package registration** (`@objectstack/objectql`'s `SchemaRegistry`, at `installPackage` ahead of every mutation and at the `registerItem` seam) — a package whose default record page meets another package's default for the same object, an environment's stored default, or a second default in its own manifest, `DEFAULT_RECORD_PAGE_CONFLICT` (422, exported as `DEFAULT_RECORD_PAGE_CONFLICT_CODE`). Two packages' pages under one name are two pages and are refused too. `OS_METADATA_COLLISION=warn` does not soften it. + - **Metadata save** (`@objectstack/metadata-protocol`'s `saveMetaItem`, so `PUT /api/v1/meta/page/:name`) — a page saved as the default record page of an object a stored page or a packaged page already holds, `DEFAULT_RECORD_PAGE_CONFLICT` (422), in draft and publish mode, before anything is written. + + **Unchanged.** A save under the existing default's own name is that page (an ADR-0005 overlay by name), not a second default. One default per object, a second record page that is not a default, and two objects with one default each are accepted. The same package registering again — a reload, or an upgrade that moves its default to another of its pages — is one holder. A save under the name of a page a package ships is still answered by the packaged-base refusal (`NOT_OVERRIDABLE`, 403), as before. A stored page reaching the registry from `sys_metadata` (hydration, write-through) is not refused again. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `pages: [{ name: 'account_a', type: 'record', object: 'crm_account', isDefault: true }, { name: 'account_b', type: 'record', object: 'crm_account', isDefault: true }]` accepted; the boot kept both and the record view opened whichever registered first | refused, `STACK_CROSS_REFERENCE_INVALID` 422 — set `isDefault: false` on all but one | + | `composeStacks` | two stacks each shipping a default record page for one object composed clean | refused in the cross-reference envelope, naming both stacks' pages | + | package registration | a second package (or a plugin's `manifest.register`) shipping a default record page for an object another package holds registered; init order decided the page | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; keep one default, or compose the deployment without the package that ships the other | + | metadata save | `PUT /api/v1/meta/page/my_page` with `isDefault: true` for an object that already has a default record page stored or shipped saved | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; save it with `isDefault: false`, or first demote the stored default | + + **The one-line fix: keep `isDefault: true` on the one record page per object that should open the record view, and set `isDefault: false` on every other.** To replace a page a package ships, compose the deployment without that package's page; shipping a second default is not an override path. + + No D2 conversion rewrites it: which page stays the default is the author's call. Its D3 record is the semantic entry `page-default-record-page-one-per-object-refused` (protocol 18). + + **`@objectstack/spec`.** `defaultRecordPageObject(page)` (exported from `@objectstack/spec/ui`) is the one reading of "which object is this page the default record page of" that the three doors share. `PageSchema.isDefault` gains its `.describe()`. `DEFAULT_RECORD_PAGE_CONFLICT` joins the error-code ledger under `@objectstack/objectql` and `@objectstack/metadata-protocol`. + + **`@objectstack/objectql`.** The registration refusal, `SchemaRegistry.defaultRecordPageHoldersOf(object)` (every page holding the object's default — the reading the save door meets with the stored pages) and the `DEFAULT_RECORD_PAGE_CONFLICT_CODE` constant. + + **`@objectstack/metadata-protocol`.** The save-door refusal. + + **`@objectstack/platform-objects`.** The page form's `isDefault` help text, which said "Set as default page for this page type" — a rule nothing enforced — now states the one-default-per-object rule, in all four locales. + + **Who is affected, measured.** In this repository every object with a default record page has exactly one: `sys_organization` and `sys_user` (`@objectstack/plugin-auth`), `sys_position` (`@objectstack/plugin-security`), `sys_approval_request` (`@objectstack/plugin-approvals`), and `showcase_project`, `showcase_task` and `showcase_preference` (`examples/app-showcase`). A deployment that also composes a package shipping its own default record page for one of those objects is refused at boot until one of the two pages stops being a default. +- 543b33d: fix(metadata-protocol)!: a metadata save or delete that names no package addresses the row of the one package the item belongs to, and a name two or more packages hold is refused `409 RESOURCE_CONFLICT`; the delete door can name its package (ADR-0048, ADR-0070) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A `sys_metadata` row is keyed by its package (ADR-0048), so one item can hold one row per package. A save that names a package (`?package=` on the REST door, `packageId` in process) writes that package's row. A save that named none always wrote the package-unbound row, even for an item a package owns, and a delete named no package at all: it removed whichever row of the item the store returned first. The draft save already resolved this: a draft that names no package is stored in the package of the item's active row. + + **FROM.** Save an item into a package (`PUT /api/v1/meta/app/my_app?package=com.acme.app`), then save it again without `?package=`: the second save answered `200` "Saved app 'my_app' (env-wide, state=active)" and wrote a second, package-less row beside the package's row. The item read kept serving the package's row, so nothing the save carried took effect, and a console "Disable" reported success while the app stayed enabled. The same held for the API, the CLI and agents, and for an overlay row bound to a code package. A `DELETE /api/v1/meta/app/my_app` of an item two packages hold removed one of the two rows, the other was then served, and the receipt said "it no longer exists". Uninstalling a package, or discarding its drafts, deleted each of its items by name the same way, so it could remove another package's row of the same name and leave its own. + + **TO.** A direct save, or a delete, that names no package resolves its address from the item's rows (the active rows for a save and a delete, the draft rows for a draft discard): + + - **Bound to one package.** The save writes that package's row, exactly as a save naming the package does, and the item read serves it next; the delete removes that row. The receipts name the package: "Saved app 'my_app' (env-wide, package=com.acme.app, state=active)" and "Deleted app 'my_app' (package=com.acme.app) — it no longer exists." Every check on the row the save writes reads that package: its stored lock, a stored credential carried forward, the optimistic lock, and the writable-base rule. The item read that names no package serves that row's `version`, so a read followed by a save or a delete with the served `If-Match` is accepted. + - **Bound to two or more packages.** The save or delete is refused `409 RESOURCE_CONFLICT` and changes nothing. The message names the packages and the remedy: name one with `?package=`. + - **Bound to none.** The save writes, and the delete removes, the package-unbound row, as before, and the receipts read as before. + + The delete door gains the address a save has: `DELETE /api/v1/meta/:type/:name?package=` (`packageId` in process, `DeleteMetaItemRequestSchema.packageId`) removes that package's row and no other; `all` and an empty value name no package, as on the save door. A repeated `?package=` is refused `400`. A delete receipt now says when another row of the item is still stored, naming it, instead of claiming the item is gone or back at its artifact default. Uninstalling a package and discarding its drafts name the package, so each removes only its own rows. + + **Also refused now**, each as the same save naming the package is refused: a package-less save of an item held by a read-only package (a code package's runtime-only item, or an installed `system`/`cloud` package's) answers `422 WRITABLE_PACKAGE_REQUIRED`; and a package-less save asserting a create (`If-None-Match: *`) of an item a package holds answers `409 METADATA_CONFLICT`. + + `packageId: null` in process names no package too, as the request contracts state. The server-side rewrites keep the row they name: `os migrate meta --stored --apply` and the stored flow credential move pass `null` for a package-unbound row they rewrite, and that row is still the one written; package duplication names its target package. + + **Unchanged.** A save or a delete that names a package exactly. A draft save, which keeps its own inheritance. An item a code package ships with no stored row: the package-less save still writes the package-unbound overlay, and the read serves it. Package-less rows already stored beside a package's row stay as they are; none is converted. A package-less delete of such an item removes the package's row and its receipt says the package-less row remains. + + **Remedy.** For an item two packages hold, send the save or the delete with `?package=` naming the package whose row it edits or removes. A receipt now names the package when the write addressed a package's row; a client that compares receipt text should expect `, package=` after `env-wide` on a save and ` (package=)` after the item on a delete. + + `@objectstack/spec`: the `SaveMetaItemRequestSchema.packageId` description now states what an `active` save that names no package writes, where it said the env-local overlay; `DeleteMetaItemRequestSchema` declares the optional `packageId`. `@objectstack/rest`: `package` joins the reset door's closed query set (`META_ITEM_RESET_PARAMS`). +- c857a93: feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as `system`, in both directions, with the prescription (ADR-0138 D2b) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0138 D2b decides that no anonymous door triggers an elevated flow directly. An `authRequired: false` endpoint of `type: 'flow'` whose `target` flow declares `runAs: 'system'` ran every anonymous request's data steps as the system principal, past row-level security, with the endpoint's rate limit as the only brake. The runtime's own elevated-start refusal covers only self-triggered flow types (`autolaunched`, `record_change`, `schedule`), so a `system` flow of type `api` or `screen` behind such an endpoint executed elevated. Publish now refuses the pair, in both directions, because either item can be the last one edited: the endpoint's publish reads its target's `runAs`, and a flow's publish that sets `runAs: 'system'` reads the anonymous endpoints that target it. Each refusal names the endpoint and the flow and carries the prescription. + + **What is refused, and where.** Every publish door that holds the pair calls one judge, `anonymousElevatedFlowPairs` in `@objectstack/spec/api`: + + - **The stack parse** (`defineStack`, `objectstack validate`, `objectstack build`, the artifact load): a stack whose anonymous flow endpoint names one of its own flows declaring `runAs: 'system'`. One zod issue per pair, `code: 'custom'` at `apis[i].authRequired`; `defineStack` throws its schema refusal, `STACK_SCHEMA_INVALID` (422), carrying the issue. + - **The package publish** (`MetadataManager.publishPackage`, `POST /api/v1/packages/:id/publish`): an `api` item of the package naming a `system` flow anywhere in the manager's index, reported at the endpoint; and a `flow` item of the package declaring `runAs: 'system'` that an anonymous endpoint outside the package names, reported at the flow. The package publishes nothing (`success: false`, the pair in `validationErrors`), and `validate: false` does not turn it off. + - **A flow published through the metadata API** (`saveMetaItem` in publish mode, so `PUT /api/v1/meta/flow/:name`; a draft's publish, `POST /api/v1/meta/flow/:name/publish`; and `POST /api/v1/packages/:id/publish-drafts`): a flow going `active` with `runAs: 'system'` while a declared anonymous flow endpoint names it. `INVALID_METADATA` (422), `issues[0].path: 'runAs'`, nothing written; a publish-drafts batch aborts on it. A draft save is not judged (a draft does not execute; its publish is), and neither is a `migrate-stored` re-save of a row that already exists. + + **Unchanged.** Each item alone publishes as before: an authenticated endpoint on a `system` flow, an anonymous endpoint on a `user` flow (or one that leaves `runAs` at its default), and an anonymous `object_operation` endpoint are all accepted. A stored flow row that already pairs this way keeps its bytes and keeps executing as published; its next publish is refused. A guest-written row that fires a record-change flow is outside the refusal: the record leaves that indirect path undecided, because its trigger is the write, not the door. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `apis: [{ name: 'acme_intake_api', type: 'flow', target: 'acme_intake', authRequired: false, rateLimit: { enabled: true, windowMs: 60000, maxRequests: 100 }, … }]` beside `flows: [{ name: 'acme_intake', runAs: 'system', … }]` accepted; every anonymous request ran `acme_intake` as the system principal | refused, `STACK_SCHEMA_INVALID` 422, the issue at `apis[0].authRequired` naming both items and the prescription | + | package publish | a package carrying either half of the pair, with the other half anywhere in the manager's index, published | refused, `success: false`, the pair reported at the package's own item | + | metadata API flow publish | `PUT /api/v1/meta/flow/acme_intake` with `runAs: 'system'` while an anonymous endpoint names it saved and armed it | refused, `INVALID_METADATA` 422, the issue at `runAs`, nothing written | + + **The one-line fix: serve the caller another way — an authenticated endpoint (`authRequired: true`) for a partner that can hold a credential, the flow's signed inbound hook (ADR-0041: a start node with `triggerType: 'api'`, a `hookId` and a `secret`, which may keep `runAs: 'system'`) for a partner webhook, or a public form (a form view whose `sharing` sets `allowAnonymous: true`) for anonymous intake.** `runAs: 'automation'` joins that list when ADR-0073 M2 adds the value. + + No D2 conversion rewrites it: which channel replaces the anonymous endpoint depends on who the caller is, and whether the flow needs elevation is a judgment about its data steps, so no conversion can choose a posture for the author. Its D3 record is the semantic entry `api-endpoint-anonymous-elevated-flow-refused` (protocol 18). + + **`@objectstack/spec`.** The judge and its two refusal sentences join `@objectstack/spec/api`: `anonymousElevatedFlowPairs`, `anonymousFlowDoorTarget`, `anonymousEndpointElevatedFlowMessage` (the endpoint direction), `elevatedFlowAnonymousEndpointsMessage` (the flow direction) and `validateAnonymousElevatedFlows` (the stack's issues), with the types `AnonymousFlowDoorCandidate`, `ElevatedFlowCandidate` and `AnonymousElevatedFlowPair`. `ObjectStackDefinitionSchema` runs the judge beside the per-endpoint gates. The `runAs` description on the flow schema, which prescribed `system` for every user-less trigger, `api` included, now names the exception and what serves that caller. + + **`@objectstack/metadata`.** `MetadataManager.publishPackage` runs the judge over the package's `api` and `flow` items against the index the endpoint matcher serves from. + + **`@objectstack/metadata-protocol`.** The flow publish doors run the judge against the declared endpoints: the engine registry's `api` items and the `metadata` service's. + + **Who is affected, measured.** No in-repository stack pairs an anonymous flow endpoint with a `system` flow: `examples/` declares no `authRequired: false` endpoint, and every anonymous flow endpoint in the test fixtures names a `runAs: 'user'` flow. The partner-webhook example, `showcase_inbound_task_webhook`, takes the signed inbound hook with `runAs: 'system'` and is untouched. +- 14a13bc: fix(metadata-protocol)!: the metadata save, restore and publish doors refuse a permission set whose body `packageId` disagrees with the package it is saved into, and the audience-binding suggestion reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the package the set is saved into: the stored row's `package_id`, served as `_packageId`. A body `packageId` that named another package was accepted and then silently overridden, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **FROM.** A permission set saved and published into package `app.crm` with `packageId: 'app.other'` in its body answered `200` and `published`, and was owned by `app.crm` everywhere it was read. A set saved into no package with `packageId: 'app.other'` was stored as an environment-authored set, while the audience-binding suggestion reader offered it, accepted it and derived it into `everyone` as `app.other`'s. + + **TO.** + + - **`@objectstack/metadata-protocol`.** The save door (`PUT /api/v1/meta/permission/:name`, draft and publish mode, and every in-process caller) the restore doors (`POST /api/v1/meta/permission/:name/rollback` and a commit revert's restore limb) and the draft promotion (`POST /api/v1/meta/permission/:name/publish` and the package publish) refuse a permission set whose body `packageId` is not the package the row is written into, with `400 VALIDATION_ERROR`, the code and status the doors already answer for a body `name` that disagrees with its row. `issues[]` carries the key at `packageId`. A body `packageId` on a set saved into no package is refused too, the empty string included: a present key declares a package-shipped set. The message names both values and both remedies. The package judged is the one the write lands in: a draft saved without naming a package lands in the package of the item's active row, and is judged against it. A package publish that meets such a draft (one stored before this change) aborts the whole batch, `outcome: 'refused'`, with the draft in `failed[]` under `VALIDATION_ERROR`. A rollback to a stored version that carries such a body (one written before this change) is refused, and a commit revert reports it in `failed[]` under the same code; the remedy there is to save the set again with `packageId` dropped or set to its package, instead of restoring that version. Nothing is written, published or restored. Package duplication copies a set whose body names the source package as naming the target package. `os migrate meta --stored --apply` records a stored set it rewrites, and the door refuses, as that row's failure. + - **`@objectstack/plugin-security`.** The audience-binding suggestion reader takes a set's owner from its binding only, never from its body's `packageId`. A set bound to no package has no owner there, so no suggestion is created for it. An acceptance recorded under the package its body named is withheld from `everyone` at the next derivation. The withholding is reported at `warn`, naming the set and its reason. Accepting a pending suggestion of that kind is refused. + - **`@objectstack/spec`.** The `PermissionSetSchema.packageId` description states the refusal. The step-18 semantic migration entry `permission-set-package-id-binding-mismatch-refused` carries the upgrade sweep. + + **Unchanged.** A body `packageId` equal to the package the set is saved into is accepted, and an absent one is accepted everywhere. Stored rows are not rewritten. A stored set whose body disagrees with its binding keeps loading and keeps reading as its binding, and is refused on its next save or publish, as is a restore of a stored version that carries one. An installed package's own sets are owned by the package that ships them, as before. + + **Remedy, in one line.** Drop `packageId` from the permission set's body, or save the set into the package it names (`?package=` on the REST door, `packageId` in process). +- 572ecfb: fix: `os migrate meta --stored` lists every stored row that protocol 18 refuses and no conversion rewrites. Before, the preview counted each such row as already on protocol. + + Clause-②: yes (widening: `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec`, `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` and `StoredFlowCanonicalization.todos?` in `@objectstack/metadata-protocol`, each a new output member; nothing narrows, and no door accepts or refuses anything it did not before) + + Three step-18 retirements refuse a stored shape that has no lossless rewrite. Upgrading a v17 database left those rows refused at load or at boot, and the stored preview read them as `canonical`. Each shape now has exactly one fate: **refused and listed**. The preview and `--apply` print one `TODO` line per site under its row. The line names the step-18 entry that refuses the shape, the site, and the choice the owner has to make. Nothing is rewritten, and the run's verdict does not change, because no run of the pass could clear these rows. + + **#23269, a flow value slot carrying a `{…}` token, including a `get_record` / `update_record` / `delete_record` filter operand: refuse and list.** A whole-path token maps mechanically to `{ dialect: 'cel', source: '' }`, but the rewrite is not lossless. A token the template resolved to nothing (an erased condition, so the node was refused) is a CEL fault on an absent root, and a flow variable named like a query placeholder used to shadow it. ADR-0087 D2 conversions run on the load path and must be lossless, so this one is not a D2. The chain also has no operator-applied rewrite of stored rows separate from the load path: `--stored` writes back the read path's own pass, and `--from 17` rewrites authored sources, not rows. So the flow is listed by flow, node and path (`flow-value-slot-template-dialect-refused`), and `registerFlow` goes on refusing it until the value is rewritten as its CEL envelope. A flow already in the CEL form, or using a query placeholder such as `{current_user_id}`, is not listed. + + **#23270, a `controlled_by_parent` object without exactly one required `master_detail`: refuse and list, by object (`cbp-master-relation-tiers-retired`).** No shape here converts losslessly. The upgrade never picks between two masters, either both required or neither. An explicit `required: false` is a contradiction the builder refuses. Turning a required `lookup` into a `master_detail` changes what deleting the master does. An object with no master has nothing to convert. Leaving `controlled_by_parent` changes who can read the rows, so that is the owner's choice and never a silent rewrite. The listing reads the same predicate `ObjectSchema` refuses with, so the preview and the parse refuse one set. The one lossless shape, a single `master_detail` with `required` omitted, is still converted by `cbp-master-detail-required-defaulted` and is not listed. + + **#23271, a view whose `data` binds the retired view-level API source: refuse and list, by view (`view-data-api-provider-retired`).** There is no mechanical move. The replacement is a page component's `dataSource`, so a page must exist to carry it. Deleting the view's `data` would make the view query its object instead. Views in every stored body spelling are covered, and so are an object's own `listViews`. The line shows the request URL and method and withholds every other request key, so a credential stored in a view's headers never reaches the report. A view on the `object`, `value` or `schema` provider is not listed. + + How it is wired: `applyConversions` reports these shapes through its existing `onTodo` sink, after the conversions run. Each TODO's `conversionId` is the id of the D3 entry that refuses the shape, so the published `ConversionTodoNotice.conversionId` now names either a conversion or a D3 entry. The published flow-canonicalization result gains one member, a widening in three packages: + + - `@objectstack/spec`: the contract `IAutomationService.canonicalizeStoredFlow(...)` declares an optional `todos?: ConversionTodoNotice[]` beside `notices` and `conflicts`. An implementation that reports none still conforms. + - `@objectstack/service-automation`: `AutomationEngine.canonicalizeStoredFlow(...)` returns `todos`. `registerFlow` ignores them, because its own refusal names the same sites. + - `@objectstack/metadata-protocol`: `StoredFlowCanonicalization` gains the matching optional `todos?`, and the stored pass lists a flow row's TODOs the way it lists any other row's. + + The authoring funnel and the read seams pass no sink, so nothing changes for them. +- 2de9c03: fix(metadata-protocol)!: a delete naming one package keeps the item's other rows registered, a draft save that names no package of an item two or more packages hold is refused `409 RESOURCE_CONFLICT`, and the delete's `_lock` refusal carries the named package's reason (ADR-0048) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A `sys_metadata` row is keyed by its package (ADR-0048), so one item can hold one row per package. Since the save and the delete name their package (`?package=` on the REST door, `packageId` in process), three paths still did not take the package they were handed. + + **The registry after a delete naming one package.** FROM: `DELETE /api/v1/meta/object/my_object?package=com.acme.a` of an object two packages hold removed `com.acme.a`'s row and the receipt said the other package's row remains, and the item read served that row, but the registry entry was retired as if the deleted row were the whole item. Data CRUD then answered `404 OBJECT_NOT_FOUND` for the object until the next boot or save, and on a kernel whose registry holds the stored rows, registry lookups missed an item of any other type the same way. TO: when a row of the item is still stored after a delete, that row is registered again, as a boot registers it, and nothing is retired. Deleting the last row still retires the item. + + **A draft save that names no package.** FROM: `PUT /api/v1/meta/view/my_view?mode=draft` with no `?package=`, of an item whose active rows two packages hold, answered `200` "(env-wide, state=draft)" and stored the draft in whichever package the database returned first. With one package and a package-less row beside it, the draft went into the package or stayed package-less by the same order. TO: the draft inherits the package of the item's active rows when they are bound to one package, whatever the storage order (a package-less row beside it is not a second package). When they are bound to two or more, the save is refused `409 RESOURCE_CONFLICT`, writes nothing, and names the packages with the `?package=` remedy, exactly as the direct save and the delete answer. A draft read that names no package serves the package-unbound draft's version for such an item. + + **The delete's `_lock` refusal.** The delete's lock gate now takes the package the delete names, as the save's does. The verdict is unchanged: the lock is the strictest of every package that ships or stores the item, so a delete naming a package is refused whenever the save naming it is. What changes is the prose: when several packages declare the strictest lock, the `ITEM_LOCKED` refusal of a delete naming a package carries that package's reason, where it carried the reason of the package whose id sorts first. + + **Remedy.** For an item two or more packages hold, send the draft save with `?package=` naming the package whose draft it is (`packageId` in process). Nothing else asks anything of a caller. +- d89f491: A stored flow the automation engine refuses is no longer served as running: the flow meta read serves the engine's verdict beside the item as `registration`, an envelope sibling of `lock`, and the flow list serves it as `registrations` keyed by flow name, so a refused flow reads `{ state: 'refused', reason: { code?, message } }` beside its stored `status`, which stays as authored. + + Clause-②: yes (widening) + + - **The shape** (`@objectstack/spec`). `FlowRegistrationSchema` is a closed set: `registered` (the engine holds the flow), `refused` (it refused the stored flow and holds nothing under the name; `reason` carries the engine's own `message` and, when the thrown error set one, its `code`, verbatim), and `unregistered` (it holds nothing and refused nothing: a read during boot, between a save and the engine following it, or after a deliberate unregister). `GetMetaItemResponseSchema` gains `registration` and `GetMetaItemsResponseSchema` gains `registrations`, both optional. New exports: `FlowRegistrationSchema`, `FlowRegistrationRefusalSchema` and the types `FlowRegistration` and `FlowRegistrationRefusal`. `IAutomationService` gains the optional member `getFlowRegistration(name)`. + - **Where the verdict comes from** (`@objectstack/service-automation`). The `kernel:ready` cold-boot bind records each flow it refuses, with what the engine threw, after it withdraws the flow; the `metadata:reloaded` re-sync and the metadata-change sync record a refusal that leaves the name unregistered. A later successful registration of the name, or its withdrawal, ends the record. `AutomationEngine` answers `getFlowRegistration(name)` and takes `recordFlowRefusal(name, error)`. The refusal's warn line, its level and the withdrawal are unchanged. + - **Where it is served** (`@objectstack/metadata-protocol`). The protocol reads the engine through the kernel's service table by name, as it already reads the engine's stored-flow canonicalizer; it does not import the automation service. The field is absent where no automation service declares the member, on every type other than `flow`, on a draft read and on the execution face the engine binds from: absence means no engine judged the flow, never `registered`. The verdict is the engine of the process that answered the read. + - **The default door** (`@objectstack/rest`, `@objectstack/metadata-protocol`, `@objectstack/spec`). `GET /api/v1/meta/flow/:name` on a default deployment (`metadata.enableCache: true`) is answered by the cached item branch, which now carries `registration` too. `getMetaItemCached` returns it beside `data`, read live from its delegate read on every call, and `GetMetaItemCachedResponseSchema` declares it. It is never part of the ETag, so a cached read that carries it is never answered `304 Not Modified`: a judged flow is always served in full. The cached branch's other envelope keys are unchanged. + +### Patch Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- c6139f2: Resetting a published overlay of a view container now stops serving the views it expanded to, without a restart + + Clause-②: no + + - A view container (a `defineView` document, such as a package's `showcase_task` views) expands to one view per entry, registered under `.` (`showcase_task.default`, `showcase_task.in_progress`, …). A published overlay of the container registers its own expanded views under those names in the process registry. + - `DELETE /api/v1/meta/view/NAME` on such an overlay answered `reset: true`, but its registry heal restored only the container's own name. `GET /api/v1/meta/view` and `GET /api/v1/meta/view?object=OBJECT` kept listing the overlay's expanded views next to the packaged ones until the process restarted, and the end-user list kept rendering the overlay's columns and label. The by-name read of an expanded view served the overlay's copy too wherever no MetadataService answered first. + - The heal now also heals every view name the container registered when it was published, through the same steps as the container's own name. A packaged view of that name is served again, a view only the overlay added is removed, and a later publish of the container is served as before. + - Unchanged: an organization-scoped delete still touches no registry entry, re-registration still happens on a control-plane kernel only, and a view name another package ships, which the publish never registered, is never touched by the reset. Dashboards and every other type reset as before. + - No public export, signature, schema or accept-set change. The built entry declarations gain three `private static` member names on `ObjectStackProtocolImplementation`. +- 78c13bd: Republishing a view container without one of its views now stops serving that view, without a restart + + Clause-②: no + + - A view container (a `defineView` document, such as a package's `showcase_task` views) expands to one view per entry, registered in the process registry under `.`. A publish of the container registered the views it expanded to and removed none it no longer expanded to. So after `POST /api/v1/meta/view/NAME/publish` of a container whose `listViews` lost an entry, `GET /api/v1/meta/view`, `GET /api/v1/meta/view?object=OBJECT` and the by-name read of that view kept serving it until the process restarted. + - Every write that changes a view container's stored row now leaves the registry holding the views that row expands to: a publish, a save that publishes, a rollback, a revert, a replica's convergence, a reset and a delete. So does the list read's and the boot's hydration of a stored row. The write answers after this, so a read that follows it serves the published set. + - A view the container no longer expands to is healed the way a reset heals it: where a package ships a view of that name, the package's view is served again, and a name nothing else serves is removed. As for a reset, a name whose MetadataService read fails stays registered until a restart. + - When one container name has two stored rows (one bound to no package, one bound to a package), each row is compared with what that row registered before. A republish of one row removes only the views that row dropped, and the list read leaves both rows' views registered however often it runs. A view both rows expand to, which one row drops, is served from the other row's stored row and registered again by the next list read. A reset or delete of the name heals every row's views, as before. + - Unchanged: an organization-scoped write touches no registry entry, a baseline is re-registered on a control-plane kernel only, and a view name another package ships is never registered or healed for the container. Every other type registers and resets as before. + - No public export, signature, schema or accept-set change. In the built entry declarations, `ObjectStackProtocolImplementation` gains one untyped `private` member name and loses two `private static` ones. +- 9543e9c: Adopting loose items into a base no longer moves an overlay of a packaged item, and the views it does move are served from the base at once + + Clause-②: no + + - `POST /api/v1/packages/ID/adopt-orphans` rebound every metadata row bound to no package into the base. That included a row that overlays an item a package ships, such as a list view edited and saved from the record list, or an edited copy of a package's view container. Once bound to the base, such a row stopped customising the packaged item. After a restart, an adopted overlay of a package's view container added views under its own name, and the package's own view was served again. An adopted overlay of one packaged view was listed twice under its name. + - ADR-0070 keeps an overlay of a packaged item bound to the item it customises, so the adoption now leaves such a row as it is stored. A row is skipped exactly when a save of it would be an overlay (`override-artifact`). An environment whose only rows bound to no package are such overlays now answers `reassignedCount: 0` and `success: false`. + - Each view row the adoption does rebind is now written through to the process registry with the binding it left. So `GET /api/v1/meta/view`, `GET /api/v1/meta/view?object=OBJECT` and the by-name read serve it from the base right away, not after the next restart. + - A view container's views are reconciled the way a publish reconciles them. What the row registered before the move counts as its own, so a later write of a row of that name bound to no package no longer removes the moved row's views. + - Unchanged: the request and the response shape (`success`, `reassignedCount`, `reassigned`, `targetPackageId`). Also unchanged: rows already bound to a package, organization-scoped rows, and rows of other types, which are rebound in storage only. + - There is no public export, signature or schema change. +- 5b68524: fix(metadata-protocol): an environment kernel that hydrates its own `sys_metadata` writes a published body through to its engine registry, so a narrowed permission set stops granting its old body without a restart + + Clause-②: no + + - **What was wrong.** The protocol's registry write-through skipped every type except `object` whenever the protocol had an `environmentId`. A kernel that hydrates its own store at boot (the standalone stack's `env_local`, or a hosted kernel with `hydrateMetadataFromDb`) put every stored body into its registry at boot and then never updated it. A save was accepted and served by `GET /api/v1/meta/:type/:name`, but the engine registry kept the boot-time body until a restart, and held nothing for an item created after boot. The security catalog reads that registry first, so a permission set narrowed through `PUT /api/v1/meta/permission/:name` kept resolving its earlier, wider body, and a permission set published after boot was not in the catalog at all: a user granted it was refused, and an `isDefault` set recorded no `everyone` suggestion. + - **What changed.** The write-through now asks whose registry it is, not whether the kernel has an environment. It registers the published body wherever the registry takes this kernel's stored rows: on an unscoped kernel, as before, and on an environment kernel once the boot has hydrated that kernel's own store into it (`loadMetaFromDb`). The delete's heal re-registers a MetadataService baseline on the same kernels, so a save followed by a reset does not leave the deleted body in the registry. + - **The owner.** The registry entry of a stored row now carries the row's package binding as `_packageId` on the boot and on the write-through, as a list read already stamped it, so a runtime package's `isDefault` set published through the metadata door is owned in the security catalog at once and records its `everyone` suggestion, on a host kernel and on a hydrating environment kernel alike; a row bound to no package stays unowned, and an artifact's own envelope still wins. + - **Unchanged.** An environment kernel whose store is never hydrated into its registry still writes nothing into it, so a registry shared with unscoped callers never receives a project-scoped row. The `object` branch and the organization-scope refusal are unchanged. What the registry holds after a save is what it holds after a restart. +- eeb95f7: fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door: + + - **The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it. + - **Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key. + + The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted. + + **Readers.** `@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report. + + **Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` / `objectstack build` | `defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm` | refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 | + | `ObjectQL.registerApp` | the same manifest registered the set under `com.acme.crm`, silently | `400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed | + | an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key | + + **Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [9fbabd7] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/lint@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + - @objectstack/sdui-parser@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 1998936961d..c8fa986b2bb 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack metadata management protocol: sys_metadata CRUD, draft/publish, locks, package ownership, diagnostics (ADR-0076).", "type": "module", diff --git a/packages/metadata/CHANGELOG.md b/packages/metadata/CHANGELOG.md index b587fe86711..09f20341506 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,90 @@ # @objectstack/metadata +## 18.0.0-next.1 + +### Minor Changes + +- c857a93: feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as `system`, in both directions, with the prescription (ADR-0138 D2b) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0138 D2b decides that no anonymous door triggers an elevated flow directly. An `authRequired: false` endpoint of `type: 'flow'` whose `target` flow declares `runAs: 'system'` ran every anonymous request's data steps as the system principal, past row-level security, with the endpoint's rate limit as the only brake. The runtime's own elevated-start refusal covers only self-triggered flow types (`autolaunched`, `record_change`, `schedule`), so a `system` flow of type `api` or `screen` behind such an endpoint executed elevated. Publish now refuses the pair, in both directions, because either item can be the last one edited: the endpoint's publish reads its target's `runAs`, and a flow's publish that sets `runAs: 'system'` reads the anonymous endpoints that target it. Each refusal names the endpoint and the flow and carries the prescription. + + **What is refused, and where.** Every publish door that holds the pair calls one judge, `anonymousElevatedFlowPairs` in `@objectstack/spec/api`: + + - **The stack parse** (`defineStack`, `objectstack validate`, `objectstack build`, the artifact load): a stack whose anonymous flow endpoint names one of its own flows declaring `runAs: 'system'`. One zod issue per pair, `code: 'custom'` at `apis[i].authRequired`; `defineStack` throws its schema refusal, `STACK_SCHEMA_INVALID` (422), carrying the issue. + - **The package publish** (`MetadataManager.publishPackage`, `POST /api/v1/packages/:id/publish`): an `api` item of the package naming a `system` flow anywhere in the manager's index, reported at the endpoint; and a `flow` item of the package declaring `runAs: 'system'` that an anonymous endpoint outside the package names, reported at the flow. The package publishes nothing (`success: false`, the pair in `validationErrors`), and `validate: false` does not turn it off. + - **A flow published through the metadata API** (`saveMetaItem` in publish mode, so `PUT /api/v1/meta/flow/:name`; a draft's publish, `POST /api/v1/meta/flow/:name/publish`; and `POST /api/v1/packages/:id/publish-drafts`): a flow going `active` with `runAs: 'system'` while a declared anonymous flow endpoint names it. `INVALID_METADATA` (422), `issues[0].path: 'runAs'`, nothing written; a publish-drafts batch aborts on it. A draft save is not judged (a draft does not execute; its publish is), and neither is a `migrate-stored` re-save of a row that already exists. + + **Unchanged.** Each item alone publishes as before: an authenticated endpoint on a `system` flow, an anonymous endpoint on a `user` flow (or one that leaves `runAs` at its default), and an anonymous `object_operation` endpoint are all accepted. A stored flow row that already pairs this way keeps its bytes and keeps executing as published; its next publish is refused. A guest-written row that fires a record-change flow is outside the refusal: the record leaves that indirect path undecided, because its trigger is the write, not the door. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `apis: [{ name: 'acme_intake_api', type: 'flow', target: 'acme_intake', authRequired: false, rateLimit: { enabled: true, windowMs: 60000, maxRequests: 100 }, … }]` beside `flows: [{ name: 'acme_intake', runAs: 'system', … }]` accepted; every anonymous request ran `acme_intake` as the system principal | refused, `STACK_SCHEMA_INVALID` 422, the issue at `apis[0].authRequired` naming both items and the prescription | + | package publish | a package carrying either half of the pair, with the other half anywhere in the manager's index, published | refused, `success: false`, the pair reported at the package's own item | + | metadata API flow publish | `PUT /api/v1/meta/flow/acme_intake` with `runAs: 'system'` while an anonymous endpoint names it saved and armed it | refused, `INVALID_METADATA` 422, the issue at `runAs`, nothing written | + + **The one-line fix: serve the caller another way — an authenticated endpoint (`authRequired: true`) for a partner that can hold a credential, the flow's signed inbound hook (ADR-0041: a start node with `triggerType: 'api'`, a `hookId` and a `secret`, which may keep `runAs: 'system'`) for a partner webhook, or a public form (a form view whose `sharing` sets `allowAnonymous: true`) for anonymous intake.** `runAs: 'automation'` joins that list when ADR-0073 M2 adds the value. + + No D2 conversion rewrites it: which channel replaces the anonymous endpoint depends on who the caller is, and whether the flow needs elevation is a judgment about its data steps, so no conversion can choose a posture for the author. Its D3 record is the semantic entry `api-endpoint-anonymous-elevated-flow-refused` (protocol 18). + + **`@objectstack/spec`.** The judge and its two refusal sentences join `@objectstack/spec/api`: `anonymousElevatedFlowPairs`, `anonymousFlowDoorTarget`, `anonymousEndpointElevatedFlowMessage` (the endpoint direction), `elevatedFlowAnonymousEndpointsMessage` (the flow direction) and `validateAnonymousElevatedFlows` (the stack's issues), with the types `AnonymousFlowDoorCandidate`, `ElevatedFlowCandidate` and `AnonymousElevatedFlowPair`. `ObjectStackDefinitionSchema` runs the judge beside the per-endpoint gates. The `runAs` description on the flow schema, which prescribed `system` for every user-less trigger, `api` included, now names the exception and what serves that caller. + + **`@objectstack/metadata`.** `MetadataManager.publishPackage` runs the judge over the package's `api` and `flow` items against the index the endpoint matcher serves from. + + **`@objectstack/metadata-protocol`.** The flow publish doors run the judge against the declared endpoints: the engine registry's `api` items and the `metadata` service's. + + **Who is affected, measured.** No in-repository stack pairs an anonymous flow endpoint with a `system` flow: `examples/` declares no `authRequired: false` endpoint, and every anonymous flow endpoint in the test fixtures names a `runAs: 'user'` flow. The partner-webhook example, `showcase_inbound_task_webhook`, takes the signed inbound hook with `runAs: 'system'` and is untouched. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/metadata-fs@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index e2ca6a5dd26..3f74df5bd4e 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Metadata loading, saving, and persistence for ObjectStack", "type": "module", diff --git a/packages/objectql/CHANGELOG.md b/packages/objectql/CHANGELOG.md index 45f729dacf6..d8b1520b892 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,232 @@ # @objectstack/objectql +## 18.0.0-next.1 + +### Minor Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 87e902d: feat(objectql): the engine composes a `ReadableIds` leaf at its executor boundary, on the final query immediately before the driver call, and refuses it by name wherever it cannot (ADR-0139 D2, D5) + + Clause-②: no + + - **Composed.** On `find`, `findOne`, `count` and `aggregate`, after the middleware chain and the `beforeFind` hooks, every `{ FIELD: { $in: ReadableIds.of(P) } }` in `where` is replaced by `ReadableIds.compose(leaf, scope)`, where `scope` is the registered security service's `getReadFilter(P, caller)`, marked `'policy'` (`undefined` composes with no row restriction). The comparand becomes `$in: []`, which matches no row, when: + - `canReadObject(P, caller)` is false or throws (a service without `canReadObject` is asked `explain`, and only `allowed: true` admits); + - `getReadFilter` answers `RLS_DENY_FILTER` (by identity) or throws; + - no security service is registered, or the registered one cannot answer both halves (the engine reads the kernel's `security` service through `ObjectQLPlugin` when a leaf is composed, so an engine used without that plugin has none); + - the registry holds no object `P`. + + Each parent object is asked once per read, and nothing is kept across reads. A leaf that a middleware or a `beforeFind` hook writes is composed the same way. + - **Refused, `INTERNAL_ERROR` / 500, before any driver is asked:** + - on `update` and `delete`: the caller's leaf at the comparand door, a middleware's at the executor's entry, before the prior-row reads; + - in `having` and in an aggregation's own `filter`; + - anywhere but a `$in` comparand, including a leaf the array form of `where` lowers into another position; + - a composed leaf the engine did not compose, written by a middleware or a hook (the caller's own, as a `$in` comparand, stays the comparand door's `INVALID_FILTER` / 400); + - ADR-0139 D5's three classes: a delegated (on-behalf-of) caller, a parent with an object-scoped read middleware, and a parent bound to another datasource than the object read. + - **Before:** the comparand door refused every `ReadableIds` leaf `INTERNAL_ERROR` / 500 ("this engine does not compose that leaf yet"), and a leaf that a middleware or a hook wrote reached the driver unrefused. + - **`@objectstack/core`:** `resolveFilterTokens` returns a `ReadableIds` leaf by reference. When the filter also held a placeholder, it used to rebuild the leaf as a plain `{ object }` and drop its brand. + - **Not yet:** no driver evaluates the composed leaf (ADR-0139 E3 and E4), and no code in the platform writes a `ReadableIds` leaf today. +- 5f37d36: feat(spec)!: at most one `isDefault: true` record page per object per deployment — a second is refused at `defineStack` / `os validate`, at package registration and at metadata save, naming both pages (#23015) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + `PageSchema.isDefault` now states the rule (maintainer ruling A on #23015): at most one `isDefault: true` record page may exist for an object in a deployment. The record view opens that page with no tie-break, so with two defaults for one object the page that opened used to be whichever plugin initialised first — an accident no author wrote. A record page is `type: 'record'` (the default when `type` is left out) bound to an `object`; `isDefault` on any other page is outside the rule. + + **What is refused, and where.** Every door that can add a second default refuses it, naming both pages: + + - **`defineStack`** (and so `objectstack validate`, `objectstack build` and a source boot) — two default record pages for one object inside one stack, `STACK_CROSS_REFERENCE_INVALID` (422), one finding per object naming the object, every page and its position (`stack.pages[0]`). **`composeStacks`** refuses the pair across the stacks it composes, in the same envelope; under `manifest: 'preserve'` one page name in two stacks is two packages' pages. + - **Package registration** (`@objectstack/objectql`'s `SchemaRegistry`, at `installPackage` ahead of every mutation and at the `registerItem` seam) — a package whose default record page meets another package's default for the same object, an environment's stored default, or a second default in its own manifest, `DEFAULT_RECORD_PAGE_CONFLICT` (422, exported as `DEFAULT_RECORD_PAGE_CONFLICT_CODE`). Two packages' pages under one name are two pages and are refused too. `OS_METADATA_COLLISION=warn` does not soften it. + - **Metadata save** (`@objectstack/metadata-protocol`'s `saveMetaItem`, so `PUT /api/v1/meta/page/:name`) — a page saved as the default record page of an object a stored page or a packaged page already holds, `DEFAULT_RECORD_PAGE_CONFLICT` (422), in draft and publish mode, before anything is written. + + **Unchanged.** A save under the existing default's own name is that page (an ADR-0005 overlay by name), not a second default. One default per object, a second record page that is not a default, and two objects with one default each are accepted. The same package registering again — a reload, or an upgrade that moves its default to another of its pages — is one holder. A save under the name of a page a package ships is still answered by the packaged-base refusal (`NOT_OVERRIDABLE`, 403), as before. A stored page reaching the registry from `sys_metadata` (hydration, write-through) is not refused again. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `pages: [{ name: 'account_a', type: 'record', object: 'crm_account', isDefault: true }, { name: 'account_b', type: 'record', object: 'crm_account', isDefault: true }]` accepted; the boot kept both and the record view opened whichever registered first | refused, `STACK_CROSS_REFERENCE_INVALID` 422 — set `isDefault: false` on all but one | + | `composeStacks` | two stacks each shipping a default record page for one object composed clean | refused in the cross-reference envelope, naming both stacks' pages | + | package registration | a second package (or a plugin's `manifest.register`) shipping a default record page for an object another package holds registered; init order decided the page | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; keep one default, or compose the deployment without the package that ships the other | + | metadata save | `PUT /api/v1/meta/page/my_page` with `isDefault: true` for an object that already has a default record page stored or shipped saved | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; save it with `isDefault: false`, or first demote the stored default | + + **The one-line fix: keep `isDefault: true` on the one record page per object that should open the record view, and set `isDefault: false` on every other.** To replace a page a package ships, compose the deployment without that package's page; shipping a second default is not an override path. + + No D2 conversion rewrites it: which page stays the default is the author's call. Its D3 record is the semantic entry `page-default-record-page-one-per-object-refused` (protocol 18). + + **`@objectstack/spec`.** `defaultRecordPageObject(page)` (exported from `@objectstack/spec/ui`) is the one reading of "which object is this page the default record page of" that the three doors share. `PageSchema.isDefault` gains its `.describe()`. `DEFAULT_RECORD_PAGE_CONFLICT` joins the error-code ledger under `@objectstack/objectql` and `@objectstack/metadata-protocol`. + + **`@objectstack/objectql`.** The registration refusal, `SchemaRegistry.defaultRecordPageHoldersOf(object)` (every page holding the object's default — the reading the save door meets with the stored pages) and the `DEFAULT_RECORD_PAGE_CONFLICT_CODE` constant. + + **`@objectstack/metadata-protocol`.** The save-door refusal. + + **`@objectstack/platform-objects`.** The page form's `isDefault` help text, which said "Set as default page for this page type" — a rule nothing enforced — now states the one-default-per-object rule, in all four locales. + + **Who is affected, measured.** In this repository every object with a default record page has exactly one: `sys_organization` and `sys_user` (`@objectstack/plugin-auth`), `sys_position` (`@objectstack/plugin-security`), `sys_approval_request` (`@objectstack/plugin-approvals`), and `showcase_project`, `showcase_task` and `showcase_preference` (`examples/app-showcase`). A deployment that also composes a package shipping its own default record page for one of those objects is refused at boot until one of the two pages stops being a default. +- 24e064f: fix(objectql,service-messaging,service-automation,plugin-approvals)!: a notification carries the organization of what it is about, and one no organization owns is refused instead of stored with `organization_id` NULL + + Clause-②: no (narrowing) — notification rows gain the organization they should already carry; what narrows is an emit that names no organization on an install where none is derivable, which the engine now refuses where it used to store the event row with `organization_id` NULL. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + Every notification emitted from a system-context run — a scheduled flow, a record change written by a system context, an approval node — was stored in `sys_notification` with `organization_id` NULL, so reports and cleanup that filter by organization did not see it. Each emit also logged a `[notify] no organization in scope` warning. On an install holding several organizations, the deliveries of the same emit were refused while the flow step reported success, so nothing reached the inbox. + + **Who owns a notification now:** + + - **The run's organization.** A run that carries one emits exactly as before. + - **Else the record it is about.** A `notify` node in a run with no organization reads the organization of the record the notification is about. That is the declared click-through record (`sourceObject` + `sourceId`), else the run's trigger record. It is read from the stored row by id, never from the record snapshot the run carries. + - **Approvals.** Every approval notification carries its request's organization: `approval.requested`, reminders, comments, send-backs, reassignments, escalations and out-of-office notices. + - **Else the owner the engine derives for a system write** (ADR-0131 D9). `sys_notification` is now in the platform-object tenancy ledger, so an emit that names no organization gets the install's sole organization under the `single` posture. + - **The rows below follow the event.** `emit()` stamps the stored event row's organization on its delivery, inbox row and receipt, on both the inline and the outbox path. + + **Refused now:** where no organization is derivable (a walled posture, or a `single` install holding more than one organization), an emit that names none is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`. Nothing is written for it. A `notify` step that hits the refusal fails with `notify failed: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED: …` instead of succeeding. `MessagingService.emit()` rejects with the engine's error for any other producer. + + **What to do when it refuses:** give the notification an owner. Name the record it is about on the `notify` node (`sourceObject` + `sourceId`), declare the scheduled flow's acting organization (`organization` on its start node), or pass `organizationId` to `emit()` from your own code. + + The `[notify] no organization in scope` warning is gone, because the NULL row it predicted is no longer written. Rows already stored with `organization_id` NULL are not rewritten here. The v18 organization-ownership ceremony attributes them from their source record. +- eeb95f7: fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door: + + - **The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it. + - **Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key. + + The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted. + + **Readers.** `@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report. + + **Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` / `objectstack build` | `defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm` | refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 | + | `ObjectQL.registerApp` | the same manifest registered the set under `com.acme.crm`, silently | `400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed | + | an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key | + + **Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again. + +### Patch Changes + +- 37510aa: feat(security): the guest's grants channel — an anonymous request at a guest-entry door holds the sets bound to the `guest` anchor, in the one organization the deployment resolves for it (ADR-0138 E1, D3/D4) + + ADR-0087 disposition: none. No authorable key, accept set or stored shape moves: no spec schema, metadata type, stored row or published response field is added, removed, renamed or re-shaped. A declared binding that was not enforced becomes enforced (ADR-0090 D9, as ADR-0138 D3 amends it). + + **The bindings that start granting.** On the reference apps, none: `app-showcase`, `app-crm`, `app-multi-package` and `app-todo` declare no definition of the `guest` anchor that names a set. `app-showcase` ships `showcase_guest_portal` as a capability an administrator may bind and binds it to no anchor (its `client_portal_user` position is an ordinary position), and `app-crm`'s `guest_portal` is the public form doors' set, which keeps its own posture. No reference app declares a permission set named `guest`, so no grant stops either. A set starts granting only where a deployment holds an environment definition of the `guest` position that names it. + + What changes: + + - **`@objectstack/core`** — the anonymous branch of `resolveAuthzContext` resolves the guest's grants on a face that takes the explicit guest entry (the new `guestEntry` input). It reads the sets the `guest` position's definition names, through the same position-bound read every position uses, and the activation ledger as for any held set. It resolves them only in the organization the guest entry's organization step derives (`resolveGuestOrganization`), which asks ADR-0131 D9's predicate and no second spelling of it. With nothing bound, the guest holds no set and no read is issued. The predicate itself, `resolveSystemWriteOrganization`, moves to `@objectstack/core` so both of its askers share it. New exports: `resolveGuestOrganization`, `resolveSystemWriteOrganization` (with `SystemWriteOrganizationDecision` and `SystemWriteRefusalReason`), and `GuestOrganizationUnresolvedError` with `isGuestOrganizationUnresolvedError` and its constants. + - **`@objectstack/objectql`** — `resolveSystemWriteOrganization` and its two types are re-exported from `@objectstack/core` as the same bindings. Nothing a caller imports changes. + - **`@objectstack/plugin-security`** — the set resolution treats the guest's sets as any requested set, and no longer folds the guest's position into the request. A permission set that merely shares the anchor's name no longer reaches an anonymous caller, and the baseline and `everyone` never did. At the CRUD gate, a guest that reaches a data operation acting in no organization, on a deployment where the organization step refuses, is refused before the operation runs. + - **`@objectstack/plugin-sharing`** — the read filter answers deny-all for the guest on every object that record sharing scopes, before the sharing-model branch: the guest reaches rows only through share links, never through the object's sharing model (ADR-0138 D3 item 6, as the maintainer's ruling C amends it). `externalSharingModel` is still not read for any caller. Share links are redeemed at their own door and are unchanged. + - **`@objectstack/runtime`** — the runtime face, which serves the guest, asks for the guest's grants. + + What an anonymous caller sees at an `authRequired: false` endpoint of `type: 'object_operation'`: + + - **One organization, a set bound to `guest`:** the operation runs as the guest, under that set, in that organization. A read the set grants is admitted and returns no row, whatever the object's sharing model (`private`, `public_read`, `public_read_write` or `controlled_by_parent`): the guest reaches a record only through a share link. A create the set grants lands in that organization with no owner. + - **Nothing bound:** `403 PERMISSION_DENIED`, as before. + - **A walled deployment, several organizations, or none:** `501 NOT_IMPLEMENTED`, naming the missing site binding (ADR-0138 D5), where it used to be `403 PERMISSION_DENIED` with nothing granted. Nothing that was served stops being served. The operator's log names the reason once. + + Unchanged: share links, which still serve an anonymous caller the one record they name, with `publicSharing.eligibility` checked at the mint and at every redemption; the public form doors, which keep their own posture and organization; the flow door, which runs under the flow's own declared posture; every surface that answers an anonymous request `401`; and every signed-in caller. + + Level: `minor` for `@objectstack/core`, because new exported symbols widen its published index; `patch` for the other four, whose exports do not move. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [c6139f2] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [78c13bd] +- Updated dependencies [9543e9c] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [2de9c03] +- Updated dependencies [5b68524] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-protocol@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index ba334a92370..a2bf8d4a09a 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index 14e657d4ad4..59bfc42296d 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/observability +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 86916ff879c..49c4335ad08 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.", "type": "module", diff --git a/packages/platform-objects/CHANGELOG.md b/packages/platform-objects/CHANGELOG.md index d7cb2093cc7..864859f7410 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,122 @@ # @objectstack/platform-objects +## 18.0.0-next.1 + +### Minor Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. + +### Patch Changes + +- 5f37d36: feat(spec)!: at most one `isDefault: true` record page per object per deployment — a second is refused at `defineStack` / `os validate`, at package registration and at metadata save, naming both pages (#23015) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + `PageSchema.isDefault` now states the rule (maintainer ruling A on #23015): at most one `isDefault: true` record page may exist for an object in a deployment. The record view opens that page with no tie-break, so with two defaults for one object the page that opened used to be whichever plugin initialised first — an accident no author wrote. A record page is `type: 'record'` (the default when `type` is left out) bound to an `object`; `isDefault` on any other page is outside the rule. + + **What is refused, and where.** Every door that can add a second default refuses it, naming both pages: + + - **`defineStack`** (and so `objectstack validate`, `objectstack build` and a source boot) — two default record pages for one object inside one stack, `STACK_CROSS_REFERENCE_INVALID` (422), one finding per object naming the object, every page and its position (`stack.pages[0]`). **`composeStacks`** refuses the pair across the stacks it composes, in the same envelope; under `manifest: 'preserve'` one page name in two stacks is two packages' pages. + - **Package registration** (`@objectstack/objectql`'s `SchemaRegistry`, at `installPackage` ahead of every mutation and at the `registerItem` seam) — a package whose default record page meets another package's default for the same object, an environment's stored default, or a second default in its own manifest, `DEFAULT_RECORD_PAGE_CONFLICT` (422, exported as `DEFAULT_RECORD_PAGE_CONFLICT_CODE`). Two packages' pages under one name are two pages and are refused too. `OS_METADATA_COLLISION=warn` does not soften it. + - **Metadata save** (`@objectstack/metadata-protocol`'s `saveMetaItem`, so `PUT /api/v1/meta/page/:name`) — a page saved as the default record page of an object a stored page or a packaged page already holds, `DEFAULT_RECORD_PAGE_CONFLICT` (422), in draft and publish mode, before anything is written. + + **Unchanged.** A save under the existing default's own name is that page (an ADR-0005 overlay by name), not a second default. One default per object, a second record page that is not a default, and two objects with one default each are accepted. The same package registering again — a reload, or an upgrade that moves its default to another of its pages — is one holder. A save under the name of a page a package ships is still answered by the packaged-base refusal (`NOT_OVERRIDABLE`, 403), as before. A stored page reaching the registry from `sys_metadata` (hydration, write-through) is not refused again. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `pages: [{ name: 'account_a', type: 'record', object: 'crm_account', isDefault: true }, { name: 'account_b', type: 'record', object: 'crm_account', isDefault: true }]` accepted; the boot kept both and the record view opened whichever registered first | refused, `STACK_CROSS_REFERENCE_INVALID` 422 — set `isDefault: false` on all but one | + | `composeStacks` | two stacks each shipping a default record page for one object composed clean | refused in the cross-reference envelope, naming both stacks' pages | + | package registration | a second package (or a plugin's `manifest.register`) shipping a default record page for an object another package holds registered; init order decided the page | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; keep one default, or compose the deployment without the package that ships the other | + | metadata save | `PUT /api/v1/meta/page/my_page` with `isDefault: true` for an object that already has a default record page stored or shipped saved | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; save it with `isDefault: false`, or first demote the stored default | + + **The one-line fix: keep `isDefault: true` on the one record page per object that should open the record view, and set `isDefault: false` on every other.** To replace a page a package ships, compose the deployment without that package's page; shipping a second default is not an override path. + + No D2 conversion rewrites it: which page stays the default is the author's call. Its D3 record is the semantic entry `page-default-record-page-one-per-object-refused` (protocol 18). + + **`@objectstack/spec`.** `defaultRecordPageObject(page)` (exported from `@objectstack/spec/ui`) is the one reading of "which object is this page the default record page of" that the three doors share. `PageSchema.isDefault` gains its `.describe()`. `DEFAULT_RECORD_PAGE_CONFLICT` joins the error-code ledger under `@objectstack/objectql` and `@objectstack/metadata-protocol`. + + **`@objectstack/objectql`.** The registration refusal, `SchemaRegistry.defaultRecordPageHoldersOf(object)` (every page holding the object's default — the reading the save door meets with the stored pages) and the `DEFAULT_RECORD_PAGE_CONFLICT_CODE` constant. + + **`@objectstack/metadata-protocol`.** The save-door refusal. + + **`@objectstack/platform-objects`.** The page form's `isDefault` help text, which said "Set as default page for this page type" — a rule nothing enforced — now states the one-default-per-object rule, in all four locales. + + **Who is affected, measured.** In this repository every object with a default record page has exactly one: `sys_organization` and `sys_user` (`@objectstack/plugin-auth`), `sys_position` (`@objectstack/plugin-security`), `sys_approval_request` (`@objectstack/plugin-approvals`), and `showcase_project`, `showcase_task` and `showcase_preference` (`examples/app-showcase`). A deployment that also composes a package shipping its own default record page for one of those objects is refused at boot until one of the two pages stops being a default. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index bc916e45d82..3527e106dcf 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 1f7586a5907..4583a2b0d04 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/embedder-openai +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index ea8acb7dd40..9f592bf92f9 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index c5f5bbb71e1..6e11665abe6 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/knowledge-memory +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/service-knowledge@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 07ba4c3175e..647d67da550 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index 61420550137..12c64beb231 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/knowledge-ragflow +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/service-knowledge@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index b80e2225016..3480d223dbf 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/organizations/CHANGELOG.md b/packages/plugins/organizations/CHANGELOG.md index 429519d50ec..05de441efde 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,54 @@ # @objectstack/organizations +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [3490bf5] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [917b1a0] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/organizations/package.json b/packages/plugins/organizations/package.json index d3998189ecb..338ad489da5 100644 --- a/packages/plugins/organizations/package.json +++ b/packages/plugins/organizations/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/organizations", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Multi-organization runtime for ObjectStack — registers the `org-scoping` service that turns single-database row-level Organization isolation on: `organization_id` auto-stamp on insert, per-org seed replay, default-organization bootstrap, and the walled-posture membership-policy gate.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index b349778c40f..43e8629912d 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/plugin-approvals +## 18.0.0-next.1 + +### Minor Changes + +- 24e064f: fix(objectql,service-messaging,service-automation,plugin-approvals)!: a notification carries the organization of what it is about, and one no organization owns is refused instead of stored with `organization_id` NULL + + Clause-②: no (narrowing) — notification rows gain the organization they should already carry; what narrows is an emit that names no organization on an install where none is derivable, which the engine now refuses where it used to store the event row with `organization_id` NULL. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + Every notification emitted from a system-context run — a scheduled flow, a record change written by a system context, an approval node — was stored in `sys_notification` with `organization_id` NULL, so reports and cleanup that filter by organization did not see it. Each emit also logged a `[notify] no organization in scope` warning. On an install holding several organizations, the deliveries of the same emit were refused while the flow step reported success, so nothing reached the inbox. + + **Who owns a notification now:** + + - **The run's organization.** A run that carries one emits exactly as before. + - **Else the record it is about.** A `notify` node in a run with no organization reads the organization of the record the notification is about. That is the declared click-through record (`sourceObject` + `sourceId`), else the run's trigger record. It is read from the stored row by id, never from the record snapshot the run carries. + - **Approvals.** Every approval notification carries its request's organization: `approval.requested`, reminders, comments, send-backs, reassignments, escalations and out-of-office notices. + - **Else the owner the engine derives for a system write** (ADR-0131 D9). `sys_notification` is now in the platform-object tenancy ledger, so an emit that names no organization gets the install's sole organization under the `single` posture. + - **The rows below follow the event.** `emit()` stamps the stored event row's organization on its delivery, inbox row and receipt, on both the inline and the outbox path. + + **Refused now:** where no organization is derivable (a walled posture, or a `single` install holding more than one organization), an emit that names none is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`. Nothing is written for it. A `notify` step that hits the refusal fails with `notify failed: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED: …` instead of succeeding. `MessagingService.emit()` rejects with the engine's error for any other producer. + + **What to do when it refuses:** give the notification an owner. Name the record it is about on the `notify` node (`sourceObject` + `sourceId`), declare the scheduled flow's acting organization (`organization` on its start node), or pass `organizationId` to `emit()` from your own code. + + The `[notify] no organization in scope` warning is gone, because the NULL row it predicted is no longer written. Rows already stored with `organization_id` NULL are not rewritten here. The v18 organization-ownership ceremony attributes them from their source record. + +### Patch Changes + +- 920bc12: fix: the approval request actions' success toasts name the request, and Explain access names the principal as a person + + Clause-②: no + + Server-authored strings that the console renders verbatim now name what and who they are about. + + - `@objectstack/plugin-approvals`: every action on `sys_approval_request` had a bare-verb `successMessage` (`Approved.`, `Rejected.`, `Reassigned.` and so on). Each now names the request through `${result.request.record_title}`. Each action's route answers `{ request, … }` with the read-back request and its display enrichment, so `record_title` is the title the inbox shows: the target record's display field, else its payload snapshot. The new copies: + - Approve and Reject use the inbox row's inline wording: `Approved "TITLE"` and `Rejected "TITLE"`. + - The others read `Reassigned "TITLE"`, `Sent "TITLE" back for revision`, `Information requested on "TITLE"`, `Reminder sent for "TITLE"`, `Recalled "TITLE"`, `Resubmitted "TITLE"` and `Reply posted on "TITLE"`. + + The zh-CN, ja-JP and es-ES values of the same keys carry the same token. No key, route or response shape changes. + - `@objectstack/plugin-security`: the explain engine's principal layer said `Principal USER_ID holds position(s) …`. It now names the person first and the id second, for example `Principal Ada Lovelace (ada@example.com; id usr_1) holds position(s) …`. Three other users in the same layer are named the same way: + - an on-behalf-of delegator; + - the user an ADR-0091 D3 delegated position came from, in the clause; + - that same user in the position's contributor attribution (`delegation from …`). + + Each user row is read with the asker's own context, so row-level and field-level security apply. An asker whose read returns no row gets the bare id, as before, and a field its read masks is left out. As a result, two askers can now be shown different names for the same user. The decision's `principal` object and every verdict are unchanged. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index f5a1a183b71..ed352526252 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index 48e7b704763..29cc1b00a4a 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,52 @@ # @objectstack/plugin-audit +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 15f7f43a20f..24d58ea82a9 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index 2a7d4b682d0..908d58ebf20 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,126 @@ # Changelog +## 18.0.0-next.1 + +### Minor Changes + +- 3490bf5: The identity import's automatic invitation email links to the Console's set-password page, no longer to an `INVALID_TOKEN` error + + Clause-②: yes (widening) + + - **Before.** For every row an admin imported under the Automatic (or Invite) password policy with email delivery, `POST /api/v1/auth/admin/import-users` requested the password reset with no `redirectTo`. better-auth then emailed a link ending in an empty `callbackURL=`, and its `GET /reset-password/:token` refused that link with a redirect to `?error=INVALID_TOKEN`, which the Console shows on its sign-in page. The token in the link was still valid, but the invited user had no page to use it on. + - **Now.** The reset request carries `redirectTo` set to the Console's set-password page (`${origin}${uiBasePath}/reset-password`, by default `/_console/reset-password`), the page the Console's own forgot-password flow lands on. The emailed link opens `/_console/reset-password?token=…`, and better-auth's origin check accepts it because it is the deployment's own origin. + - **The two new members.** Both are additions; nothing existing changes shape. + - `AuthManager.getConsoleUrl(path)` is a public reader of the absolute URL of a Console page. It returns what the class already composes for every email and SMS link it sends. + - `IdentityImportDeps.consoleResetPasswordUrl?()` is optional. `AuthPlugin` wires it to `getConsoleUrl('/reset-password')`. A host that calls `runAdminImportUsers` itself should pass it to get a working invitation link. A caller that omits it sends the request exactly as before (`{ email }`), so the link keeps the empty `callbackURL`. + - **Unchanged.** The SMS invitation, the temporary-password policy, and the email copy (the invitation still uses the password-reset template). +- 917b1a0: fix(plugin-auth)!: IdentityObjectsPlugin keeps the single posture's Default Organization at boot + + Clause-②: no (narrowing) — it restores ADR-0131 D3's Default Organization boot invariant, which `default-organization-invariant.ts` states as "[ADR-0131 D3] The Default Organization as a BOOT INVARIANT under the single posture", in the kernel that mounts `createIdentityObjectsPlugin()` without `AuthPlugin`; no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A kernel that mounts `createIdentityObjectsPlugin()` without `AuthPlugin` now holds its Default Organization under the `single` tenancy posture, so its seeds and system writes on tenant-scoped objects are stamped with it instead of refused with "the install holds no organization at all". + + - **Refused now:** a composition that mounts `createIdentityObjectsPlugin()` and publishes its driver after `bootstrap()` (the identity objects' `start()` reads `sys_organization` under `single`, so the boot fails with `No driver available for object 'sys_organization'`); and a kit that inserts its own organization row after boot, which now holds two organizations under `single`, so every system write that names no organization is refused as ambiguous (ADR-0131 D9). + - **Unchanged:** the walled postures (`group`, `isolated`), where `start()` creates nothing; the refusal at `init()` of `createIdentityObjectsPlugin()` mounted beside `AuthPlugin`; every composition that publishes its driver before boot; and a declaration-only boot (`os migrate`), which suppresses `start()`. + + - **Why.** ADR-0131 D3 makes the Default Organization a boot invariant under `single`: it owns every row. Registering `sys_organization` is what turns the engine's and the seed loader's zero-organization branch from "unstamped" into "refused" (D9). `IdentityObjectsPlugin` registers that object, but only `AuthPlugin.start()` created the organization, so the reduced kernel the plugin exists for (ObjectQL, `createIdentityObjectsPlugin()`, `SecurityPlugin`) refused every seed row of an organization-owned object. + - **What changed in `@objectstack/plugin-auth`.** `IdentityObjectsPlugin` gains a `start()` that, under `single`, finds or creates the Default Organization with the same invariant `AuthPlugin.start()` runs. It is idempotent, and a failure fails the boot. The posture is the `tenancy` service's when one is registered, otherwise the requested one (`OS_TENANCY_POSTURE`). Under `group` and `isolated` it does nothing. Nothing new is exported, and the plugin is still refused beside `AuthPlugin`. + - **What changed in `@objectstack/runtime`.** `AppPlugin` orders `com.objectstack.auth.identity-objects` ahead of itself, as it already ordered `com.objectstack.auth`, so the organization exists before the inline seed loads, wherever a host lists the two plugins. + - **If your test kit mounts `createIdentityObjectsPlugin()`.** Its kernel now reads and writes `sys_organization` during `bootstrap()` under `single`. Publish the driver before the boot (a `driver.default` service, or a datasource plugin), not with `registerDriver()` after it. Otherwise the boot fails with `No driver available for object 'sys_organization'`. A kit that inserted its own organization row to stand in for the missing one now holds two, and a system write that names no organization is refused as ambiguous; drop that insert and read the Default Organization (`slug: 'default'`) the boot created. +- 404fc47: feat(plugin-auth): `registerOrganizationRecordPage: false` leaves out the platform's `sys_organization` default record page, so a host that ships its own page is the only default + + Clause-②: yes (widening) + + A deployment holds at most one `isDefault: true` record page per object, and a second one is refused at registration with `DEFAULT_RECORD_PAGE_CONFLICT`. `AuthPlugin` always registered `sys_organization_detail` (the Members / Invitations / Teams tab strip) as the default record page of `sys_organization`, so a deployment composing `AuthPlugin` beside a host that ships its own default page for that object was refused at boot. + + - **New option.** `AuthPluginOptions.registerOrganizationRecordPage` defaults to `true`. Only an explicit `false` leaves out `sys_organization_detail`, so the host's page is the only default whichever plugin starts first. Plugin order and precedence rules play no part. + - **Nothing else moves.** With `false`, `AuthPlugin` still registers the identity objects, the `sys_user_detail` record page, the System Overview dashboard, and the auth routes and services. It logs one `info` line at boot naming the option. + - **The default is unchanged.** A deployment that does not set the option registers `sys_organization_detail` as before. A host page beside it, with no option set, is still refused with `DEFAULT_RECORD_PAGE_CONFLICT`. + - **To adopt it:** construct `new AuthPlugin({ …, registerOrganizationRecordPage: false })` in the host that registers its own `isDefault: true` record page for `sys_organization`. With the option set and no default page of the host's own, the organization record view opens the synthesized default layout, without the Members / Invitations / Teams tabs. +- 02a691a: fix(plugin-auth)!: the platform-admin actions refuse a session the ADR-0069 authentication policy holds, a platform administrator's included + + Clause-②: no (narrowing: a held session is refused on the platform-admin surface, which ADR-0069 D3 already declares gated) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 D3 holds a signed-in user out of every protected resource while their password has expired (or an administrator has forced a change) or while enforced MFA is not yet enrolled, until they remediate. REST and the runtime dispatcher already refuse such a session. The platform-admin actions plugin-auth mounts on the host server (user creation, password, ban, removal, unlock, manager, import, membership, OAuth-client and SSO administration) are judged by one shared judge, `judgePlatformAdmin`, which read the session and the admin rung but never the posture, so a held platform administrator could still administer the platform. + + - **Refused now:** a held session meets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message` on every platform-admin action that judge guards, in the family's declared envelope, before the action runs. The judge asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) without a request path, so these actions never inherit the exemption that keeps the auth endpoints reachable. + - **Order:** an anonymous caller still meets `401 UNAUTHENTICATED` first. A held signed-in caller who is not a platform administrator now meets the posture's `403` code instead of `403 PERMISSION_DENIED`: same status, the remedy named. + - **Unchanged:** a session the policy does not hold is served exactly as before. Sign-in, change-password, two-factor enrollment and sign-out still answer a held session, so remediation works as before. + + What changes for you. A platform administrator the policy holds must remediate (change the password, or enroll an authenticator) before the platform-admin actions answer them, as on every other protected route. + +### Patch Changes + +- 6f4c518: fix(plugin-auth): a fresh `os dev` database opens on the console shell, not on the "Set the workspace timezone" dialog — the dev seed gives the workspace it lands its admin in the host's time zone + + Clause-②: no + + **FROM.** The dev admin seed (`NODE_ENV=development`, on unless `OS_SEED_ADMIN=0`) created its admin and left the admin's workspace with `localization.timezone` unset. The console asks an administrator for a zone while that key still resolves from the manifest default, so every fresh `os dev` database (each `--fresh` run, each new database file) opened on the dialog. The console's own workspace-creation paths already send the creator's browser zone; the seed did not go through them. + + **TO.** On the boot that seeds the admin, the seed writes the host's resolved zone (`Intl.DateTimeFormat().resolvedOptions().timeZone`, so `TZ` chooses it) to `localization.timezone` for each organization the admin is a member of. Under the default `single` posture that is the Default Organization. The write goes through the settings service, the same door the dialog's confirm uses. It is one `tenant` row per organization, with the key's IANA zone check applied there. + + **Unchanged.** + - A zone already set on any rung (the organization's, the global one, an `OS_LOCALIZATION_TIMEZONE` override) is never overwritten. + - A host that reports no zone, or reports one the settings door refuses, gets nothing written. The workspace keeps the default and the dialog still asks. + - Boots after the seeding one write nothing, so a database that already exists is not touched. + - Production is not affected: the seed stays gated to `NODE_ENV=development`. + - `sys_organization.timezone` is a separate key, which the dialog does not read. It is not written. + + **Note for in-process test boots.** `@objectstack/verify`'s harness forces `NODE_ENV=development` to get the seeded admin, so its boots now give that admin's workspace the host's zone too. On a host in `UTC` the resolved value is the same as before. On a host in another zone, a suite that relies on the default `UTC` can set `TZ=UTC`, or set the zone through `PUT /api/settings/localization` as it would for any workspace. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/rest@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-messaging@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 26a11088caa..1b1fa063ce5 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index af92cdaf3a3..d257d56b37f 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,81 @@ # @objectstack/plugin-dev +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [5c9bfb0] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [3490bf5] +- Updated dependencies [920bc12] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [08b38cb] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [e790cfc] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [09ef77c] +- Updated dependencies [5d3018d] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [6d96721] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [be50633] +- Updated dependencies [325a92e] + - @objectstack/plugin-security@18.0.0-next.1 + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/rest@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-storage@18.0.0-next.1 + - @objectstack/service-i18n@18.0.0-next.1 + - @objectstack/account@18.0.0-next.1 + - @objectstack/setup@18.0.0-next.1 + - @objectstack/driver-memory@18.0.0-next.1 + - @objectstack/plugin-hono-server@18.0.0-next.1 + - @objectstack/service-realtime@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index bfaaf5760be..91536ff779b 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index 77f98790eab..e6083c642ab 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/plugin-email +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 1bd6a1d1220..bd9cc440785 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index 89912407e9c..1fd43e34e06 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/plugin-hono-server +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index f5d79f4199d..e51237ecbe0 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": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index b4b88aebd9c..90e5846cfd6 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,22 @@ # @objectstack/plugin-pinyin-search +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [66949b7] +- Updated dependencies [87e902d] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [24e064f] +- Updated dependencies [eeb95f7] + - @objectstack/core@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index a5c5c2e644e..233f231d564 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": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index f27bacd5414..4010cc70a00 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,296 @@ # @objectstack/plugin-security +## 18.0.0-next.1 + +### Minor Changes + +- dac99c2: feat(plugin-security)!: the permission-set projection, its seed-name lookup and refusal sink are deleted (ADR-0131 D3, C3 stage 7a) + + Clause-②: no (narrowing) + + + + **BREAKING** (retired exports of `@objectstack/plugin-security`), graded `minor` on the v18 prerelease line. + + Stage 8 stopped registering the `sys_permission_set` row object, the `permission` mutation projector and the data-door write-through. This change deletes the modules behind them: `permission-set-projection`, `seed-name-lookup` and `seed-refusal-sink`. A permission set is written through the metadata door alone (`PUT /api/v1/meta/permission/:name`), and the security catalog serves it by name. + + What a consumer sees: + + - FROM `createPermissionSetWriteThrough(deps)` TO nothing: no `sys_permission_set` data-door write exists to redirect. Write the set through the metadata door. Delete the call. + - FROM `registerPermissionSetProjection(protocol, deps)` TO nothing: no row mirrors a definition. Delete the call. + - FROM `reconcilePermissionSetProjection(...)` TO nothing: there is no projection to reconcile at boot. Delete the call. + - FROM `projectPermissionMutation(...)` TO nothing. Delete the call. + - FROM `upsertEnvPermissionSet(...)` TO `PUT /api/v1/meta/permission/:name`, or the metadata protocol's `saveMetaItem({ type: 'permission', name, item })`. + - FROM `permissionSetBodyFromRow(row)` TO `permissionSetDefinitionFromRow(row)` from `@objectstack/core`: the same mapping, for converting a stored `sys_permission_set` row into its definition. + - FROM `permissionSetRowFields(definition)` TO nothing: no row is written. Delete the call. + - FROM the types `PermissionSeedOutcome`, `ProjectionDeps`, `ProjectionReconcileOutcome` and `WriteThroughDeps` TO nothing. Delete the import. + + Unchanged: the security catalog's echo of a metadata-door permission set or position on an environment kernel, which lives in its own module since #23308. The packaged-permission-set lock still refuses a metadata-door save of a set a code package ships (`403 NOT_OVERRIDABLE`), and the boot still reports environment overlays that shadow such a set. `OverlayDetectionOptions.logger` now asks only for `warn`, the one channel the reading reports on. + + **`@objectstack/spec`** (patch): the `permission.tabPermissions` liveness entry no longer cites the deleted projection, and the liveness README's registered-but-unbound `permission-set-projection` proof row is retired with the invariant it described (no `@proof` tag carries it any more). +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 76815b9: feat(spec)!: a `controlled_by_parent` object derives its master from exactly one required `master_detail` — the parse refuses anything else, and the runtime's two fallback tiers retire (#23100) + + Clause-②: no (narrowing) + + ADR-0055 §1 says a `controlled_by_parent` object "must declare exactly one required `master_detail` field … Validation error otherwise (fail closed)". Until now neither the parse nor the runtime kept it. `ObjectSchema` accepted such an object with no master at all. The security runtime derived a master through a three-tier fallback the record never stated: a required `master_detail`, then any `master_detail`, then a required `lookup`, taking the first match by field order within a tier. The maintainer ruling (letter B) enforces the record, under ADR-0049 (enforce-or-remove). + + **BREAKING — what moves for consumers.** + + - **`@objectstack/spec`.** `ObjectSchema` (raw `.parse()` / `.safeParse()`, `ObjectSchema.create()`, `defineStack`, `os validate` and the metadata save door) refuses an object declaring `sharingModel: 'controlled_by_parent'` unless exactly one of its fields is a `master_detail` declared `required: true`. With none, the issue sits at `sharingModel` and names each field that relied on a retired tier. With two or more, it sits at `fields` and names them. A plain object literal is judged like a raw parse: an omitted `required` on its `master_detail` is refused, while `ObjectSchema.create()` keeps forcing an omitted `required` to `true`. The step-18 semantic migration entry `cbp-master-relation-tiers-retired` carries the prescription for `os migrate meta`. The step-18 conversion `cbp-master-detail-required-defaulted` covers the one lossless shape: a `controlled_by_parent` object whose one `master_detail` omits `required`, which Studio's field designer writes. It gets `required: true`, exactly what `ObjectSchema.create()` does. It runs on stored rows, built artifacts and `os migrate meta --from 17`. A live authoring parse still refuses the shape and names the fix. + - **`@objectstack/plugin-security`.** The relation resolver derives a master from that one field and nothing else. An object that declares a non-required `master_detail` only, a required `lookup` only, or two required `master_detail` fields resolves no master. Every read of it is denied, and every write and insert is refused with `422 INVALID_METADATA`. The plugin reports such an object once per object (`[security/controlled-by-parent] … resolves no master relation`), naming the shape it relied on. The `422` message now says "not exactly one required master_detail field". + - **`@objectstack/lint`.** `security-controlled-by-parent-no-relation` now refuses the non-required `master_detail` and required `lookup` shapes as well. `security-controlled-by-parent-ambiguous-relation` reports two or more required `master_detail` fields as "no master is derived", since the runtime no longer picks one by field order. Two required `lookup` fields are now a no-relation object, not a tie. `security-master-detail-ungranted` no longer treats a lookup-bound `controlled_by_parent` object as a detail. + + **FROM → TO — HotCRM's three objects.** These are the only producers measured on the retired third tier: the read-only HotCRM clone at `0f84c9c`, 8 `controlled_by_parent` objects, 5 of them already on one required `master_detail`. HotCRM's pinned engine is unaffected until it bumps past the release that carries this change. + + | object | FROM | TO | + |---|---|---| + | `crm_campaign_member` | `crm_campaign: Field.lookup('crm_campaign', { required: true })` | `crm_campaign: Field.masterDetail('crm_campaign', { required: true, deleteBehavior: 'restrict' })` | + | `crm_event_attendee` | `crm_event: Field.lookup('crm_event', { required: true })` | `crm_event: Field.masterDetail('crm_event', { required: true, deleteBehavior: 'restrict' })` | + | `crm_article_feedback` | `crm_knowledge_article: Field.lookup('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | `crm_knowledge_article: Field.masterDetail('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | + + `deleteBehavior: 'restrict'` keeps what the two lookups do today: a campaign or an event with members or attendees cannot be deleted. Without it a `master_detail` cascades, and a detail's rows are deleted with their master. An object whose rows must outlive their parent is not a detail. It leaves `controlled_by_parent` for `private`, `public_read` or `public_read_write`. + + **Remedy, in one line.** On every object with `sharingModel: 'controlled_by_parent'`, declare exactly one `master_detail` field with `required: true`, pointing at the master. Every other parent is a `lookup`. + + **Unchanged.** + + - An object with one required `master_detail` resolves and derives exactly as before, chain walk included. So does a required `master_detail` beside other `lookup` fields, or beside a `master_detail` that is not required. Every in-tree `controlled_by_parent` object is in that set: `examples/app-crm` (1), `examples/app-showcase` (2), the CLI's eval corpus (4) and the dogfood fixtures. + - Stored metadata whose one `master_detail` merely omits `required` keeps serving, converted as above. A stored row in any other retired shape is not rewritten: it still registers, under its `[metadata_spec_invalid]` diagnostic, and is denied until it is corrected and saved. Those shapes are an explicit `required: false`, two masters, or a required `lookup`. None of them is converted: turning a `lookup` into a `master_detail` changes what deleting the master does to every row, choosing between two masters is the pick ADR-0055 §1 refuses, and leaving `controlled_by_parent` is the author's decision. + - No export, signature or error code is added or removed. + + +- e790cfc: feat(security)!: an anonymous caller never owns a record and never sets its owner — the owner-anchor stamp's guest branch (ADR-0138 E3, D1) + + Clause-②: no (narrowing) — a guest-supplied owner is refused at the endpoints and the owner column never holds the guest or the system principal. That tightens what a guest write may carry and admits nothing refused today. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0138 D1 rules that the guest is a principal, never an owner. The owner-anchor stamp in `SecurityPlugin` (the step that stamps an absent `owner_id` with the acting user and refuses a forged one) now carries the guest branch the record names as D1's enforcement point. It applies to the guest envelope, an anonymous request served by an `authRequired: false` endpoint: + + - **Nothing is stamped for a guest.** An absent owner stays unset: never the guest, never the system principal. An empty `owner_id` (an empty string) is left unset too, where it used to be stored as an empty string. + - **A guest-supplied owner is refused**, on create and on update, with `PERMISSION_DENIED` (`403`), whatever the guest is granted. The transfer grant is never consulted for a guest. Before this change a guest whose bound permission set carried the transfer bit could create a row owned by any user, the system principal included. The anchor tier already refuses that bit on a guest binding, so no binding a deployment can make today reached it, and this branch does not depend on the tier. Without the transfer bit the refusal already happened before this change; its message now says that no grant admits a guest's owner, instead of naming the transfer grant as the remedy. + - **An owner the scenario sets stands.** A `beforeInsert` hook runs after the stamp, inside the engine's operation, and a record-change flow writes as itself, so neither is mistaken for a value the guest sent. + + Unchanged: the public form doors, which strip a submitted owner and leave it unset (`PUBLIC_FORM_SERVER_MANAGED_FIELDS`); every signed-in caller, whose absent owner is still stamped with its own id and whose foreign owner still needs the transfer grant; and system writes. + + What a guest client does: leave `owner_id` out of an anonymous write. The object's own hooks or record-change flows decide who owns the row, and the row stays unowned when none does. +- 14a13bc: fix(metadata-protocol)!: the metadata save, restore and publish doors refuse a permission set whose body `packageId` disagrees with the package it is saved into, and the audience-binding suggestion reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the package the set is saved into: the stored row's `package_id`, served as `_packageId`. A body `packageId` that named another package was accepted and then silently overridden, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **FROM.** A permission set saved and published into package `app.crm` with `packageId: 'app.other'` in its body answered `200` and `published`, and was owned by `app.crm` everywhere it was read. A set saved into no package with `packageId: 'app.other'` was stored as an environment-authored set, while the audience-binding suggestion reader offered it, accepted it and derived it into `everyone` as `app.other`'s. + + **TO.** + + - **`@objectstack/metadata-protocol`.** The save door (`PUT /api/v1/meta/permission/:name`, draft and publish mode, and every in-process caller) the restore doors (`POST /api/v1/meta/permission/:name/rollback` and a commit revert's restore limb) and the draft promotion (`POST /api/v1/meta/permission/:name/publish` and the package publish) refuse a permission set whose body `packageId` is not the package the row is written into, with `400 VALIDATION_ERROR`, the code and status the doors already answer for a body `name` that disagrees with its row. `issues[]` carries the key at `packageId`. A body `packageId` on a set saved into no package is refused too, the empty string included: a present key declares a package-shipped set. The message names both values and both remedies. The package judged is the one the write lands in: a draft saved without naming a package lands in the package of the item's active row, and is judged against it. A package publish that meets such a draft (one stored before this change) aborts the whole batch, `outcome: 'refused'`, with the draft in `failed[]` under `VALIDATION_ERROR`. A rollback to a stored version that carries such a body (one written before this change) is refused, and a commit revert reports it in `failed[]` under the same code; the remedy there is to save the set again with `packageId` dropped or set to its package, instead of restoring that version. Nothing is written, published or restored. Package duplication copies a set whose body names the source package as naming the target package. `os migrate meta --stored --apply` records a stored set it rewrites, and the door refuses, as that row's failure. + - **`@objectstack/plugin-security`.** The audience-binding suggestion reader takes a set's owner from its binding only, never from its body's `packageId`. A set bound to no package has no owner there, so no suggestion is created for it. An acceptance recorded under the package its body named is withheld from `everyone` at the next derivation. The withholding is reported at `warn`, naming the set and its reason. Accepting a pending suggestion of that kind is refused. + - **`@objectstack/spec`.** The `PermissionSetSchema.packageId` description states the refusal. The step-18 semantic migration entry `permission-set-package-id-binding-mismatch-refused` carries the upgrade sweep. + + **Unchanged.** A body `packageId` equal to the package the set is saved into is accepted, and an absent one is accepted everywhere. Stored rows are not rewritten. A stored set whose body disagrees with its binding keeps loading and keeps reading as its binding, and is refused on its next save or publish, as is a restore of a stored version that carries one. An installed package's own sets are owned by the package that ships them, as before. + + **Remedy, in one line.** Drop `packageId` from the permission set's body, or save the set into the package it names (`?package=` on the REST door, `packageId` in process). +- eeb95f7: fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door: + + - **The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it. + - **Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key. + + The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted. + + **Readers.** `@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report. + + **Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` / `objectstack build` | `defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm` | refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 | + | `ObjectQL.registerApp` | the same manifest registered the set under `com.acme.crm`, silently | `400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed | + | an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key | + + **Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again. + +### Patch Changes + +- 5c9bfb0: fix(plugin-security): the position write-through and the row-only position backfill are deleted, and the boot reference report stops reading position rows (ADR-0131 D3, C3 stage 7b) + + Clause-②: no + + Stage 8 stopped registering the `sys_position` row object, the `sys_position` data-door write-through and the row-only position backfill. This change deletes the modules behind them, `position-write-through` and `position-environment-backfill`. A position is written through the metadata door alone (`PUT /api/v1/meta/position/:name`). No export of the package's entry changes. + + What a deployment observes: + + - **The boot reference report reads no `sys_position` row.** It still lists, per organization, every position assignment and permission-set grant whose name the security catalog does not resolve, and every grant that names no permission set. It no longer has the two position-row lines ("position(s) … have sys_position rows and no definition" and "position name(s) are carried by more than one sys_position row"). No composition provisions that table any more, so those lines could not fire. Legacy position rows are converted and reported by the upgrade ceremony (ADR-0131 D10). + - The Setup-create refusal for a position name the catalog already holds (`409 UNIQUE_VIOLATION`, raised by the deleted write-through) goes with its module. A `sys_position` data-door create cannot reach the engine any more, so nothing raised it. +- 920bc12: fix: the approval request actions' success toasts name the request, and Explain access names the principal as a person + + Clause-②: no + + Server-authored strings that the console renders verbatim now name what and who they are about. + + - `@objectstack/plugin-approvals`: every action on `sys_approval_request` had a bare-verb `successMessage` (`Approved.`, `Rejected.`, `Reassigned.` and so on). Each now names the request through `${result.request.record_title}`. Each action's route answers `{ request, … }` with the read-back request and its display enrichment, so `record_title` is the title the inbox shows: the target record's display field, else its payload snapshot. The new copies: + - Approve and Reject use the inbox row's inline wording: `Approved "TITLE"` and `Rejected "TITLE"`. + - The others read `Reassigned "TITLE"`, `Sent "TITLE" back for revision`, `Information requested on "TITLE"`, `Reminder sent for "TITLE"`, `Recalled "TITLE"`, `Resubmitted "TITLE"` and `Reply posted on "TITLE"`. + + The zh-CN, ja-JP and es-ES values of the same keys carry the same token. No key, route or response shape changes. + - `@objectstack/plugin-security`: the explain engine's principal layer said `Principal USER_ID holds position(s) …`. It now names the person first and the id second, for example `Principal Ada Lovelace (ada@example.com; id usr_1) holds position(s) …`. Three other users in the same layer are named the same way: + - an on-behalf-of delegator; + - the user an ADR-0091 D3 delegated position came from, in the clause; + - that same user in the position's contributor attribution (`delegation from …`). + + Each user row is read with the asker's own context, so row-level and field-level security apply. An asker whose read returns no row gets the bare id, as before, and a field its read masks is left out. As a result, two askers can now be shown different names for the same user. The decision's `principal` object and every verdict are unchanged. +- 37510aa: feat(security): the guest's grants channel — an anonymous request at a guest-entry door holds the sets bound to the `guest` anchor, in the one organization the deployment resolves for it (ADR-0138 E1, D3/D4) + + ADR-0087 disposition: none. No authorable key, accept set or stored shape moves: no spec schema, metadata type, stored row or published response field is added, removed, renamed or re-shaped. A declared binding that was not enforced becomes enforced (ADR-0090 D9, as ADR-0138 D3 amends it). + + **The bindings that start granting.** On the reference apps, none: `app-showcase`, `app-crm`, `app-multi-package` and `app-todo` declare no definition of the `guest` anchor that names a set. `app-showcase` ships `showcase_guest_portal` as a capability an administrator may bind and binds it to no anchor (its `client_portal_user` position is an ordinary position), and `app-crm`'s `guest_portal` is the public form doors' set, which keeps its own posture. No reference app declares a permission set named `guest`, so no grant stops either. A set starts granting only where a deployment holds an environment definition of the `guest` position that names it. + + What changes: + + - **`@objectstack/core`** — the anonymous branch of `resolveAuthzContext` resolves the guest's grants on a face that takes the explicit guest entry (the new `guestEntry` input). It reads the sets the `guest` position's definition names, through the same position-bound read every position uses, and the activation ledger as for any held set. It resolves them only in the organization the guest entry's organization step derives (`resolveGuestOrganization`), which asks ADR-0131 D9's predicate and no second spelling of it. With nothing bound, the guest holds no set and no read is issued. The predicate itself, `resolveSystemWriteOrganization`, moves to `@objectstack/core` so both of its askers share it. New exports: `resolveGuestOrganization`, `resolveSystemWriteOrganization` (with `SystemWriteOrganizationDecision` and `SystemWriteRefusalReason`), and `GuestOrganizationUnresolvedError` with `isGuestOrganizationUnresolvedError` and its constants. + - **`@objectstack/objectql`** — `resolveSystemWriteOrganization` and its two types are re-exported from `@objectstack/core` as the same bindings. Nothing a caller imports changes. + - **`@objectstack/plugin-security`** — the set resolution treats the guest's sets as any requested set, and no longer folds the guest's position into the request. A permission set that merely shares the anchor's name no longer reaches an anonymous caller, and the baseline and `everyone` never did. At the CRUD gate, a guest that reaches a data operation acting in no organization, on a deployment where the organization step refuses, is refused before the operation runs. + - **`@objectstack/plugin-sharing`** — the read filter answers deny-all for the guest on every object that record sharing scopes, before the sharing-model branch: the guest reaches rows only through share links, never through the object's sharing model (ADR-0138 D3 item 6, as the maintainer's ruling C amends it). `externalSharingModel` is still not read for any caller. Share links are redeemed at their own door and are unchanged. + - **`@objectstack/runtime`** — the runtime face, which serves the guest, asks for the guest's grants. + + What an anonymous caller sees at an `authRequired: false` endpoint of `type: 'object_operation'`: + + - **One organization, a set bound to `guest`:** the operation runs as the guest, under that set, in that organization. A read the set grants is admitted and returns no row, whatever the object's sharing model (`private`, `public_read`, `public_read_write` or `controlled_by_parent`): the guest reaches a record only through a share link. A create the set grants lands in that organization with no owner. + - **Nothing bound:** `403 PERMISSION_DENIED`, as before. + - **A walled deployment, several organizations, or none:** `501 NOT_IMPLEMENTED`, naming the missing site binding (ADR-0138 D5), where it used to be `403 PERMISSION_DENIED` with nothing granted. Nothing that was served stops being served. The operator's log names the reason once. + + Unchanged: share links, which still serve an anonymous caller the one record they name, with `publicSharing.eligibility` checked at the mint and at every redemption; the public form doors, which keep their own posture and organization; the flow door, which runs under the flow's own declared posture; every surface that answers an anonymous request `401`; and every signed-in caller. + + Level: `minor` for `@objectstack/core`, because new exported symbols widen its published index; `patch` for the other four, whose exports do not move. +- 09ef77c: fix(plugin-security): a permission set published into a runtime package names that package to the security catalog, so its `isDefault: true` records an `everyone` suggestion + + Clause-②: no + + - **What was wrong.** A writable runtime package's permission set marked `isDefault: true`, published through the package door with no `packageId` in its body, recorded no audience-binding suggestion on an environment kernel, so ADR-0090 D5's "packages suggest, admins confirm" never happened for it. On that kernel the protocol hydrates no stored row into the engine registry, so the security catalog finds the set in the metadata service. The only body there is the permission-set projection's in-memory copy, and that copy carried no `_packageId`. The catalog served the set as unowned while `GET /api/v1/meta/permission/NAME` named the package, and the suggestion collector skipped it. + - **What changed.** The projection's copy of a tenant-authored set now names the package its stored row is bound to. It reads that binding from the protocol's by-name read (the `_packageId` that read serves) and derives none of its own. The catalog, the by-name read and the layered read (`?layers=true`, whose `packageId` comes from the same copy) now name the same owner. `GET /api/v1/security/suggested-bindings` lists one pending suggestion under that package. A `manage_metadata` holder still has to accept it: the publish grants nothing. + - **When the body's `packageId` disagrees.** A set whose body declares a `packageId` other than the package it is published into is now owned, for the catalog and the suggestions, by the package it is published into, which is what the by-name read already served. On an environment kernel, a suggestion recorded under the body's package is pruned at the next sync while pending, and withheld from `everyone` with its reason logged if it was accepted. A pending suggestion under the publishing package replaces it. + - **Unchanged.** The packaged-permission-set lock and every edit door. (No `sys_permission_set` row is written for the set: C3 stage 7a retires the last writer.) A code package's set, whose owner the catalog reads from its artifact. A set saved into no package, which stays unowned and records no suggestion. A copy of a code-shipped set's legacy overlay, which carries no owner, as before. + - **If the binding cannot be read**, the copy is registered without an owner, as before, and one warning says so. It still enforces the new body, and the next save, publish or boot reads the binding again. +- 6d96721: fix(plugin-security): an environment kernel's security catalog serves metadata-door permission sets and positions again + + Clause-②: no + + On an environment kernel (the per-project kernel a hosted tenant runs), a permission set or a position saved, published or deleted through the metadata door is in the security catalog again: + + - It resolves from the kernel's metadata service, under the package its stored row is bound to. + - A user granted it is authorized by it. + - An `isDefault: true` set in a runtime package records its pending `everyone` suggestion again (ADR-0090 D5). + + After a restart, the stored overlays are served again too. + + This regressed in C3 stage 8 (`feat(plugin-security)!: retire the four security-catalog row objects`). That change unwired the permission-set projector. On an environment kernel, the projector's in-memory echo was the only thing that put a stored set into the metadata service, because the metadata door hydrates no project row into the engine registry. Since stage 8, such a set resolved to nothing and granted nothing (fail-closed), and no suggestion was recorded. + + Positions never had an echo. On an environment kernel, a metadata-door position was unresolved before stage 8 as well, and it now resolves. + + `SecurityPlugin` now registers its own mutation projector for `permission` and `position` on the protocol, plus one pass over the stored environment-wide overlays at `kernel:ready`. Both only register the overlay body in the metadata service. They write no row, and they change nothing a host kernel resolves, since a host kernel reads these from the engine registry first. +- be50633: refactor(plugin-security): the audience-anchor binding gate on `sys_position_permission_set` is deleted + + Clause-②: no + + `SecurityPlugin`'s private engine-middleware gate that refused binding a high-privilege permission set to the `everyone` or `guest` position guarded writes to `sys_position_permission_set` only. C3 stage 8 (#23222) unregistered that object, so no write has reached the gate since. The gate, its call in the middleware and its three unit cases are removed. + + No behaviour changes, and nothing exported changes. The anchor tier itself (`describeAnchorForbiddenBits` from `@objectstack/spec/security`) still applies wherever a binding is made: when an `everyone` suggestion is confirmed (`confirmAudienceBindingSuggestion`), and on every derivation of the `everyone` anchor. No door binds a set to `guest`. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 750a675ae32..4d5e42cec7a 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index cf5bdd9addf..21488a576d8 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,174 @@ # @objectstack/plugin-sharing +## 18.0.0-next.1 + +### Minor Changes + +- 08b38cb: fix(services)!: the datasource admin, share-link, webhook redeliver and storage families refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by families that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST and the runtime dispatcher already refuse such a session. Four families of routes mounted straight on the host server never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/service-datasource`**: the datasource admin family (`registerDatasourceAdminRoutes`). Its door now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) about the session it already reads, before the anonymous floor and the capability check. + - **`@objectstack/plugin-sharing`**: the share-link family (`registerShareLinkRoutes`). Every route, the two capability-token routes included, is now mounted through one door that refuses a context carrying `authGate`. `SharingServicePlugin`'s resolver now puts the session's posture on that context, as REST does. + - **`@objectstack/plugin-webhooks`**: the redeliver door (`WebhookOutboxPlugin`), on both of its faces: the route the plugin mounts, and the `webhooks` service's `handleRedeliver`. + - **`@objectstack/service-storage`**: the storage family, the two capability-token raw-transfer routes included. Every route is now mounted through one door. The posture is bound where every other storage gate is bound, so both the plugin's own mount and the host door `mountStorageRoutes` carry it. When a gate feature is active, the door reads the session and refuses a held one. When the gate is active but the session cannot be read, the door answers the declared `503` (`SERVICE_UNAVAILABLE`), as REST does. A direct call of `registerStorageRoutes` mounts the family exactly as before, because like the other storage gates this one is bound by the package's composition. No exported declaration changes. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each family's declared error envelope, before the family does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, and an API-key caller is unaffected because it has no session to hold. A CORS preflight is answered as before. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these routes answer them, as on every other protected route. If you mount the storage routes through `mountStorageRoutes`, the gate is bound from the kernel's `auth` service with nothing to configure. If you mount `registerShareLinkRoutes` yourself with your own `contextFromRequest`, set `authGate` on the context it returns for a held session; the door refuses on that field. +- 345d1be: fix(spec,plugin-sharing)!: `sys_record_share` no longer offers a `guest` recipient, which no grant ever honoured (ADR-0138 D8) + + Clause-②: no (narrowing: a declared recipient value no grant honours is refused) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A record share to the anonymous principal was declared in two places and honoured in none. Both declarations are gone: + + - `RecordShareRecipientType` (`@objectstack/spec/contracts`) is now `'user' | 'group' | 'position' | 'unit_and_subordinates'`. Passing `recipientType: 'guest'` to `ISharingService.grant`, or typing a `RecordShare.recipient_type` as `'guest'`, is a compile error. + - The `recipient_type` select on `sys_record_share` (`@objectstack/plugin-sharing`) offers those same four options, and its `guest` label is gone from the four translation bundles. + + Why: the grant refused `guest` (ADR-0078), and no gate reads such a row, because the read and write gates match `recipient_type: 'user'` only. ADR-0090 D11 routes guest access through the guest position and share links, so a record share to the guest had nothing to gain. The sharing-rule vocabulary retired the same recipient in v17. + + What a caller sees now: + + - **The grant door** (`POST /data/:object/:id/shares`, or `grant()` from untyped code). A body naming `recipientType: 'guest'` is still refused with `VALIDATION_FAILED` (`400`), as before. The sentence now says the value was removed and that guest access flows through the guest position and share links. It used to say that sharing rules deliver non-`user` recipients, which was never true of a guest. To let a guest reach a record, use a public form or a share link. + - **The row door** (a system-context engine write: a seed, a plugin, a migration script). A row carrying `recipient_type: 'guest'` is now refused with `VALIDATION_FAILED`, an `invalid_option` error on `recipient_type` that lists the four values a row may carry. Before this change such a write stored a row that looked granted and enforced nothing. + - **Unchanged:** every `user` grant, the other three row values, the sharing-rule service, and the gates. A `guest` row that a system write stored before this release is read by no gate, both before and after. + +### Patch Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 3c1a046: fix: a route's catch-all `500` answers its own generic text, never the caught error's message, and `@objectstack/types` exports the writer that does it, `sendServerFault` + + Clause-②: yes (widening) — `@objectstack/types` gains one export, `sendServerFault`; nothing is removed, renamed or narrowed, and no response field changes shape. + + A declared 5xx does not carry the text of the error it caught: that text can name server internals, such as a path, a host or a table. The REST layer already withheld it. The storage, settings and i18n route families and the share-link routes answer in the nested `{ success: false, error: { code, message } }` envelope and did not: their catch-all `500` sent the caught error's message to the caller. + + - **What a caller now reads.** The same `status` and `code` as before, with the route's own generic text as `error.message`: `Internal error` on the storage and i18n routes, the route's message on the settings routes (`Failed to list manifests`, `Failed to read namespace`, `Failed to write namespace`, `Action failed`), and the route's `Failed to …` message on the share-link routes. On the storage routes this includes the metadata store's outage sentence, which an operator now reads in the server log. + - **What the operator reads.** The original error, stack included, in one `[5xx]` server-fault line. The storage byte routes, which already withheld the text, logged two lines per fault and now log one. A line names the mounted route pattern when the route passes one, never the request URL. + - **Unchanged.** Every `4xx` answer. The settings `SETTINGS_CRYPTO_UNAVAILABLE` refusal, which keeps its prescription. The authorization-store outage `503`, which keeps its declared code and message. The storage `501 NOT_IMPLEMENTED` answers. On the share-link routes, any error that declares its own `status` keeps the envelope it declares. + - **`sendServerFault(res, status, code, message, thrown, request?)`** (new, `@objectstack/types`): writes `message` in the declared envelope, and hands `thrown` to the server-fault log with its stack, as the fault's one line. Use it for an arm that answers a caught fault. Keep `sendError` for a `4xx` and for a `5xx` whose text you wrote for the reader. `request` is the mounted route pattern, never the request URL. +- 37510aa: feat(security): the guest's grants channel — an anonymous request at a guest-entry door holds the sets bound to the `guest` anchor, in the one organization the deployment resolves for it (ADR-0138 E1, D3/D4) + + ADR-0087 disposition: none. No authorable key, accept set or stored shape moves: no spec schema, metadata type, stored row or published response field is added, removed, renamed or re-shaped. A declared binding that was not enforced becomes enforced (ADR-0090 D9, as ADR-0138 D3 amends it). + + **The bindings that start granting.** On the reference apps, none: `app-showcase`, `app-crm`, `app-multi-package` and `app-todo` declare no definition of the `guest` anchor that names a set. `app-showcase` ships `showcase_guest_portal` as a capability an administrator may bind and binds it to no anchor (its `client_portal_user` position is an ordinary position), and `app-crm`'s `guest_portal` is the public form doors' set, which keeps its own posture. No reference app declares a permission set named `guest`, so no grant stops either. A set starts granting only where a deployment holds an environment definition of the `guest` position that names it. + + What changes: + + - **`@objectstack/core`** — the anonymous branch of `resolveAuthzContext` resolves the guest's grants on a face that takes the explicit guest entry (the new `guestEntry` input). It reads the sets the `guest` position's definition names, through the same position-bound read every position uses, and the activation ledger as for any held set. It resolves them only in the organization the guest entry's organization step derives (`resolveGuestOrganization`), which asks ADR-0131 D9's predicate and no second spelling of it. With nothing bound, the guest holds no set and no read is issued. The predicate itself, `resolveSystemWriteOrganization`, moves to `@objectstack/core` so both of its askers share it. New exports: `resolveGuestOrganization`, `resolveSystemWriteOrganization` (with `SystemWriteOrganizationDecision` and `SystemWriteRefusalReason`), and `GuestOrganizationUnresolvedError` with `isGuestOrganizationUnresolvedError` and its constants. + - **`@objectstack/objectql`** — `resolveSystemWriteOrganization` and its two types are re-exported from `@objectstack/core` as the same bindings. Nothing a caller imports changes. + - **`@objectstack/plugin-security`** — the set resolution treats the guest's sets as any requested set, and no longer folds the guest's position into the request. A permission set that merely shares the anchor's name no longer reaches an anonymous caller, and the baseline and `everyone` never did. At the CRUD gate, a guest that reaches a data operation acting in no organization, on a deployment where the organization step refuses, is refused before the operation runs. + - **`@objectstack/plugin-sharing`** — the read filter answers deny-all for the guest on every object that record sharing scopes, before the sharing-model branch: the guest reaches rows only through share links, never through the object's sharing model (ADR-0138 D3 item 6, as the maintainer's ruling C amends it). `externalSharingModel` is still not read for any caller. Share links are redeemed at their own door and are unchanged. + - **`@objectstack/runtime`** — the runtime face, which serves the guest, asks for the guest's grants. + + What an anonymous caller sees at an `authRequired: false` endpoint of `type: 'object_operation'`: + + - **One organization, a set bound to `guest`:** the operation runs as the guest, under that set, in that organization. A read the set grants is admitted and returns no row, whatever the object's sharing model (`private`, `public_read`, `public_read_write` or `controlled_by_parent`): the guest reaches a record only through a share link. A create the set grants lands in that organization with no owner. + - **Nothing bound:** `403 PERMISSION_DENIED`, as before. + - **A walled deployment, several organizations, or none:** `501 NOT_IMPLEMENTED`, naming the missing site binding (ADR-0138 D5), where it used to be `403 PERMISSION_DENIED` with nothing granted. Nothing that was served stops being served. The operator's log names the reason once. + + Unchanged: share links, which still serve an anonymous caller the one record they name, with `publicSharing.eligibility` checked at the mint and at every redemption; the public form doors, which keep their own posture and organization; the flow door, which runs under the flow's own declared posture; every surface that answers an anonymous request `401`; and every signed-in caller. + + Level: `minor` for `@objectstack/core`, because new exported symbols widen its published index; `patch` for the other four, whose exports do not move. +- 28e294f: fix(plugin-sharing, runtime): a share link that has a password and is limited to signed-in users answers an anonymous visitor `401 SIGN_IN_REQUIRED`, whatever password was sent + + Clause-②: no — every request refused before is still refused; only which refusal code an anonymous caller reads changes. + + A share link can carry a password and the `signed_in` audience together. For such a link, the share-link resolve door answered an anonymous visitor from the password before the audience: `NEEDS_PASSWORD` with no password, and `WRONG_PASSWORD` with any password, the correct one included. The console's shared-record page then told a visitor who typed the right password "Wrong password.", and never offered sign-in. + + - **Anonymous caller, `signed_in` link:** `401 SIGN_IN_REQUIRED` with no password, a wrong one or the correct one. The answer is the same for all three, so it never tells an anonymous caller whether a password is right. Once signed in, the visitor is asked for the password as before. + - **Signed-in caller:** unchanged. `401 NEEDS_PASSWORD` with no password, `401 WRONG_PASSWORD` with a wrong one, and the record with the correct one. + - **Unchanged:** links with any other audience (a `public` link with a password still answers `NEEDS_PASSWORD` / `WRONG_PASSWORD` to an anonymous caller), the expired-or-revoked `410 EXPIRED_OR_REVOKED`, the `404 INVALID_OR_EXPIRED` for an unknown token or an object with public sharing switched off, and every link that resolves. + - Both mounts of the resolve door answer alike: plugin-sharing's share-link routes and the runtime dispatcher's share-links domain. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 573926313a1..24d57f770f3 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index 08c99b60d9c..b6ceb1944c4 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,73 @@ # @objectstack/plugin-webhooks +## 18.0.0-next.1 + +### Minor Changes + +- 08b38cb: fix(services)!: the datasource admin, share-link, webhook redeliver and storage families refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by families that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST and the runtime dispatcher already refuse such a session. Four families of routes mounted straight on the host server never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/service-datasource`**: the datasource admin family (`registerDatasourceAdminRoutes`). Its door now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) about the session it already reads, before the anonymous floor and the capability check. + - **`@objectstack/plugin-sharing`**: the share-link family (`registerShareLinkRoutes`). Every route, the two capability-token routes included, is now mounted through one door that refuses a context carrying `authGate`. `SharingServicePlugin`'s resolver now puts the session's posture on that context, as REST does. + - **`@objectstack/plugin-webhooks`**: the redeliver door (`WebhookOutboxPlugin`), on both of its faces: the route the plugin mounts, and the `webhooks` service's `handleRedeliver`. + - **`@objectstack/service-storage`**: the storage family, the two capability-token raw-transfer routes included. Every route is now mounted through one door. The posture is bound where every other storage gate is bound, so both the plugin's own mount and the host door `mountStorageRoutes` carry it. When a gate feature is active, the door reads the session and refuses a held one. When the gate is active but the session cannot be read, the door answers the declared `503` (`SERVICE_UNAVAILABLE`), as REST does. A direct call of `registerStorageRoutes` mounts the family exactly as before, because like the other storage gates this one is bound by the package's composition. No exported declaration changes. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each family's declared error envelope, before the family does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, and an API-key caller is unaffected because it has no session to hold. A CORS preflight is answered as before. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these routes answer them, as on every other protected route. If you mount the storage routes through `mountStorageRoutes`, the gate is bound from the kernel's `auth` service with nothing to configure. If you mount `registerShareLinkRoutes` yourself with your own `contextFromRequest`, set `authGate` on the context it returns for a held session; the door refuses on that field. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-messaging@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 5ca6c050396..722b433a234 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.", "type": "module", diff --git a/packages/qa/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md index 528bc55a954..6002d62cdb3 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,88 @@ # @objectstack/dogfood +## 0.0.48-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [5c9bfb0] +- Updated dependencies [66949b7] +- Updated dependencies [06dbc4e] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [3490bf5] +- Updated dependencies [920bc12] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [08b38cb] +- Updated dependencies [917b1a0] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [e790cfc] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [09ef77c] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [6d96721] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [be50633] +- Updated dependencies [325a92e] + - @objectstack/plugin-security@18.0.0-next.1 + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/plugin-sharing@18.0.0-next.1 + - @objectstack/verify@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/plugin-approvals@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-storage@18.0.0-next.1 + - @objectstack/service-datasource@18.0.0-next.1 + - @objectstack/plugin-webhooks@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/service-messaging@18.0.0-next.1 + - @objectstack/example-crm@4.0.100-next.1 + - @objectstack/example-multi-package@0.0.7-next.1 + - @objectstack/example-showcase@0.3.22-next.1 + - @objectstack/connector-mcp@18.0.0-next.1 + - @objectstack/connector-openapi@18.0.0-next.1 + - @objectstack/connector-rest@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + - @objectstack/mcp@18.0.0-next.1 + - @objectstack/plugin-audit@18.0.0-next.1 + - @objectstack/plugin-email@18.0.0-next.1 + - @objectstack/service-analytics@18.0.0-next.1 + - @objectstack/trigger-record-change@18.0.0-next.1 + - @objectstack/trigger-schedule@18.0.0-next.1 + - @objectstack/plugin-pinyin-search@18.0.0-next.1 + ## 0.0.48-next.0 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index a5e03ffb103..30a61f1c588 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.48-next.0", + "version": "0.0.48-next.1", "private": true, "license": "Apache-2.0", "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", diff --git a/packages/qa/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md index c842209a2ee..4dc5d24cc92 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/downstream-contract +## 0.0.46-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 0.0.46-next.0 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index 8d2cd2e08e9..d232017a5a4 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.46-next.0", + "version": "0.0.46-next.1", "description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).", "license": "Apache-2.0", "private": true, diff --git a/packages/qa/http-conformance/CHANGELOG.md b/packages/qa/http-conformance/CHANGELOG.md index e24be647dde..a30783e213f 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,16 @@ # @objectstack/http-conformance +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies [66949b7] +- Updated dependencies [87e902d] +- Updated dependencies [ed03834] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] + - @objectstack/core@18.0.0-next.1 + ## 0.1.8-next.0 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index c1879a49a34..60d37aab584 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.1.8-next.0", + "version": "0.1.8-next.1", "private": true, "license": "Apache-2.0", "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index bc41e6fb2ee..471c128d87d 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,177 @@ # @objectstack/rest +## 18.0.0-next.1 + +### Minor Changes + +- 8024e12: fix(runtime)!: the runtime dispatcher's by-name `/meta` doors ask the query decision `RestServer` asks, so `?member=` is honoured on the item read and refused where it is not read, never dropped + + `@objectstack/rest` exports one portable decision for the query a by-name `/meta` door accepts, `metaItemQueryRefusal(door, query)`, and the bundle member selector's one reading, `metaMemberSelector(query)`. Both transports call them, so a door's closed set, its sentences and their order are one answer. On a host whose `/meta` is answered by the runtime dispatcher alone (the `@objectstack/hono` `createHonoApp` catch-all), the item read, `PUT`, `/layers` and `/published` declared no closed query set: `GET /meta/email_template/x?member=zh-CN` was answered with the canonical member, a misspelling such as `?memeber=` the same, and `PUT ?member=zh-CN` beside an `en-US` body wrote `en-US`. + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + What each runtime dispatcher door accepts now, and what to do when it refuses. The answers are `RestServer`'s, in the dispatcher's own envelope, `{ success: false, error: { code: 'VALIDATION_ERROR', message } }`, and the message names the refused parameter: + + - `GET /meta/:type/:name` accepts `layers`, `state`, `preview`, `package`, `locale` and `member`, the six names `RestServer`'s item read accepts. `?member=` now reads that member of a bundled type (an `email_template`'s `zh-CN`); a member the bundle does not hold is `404`, and a selector on a type with no members is the protocol's located `400`. A single-valued name supplied twice is refused, and `?member=` beside `?layers=` is refused (the layered view shows the canonical member's layers). Read one member with `?member=` alone. + - `PUT /meta/:type/:name` accepts `package` and `mode`. It takes no `force`: this door never honoured one, and its `409` remedy already says so. Drop `?force=`, and send a body that keeps what the stored item carries. A save is addressed by its body: to save one member of a bundled type, put that member's own discriminator in the body and drop `?member=`. + - `GET /meta/:type/:name/layers` accepts `package`. Read one member with `GET /meta/:type/:name?member=…`. + - `GET /meta/:type/:name/published` accepts no query parameter. Send none. + + A request that sends only names the door reads is answered as before, except that `?member=` on the item read is now honoured. `RestServer`'s item read asks the same decision, and its answers are byte for byte what they were. Every name the `@objectstack/client` SDK and the Studio metadata client send to these doors is accepted, except `force` on a save: the SDK sends it only when its caller passes `force: true`, and Studio only to retry a save after a `409 DESTRUCTIVE_CHANGE`. On a dispatcher-only host that retry already failed with the same `409`, and it now answers the located `400` that names `force`. +- 543b33d: fix(metadata-protocol)!: a metadata save or delete that names no package addresses the row of the one package the item belongs to, and a name two or more packages hold is refused `409 RESOURCE_CONFLICT`; the delete door can name its package (ADR-0048, ADR-0070) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A `sys_metadata` row is keyed by its package (ADR-0048), so one item can hold one row per package. A save that names a package (`?package=` on the REST door, `packageId` in process) writes that package's row. A save that named none always wrote the package-unbound row, even for an item a package owns, and a delete named no package at all: it removed whichever row of the item the store returned first. The draft save already resolved this: a draft that names no package is stored in the package of the item's active row. + + **FROM.** Save an item into a package (`PUT /api/v1/meta/app/my_app?package=com.acme.app`), then save it again without `?package=`: the second save answered `200` "Saved app 'my_app' (env-wide, state=active)" and wrote a second, package-less row beside the package's row. The item read kept serving the package's row, so nothing the save carried took effect, and a console "Disable" reported success while the app stayed enabled. The same held for the API, the CLI and agents, and for an overlay row bound to a code package. A `DELETE /api/v1/meta/app/my_app` of an item two packages hold removed one of the two rows, the other was then served, and the receipt said "it no longer exists". Uninstalling a package, or discarding its drafts, deleted each of its items by name the same way, so it could remove another package's row of the same name and leave its own. + + **TO.** A direct save, or a delete, that names no package resolves its address from the item's rows (the active rows for a save and a delete, the draft rows for a draft discard): + + - **Bound to one package.** The save writes that package's row, exactly as a save naming the package does, and the item read serves it next; the delete removes that row. The receipts name the package: "Saved app 'my_app' (env-wide, package=com.acme.app, state=active)" and "Deleted app 'my_app' (package=com.acme.app) — it no longer exists." Every check on the row the save writes reads that package: its stored lock, a stored credential carried forward, the optimistic lock, and the writable-base rule. The item read that names no package serves that row's `version`, so a read followed by a save or a delete with the served `If-Match` is accepted. + - **Bound to two or more packages.** The save or delete is refused `409 RESOURCE_CONFLICT` and changes nothing. The message names the packages and the remedy: name one with `?package=`. + - **Bound to none.** The save writes, and the delete removes, the package-unbound row, as before, and the receipts read as before. + + The delete door gains the address a save has: `DELETE /api/v1/meta/:type/:name?package=` (`packageId` in process, `DeleteMetaItemRequestSchema.packageId`) removes that package's row and no other; `all` and an empty value name no package, as on the save door. A repeated `?package=` is refused `400`. A delete receipt now says when another row of the item is still stored, naming it, instead of claiming the item is gone or back at its artifact default. Uninstalling a package and discarding its drafts name the package, so each removes only its own rows. + + **Also refused now**, each as the same save naming the package is refused: a package-less save of an item held by a read-only package (a code package's runtime-only item, or an installed `system`/`cloud` package's) answers `422 WRITABLE_PACKAGE_REQUIRED`; and a package-less save asserting a create (`If-None-Match: *`) of an item a package holds answers `409 METADATA_CONFLICT`. + + `packageId: null` in process names no package too, as the request contracts state. The server-side rewrites keep the row they name: `os migrate meta --stored --apply` and the stored flow credential move pass `null` for a package-unbound row they rewrite, and that row is still the one written; package duplication names its target package. + + **Unchanged.** A save or a delete that names a package exactly. A draft save, which keeps its own inheritance. An item a code package ships with no stored row: the package-less save still writes the package-unbound overlay, and the read serves it. Package-less rows already stored beside a package's row stay as they are; none is converted. A package-less delete of such an item removes the package's row and its receipt says the package-less row remains. + + **Remedy.** For an item two packages hold, send the save or the delete with `?package=` naming the package whose row it edits or removes. A receipt now names the package when the write addressed a package's row; a client that compares receipt text should expect `, package=` after `env-wide` on a save and ` (package=)` after the item on a delete. + + `@objectstack/spec`: the `SaveMetaItemRequestSchema.packageId` description now states what an `active` save that names no package writes, where it said the env-local overlay; `DeleteMetaItemRequestSchema` declares the optional `packageId`. `@objectstack/rest`: `package` joins the reset door's closed query set (`META_ITEM_RESET_PARAMS`). +- d9d7edd: fix(runtime,rest,cloud-connection)!: the declared-endpoint fallback, REST's direct-mounted publish and federation routes, and cloud-connection's session doors refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by doors that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST's `enforceAuth` and the runtime dispatcher already refuse such a session. Three groups of doors that resolve a session on their own never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/runtime`**: the declared-endpoint fallback (app-declared `apis:` endpoints, served by the dispatcher plugin's fallback seam). Its policy chain now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) before `authRequired` and before the endpoint's target runs, on every declared endpoint, an `authRequired: false` one included, because the target runs as the caller. While a gate feature is active the chain reads the session for this, and when an active gate cannot read the session the request is answered `503` (`SERVICE_UNAVAILABLE`), as REST answers it. No declared path is handed to the allow-list, so no endpoint can exempt itself through the prefix it sits under. + - **`@objectstack/rest`**: the direct-mounted package publish door (`registerPackageRoutes`) and the external-datasource federation family (`registerExternalDatasourceRoutes`). Each refuses on the `authGate` the REST resolver already puts on the execution context it hands them. + - **`@objectstack/cloud-connection`**: `CloudConnectionPlugin`'s seven session doors (bind, poll, unbind, install, and the three installed-state reads) and all five `MarketplaceInstallLocalPlugin` doors (install, list, uninstall, reseed, purge). Each plugin asks the gate once, where it resolves the session, and every door that resolves one refuses on it. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each door's declared error envelope, before the door does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, an API-key caller is unaffected because it has no session to hold, and an anonymous caller gets the same `401`. `CloudConnectionPlugin`'s status read and `MarketplaceProxyPlugin`'s catalog proxy resolve no session and answer every caller alike, so they are unchanged. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these doors answer them, as on every other protected route. Nothing needs configuring: the posture comes from the kernel's `auth` service, as it does for REST and the dispatcher. +- d45d0e9: fix(runtime)!: the runtime dispatcher's read points refuse a repeated single-valued query name as their `RestServer` twins do, instead of reading one value, a joined value or an array + + The runtime dispatcher's `/meta`, `/ui`, `/i18n`, `/security`, `/packages` and `/data` read points each read a query name as ONE value. None of them guarded against the name arriving twice, because the only door in front of most of them, the `@objectstack/hono` `createHonoApp` catch-all, has always flattened a repeat to its last value. Handed an array, they answered wrongly: `GET /meta/object/:name/state/:field?from=a&from=b` answered a dead end for the joined state `'a,b'`; `_drafts`, `/ui/view` and the `/data` filter slot passed the array into the protocol call; both `/i18n` query-locale arms threw, which surfaced as a `500`; and the `/meta` list, the book tree, `/security/suggested-bindings` and `/packages` answered an empty or unscoped `200`. Each now refuses the repeat before any protocol or service call. + + `@objectstack/rest` publishes the lists and rules both transports read, so the two cannot drift: `META_QUERY_ARITY_DOORS` (the single-valued names of the `/meta` list, book tree, `_drafts` and state machine doors, which `RestServer`'s twins of those doors now read from), the decision `metaQueryArityRefusal(door, query)`, and `assertFilterParamSuppliedOnce`, the `/data` list's filter-slot rule, on the package entry. + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + What changes for you: a request that repeats one of the names below to the runtime dispatcher is now refused `400` in the dispatcher's own envelope, `{ success: false, error: { code, message } }`, instead of being answered with one value, a joined value or an array. The `code` is `VALIDATION_ERROR` with the sentence `RestServer` answers ("The "NAME" query parameter was supplied N times…"), except on the `/data` filter slot, which answers `INVALID_FILTER` with `RestServer`'s repeated-filter sentence. Send each name once. + + - `GET /meta/:type`: `package`, `preview`, `object`, `include`, `id`. + - `GET /meta/book/:name/tree`: `package`. + - `GET /meta/_drafts`: `packageId`, `type`. + - `GET /meta/object/:name/state/:field`: `from`. + - `GET /ui/view/:object`: `type`, when the path names no type. + - `GET /i18n/translations` and `GET /i18n/labels/:object`: `locale`, when the path names no locale. + - `GET /security/suggested-bindings`: `status`, `packageId`. A repeated `status` was already refused `400`, as an unknown status; it now gets the multiplicity sentence. + - `GET /packages`: `status`, `type`, beside `enabled`, which was already refused. + - `GET /data/:object`: every spelling of the filter slot (`filter`, `where` and their aliases, `filters`, `$filter`). Genuinely multi-valued names such as `select` and `expand` are untouched. + + A name sent once is answered as before, and one sent once as a one-element array now reads as that one value everywhere. Where a caller meets this today: `GET /packages` through `@objectstack/plugin-hono-server`, whose adapter already keeps repeats as arrays. Every other point is reached on a shipped host only through `createHonoApp`, which still flattens a repeat to its last value, so until that adapter keeps repeats the refusals are visible only to a caller of `HttpDispatcher.dispatch()` that passes an array. The `@objectstack/client` SDK sends each of these names once. +- 430e5f5: fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs. + + **Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not. + + **FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner): + + - `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`). + - Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items. + - `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`. + - `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched. + - `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read. + - Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence. + - The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator. + + **Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before. + + **The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter. +- d89f491: A stored flow the automation engine refuses is no longer served as running: the flow meta read serves the engine's verdict beside the item as `registration`, an envelope sibling of `lock`, and the flow list serves it as `registrations` keyed by flow name, so a refused flow reads `{ state: 'refused', reason: { code?, message } }` beside its stored `status`, which stays as authored. + + Clause-②: yes (widening) + + - **The shape** (`@objectstack/spec`). `FlowRegistrationSchema` is a closed set: `registered` (the engine holds the flow), `refused` (it refused the stored flow and holds nothing under the name; `reason` carries the engine's own `message` and, when the thrown error set one, its `code`, verbatim), and `unregistered` (it holds nothing and refused nothing: a read during boot, between a save and the engine following it, or after a deliberate unregister). `GetMetaItemResponseSchema` gains `registration` and `GetMetaItemsResponseSchema` gains `registrations`, both optional. New exports: `FlowRegistrationSchema`, `FlowRegistrationRefusalSchema` and the types `FlowRegistration` and `FlowRegistrationRefusal`. `IAutomationService` gains the optional member `getFlowRegistration(name)`. + - **Where the verdict comes from** (`@objectstack/service-automation`). The `kernel:ready` cold-boot bind records each flow it refuses, with what the engine threw, after it withdraws the flow; the `metadata:reloaded` re-sync and the metadata-change sync record a refusal that leaves the name unregistered. A later successful registration of the name, or its withdrawal, ends the record. `AutomationEngine` answers `getFlowRegistration(name)` and takes `recordFlowRefusal(name, error)`. The refusal's warn line, its level and the withdrawal are unchanged. + - **Where it is served** (`@objectstack/metadata-protocol`). The protocol reads the engine through the kernel's service table by name, as it already reads the engine's stored-flow canonicalizer; it does not import the automation service. The field is absent where no automation service declares the member, on every type other than `flow`, on a draft read and on the execution face the engine binds from: absence means no engine judged the flow, never `registered`. The verdict is the engine of the process that answered the read. + - **The default door** (`@objectstack/rest`, `@objectstack/metadata-protocol`, `@objectstack/spec`). `GET /api/v1/meta/flow/:name` on a default deployment (`metadata.enableCache: true`) is answered by the cached item branch, which now carries `registration` too. `getMetaItemCached` returns it beside `data`, read live from its delegate read on every call, and `GetMetaItemCachedResponseSchema` declares it. It is never part of the ETag, so a cached read that carries it is never answered `304 Not Modified`: a judged flow is always served in full. The cached branch's other envelope keys are unchanged. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + - @objectstack/service-package@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 26d36689e5b..234ee1b1e0c 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack REST API Server - automatic REST endpoint generation from protocol", "type": "module", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 66ec567bc53..d50319f0b6e 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,280 @@ # @objectstack/runtime +## 18.0.0-next.1 + +### Minor Changes + +- 8024e12: fix(runtime)!: the runtime dispatcher's by-name `/meta` doors ask the query decision `RestServer` asks, so `?member=` is honoured on the item read and refused where it is not read, never dropped + + `@objectstack/rest` exports one portable decision for the query a by-name `/meta` door accepts, `metaItemQueryRefusal(door, query)`, and the bundle member selector's one reading, `metaMemberSelector(query)`. Both transports call them, so a door's closed set, its sentences and their order are one answer. On a host whose `/meta` is answered by the runtime dispatcher alone (the `@objectstack/hono` `createHonoApp` catch-all), the item read, `PUT`, `/layers` and `/published` declared no closed query set: `GET /meta/email_template/x?member=zh-CN` was answered with the canonical member, a misspelling such as `?memeber=` the same, and `PUT ?member=zh-CN` beside an `en-US` body wrote `en-US`. + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + What each runtime dispatcher door accepts now, and what to do when it refuses. The answers are `RestServer`'s, in the dispatcher's own envelope, `{ success: false, error: { code: 'VALIDATION_ERROR', message } }`, and the message names the refused parameter: + + - `GET /meta/:type/:name` accepts `layers`, `state`, `preview`, `package`, `locale` and `member`, the six names `RestServer`'s item read accepts. `?member=` now reads that member of a bundled type (an `email_template`'s `zh-CN`); a member the bundle does not hold is `404`, and a selector on a type with no members is the protocol's located `400`. A single-valued name supplied twice is refused, and `?member=` beside `?layers=` is refused (the layered view shows the canonical member's layers). Read one member with `?member=` alone. + - `PUT /meta/:type/:name` accepts `package` and `mode`. It takes no `force`: this door never honoured one, and its `409` remedy already says so. Drop `?force=`, and send a body that keeps what the stored item carries. A save is addressed by its body: to save one member of a bundled type, put that member's own discriminator in the body and drop `?member=`. + - `GET /meta/:type/:name/layers` accepts `package`. Read one member with `GET /meta/:type/:name?member=…`. + - `GET /meta/:type/:name/published` accepts no query parameter. Send none. + + A request that sends only names the door reads is answered as before, except that `?member=` on the item read is now honoured. `RestServer`'s item read asks the same decision, and its answers are byte for byte what they were. Every name the `@objectstack/client` SDK and the Studio metadata client send to these doors is accepted, except `force` on a save: the SDK sends it only when its caller passes `force: true`, and Studio only to retry a save after a `409 DESTRUCTIVE_CHANGE`. On a dispatcher-only host that retry already failed with the same `409`, and it now answers the located `400` that names `force`. +- d9d7edd: fix(runtime,rest,cloud-connection)!: the declared-endpoint fallback, REST's direct-mounted publish and federation routes, and cloud-connection's session doors refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by doors that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST's `enforceAuth` and the runtime dispatcher already refuse such a session. Three groups of doors that resolve a session on their own never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/runtime`**: the declared-endpoint fallback (app-declared `apis:` endpoints, served by the dispatcher plugin's fallback seam). Its policy chain now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) before `authRequired` and before the endpoint's target runs, on every declared endpoint, an `authRequired: false` one included, because the target runs as the caller. While a gate feature is active the chain reads the session for this, and when an active gate cannot read the session the request is answered `503` (`SERVICE_UNAVAILABLE`), as REST answers it. No declared path is handed to the allow-list, so no endpoint can exempt itself through the prefix it sits under. + - **`@objectstack/rest`**: the direct-mounted package publish door (`registerPackageRoutes`) and the external-datasource federation family (`registerExternalDatasourceRoutes`). Each refuses on the `authGate` the REST resolver already puts on the execution context it hands them. + - **`@objectstack/cloud-connection`**: `CloudConnectionPlugin`'s seven session doors (bind, poll, unbind, install, and the three installed-state reads) and all five `MarketplaceInstallLocalPlugin` doors (install, list, uninstall, reseed, purge). Each plugin asks the gate once, where it resolves the session, and every door that resolves one refuses on it. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each door's declared error envelope, before the door does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, an API-key caller is unaffected because it has no session to hold, and an anonymous caller gets the same `401`. `CloudConnectionPlugin`'s status read and `MarketplaceProxyPlugin`'s catalog proxy resolve no session and answer every caller alike, so they are unchanged. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these doors answer them, as on every other protected route. Nothing needs configuring: the posture comes from the kernel's `auth` service, as it does for REST and the dispatcher. +- d45d0e9: fix(runtime)!: the runtime dispatcher's read points refuse a repeated single-valued query name as their `RestServer` twins do, instead of reading one value, a joined value or an array + + The runtime dispatcher's `/meta`, `/ui`, `/i18n`, `/security`, `/packages` and `/data` read points each read a query name as ONE value. None of them guarded against the name arriving twice, because the only door in front of most of them, the `@objectstack/hono` `createHonoApp` catch-all, has always flattened a repeat to its last value. Handed an array, they answered wrongly: `GET /meta/object/:name/state/:field?from=a&from=b` answered a dead end for the joined state `'a,b'`; `_drafts`, `/ui/view` and the `/data` filter slot passed the array into the protocol call; both `/i18n` query-locale arms threw, which surfaced as a `500`; and the `/meta` list, the book tree, `/security/suggested-bindings` and `/packages` answered an empty or unscoped `200`. Each now refuses the repeat before any protocol or service call. + + `@objectstack/rest` publishes the lists and rules both transports read, so the two cannot drift: `META_QUERY_ARITY_DOORS` (the single-valued names of the `/meta` list, book tree, `_drafts` and state machine doors, which `RestServer`'s twins of those doors now read from), the decision `metaQueryArityRefusal(door, query)`, and `assertFilterParamSuppliedOnce`, the `/data` list's filter-slot rule, on the package entry. + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + What changes for you: a request that repeats one of the names below to the runtime dispatcher is now refused `400` in the dispatcher's own envelope, `{ success: false, error: { code, message } }`, instead of being answered with one value, a joined value or an array. The `code` is `VALIDATION_ERROR` with the sentence `RestServer` answers ("The "NAME" query parameter was supplied N times…"), except on the `/data` filter slot, which answers `INVALID_FILTER` with `RestServer`'s repeated-filter sentence. Send each name once. + + - `GET /meta/:type`: `package`, `preview`, `object`, `include`, `id`. + - `GET /meta/book/:name/tree`: `package`. + - `GET /meta/_drafts`: `packageId`, `type`. + - `GET /meta/object/:name/state/:field`: `from`. + - `GET /ui/view/:object`: `type`, when the path names no type. + - `GET /i18n/translations` and `GET /i18n/labels/:object`: `locale`, when the path names no locale. + - `GET /security/suggested-bindings`: `status`, `packageId`. A repeated `status` was already refused `400`, as an unknown status; it now gets the multiplicity sentence. + - `GET /packages`: `status`, `type`, beside `enabled`, which was already refused. + - `GET /data/:object`: every spelling of the filter slot (`filter`, `where` and their aliases, `filters`, `$filter`). Genuinely multi-valued names such as `select` and `expand` are untouched. + + A name sent once is answered as before, and one sent once as a one-element array now reads as that one value everywhere. Where a caller meets this today: `GET /packages` through `@objectstack/plugin-hono-server`, whose adapter already keeps repeats as arrays. Every other point is reached on a shipped host only through `createHonoApp`, which still flattens a repeat to its last value, so until that adapter keeps repeats the refusals are visible only to a caller of `HttpDispatcher.dispatch()` that passes an array. The `@objectstack/client` SDK sends each of these names once. +- 01d5034: fix(runtime)!: the notification inbox, API-key mint and share-link management dispatcher domains decide an anonymous request once, as each domain's first statement, so an anonymous caller is answered 401 whatever the deployment carries + + ADR-0138 D2 decides that everything off its closed list of six door classes answers 401, and that "the decision is taken once per domain, as the domain's first statement, never surface by surface". Three dispatcher domains decided it per face, below another answer: `/notifications` below its inbox capability probe, `/keys` below its method check, and `/share-links` inside its management faces, below the share-link service probe. Each now opens with the shared `shouldDenyAnonymous` decision, before any service is resolved and before any face is chosen, so a face added to one of them later is refused to an anonymous caller from the day it lands. + + Clause-②: no (narrowing) + + + + **BREAKING** (a refusal moved ahead of the answers it used to follow), shipped as `minor` under the launch-window convention for breaking changes. + + What an unauthenticated caller is answered now, on each domain: + + - `/notifications`: `401 UNAUTHENTICATED` on every face. On a deployment with no inbox (a send-only notification provider, or none) the anonymous answer was the `501` capability answer; it is now the 401, and a signed-in caller there still gets the 501. + - `/keys`: `401 UNAUTHENTICATED` for every verb. A verb other than `POST` used to be answered `405` to an anonymous caller; a signed-in caller still gets the 405. + - `/share-links`: `401 UNAUTHENTICATED` on the create, list and revoke faces and on every path the domain does not serve. On a deployment without the share-link service the anonymous answer was `501`; a signed-in caller there still gets the 501. The two token faces, `GET /share-links/:token/resolve` and `GET /share-links/:token/messages`, are unchanged: the capability token admits them (ADR-0138 D2 class 2). + + The refusal is the dispatcher's standard envelope, `{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`, with the shared message every other dispatcher domain answers with, so a client reads one anonymous refusal on all of them. Nothing a signed-in caller is served changes, and on the standalone server the booted showcase already answered 401 on the faces it mounts. +- f24574a: fix(runtime)!: the `/ui` and `/data` dispatcher domains decide an anonymous request once, as each domain's first statement, so an unauthenticated caller is answered 401 on every face they serve + + ADR-0138 D2 decides that everything off its closed list of six door classes answers 401, and that "the decision is taken once per domain, as the domain's first statement, never surface by surface". The rendered-view domain `/ui` and the CRUD and query domain `/data` made no anonymous decision of their own: an unauthenticated caller was answered by whichever face it named. Each now opens with the shared `shouldDenyAnonymous` decision, before the path is parsed and before any service is resolved, so a face added to either later is refused to an anonymous caller from the day it lands. No door class D2 lists runs through either domain. + + Clause-②: no (narrowing) + + + + **BREAKING** (a refusal moved ahead of the answers it used to follow), shipped as `minor` under the launch-window convention for breaking changes. + + What an unauthenticated caller is answered now, wherever these dispatcher domains serve the request. Where `@objectstack/rest` is mounted it serves the same paths itself and already answered 401, so the standalone server's answers do not move. + + - `/data`: `401 UNAUTHENTICATED` on every read and write face, and on every path or verb the domain does not serve. The answer used to come from the face: the CRUD gate's `403 PERMISSION_DENIED` for the guest, a `400` for a request naming no object, a `428` for an unresolved environment on a multi-tenant host. A CORS preflight keeps its pass. + - `/ui`: `401 UNAUTHENTICATED` on every face, and on every path the domain does not serve. + + The refusal is the dispatcher's standard envelope, `{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`, with the shared message every other dispatcher domain answers with. Nothing a signed-in caller is served changes. The decision is asked about the caller's user id, as the `/notifications` and `/keys` floors ask it: `dispatch()` resolves identity for every request, so no caller on the wire is a system context, and an in-process call of either handler that passes a system context with no user id is refused too. +- 430e5f5: fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs. + + **Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not. + + **FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner): + + - `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`). + - Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items. + - `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`. + - `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched. + - `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read. + - Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence. + - The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator. + + **Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before. + + **The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter. +- 5d3018d: fix(runtime)!: the runtime dispatcher's ADR-0069 gate answers `503` when an active gate cannot read the session, as REST does + + Clause-②: no (narrowing) — an active gate's outage now refuses what it served; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled. While a gate feature is active, the runtime dispatcher (`HttpDispatcher`) reads the session again at its own gate to ask that question. When that read failed, because the auth service could not answer, the dispatcher took the failure for "no session" and served the request, so a session the policy holds passed the gate on every dispatcher-served route for as long as the outage lasted. REST, and the dispatcher plugin's declared-endpoint fallback, answer the same window `503`. + + - **Refused now:** with a gate feature active, a request whose gate read fails (the session read rejects, or the auth service cannot hand back its session API) is answered `503` with the code `SERVICE_UNAVAILABLE`, in the dispatcher's existing error envelope. It is the authorization-store outage (`AuthzStoreUnavailableError`, `@objectstack/core`, read object `auth_gate`) that REST raises for the same window, and the route's own handler does not run. + - **Unchanged:** with no gate feature active the gate makes no read at all, and a deployment whose gate probe itself fails is treated as having no active gate, as on REST. Whenever the read answers, the answers are as before: a held session gets the gate's `403` with its own `code`, and a session the policy does not hold, an API-key caller and an anonymous caller are served as before. + + What changes for you. Only deployments with password expiry or enforced MFA turned on are affected, and only while their auth service cannot answer a session read: requests on routes the runtime dispatcher serves now answer a retriable `503` instead of being served. Retry once the auth service answers. Nothing needs configuring. + +### Patch Changes + +- d4aaeaf: fix(runtime): cloning a flow, and publishing a package's drafts, no longer fail on a `protection` block left in a row stored before the metadata save door refused it + + Clause-②: no + + Two runtime doors re-save a body the server builds from a stored row: the flow clone (`POST /api/v1/automation/:name/clone`), and the visibility flip of `POST /api/v1/packages/:id/publish-drafts`, which re-saves each of the package's apps still stored with `_unpublished: true`. The metadata save door now refuses a `protection` block with `400 VALIDATION_ERROR` (entry `23096-save-door-protection`). A row stored before that refusal can still carry a block, and both doors copied it into the body they saved. + + **FROM.** A clone of such a flow answered `400 VALIDATION_ERROR` about a block the admin never sent. The flip of such an app was refused the same way: the publish still answered `200`, the app stayed `_unpublished`, and the refusal was reported only under `unhideError`. + + **TO.** Both doors drop the `protection` block from the body they save, as the permission-set clone already does. The clone and the flip save, and the saved row carries no block. Nothing else in either body changes, and neither door states a `source`, so every other save-door check still applies. The block never bound anything: only a code package's loader translates `protection` into a lock, so dropping it changes no lock and no verdict. + + **Unchanged.** A flow or an app with no block is saved exactly as before. +- 917b1a0: fix(plugin-auth)!: IdentityObjectsPlugin keeps the single posture's Default Organization at boot + + Clause-②: no (narrowing) — it restores ADR-0131 D3's Default Organization boot invariant, which `default-organization-invariant.ts` states as "[ADR-0131 D3] The Default Organization as a BOOT INVARIANT under the single posture", in the kernel that mounts `createIdentityObjectsPlugin()` without `AuthPlugin`; no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A kernel that mounts `createIdentityObjectsPlugin()` without `AuthPlugin` now holds its Default Organization under the `single` tenancy posture, so its seeds and system writes on tenant-scoped objects are stamped with it instead of refused with "the install holds no organization at all". + + - **Refused now:** a composition that mounts `createIdentityObjectsPlugin()` and publishes its driver after `bootstrap()` (the identity objects' `start()` reads `sys_organization` under `single`, so the boot fails with `No driver available for object 'sys_organization'`); and a kit that inserts its own organization row after boot, which now holds two organizations under `single`, so every system write that names no organization is refused as ambiguous (ADR-0131 D9). + - **Unchanged:** the walled postures (`group`, `isolated`), where `start()` creates nothing; the refusal at `init()` of `createIdentityObjectsPlugin()` mounted beside `AuthPlugin`; every composition that publishes its driver before boot; and a declaration-only boot (`os migrate`), which suppresses `start()`. + + - **Why.** ADR-0131 D3 makes the Default Organization a boot invariant under `single`: it owns every row. Registering `sys_organization` is what turns the engine's and the seed loader's zero-organization branch from "unstamped" into "refused" (D9). `IdentityObjectsPlugin` registers that object, but only `AuthPlugin.start()` created the organization, so the reduced kernel the plugin exists for (ObjectQL, `createIdentityObjectsPlugin()`, `SecurityPlugin`) refused every seed row of an organization-owned object. + - **What changed in `@objectstack/plugin-auth`.** `IdentityObjectsPlugin` gains a `start()` that, under `single`, finds or creates the Default Organization with the same invariant `AuthPlugin.start()` runs. It is idempotent, and a failure fails the boot. The posture is the `tenancy` service's when one is registered, otherwise the requested one (`OS_TENANCY_POSTURE`). Under `group` and `isolated` it does nothing. Nothing new is exported, and the plugin is still refused beside `AuthPlugin`. + - **What changed in `@objectstack/runtime`.** `AppPlugin` orders `com.objectstack.auth.identity-objects` ahead of itself, as it already ordered `com.objectstack.auth`, so the organization exists before the inline seed loads, wherever a host lists the two plugins. + - **If your test kit mounts `createIdentityObjectsPlugin()`.** Its kernel now reads and writes `sys_organization` during `bootstrap()` under `single`. Publish the driver before the boot (a `driver.default` service, or a datasource plugin), not with `registerDriver()` after it. Otherwise the boot fails with `No driver available for object 'sys_organization'`. A kit that inserted its own organization row to stand in for the missing one now holds two, and a system write that names no organization is refused as ambiguous; drop that insert and read the Default Organization (`slug: 'default'`) the boot created. +- 37510aa: feat(security): the guest's grants channel — an anonymous request at a guest-entry door holds the sets bound to the `guest` anchor, in the one organization the deployment resolves for it (ADR-0138 E1, D3/D4) + + ADR-0087 disposition: none. No authorable key, accept set or stored shape moves: no spec schema, metadata type, stored row or published response field is added, removed, renamed or re-shaped. A declared binding that was not enforced becomes enforced (ADR-0090 D9, as ADR-0138 D3 amends it). + + **The bindings that start granting.** On the reference apps, none: `app-showcase`, `app-crm`, `app-multi-package` and `app-todo` declare no definition of the `guest` anchor that names a set. `app-showcase` ships `showcase_guest_portal` as a capability an administrator may bind and binds it to no anchor (its `client_portal_user` position is an ordinary position), and `app-crm`'s `guest_portal` is the public form doors' set, which keeps its own posture. No reference app declares a permission set named `guest`, so no grant stops either. A set starts granting only where a deployment holds an environment definition of the `guest` position that names it. + + What changes: + + - **`@objectstack/core`** — the anonymous branch of `resolveAuthzContext` resolves the guest's grants on a face that takes the explicit guest entry (the new `guestEntry` input). It reads the sets the `guest` position's definition names, through the same position-bound read every position uses, and the activation ledger as for any held set. It resolves them only in the organization the guest entry's organization step derives (`resolveGuestOrganization`), which asks ADR-0131 D9's predicate and no second spelling of it. With nothing bound, the guest holds no set and no read is issued. The predicate itself, `resolveSystemWriteOrganization`, moves to `@objectstack/core` so both of its askers share it. New exports: `resolveGuestOrganization`, `resolveSystemWriteOrganization` (with `SystemWriteOrganizationDecision` and `SystemWriteRefusalReason`), and `GuestOrganizationUnresolvedError` with `isGuestOrganizationUnresolvedError` and its constants. + - **`@objectstack/objectql`** — `resolveSystemWriteOrganization` and its two types are re-exported from `@objectstack/core` as the same bindings. Nothing a caller imports changes. + - **`@objectstack/plugin-security`** — the set resolution treats the guest's sets as any requested set, and no longer folds the guest's position into the request. A permission set that merely shares the anchor's name no longer reaches an anonymous caller, and the baseline and `everyone` never did. At the CRUD gate, a guest that reaches a data operation acting in no organization, on a deployment where the organization step refuses, is refused before the operation runs. + - **`@objectstack/plugin-sharing`** — the read filter answers deny-all for the guest on every object that record sharing scopes, before the sharing-model branch: the guest reaches rows only through share links, never through the object's sharing model (ADR-0138 D3 item 6, as the maintainer's ruling C amends it). `externalSharingModel` is still not read for any caller. Share links are redeemed at their own door and are unchanged. + - **`@objectstack/runtime`** — the runtime face, which serves the guest, asks for the guest's grants. + + What an anonymous caller sees at an `authRequired: false` endpoint of `type: 'object_operation'`: + + - **One organization, a set bound to `guest`:** the operation runs as the guest, under that set, in that organization. A read the set grants is admitted and returns no row, whatever the object's sharing model (`private`, `public_read`, `public_read_write` or `controlled_by_parent`): the guest reaches a record only through a share link. A create the set grants lands in that organization with no owner. + - **Nothing bound:** `403 PERMISSION_DENIED`, as before. + - **A walled deployment, several organizations, or none:** `501 NOT_IMPLEMENTED`, naming the missing site binding (ADR-0138 D5), where it used to be `403 PERMISSION_DENIED` with nothing granted. Nothing that was served stops being served. The operator's log names the reason once. + + Unchanged: share links, which still serve an anonymous caller the one record they name, with `publicSharing.eligibility` checked at the mint and at every redemption; the public form doors, which keep their own posture and organization; the flow door, which runs under the flow's own declared posture; every surface that answers an anonymous request `401`; and every signed-in caller. + + Level: `minor` for `@objectstack/core`, because new exported symbols widen its published index; `patch` for the other four, whose exports do not move. +- 28e294f: fix(plugin-sharing, runtime): a share link that has a password and is limited to signed-in users answers an anonymous visitor `401 SIGN_IN_REQUIRED`, whatever password was sent + + Clause-②: no — every request refused before is still refused; only which refusal code an anonymous caller reads changes. + + A share link can carry a password and the `signed_in` audience together. For such a link, the share-link resolve door answered an anonymous visitor from the password before the audience: `NEEDS_PASSWORD` with no password, and `WRONG_PASSWORD` with any password, the correct one included. The console's shared-record page then told a visitor who typed the right password "Wrong password.", and never offered sign-in. + + - **Anonymous caller, `signed_in` link:** `401 SIGN_IN_REQUIRED` with no password, a wrong one or the correct one. The answer is the same for all three, so it never tells an anonymous caller whether a password is right. Once signed in, the visitor is asked for the password as before. + - **Signed-in caller:** unchanged. `401 NEEDS_PASSWORD` with no password, `401 WRONG_PASSWORD` with a wrong one, and the record with the correct one. + - **Unchanged:** links with any other audience (a `public` link with a password still answers `NEEDS_PASSWORD` / `WRONG_PASSWORD` to an anonymous caller), the expired-or-revoked `410 EXPIRED_OR_REVOKED`, the `404 INVALID_OR_EXPIRED` for an unknown token or an object with public sharing switched off, and every link that resolves. + - Both mounts of the resolve door answer alike: plugin-sharing's share-link routes and the runtime dispatcher's share-links domain. +- Updated dependencies [dac99c2] +- Updated dependencies [5c9bfb0] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [c6139f2] +- Updated dependencies [3490bf5] +- Updated dependencies [920bc12] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [08b38cb] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [78c13bd] +- Updated dependencies [9543e9c] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [e790cfc] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [09ef77c] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [2de9c03] +- Updated dependencies [5b68524] +- Updated dependencies [5e725b8] +- Updated dependencies [6d96721] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [be50633] +- Updated dependencies [325a92e] + - @objectstack/plugin-security@18.0.0-next.1 + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-protocol@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/rest@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-i18n@18.0.0-next.1 + - @objectstack/service-datasource@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/driver-memory@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + - @objectstack/driver-sqlite-wasm@18.0.0-next.1 + - @objectstack/driver-turso@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + - @objectstack/service-cluster@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5af476586df..d99667c6fea 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack Core Runtime & Query Engine", "type": "module", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 90db724c50a..e10607b2e78 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/sdui-parser +## 18.0.0-next.1 + +No changes in this release. + ## 18.0.0-next.0 No changes in this release. diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 34641b72d50..06a576e15b4 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index de992bd652d..3835bce2df3 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog — @objectstack/service-analytics +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 2394e25b83a..6187be9c3f3 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Analytics Service for ObjectStack — implements IAnalyticsService with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory)", "type": "module", diff --git a/packages/services/service-automation/CHANGELOG.md b/packages/services/service-automation/CHANGELOG.md index 4bf3a6179b5..4c44540e86d 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,160 @@ # @objectstack/service-automation +## 18.0.0-next.1 + +### Major Changes + +- 7958bdf: No flow position reads the single-brace `{…}` template dialect any more, and its resolver is deleted (ADR-0032 Sequencing 6: "delete the single-brace resolver"). `AutomationEngine.evaluateCondition` evaluates every condition as CEL: a condition carrying a `{var}` hole is refused as the CEL brace trap, naming its source, and a `template` or `flow` envelope is refused before evaluation, where the deleted path substituted the variables and compared the text. A literal in a value slot is handed on as written: a `Date` in a code-defined flow reaches the write as the `Date`, where the deleted interpolator wrote `{}`. + + Clause-②: no (narrowing) + + + + **BREAKING**: a narrowing of a published runtime method and of published lint exports, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`). + + **Why.** ADR-0032 Decision 3 keeps one template delimiter, `{{ }}`, and its Sequencing 6 deletes the single-brace resolver. The earlier changes on this line retired the dialect position by position — every value slot, the text slots, each CRUD filter operand — so the resolver's last readers were two calls that saw literals only and a condition path that only a direct call reached. + + **What changes.** + + - **Conditions.** `AutomationEngine.evaluateCondition(condition, variables, context?)` reads every condition as CEL — bare text, an envelope with `dialect: 'cel'`, or one naming no dialect. The template path it kept for a string carrying a `{var}` hole is deleted: such a string is refused as a CEL fault that names its source and says not to wrap field references in `{…}`, and an envelope with `dialect: 'template'` or `dialect: 'flow'` is refused before evaluation. `registerFlow` already refused both at every condition slot (a start node's `condition`, an edge `condition`, a `decision` branch's `expression`, a screen field's `visibleWhen`), so no registered flow changes. What changes is a direct caller: a plugin calling the method, or `@objectstack/verify`'s `stack.automation.evaluateCondition`, which hands the condition over as given. + - **Value-slot literals.** A literal in a value slot — a `fields`, `input` or `templateData` value, an `assignment` value in any of its three shapes, a `notify` recipient, an `http` `body`, a screen's `defaults`, `defaultValue` or `recordId` — is handed on as a copy of what was written (arrays and plain objects copied, every other value as it is). The deleted interpolator walked every object's own entries, so a `Date` written in a code-defined flow reached the write as `{}`; it reaches it as the `Date`. + - **`@objectstack/lint`.** `validateFlowTemplatePaths` reads the `{{ }}` holes of a node's strings and no single-brace token; a CRUD node's `filter` is not scanned, since each operand is a value slot that renders no template. So `flow-template-unknown-field` and `flow-template-lookup-traversal` report only `warning` now: their `error` at a filter operand, and every finding on a single-brace token, are gone, and the value-slot and text-slot judges refuse those tokens at the same doors as `expression-invalid`. Messages and hints spell a hole `{{ record.x }}`. `flow-double-brace-interpolation` and `flow-bare-dollar-reference`, at a string outside every value slot, text slot and declared literal (a plugin node's own config, a `connector_action` input), say the value is read as written, where they prescribed a `{var}` token nothing reads. The lint's mirror of the retired grammar is removed with its exports. + - **`@objectstack/spec`.** The `FlowSchema` example's `connector_action` input holds a literal, since nothing reads a token there. The upgrade guide's step-18 text-slot entry says no other flow string reads the single-brace dialect, where it said every other one kept it, and its value-slot entry names every value slot the retirement reached on this line, where it named only the `assignment` values and the CRUD `fields` map. + + ## FROM → TO + + | you wrote | write instead | what changes | + |:--|:--|:--| + | `engine.evaluateCondition('{amount} > 100', vars)` | `engine.evaluateCondition('amount > 100', vars)` | the template substituted text and compared it; CEL compares the values and fails the run on an absent variable | + | `"{status} == 'open'"`, `'{status} == open'` | `"status == 'open'"` | a string literal is quoted | + | `{ dialect: 'template', source: "{flag} == true" }`, `dialect: 'flow'` | `{ dialect: 'cel', source: 'flag == true' }` | — | + | `classifyFlowTemplateToken(inner)` (`@objectstack/lint`) | `valueSlotTemplateRefusals(value)` / `textSlotTemplateRefusal(text)` (`@objectstack/spec/automation`), which refuse a token with the spelling that replaces it | the grammar it classified is read by nothing | + | `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` | the CEL stdlib: `isoDate(today())`, `isoDatetime(now())`, `round`, `floor`, `ceil`, `abs`, `min`, `max` | — | + | `FlowTemplateTokenVerdict` | nothing: the judges' reading of the retired grammar is internal to `@objectstack/spec` | — | + + **The one-line fix: write a condition as CEL, without `{…}` around a reference.** + + **Who is affected, measured.** In this repository at the base: no braced condition and no `template` or `flow` condition envelope in `examples/`, the docs or any package source; the deleted path's own pins in `@objectstack/service-automation` (`engine.test.ts`, `nested-region-parity.test.ts`) and `@objectstack/verify`'s door test are re-pinned to the refusal here. No authored `Date` literal in a flow was found; a compiled artifact serialises one to text, so only a flow handed to `registerFlow` in code carried one. Neither repository imports the removed lint exports. hotcrm at `0f84c9c` carries no braced condition and no `connector_action` input; its flows still author the single-brace dialect at value and text positions earlier changes on this line refuse, which its own upgrade migrates. + + ### The kit + + - **The engine.** `AutomationEngine.evaluateCondition` (`@objectstack/service-automation`): the template path, its hole discriminator and its text comparison are deleted. The value-slot resolvers' literal arm and the `assignment` executor copy a literal (`copyLiteral`, `builtin/value-slot-map.ts`). The single-brace interpolator (`interpolate`, `interpolateString`, `resolveToken`, its function table and `FlowExpressionFunctionError`), which the package did not export, is deleted with its tests. + - **The lint.** `validateFlowTemplatePaths` and the two wrong-syntax hints (`lint-flow-patterns.ts`); `classifyFlowTemplateToken`, `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` and `FlowTemplateTokenVerdict` are removed. + - **The verify handle.** `stack.automation.evaluateCondition`'s documentation states the engine's reading: CEL, a `{var}` hole refused. + - **ADR-0087.** No new entry: the step-18 D3 entries `flow-value-slot-template-dialect-refused` and `flow-text-slot-single-brace-refused` record the retirement. + +### Minor Changes + +- 572ecfb: fix: `os migrate meta --stored` lists every stored row that protocol 18 refuses and no conversion rewrites. Before, the preview counted each such row as already on protocol. + + Clause-②: yes (widening: `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec`, `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` and `StoredFlowCanonicalization.todos?` in `@objectstack/metadata-protocol`, each a new output member; nothing narrows, and no door accepts or refuses anything it did not before) + + Three step-18 retirements refuse a stored shape that has no lossless rewrite. Upgrading a v17 database left those rows refused at load or at boot, and the stored preview read them as `canonical`. Each shape now has exactly one fate: **refused and listed**. The preview and `--apply` print one `TODO` line per site under its row. The line names the step-18 entry that refuses the shape, the site, and the choice the owner has to make. Nothing is rewritten, and the run's verdict does not change, because no run of the pass could clear these rows. + + **#23269, a flow value slot carrying a `{…}` token, including a `get_record` / `update_record` / `delete_record` filter operand: refuse and list.** A whole-path token maps mechanically to `{ dialect: 'cel', source: '' }`, but the rewrite is not lossless. A token the template resolved to nothing (an erased condition, so the node was refused) is a CEL fault on an absent root, and a flow variable named like a query placeholder used to shadow it. ADR-0087 D2 conversions run on the load path and must be lossless, so this one is not a D2. The chain also has no operator-applied rewrite of stored rows separate from the load path: `--stored` writes back the read path's own pass, and `--from 17` rewrites authored sources, not rows. So the flow is listed by flow, node and path (`flow-value-slot-template-dialect-refused`), and `registerFlow` goes on refusing it until the value is rewritten as its CEL envelope. A flow already in the CEL form, or using a query placeholder such as `{current_user_id}`, is not listed. + + **#23270, a `controlled_by_parent` object without exactly one required `master_detail`: refuse and list, by object (`cbp-master-relation-tiers-retired`).** No shape here converts losslessly. The upgrade never picks between two masters, either both required or neither. An explicit `required: false` is a contradiction the builder refuses. Turning a required `lookup` into a `master_detail` changes what deleting the master does. An object with no master has nothing to convert. Leaving `controlled_by_parent` changes who can read the rows, so that is the owner's choice and never a silent rewrite. The listing reads the same predicate `ObjectSchema` refuses with, so the preview and the parse refuse one set. The one lossless shape, a single `master_detail` with `required` omitted, is still converted by `cbp-master-detail-required-defaulted` and is not listed. + + **#23271, a view whose `data` binds the retired view-level API source: refuse and list, by view (`view-data-api-provider-retired`).** There is no mechanical move. The replacement is a page component's `dataSource`, so a page must exist to carry it. Deleting the view's `data` would make the view query its object instead. Views in every stored body spelling are covered, and so are an object's own `listViews`. The line shows the request URL and method and withholds every other request key, so a credential stored in a view's headers never reaches the report. A view on the `object`, `value` or `schema` provider is not listed. + + How it is wired: `applyConversions` reports these shapes through its existing `onTodo` sink, after the conversions run. Each TODO's `conversionId` is the id of the D3 entry that refuses the shape, so the published `ConversionTodoNotice.conversionId` now names either a conversion or a D3 entry. The published flow-canonicalization result gains one member, a widening in three packages: + + - `@objectstack/spec`: the contract `IAutomationService.canonicalizeStoredFlow(...)` declares an optional `todos?: ConversionTodoNotice[]` beside `notices` and `conflicts`. An implementation that reports none still conforms. + - `@objectstack/service-automation`: `AutomationEngine.canonicalizeStoredFlow(...)` returns `todos`. `registerFlow` ignores them, because its own refusal names the same sites. + - `@objectstack/metadata-protocol`: `StoredFlowCanonicalization` gains the matching optional `todos?`, and the stored pass lists a flow row's TODOs the way it lists any other row's. + + The authoring funnel and the read seams pass no sink, so nothing changes for them. +- 24e064f: fix(objectql,service-messaging,service-automation,plugin-approvals)!: a notification carries the organization of what it is about, and one no organization owns is refused instead of stored with `organization_id` NULL + + Clause-②: no (narrowing) — notification rows gain the organization they should already carry; what narrows is an emit that names no organization on an install where none is derivable, which the engine now refuses where it used to store the event row with `organization_id` NULL. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + Every notification emitted from a system-context run — a scheduled flow, a record change written by a system context, an approval node — was stored in `sys_notification` with `organization_id` NULL, so reports and cleanup that filter by organization did not see it. Each emit also logged a `[notify] no organization in scope` warning. On an install holding several organizations, the deliveries of the same emit were refused while the flow step reported success, so nothing reached the inbox. + + **Who owns a notification now:** + + - **The run's organization.** A run that carries one emits exactly as before. + - **Else the record it is about.** A `notify` node in a run with no organization reads the organization of the record the notification is about. That is the declared click-through record (`sourceObject` + `sourceId`), else the run's trigger record. It is read from the stored row by id, never from the record snapshot the run carries. + - **Approvals.** Every approval notification carries its request's organization: `approval.requested`, reminders, comments, send-backs, reassignments, escalations and out-of-office notices. + - **Else the owner the engine derives for a system write** (ADR-0131 D9). `sys_notification` is now in the platform-object tenancy ledger, so an emit that names no organization gets the install's sole organization under the `single` posture. + - **The rows below follow the event.** `emit()` stamps the stored event row's organization on its delivery, inbox row and receipt, on both the inline and the outbox path. + + **Refused now:** where no organization is derivable (a walled posture, or a `single` install holding more than one organization), an emit that names none is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`. Nothing is written for it. A `notify` step that hits the refusal fails with `notify failed: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED: …` instead of succeeding. `MessagingService.emit()` rejects with the engine's error for any other producer. + + **What to do when it refuses:** give the notification an owner. Name the record it is about on the `notify` node (`sourceObject` + `sourceId`), declare the scheduled flow's acting organization (`organization` on its start node), or pass `organizationId` to `emit()` from your own code. + + The `[notify] no organization in scope` warning is gone, because the NULL row it predicted is no longer written. Rows already stored with `organization_id` NULL are not rewritten here. The v18 organization-ownership ceremony attributes them from their source record. +- d89f491: A stored flow the automation engine refuses is no longer served as running: the flow meta read serves the engine's verdict beside the item as `registration`, an envelope sibling of `lock`, and the flow list serves it as `registrations` keyed by flow name, so a refused flow reads `{ state: 'refused', reason: { code?, message } }` beside its stored `status`, which stays as authored. + + Clause-②: yes (widening) + + - **The shape** (`@objectstack/spec`). `FlowRegistrationSchema` is a closed set: `registered` (the engine holds the flow), `refused` (it refused the stored flow and holds nothing under the name; `reason` carries the engine's own `message` and, when the thrown error set one, its `code`, verbatim), and `unregistered` (it holds nothing and refused nothing: a read during boot, between a save and the engine following it, or after a deliberate unregister). `GetMetaItemResponseSchema` gains `registration` and `GetMetaItemsResponseSchema` gains `registrations`, both optional. New exports: `FlowRegistrationSchema`, `FlowRegistrationRefusalSchema` and the types `FlowRegistration` and `FlowRegistrationRefusal`. `IAutomationService` gains the optional member `getFlowRegistration(name)`. + - **Where the verdict comes from** (`@objectstack/service-automation`). The `kernel:ready` cold-boot bind records each flow it refuses, with what the engine threw, after it withdraws the flow; the `metadata:reloaded` re-sync and the metadata-change sync record a refusal that leaves the name unregistered. A later successful registration of the name, or its withdrawal, ends the record. `AutomationEngine` answers `getFlowRegistration(name)` and takes `recordFlowRefusal(name, error)`. The refusal's warn line, its level and the withdrawal are unchanged. + - **Where it is served** (`@objectstack/metadata-protocol`). The protocol reads the engine through the kernel's service table by name, as it already reads the engine's stored-flow canonicalizer; it does not import the automation service. The field is absent where no automation service declares the member, on every type other than `flow`, on a draft read and on the execution face the engine binds from: absence means no engine judged the flow, never `registered`. The verdict is the engine of the process that answered the read. + - **The default door** (`@objectstack/rest`, `@objectstack/metadata-protocol`, `@objectstack/spec`). `GET /api/v1/meta/flow/:name` on a default deployment (`metadata.enableCache: true`) is answered by the cached item branch, which now carries `registration` too. `getMetaItemCached` returns it beside `data`, read live from its delegate read on every call, and `GetMetaItemCachedResponseSchema` declares it. It is never part of the ETag, so a cached read that carries it is never answered `304 Not Modified`: a judged flow is always served in full. The cached branch's other envelope keys are unchanged. +- dc39326: fix: `os migrate meta --stored` lists the two families of stored rows it still read as on protocol: a page component whose `data` prop binds the retired view-level API source, and every other site `registerFlow` refuses in a stored flow the parse accepts. + + Clause-②: yes (widening: `ConversionTodoNotice.conversionId` in `@objectstack/spec` may now name `registerFlow`, the refusing door; `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec` and `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` now also carry each site flow registration refuses; the `onTodo` sink of `applyConversions` now also receives a page component bound to the retired API source; nothing narrows, and no door accepts or refuses anything it did not before) + + **A page component bound to the retired view-level API source: refuse and list, by page and component (`view-data-api-provider-retired`).** `object-grid`, `object-map`, `object-gantt` and `object-tree` take a view data source in their `data` prop, so the step-18 retirement of the `provider: 'api'` arm reaches them through the same schema. The listing judges each stored page component with its own props row, the one the component-props gate parses, and lists it only when that row refuses the binding with the retirement's prescription; a component whose `data` is a row array, a custom type and a component's own `dataSource` binding are not listed. The line names the page, the component and its position, shows the request URL and method, and withholds every other request key. Nothing is rewritten: deleting `data` would make the component query its object instead. Left as stored, the page still loads and saves, because the page parse does not judge a component's properties, while `os validate`, `os build` and `os lint` refuse the binding in source. The D3 entry `view-data-api-provider-retired` now names these components in its scope. + + **A stored flow refused by `registerFlow` after the parse: refuse and list, by site (`registerFlow`).** The stored pass ran only one of registration's judges, the value-slot one. `canonicalizeStoredFlow` now runs registration's whole validation without registering: every check `registerFlow` runs once the parse has accepted the flow, through the one list `registerFlow` itself throws from. Each refused site is a `TODO registerFlow` line under its row, in the check's own words: a text slot still on the single-brace dialect or with a hole that does not compile, a decision's `mode`, a malformed node or edge predicate, a plugin node's undeclared config key, an `api` flow with no secret. A check added to registration later is listed the same way, with no second change. A site the chain already lists under a step-18 entry is not repeated. What the parse itself refuses, the builtin config contracts' value arm among it, is not a TODO: `canonicalizeStoredFlow` throws it, and the preview reports the row among those that could not be rewritten, in the parse's words. + + `registerFlow` refuses exactly what it refused before, with the same message: its checks now return their refusal instead of throwing it, and `registerFlow` throws the first one. The doors that canonicalize a flow without registering it (the metadata save door, a package duplication) read no `todos`, and the stored pass's run of the checks logs nothing, so they behave as before. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [c6139f2] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [78c13bd] +- Updated dependencies [9543e9c] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [2de9c03] +- Updated dependencies [5b68524] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-protocol@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/formula@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index ec63b893416..4c6647d6126 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Automation Service for ObjectStack — implements IAutomationService with plugin-based DAG flow execution engine", "type": "module", diff --git a/packages/services/service-cache/CHANGELOG.md b/packages/services/service-cache/CHANGELOG.md index 7f3a13b58e7..ec44cc5e306 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-cache +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 1027e2ca7f0..846a485011d 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters", "type": "module", diff --git a/packages/services/service-cluster-redis/CHANGELOG.md b/packages/services/service-cluster-redis/CHANGELOG.md index 99b69860f81..6420e98af34 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/service-cluster-redis +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/service-cluster@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 07cb2cc7205..3d3605be9c9 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": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Redis cluster driver for ObjectStack — implements IPubSub/ILock/IKV/ICounter against Redis using ioredis.", "type": "module", diff --git a/packages/services/service-cluster/CHANGELOG.md b/packages/services/service-cluster/CHANGELOG.md index b4849eac69d..b4f30085aa9 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/service-cluster +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 613a94c8da4..dfc1d42e63e 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.", "type": "module", diff --git a/packages/services/service-datasource/CHANGELOG.md b/packages/services/service-datasource/CHANGELOG.md index 39763b475e8..6586f019a99 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/service-external-datasource +## 18.0.0-next.1 + +### Minor Changes + +- 08b38cb: fix(services)!: the datasource admin, share-link, webhook redeliver and storage families refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by families that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST and the runtime dispatcher already refuse such a session. Four families of routes mounted straight on the host server never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/service-datasource`**: the datasource admin family (`registerDatasourceAdminRoutes`). Its door now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) about the session it already reads, before the anonymous floor and the capability check. + - **`@objectstack/plugin-sharing`**: the share-link family (`registerShareLinkRoutes`). Every route, the two capability-token routes included, is now mounted through one door that refuses a context carrying `authGate`. `SharingServicePlugin`'s resolver now puts the session's posture on that context, as REST does. + - **`@objectstack/plugin-webhooks`**: the redeliver door (`WebhookOutboxPlugin`), on both of its faces: the route the plugin mounts, and the `webhooks` service's `handleRedeliver`. + - **`@objectstack/service-storage`**: the storage family, the two capability-token raw-transfer routes included. Every route is now mounted through one door. The posture is bound where every other storage gate is bound, so both the plugin's own mount and the host door `mountStorageRoutes` carry it. When a gate feature is active, the door reads the session and refuses a held one. When the gate is active but the session cannot be read, the door answers the declared `503` (`SERVICE_UNAVAILABLE`), as REST does. A direct call of `registerStorageRoutes` mounts the family exactly as before, because like the other storage gates this one is bound by the package's composition. No exported declaration changes. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each family's declared error envelope, before the family does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, and an API-key caller is unaffected because it has no session to hold. A CORS preflight is answered as before. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these routes answer them, as on every other protected route. If you mount the storage routes through `mountStorageRoutes`, the gate is bound from the kernel's `auth` service with nothing to configure. If you mount `registerShareLinkRoutes` yourself with your own `contextFromRequest`, set `authGate` on the context it returns for a held session; the door refuses on that field. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + - @objectstack/driver-memory@18.0.0-next.1 + - @objectstack/driver-mongodb@18.0.0-next.1 + - @objectstack/driver-sql@18.0.0-next.1 + - @objectstack/driver-sqlite-wasm@18.0.0-next.1 + - @objectstack/driver-turso@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 33e9e315057..e60f72027a2 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.", "type": "module", diff --git a/packages/services/service-i18n/CHANGELOG.md b/packages/services/service-i18n/CHANGELOG.md index d355255f812..d026ba63ae6 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/service-i18n +## 18.0.0-next.1 + +### Patch Changes + +- 3c1a046: fix: a route's catch-all `500` answers its own generic text, never the caught error's message, and `@objectstack/types` exports the writer that does it, `sendServerFault` + + Clause-②: yes (widening) — `@objectstack/types` gains one export, `sendServerFault`; nothing is removed, renamed or narrowed, and no response field changes shape. + + A declared 5xx does not carry the text of the error it caught: that text can name server internals, such as a path, a host or a table. The REST layer already withheld it. The storage, settings and i18n route families and the share-link routes answer in the nested `{ success: false, error: { code, message } }` envelope and did not: their catch-all `500` sent the caught error's message to the caller. + + - **What a caller now reads.** The same `status` and `code` as before, with the route's own generic text as `error.message`: `Internal error` on the storage and i18n routes, the route's message on the settings routes (`Failed to list manifests`, `Failed to read namespace`, `Failed to write namespace`, `Action failed`), and the route's `Failed to …` message on the share-link routes. On the storage routes this includes the metadata store's outage sentence, which an operator now reads in the server log. + - **What the operator reads.** The original error, stack included, in one `[5xx]` server-fault line. The storage byte routes, which already withheld the text, logged two lines per fault and now log one. A line names the mounted route pattern when the route passes one, never the request URL. + - **Unchanged.** Every `4xx` answer. The settings `SETTINGS_CRYPTO_UNAVAILABLE` refusal, which keeps its prescription. The authorization-store outage `503`, which keeps its declared code and message. The storage `501 NOT_IMPLEMENTED` answers. On the share-link routes, any error that declares its own `status` keeps the envelope it declares. + - **`sendServerFault(res, status, code, message, thrown, request?)`** (new, `@objectstack/types`): writes `message` in the declared envelope, and hands `thrown` to the server-fault log with its stack, as the fault's one line. Use it for an arm that answers a caught fault. Keep `sendError` for a `4xx` and for a `5xx` whose text you wrote for the reader. `request` is the mounted route pattern, never the request URL. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 0b4d151f066..8cc523607be 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading", "type": "module", diff --git a/packages/services/service-job/CHANGELOG.md b/packages/services/service-job/CHANGELOG.md index b013c4b4137..5478f6db3cd 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-job +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index e294a94ec50..3fa9bb3293f 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling", "type": "module", diff --git a/packages/services/service-knowledge/CHANGELOG.md b/packages/services/service-knowledge/CHANGELOG.md index 432bef06d4f..6b3265b6bcd 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/service-knowledge +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 7931924f664..ad10ca09f56 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-mailbox/CHANGELOG.md b/packages/services/service-mailbox/CHANGELOG.md index 4468baa8592..af2f34d69e3 100644 --- a/packages/services/service-mailbox/CHANGELOG.md +++ b/packages/services/service-mailbox/CHANGELOG.md @@ -1,5 +1,90 @@ # @objectstack/service-mailbox +## 18.0.0-next.1 + +### Minor Changes + +- 3d830b7: Inbound mail has a mail source: the new `@objectstack/connector-imap` reads a mailbox over IMAP, and `@objectstack/service-mailbox` polls each mailbox's folder into its ingest seam, keeping a UID cursor on `sys_mailbox` + + Clause-②: yes (a new published package, `@objectstack/connector-imap`, and its exports; new exports and plugin options in `@objectstack/service-mailbox`, and four new fields on its published `sys_mailbox` object) + + - **`@objectstack/connector-imap` (new) — protocol and authentication only.** + - `openImapSession({ account, credentials, timeoutMs?, maxLiteralBytes?, maxCommandBytes? })` opens a read-only IMAP4rev1 session: `examine(folder)` (`EXAMINE`, never `SELECT`; answers `uidValidity`, `uidNext`, `exists`), `listMessages(fromUid)`, `fetchMessages(uids)` (`BODY.PEEK[]`, so no message is marked `\Seen`), `logout()` and `close()`. A folder opened without a UIDVALIDITY is refused. + - The account and credential abstraction is shaped for the per-user mailbox: a `MailAccount` (`{ id, username, imap: { host, port?, security? } }`) and a `MailCredentialProvider` that resolves the account's credential each time a session opens. `staticCredentials(credential)` answers one fixed credential. + - A `password` credential is sent with `AUTHENTICATE PLAIN` when the server offers it, otherwise `LOGIN`; an `oauth2` access token with `AUTHENTICATE XOAUTH2`. A server that refuses `LOGIN` and offers no `PLAIN` is refused before anything is sent. + - `security: 'tls'` (the default) is implicit TLS with the certificate verified. A plaintext connection (`security: 'none'`) is refused to any host but loopback, before a socket opens. + - Failures are `ImapError` with a `kind`: `configuration`, `connection`, `timeout`, `authentication`, `refused`, `protocol` or `oversized`. + - An IMAP server is treated as an untrusted peer. A line above 4 MiB, a literal above `maxLiteralBytes` (64 MiB by default), a response of more than `IMAP_MAX_RESPONSE_LITERALS` (32) literals, and more than `maxCommandBytes` of responses to one command (128 MiB by default) are refused as `oversized` before the excess is kept, and the connection is closed. + - `@objectstack/connector-imap/testing` exports `ImapServerDouble`, an in-process IMAP server on loopback for tests. + - No runtime dependencies. + - **`@objectstack/service-mailbox` — the IMAP sync.** + - `new MailboxServicePlugin({ imap: { accountFor, pollIntervalMs?, batchSize?, maxMessagesPerRun?, maxMessageBytes?, maxBatchBytes? } })` schedules the interval job `mailbox:imap-sync` on the composed job service. For each mailbox `accountFor` answers (asked per mailbox per run; `null` for a mailbox the host does not sync), it fetches the messages above the stored cursor, in UID order, and hands each one to `ingest` under the system context. Dedupe, association, the pool and auto-reply suppression are unchanged. + - Without `imap` — the capability boot constructs the plugin with no options — no mailbox is synced, and the boot says so once. With `imap` but no job service, the boot warns, and `plugin.syncService.syncAll({ isSystem: true })` runs a pass on the host's own schedule. A run in which a mailbox could not be synced reports `degraded` to the job service. + - `sys_mailbox` gains the cursor, written by the sync only: `imap_folder`, `imap_uid_validity`, `imap_last_uid` and `imap_synced_at`. A cursor taken under another UIDVALIDITY, or in another folder, is discarded and the folder is read from its start; the messages already stored answer `duplicate`. No credential is stored on the object: the account and its credential come from `accountFor`. + - The cursor moves after each batch past every message stored, already stored, suppressed or skipped. A store failure stops the run below the failed message, so the next run retries it. A message above `maxMessageBytes` (32 MiB by default) or one the seam refuses as unreadable is skipped and logged at `error` with its UID. + - A batch is bounded by count (`batchSize`, 25) and by listed bytes (`maxBatchBytes`, 64 MiB). The session's literal bound is `maxMessageBytes` plus a 64 KiB framing envelope, so a server that omits or understates a message's size cannot hand over more. That message is skipped as `too_large`, after the batch is read again one message at a time on a fresh session, and the rest of the mailbox keeps arriving. + - A forwarded `message/global` part (RFC 6532, the internationalized `message/rfc822`) is now read as an `attachment` source, the same as `message/rfc822`. + - New exports: `MailboxSyncService`, `MAILBOX_SYNC_JOB`, `DEFAULT_MAILBOX_POLL_INTERVAL_MS`, `DEFAULT_SYNC_FOLDER`, `DEFAULT_SYNC_BATCH_SIZE`, `DEFAULT_SYNC_MAX_MESSAGES_PER_RUN`, `DEFAULT_SYNC_MAX_MESSAGE_BYTES`, `DEFAULT_SYNC_MAX_BATCH_BYTES`, `SYNC_LITERAL_ENVELOPE_BYTES`, and the types `MailboxServicePluginOptions`, `MailboxImapSyncOptions`, `MailboxAccountResolver`, `MailboxImapAccount`, `MailboxSyncTarget`, `MailboxSyncResult`, `MailboxSyncAllResult`, `MailboxSyncSkipReason`, `MailboxSyncServiceOptions`, `MailSyncEngine` and `MailSyncLogger`. + - This supersedes the ingest seam entry's "no mail source feeds the seam": the IMAP sync does. Attachments are still not stored. +- 8258181: Inbound mail has an ingest seam: `MailIngestService`, the kernel service `mailbox`, turns one raw RFC 822 message into one `sys_mail_message` row, associates it at ingest with the records whose `mailAssociation` email fields hold a participant's address, and re-matches a mailbox on request + + Clause-②: yes (the package gains an exported ingest entry point it does not export today; a widening of the public surface) + + - **`ingest({ mailboxId, raw, receivedAt? }, context)`** reads one message and answers `stored`, `duplicate` or `suppressed`. Only the system context may call it: a message enters a mailbox from its mail source, never from a user. + - **Participants.** The addresses of From, Sender, Reply-To, To and Cc, plus the same five headers of a message forwarded as an attachment (a `message/rfc822` part of the received message). The mailbox's own address is left out. The set is stored on the row in `match_addresses`, a JSON list of `{ address, source }` where `source` is `outer` or `attachment`. A forward quoted inline in the body is not read yet, and `inline` is declared so a later reader of it can store its addresses. + - **Association.** The message is linked, one `sys_mail_association` row each (`matched_by: 'ingest'`), to every record of every object with an `email` field declaring `mailAssociation: true` whose value equals a participant address (ASCII case-insensitive), inside the mailbox's organization only. A flagged object with no organization column is skipped and logged once. No app code and no resolver service is involved. A message nothing matches is stored `association_state: 'unassociated'`: the restricted pool. + - **Dedupe.** One row per message per mailbox. The key is `mid:` plus the SHA-256 of the Message-ID, or `hdr:` plus the SHA-256 of the header block as received for a message without one. A second fetch answers `duplicate` and writes nothing, and the unique key settles a concurrent pair. + - **Threading.** `message_id`, `in_reply_to` and `reference_ids` hold the RFC 5322 ids with their angle brackets stripped. + - **Suppression.** No row is written for an auto-reply (`Auto-Submitted` other than `no`, `Precedence: bulk`, `junk` or `list`, `X-Autoreply` or `X-Autorespond` other than `no`) or a bounce (`multipart/report; report-type=delivery-status`). + - **`rematch({ mailboxId, messageIds? }, context)`** replays each message's stored participant set against the records that exist now. It adds the missing links (`matched_by: 'rematch'`) and moves a pool message to `associated` once it has one. It never removes a link. A stored set that is not the stored shape is reported in `unreadable` and not matched. The system context may run it, and so may the mailbox's owner and a `manage_mailboxes` holder in the mailbox's organization. Anyone else is refused with `PERMISSION_DENIED` (403), and another organization's mailbox answers `RECORD_NOT_FOUND` (404). + - **New exports:** `MailIngestService`, `MAILBOX_SERVICE`, `MailboxServiceError` (`code` and `status`), `MAIL_MATCH_ADDRESS_SOURCES`, `MAIL_SUPPRESSION_REASONS`, and the types `MailIngestInput`, `MailIngestResult`, `MailRematchInput`, `MailRematchResult`, `MailboxCallerContext`, `MailIngestEngine`, `MailIngestLogger`, `MailAssociationMatch`, `MailMatchAddress`, `MailMatchAddressSource`, `MailSuppressionReason`, `MailboxServiceErrorCode` and `RawMailMessage`. `MailboxServicePlugin` registers the service in `init()`. + - **New dependencies:** `postal-mime` (MIT-0, no dependencies of its own, ES module and CommonJS builds) reads the MIME structure, the encoded words and the address lists. `@objectstack/types`, already installed through `@objectstack/core`, supplies the keyset walk the re-match pages with. + - **Not yet:** no mail source feeds the seam (the IMAP connector ships separately), and attachments are not stored, so a message forwarded as an attachment keeps the forwarder's own text and not the forwarded message's body. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [3d830b7] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/connector-imap@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-mailbox/package.json b/packages/services/service-mailbox/package.json index 28ee82bba2c..2b238fab36d 100644 --- a/packages/services/service-mailbox/package.json +++ b/packages/services/service-mailbox/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-mailbox", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Mailbox Service for ObjectStack \u2014 inbound mail: the mirrored message (sys_mail_message), its record associations and their read gates.", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index 6dc4d86f00b..7e589bbfe31 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,75 @@ # @objectstack/service-messaging +## 18.0.0-next.1 + +### Minor Changes + +- 24e064f: fix(objectql,service-messaging,service-automation,plugin-approvals)!: a notification carries the organization of what it is about, and one no organization owns is refused instead of stored with `organization_id` NULL + + Clause-②: no (narrowing) — notification rows gain the organization they should already carry; what narrows is an emit that names no organization on an install where none is derivable, which the engine now refuses where it used to store the event row with `organization_id` NULL. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + Every notification emitted from a system-context run — a scheduled flow, a record change written by a system context, an approval node — was stored in `sys_notification` with `organization_id` NULL, so reports and cleanup that filter by organization did not see it. Each emit also logged a `[notify] no organization in scope` warning. On an install holding several organizations, the deliveries of the same emit were refused while the flow step reported success, so nothing reached the inbox. + + **Who owns a notification now:** + + - **The run's organization.** A run that carries one emits exactly as before. + - **Else the record it is about.** A `notify` node in a run with no organization reads the organization of the record the notification is about. That is the declared click-through record (`sourceObject` + `sourceId`), else the run's trigger record. It is read from the stored row by id, never from the record snapshot the run carries. + - **Approvals.** Every approval notification carries its request's organization: `approval.requested`, reminders, comments, send-backs, reassignments, escalations and out-of-office notices. + - **Else the owner the engine derives for a system write** (ADR-0131 D9). `sys_notification` is now in the platform-object tenancy ledger, so an emit that names no organization gets the install's sole organization under the `single` posture. + - **The rows below follow the event.** `emit()` stamps the stored event row's organization on its delivery, inbox row and receipt, on both the inline and the outbox path. + + **Refused now:** where no organization is derivable (a walled posture, or a `single` install holding more than one organization), an emit that names none is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`. Nothing is written for it. A `notify` step that hits the refusal fails with `notify failed: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED: …` instead of succeeding. `MessagingService.emit()` rejects with the engine's error for any other producer. + + **What to do when it refuses:** give the notification an owner. Name the record it is about on the `notify` node (`sourceObject` + `sourceId`), declare the scheduled flow's acting organization (`organization` on its start node), or pass `organizationId` to `emit()` from your own code. + + The `[notify] no organization in scope` warning is gone, because the NULL row it predicted is no longer written. Rows already stored with `organization_id` NULL are not rewritten here. The v18 organization-ownership ceremony attributes them from their source record. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index 0c7eacffb54..fe6109c9387 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Messaging Service for ObjectStack — outbound notification dispatch (ADR-0012). Ships the MessagingChannel registry, emit() fan-out, and the always-on inbox channel; other channels (email/webhook/push/IM) plug in.", "type": "module", diff --git a/packages/services/service-package/CHANGELOG.md b/packages/services/service-package/CHANGELOG.md index 88351a90a40..4a73a60fab5 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-package +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index cb4afda6f27..eb3f345d1ed 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index b666221a7c9..f14d267e9fb 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-queue +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index edbb09de8a4..f867afc5047 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters", "type": "module", diff --git a/packages/services/service-realtime/CHANGELOG.md b/packages/services/service-realtime/CHANGELOG.md index 3f8d16a485a..e84656af761 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-realtime +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 60516c3516c..acc4738f498 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub", "type": "module", diff --git a/packages/services/service-settings/CHANGELOG.md b/packages/services/service-settings/CHANGELOG.md index 3cf3d05238d..ebe9f047bfa 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,116 @@ # @objectstack/service-settings +## 18.0.0-next.1 + +### Minor Changes + +- bae5841: fix(service-settings)!: the settings route family refuses a session the ADR-0069 authentication policy holds, and `SettingsContext` declares the posture as an optional `authGate` + + Clause-②: yes (narrowing) — an optional `authGate` on the exported `SettingsContext` widens the public surface; the family door's refusal of a held session narrows what it answers. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST, the runtime dispatcher and the four sibling families already refuse such a session. The settings family (`registerSettingsRoutes`) resolves its own caller and never asked, so it served a held session as an ordinary signed-in caller. + + - **`SettingsContext.authGate`** (new, optional): the caller's posture, `{ code, message }`, the same shape and meaning as `ExecutionContext.authGate`. Present only while the session is held. + - **The family door** asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) about the context it resolved, on every route, before the anonymous floor and before any settings logic. + - **`SettingsServicePlugin`'s resolver** puts the posture on the context through the shared reader (`normalizeAuthGate`), from the same session read that resolves the caller. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in the family's declared error envelope. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, an API-key caller is unaffected because it has no session to hold, an anonymous caller gets the same `401`, and a CORS preflight is answered as before. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before the settings routes answer them, as on every other protected route. With `SettingsServicePlugin` nothing needs configuring. If you mount `registerSettingsRoutes` yourself with your own `contextFromRequest`, a resolver that sets no `authGate` behaves exactly as before; set `authGate` on the context it returns for a held session (`normalizeAuthGate(session.user)` from `@objectstack/core` reads it), and the door refuses on that field. + +### Patch Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 3c1a046: fix: a route's catch-all `500` answers its own generic text, never the caught error's message, and `@objectstack/types` exports the writer that does it, `sendServerFault` + + Clause-②: yes (widening) — `@objectstack/types` gains one export, `sendServerFault`; nothing is removed, renamed or narrowed, and no response field changes shape. + + A declared 5xx does not carry the text of the error it caught: that text can name server internals, such as a path, a host or a table. The REST layer already withheld it. The storage, settings and i18n route families and the share-link routes answer in the nested `{ success: false, error: { code, message } }` envelope and did not: their catch-all `500` sent the caught error's message to the caller. + + - **What a caller now reads.** The same `status` and `code` as before, with the route's own generic text as `error.message`: `Internal error` on the storage and i18n routes, the route's message on the settings routes (`Failed to list manifests`, `Failed to read namespace`, `Failed to write namespace`, `Action failed`), and the route's `Failed to …` message on the share-link routes. On the storage routes this includes the metadata store's outage sentence, which an operator now reads in the server log. + - **What the operator reads.** The original error, stack included, in one `[5xx]` server-fault line. The storage byte routes, which already withheld the text, logged two lines per fault and now log one. A line names the mounted route pattern when the route passes one, never the request URL. + - **Unchanged.** Every `4xx` answer. The settings `SETTINGS_CRYPTO_UNAVAILABLE` refusal, which keeps its prescription. The authorization-store outage `503`, which keeps its declared code and message. The storage `501 NOT_IMPLEMENTED` answers. On the share-link routes, any error that declares its own `status` keeps the envelope it declares. + - **`sendServerFault(res, status, code, message, thrown, request?)`** (new, `@objectstack/types`): writes `message` in the declared envelope, and hands `thrown` to the server-fault log with its stack, as the fault's one line. Use it for an arm that answers a caught fault. Keep `sendError` for a `4xx` and for a `5xx` whose text you wrote for the reader. `request` is the mounted route pattern, never the request URL. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 2b9885e7863..c25b5c7c365 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Settings service for ObjectStack — manifest registry + K/V resolver (OS_* env > Tenant > User > Default) + REST routes. See ADR-0007.", "type": "module", diff --git a/packages/services/service-sms/CHANGELOG.md b/packages/services/service-sms/CHANGELOG.md index e88da69a817..085603e1f7c 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,52 @@ # @objectstack/service-sms +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [3490bf5] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [6f4c518] +- Updated dependencies [917b1a0] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index ebabb385d5a..36280e584d8 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 9bc68ba8141..dd0cd2b0068 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/service-storage +## 18.0.0-next.1 + +### Minor Changes + +- 08b38cb: fix(services)!: the datasource admin, share-link, webhook redeliver and storage families refuse a session the ADR-0069 authentication policy holds + + Clause-②: no (narrowing) — a session ADR-0069 holds out of protected resources is refused by families that answered it; core's existing exports only, no published schema or export moves. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0069 holds a signed-in user out of every protected resource while their password has expired or while enforced MFA is not yet enrolled, until they remediate. REST and the runtime dispatcher already refuse such a session. Four families of routes mounted straight on the host server never asked, so they served it as an ordinary signed-in caller: + + - **`@objectstack/service-datasource`**: the datasource admin family (`registerDatasourceAdminRoutes`). Its door now asks the shared decision (`evaluateAuthGate`, `@objectstack/core`) about the session it already reads, before the anonymous floor and the capability check. + - **`@objectstack/plugin-sharing`**: the share-link family (`registerShareLinkRoutes`). Every route, the two capability-token routes included, is now mounted through one door that refuses a context carrying `authGate`. `SharingServicePlugin`'s resolver now puts the session's posture on that context, as REST does. + - **`@objectstack/plugin-webhooks`**: the redeliver door (`WebhookOutboxPlugin`), on both of its faces: the route the plugin mounts, and the `webhooks` service's `handleRedeliver`. + - **`@objectstack/service-storage`**: the storage family, the two capability-token raw-transfer routes included. Every route is now mounted through one door. The posture is bound where every other storage gate is bound, so both the plugin's own mount and the host door `mountStorageRoutes` carry it. When a gate feature is active, the door reads the session and refuses a held one. When the gate is active but the session cannot be read, the door answers the declared `503` (`SERVICE_UNAVAILABLE`), as REST does. A direct call of `registerStorageRoutes` mounts the family exactly as before, because like the other storage gates this one is bound by the package's composition. No exported declaration changes. + + - **Refused now:** a held session gets `403` with the gate's own `code` (`PASSWORD_EXPIRED` or `MFA_REQUIRED`) and `message`, in each family's declared error envelope, before the family does anything else. This is the answer REST gives the same session. + - **Unchanged:** a session the policy does not hold is served exactly as before, and an API-key caller is unaffected because it has no session to hold. A CORS preflight is answered as before. + + What changes for you. A user the policy holds must remediate (change the password, or enroll an authenticator) before these routes answer them, as on every other protected route. If you mount the storage routes through `mountStorageRoutes`, the gate is bound from the kernel's `auth` service with nothing to configure. If you mount `registerShareLinkRoutes` yourself with your own `contextFromRequest`, set `authGate` on the context it returns for a held session; the door refuses on that field. +- 5e725b8: A server-side producer can store a file and attach it to a record: `IFileAttachmentService.attachFile`, registered by `StorageServicePlugin` as the kernel service `file-attachments` (`FILE_ATTACHMENT_SERVICE`), writes the bytes, one `sys_file` row and one `sys_attachment` row linking it to the parent, under the checks every other attachment of that parent meets + + Clause-②: yes (widening: `@objectstack/spec/contracts` gains `IFileAttachmentService`, `AttachFileInput`, `AttachedFile` and `FILE_ATTACHMENT_SERVICE`, and `StorageServicePlugin` registers and declares one more service) + + - **The call.** `attachFile({ parentObject, parentId, organizationId, filename, mimeType, data })` answers `{ fileId, attachmentId, size }`. `organizationId` is required: the organization the caller acts in, or `null` for none. It acts as the system, for a producer with no browser upload in front of it (inbound mail first). Reach it with `ctx.getService(FILE_ATTACHMENT_SERVICE)` once the kernel has bootstrapped. + - **What it runs.** Each check is the one the rest of the platform already runs, not a copy: + - The `max_upload_mb` storage setting, through the same verdict the REST upload doors ask. Over it, the call is refused `413 PAYLOAD_TOO_LARGE` before anything is stored. + - The parent's organization. A parent walled into another organization than `organizationId` is refused `404 RECORD_NOT_FOUND`, as a missing parent is. Both rows are written in the parent's organization, or in the caller's for a global parent. + - Every gate on the `sys_attachment` insert, among them the `enable.files` opt-in: a parent object without it is refused `403 FILES_DISABLED`. + - Before the kernel has bootstrapped, or with no data engine, the call is refused `503 SERVICE_UNAVAILABLE`. A malformed input is refused `400 VALIDATION_ERROR`. + - **No refusal leaves a committed `sys_file`.** The row is written `pending` before the bytes, and turns `committed` only in the same transaction as the `sys_attachment` insert. A refused call removes the bytes and the row before it throws. If the process dies mid-call, the pending-upload retention reclaims both. + - `@objectstack/service-storage`: the upload doors' size refusal moved from the route module into `storage-limits.ts`, beside the limit it enforces, so the doors and the verb read one status, one code and one sentence. The doors answer exactly as before. + +### Patch Changes + +- 3c1a046: fix: a route's catch-all `500` answers its own generic text, never the caught error's message, and `@objectstack/types` exports the writer that does it, `sendServerFault` + + Clause-②: yes (widening) — `@objectstack/types` gains one export, `sendServerFault`; nothing is removed, renamed or narrowed, and no response field changes shape. + + A declared 5xx does not carry the text of the error it caught: that text can name server internals, such as a path, a host or a table. The REST layer already withheld it. The storage, settings and i18n route families and the share-link routes answer in the nested `{ success: false, error: { code, message } }` envelope and did not: their catch-all `500` sent the caught error's message to the caller. + + - **What a caller now reads.** The same `status` and `code` as before, with the route's own generic text as `error.message`: `Internal error` on the storage and i18n routes, the route's message on the settings routes (`Failed to list manifests`, `Failed to read namespace`, `Failed to write namespace`, `Action failed`), and the route's `Failed to …` message on the share-link routes. On the storage routes this includes the metadata store's outage sentence, which an operator now reads in the server log. + - **What the operator reads.** The original error, stack included, in one `[5xx]` server-fault line. The storage byte routes, which already withheld the text, logged two lines per fault and now log one. A line names the mounted route pattern when the route passes one, never the request URL. + - **Unchanged.** Every `4xx` answer. The settings `SETTINGS_CRYPTO_UNAVAILABLE` refusal, which keeps its prescription. The authorization-store outage `503`, which keeps its declared code and message. The storage `501 NOT_IMPLEMENTED` answers. On the share-link routes, any error that declares its own `status` keeps the envelope it declares. + - **`sendServerFault(res, status, code, message, thrown, request?)`** (new, `@objectstack/types`): writes `message` in the declared envelope, and hands `thrown` to the server-fault log with its stack, as the fault's one line. Use it for an arm that answers a caught fault. Keep `sendError` for a `4xx` and for a `5xx` whose text you wrote for the reader. `request` is the mounted route pattern, never the request URL. +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/observability@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index da71c51f4ca..54781b17ad0 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/skills/CHANGELOG.md b/packages/skills/CHANGELOG.md index 4f38e4e3281..ac61bf65c5d 100644 --- a/packages/skills/CHANGELOG.md +++ b/packages/skills/CHANGELOG.md @@ -1,5 +1,44 @@ # @objectstack/skills +## 18.0.0-next.1 + +### Patch Changes + +- 44cadd3: fix(skills): the automation skill teaches flow value slots as a CEL envelope or a literal and text slots as `{{ }}` holes; the single-brace `{…}` dialect leaves the catalog text + + Clause-②: no + + The `objectstack-automation` skill no longer lists `{$User.}`, `{NOW()}` / `{TODAY() ± N}` or any other single-brace `{…}` token as a spelling a flow value slot still reads, and the remedies it gives no longer pass through a refused spelling. What the skill now teaches is what the platform accepts: + + - A value slot — `assignments.*`, `fields.*`, a `subflow` / `map` `input.*`, a `script` `inputs.*`, a `loop` / `map` `collection`, a notify node's `recipients` / `templateData.*` / `sourceId`, an `http` node's `body`, each CRUD `filter` operand — takes a CEL value envelope `{ dialect: 'cel', source: '…' }` or a literal. A `{…}` token there is refused at `objectstack validate` (`expression-invalid`), `registerFlow` and the executor, naming its CEL spelling. + - A text slot — notify `title` / `message` / `actionUrl`, screen `title` / `description`, end `message`, `http` `url` and `headers` — renders `{{ }}` holes. + - The run user is `current_user.id`, guarded as `current_user != null ? current_user.id : null` in a flow that can run without a user; the date macros are `isoDate(today())`, `isoDate(daysFromNow(N))` / `isoDate(daysAgo(N))`, `isoDatetime(now())` and `isoDatetime(addDays(now(), N))`. + - A CRUD `filter` operand that is a whole-string query placeholder (`{current_user_id}`, `{today}`) passes through to the query engine untouched; an operand that evaluates to `null` refuses the node. + - A notify `recipients` value is one id, a list, or an envelope that evaluates to one, a list or `null` — a comma-separated string is one recipient. + + The `objectstack-query` skill's filters rule names the value-slot judge (`expression-invalid`) in place of the retired lint rule `flow-filter-token-unknown`, and the automation eval set's two expected outputs that spelled a retired token move with the text. +- 197fbe2: docs(skills): both READMEs the package ships install the catalog from `@objectstack/skills` at the installed `@objectstack/spec`'s version, and name the GitHub path only as the `next` channel (#22668) + + Clause-②: no + + `README.md` (the npm page) and `dist/skills/README.md` (the catalog's front page, copied from the repository's `skills/README.md`) are the two READMEs this package ships. Both now give the same two-step install the docs give: `npm install --save-dev "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")"`, then `npx skills experimental_sync --agent claude-code -y` — the catalog's version is then the installed spec's. + + Before: the npm page taught a bare `npm install --save-dev @objectstack/skills`, which on a project depending on `@objectstack/spec` 17.x installs `18.0.0-next.0`, the next major's catalog (the registry's `latest`); its example named `@objectstack/skills@17.7.0`, a version that was never published; and it did not say that the sync skips every skill a `skills add` installed, which is every project `create-objectstack` scaffolded through `18.0.0-next.0`. The catalog's front page still taught `npx skills add objectstack-ai/objectstack/skills …` as the install command for existing projects. + + Both pages now state which versions exist (published from `18.0.0-next.0` on; no 17.x, so step 1 answers `ETARGET` there), the update story (bump `@objectstack/skills` with the other `@objectstack/*` packages, then re-run the sync), and the GitHub path once, as the `next` channel: the unreleased `main`, for trying the next version's skills, not for a released project. Nothing under `dist/skills/*/` changes. +- 713f231: docs(skills): the upgrade skill's preflight re-installs the catalog from `@objectstack/skills` when a 17.x project moves to 18, and both READMEs give the scaffolder's pinned `--copy` sync (#23276) + + Clause-②: no + + `objectstack-upgrade`'s §0 Preflight gains one step for the skills themselves. A 17.x project's skills were installed with `skills add` from the repository's `main`; their `skills-lock.json` entries read `sourceType: github`, and `experimental_sync` skips them (it prints `Skipped … installed with skills add` and exits 0), so a project that only bumps its packages keeps its 17.x-era skill text. The step removes the eleven `objectstack-*` skills the catalog carried during 17.x (a name not installed is skipped; `--all` when they are the project's only skills), installs `@objectstack/skills` at the installed `@objectstack/spec`'s version, and copies it with `npx skills@1.7.2 experimental_sync --agent claude-code --copy -y` — the command `create-objectstack` runs. The step is paid under the token ratchet by deleting text the same file restates elsewhere; the ceiling is unchanged. + + `README.md` (the npm page) and `dist/skills/README.md` (the catalog's front page, copied from the repository's `skills/README.md`) replace the unpinned linked sync (`npx skills experimental_sync --agent claude-code -y`) with that pinned `--copy` spelling and say why: without `--copy` the sync writes links into `node_modules`, which a clone of a project that commits its skills does not have. +- 68e64dd: fix(skills): the automation skill's two reference files write a CRUD `filter` operand as a CEL value envelope or the `{today}` query placeholder; the single-brace `{…}` filter spelling leaves them + + Clause-②: no + + `skills/objectstack-automation/references/examples-flows.md` and `references/state-machines-and-approvals.md` no longer teach `'{TODAY()}'`, `'{TODAY() + N}'` or `'{record.id}'` at an `update_record` filter operand — spellings the platform refuses (`expression-invalid`) at `objectstack validate`, `registerFlow` and the executor, so an author who copied the example got a refused node. The examples now write what the platform accepts: the query placeholder `'{today}'` for today's date, a CEL value envelope `{ dialect: 'cel', source: 'isoDate(daysFromNow(N))' }` for a one-day window at a date offset, and `{ dialect: 'cel', source: 'record.id' }` for the triggering record's id. The prose names the CEL value envelope where it named a template token (and no longer says a CEL envelope is compared as a literal at a filter operand), and the time-word cheat sheet names the CEL envelope and the `{{ }}` text hole where it named the retired `{…}` template. + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/skills/package.json b/packages/skills/package.json index cdcf363e303..fc32278653f 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/skills", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "description": "The published ObjectStack skills catalog \u2014 the repository's skills/** tree, shipped at the version of the @objectstack/* packages it teaches.", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/skills", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 8a11ccffda1..030f312056c 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,765 @@ # @objectstack/spec +## 18.0.0-next.1 + +### Minor Changes + +- 66949b7: feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3) + + Clause-②: no (narrowing) + + + + **BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. + + Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted. + + FROM → TO: + + - `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`. + - `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`. + - `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`). + - `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`). + - Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed. + - An object permission on one of the four names in a permission set grants nothing and can be deleted. + + What else changes: + + - `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`. + - A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. + - On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it. + - Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it. + - On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged. + - `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`. + - On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration. + + Removed exports, no replacement: + + - `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three. + - `@objectstack/platform-objects/pages`: `SysPositionDetailPage`. + - `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`. + - `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`). + - `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes. +- 5f37d36: feat(spec)!: at most one `isDefault: true` record page per object per deployment — a second is refused at `defineStack` / `os validate`, at package registration and at metadata save, naming both pages (#23015) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + `PageSchema.isDefault` now states the rule (maintainer ruling A on #23015): at most one `isDefault: true` record page may exist for an object in a deployment. The record view opens that page with no tie-break, so with two defaults for one object the page that opened used to be whichever plugin initialised first — an accident no author wrote. A record page is `type: 'record'` (the default when `type` is left out) bound to an `object`; `isDefault` on any other page is outside the rule. + + **What is refused, and where.** Every door that can add a second default refuses it, naming both pages: + + - **`defineStack`** (and so `objectstack validate`, `objectstack build` and a source boot) — two default record pages for one object inside one stack, `STACK_CROSS_REFERENCE_INVALID` (422), one finding per object naming the object, every page and its position (`stack.pages[0]`). **`composeStacks`** refuses the pair across the stacks it composes, in the same envelope; under `manifest: 'preserve'` one page name in two stacks is two packages' pages. + - **Package registration** (`@objectstack/objectql`'s `SchemaRegistry`, at `installPackage` ahead of every mutation and at the `registerItem` seam) — a package whose default record page meets another package's default for the same object, an environment's stored default, or a second default in its own manifest, `DEFAULT_RECORD_PAGE_CONFLICT` (422, exported as `DEFAULT_RECORD_PAGE_CONFLICT_CODE`). Two packages' pages under one name are two pages and are refused too. `OS_METADATA_COLLISION=warn` does not soften it. + - **Metadata save** (`@objectstack/metadata-protocol`'s `saveMetaItem`, so `PUT /api/v1/meta/page/:name`) — a page saved as the default record page of an object a stored page or a packaged page already holds, `DEFAULT_RECORD_PAGE_CONFLICT` (422), in draft and publish mode, before anything is written. + + **Unchanged.** A save under the existing default's own name is that page (an ADR-0005 overlay by name), not a second default. One default per object, a second record page that is not a default, and two objects with one default each are accepted. The same package registering again — a reload, or an upgrade that moves its default to another of its pages — is one holder. A save under the name of a page a package ships is still answered by the packaged-base refusal (`NOT_OVERRIDABLE`, 403), as before. A stored page reaching the registry from `sys_metadata` (hydration, write-through) is not refused again. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `pages: [{ name: 'account_a', type: 'record', object: 'crm_account', isDefault: true }, { name: 'account_b', type: 'record', object: 'crm_account', isDefault: true }]` accepted; the boot kept both and the record view opened whichever registered first | refused, `STACK_CROSS_REFERENCE_INVALID` 422 — set `isDefault: false` on all but one | + | `composeStacks` | two stacks each shipping a default record page for one object composed clean | refused in the cross-reference envelope, naming both stacks' pages | + | package registration | a second package (or a plugin's `manifest.register`) shipping a default record page for an object another package holds registered; init order decided the page | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; keep one default, or compose the deployment without the package that ships the other | + | metadata save | `PUT /api/v1/meta/page/my_page` with `isDefault: true` for an object that already has a default record page stored or shipped saved | refused, `DEFAULT_RECORD_PAGE_CONFLICT` 422, naming both pages; save it with `isDefault: false`, or first demote the stored default | + + **The one-line fix: keep `isDefault: true` on the one record page per object that should open the record view, and set `isDefault: false` on every other.** To replace a page a package ships, compose the deployment without that package's page; shipping a second default is not an override path. + + No D2 conversion rewrites it: which page stays the default is the author's call. Its D3 record is the semantic entry `page-default-record-page-one-per-object-refused` (protocol 18). + + **`@objectstack/spec`.** `defaultRecordPageObject(page)` (exported from `@objectstack/spec/ui`) is the one reading of "which object is this page the default record page of" that the three doors share. `PageSchema.isDefault` gains its `.describe()`. `DEFAULT_RECORD_PAGE_CONFLICT` joins the error-code ledger under `@objectstack/objectql` and `@objectstack/metadata-protocol`. + + **`@objectstack/objectql`.** The registration refusal, `SchemaRegistry.defaultRecordPageHoldersOf(object)` (every page holding the object's default — the reading the save door meets with the stored pages) and the `DEFAULT_RECORD_PAGE_CONFLICT_CODE` constant. + + **`@objectstack/metadata-protocol`.** The save-door refusal. + + **`@objectstack/platform-objects`.** The page form's `isDefault` help text, which said "Set as default page for this page type" — a rule nothing enforced — now states the one-default-per-object rule, in all four locales. + + **Who is affected, measured.** In this repository every object with a default record page has exactly one: `sys_organization` and `sys_user` (`@objectstack/plugin-auth`), `sys_position` (`@objectstack/plugin-security`), `sys_approval_request` (`@objectstack/plugin-approvals`), and `showcase_project`, `showcase_task` and `showcase_preference` (`examples/app-showcase`). A deployment that also composes a package shipping its own default record page for one of those objects is refused at boot until one of the two pages stops being a default. +- 1912af4: feat(spec)!: retire the view-level `api` data source — a view binds its object or static rows, and a metadata-declared API call is the page element's binding (#23082) + + **BREAKING** — `provider: 'api'` on a view's `data` (`ViewDataSchema`, with its `read` / `write` request blocks) is now refused at parse. The arm took any `url` and any `headers`, so a credential or a cross-origin call written into a view was stored as metadata, served to every client that read the view and sent by the console as written. The page element's API binding (`ElementApiDataSourceSchema`, `dataSource` on a page component) is the one door for a metadata-declared API call: same-origin, no credential key, read with the console's own signed-in fetch. One operation with two implementations that behave differently keeps the governed one — the view-level source is re-homed on the page element and removed, not aligned. + + Clause-②: no (narrowing) + + Measured before removal: no producer wrote `provider: 'api'` on a view outside documentation in objectstack (`bcebd8409`), objectui (`origin/main` `f228d710` — the console's approval-requests source builds `ApiDataSource` in code from an `ApiDataSourceConfig`, never a `ViewData`) or hotcrm (`0f84c9c`), each with a lit `provider: 'object'` control. A renderer reader existed in objectui, measured at `7578323a544e` (`ListView` routed the block to `ApiDataSource`; `ObjectGantt` built one from `read` / `write`), and is now unreachable from authored metadata. + + ## FROM → TO + + | you wrote (17.7 and earlier) | write instead | + | --- | --- | + | `data: { provider: 'api', read: { url, method?, params?, headers? } }` on a list or form view | `data: { provider: 'object', object: '' }` (or `{ provider: 'value', items: [...] }`), and the read as a page component's `dataSource: { provider: 'api', read: { url: '/api/...' }, path? }` | + | `data: { provider: 'api', write: { url, method, body? } }` on a form view | nothing — a form submits to its object; a submit-to-endpoint is not a view concern | + | `read.headers: { Authorization: '...' }` | nothing — a credential never lives in metadata; the page element binding refuses a credential key and the console sends its signed-in fetch | + + **The one-line fix:** bind every `provider: 'api'` view to its object or to static rows, and move the endpoint read onto a page component's `dataSource`. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`ViewData` no longer has the arm), and the parse — `defineView()`, `defineStack({ views })`, `os validate`, `PUT /api/v1/meta/view/:name` — refuses it at `data.provider` with the prescription: + + > `view.data.provider: 'api'` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — a view-level API source took any `url` and any `headers`, so a credential or a cross-origin call written there was stored with the view, served to every client that read it and sent by the console as written; a metadata-declared API call now has one door, the page element's binding, which is same-origin and credential-free. Bind the view to its object — `data: { provider: 'object', object: '' }` — or to static rows — `data: { provider: 'value', items: [...] }` — and move the API read onto a page component: `dataSource: { provider: 'api', read: { url: '/api/...' } }` (with an optional `path` into the response) under a page's `regions[].components[]`, which the console reads with its own signed-in fetch. + + ## The retirement kit + + - **A value retirement, not a key**: `data` survives one arm lighter, so there is no `retiredKey()` tombstone and no `RETIRED_KEYS_BY_MAJOR` row. The prescription rides the discriminated union's own `error` map, keyed on the input's `provider === 'api'` — the retired bare-string `sort` clause's shape — so every other unknown provider keeps zod's own report, which lists the live arms (`object`, `value`, `schema`). + - **No D2 conversion, deliberately.** The replacement lives on a different metadata type (a page element), and the only mechanical edit a conversion could make — deleting the view's `data` so the view falls back to querying its bound object — would change what the view shows without telling anyone. An authored source or a stored `sys_metadata` row still carrying the arm meets the prescription at parse instead. + - **The D3 entry `view-data-api-provider-retired`** asks the author, per such view, which page composes the endpoint it read and what the view itself should now bind. + - **`HttpRequestSchema` / `HttpMethodSubsetSchema` stay exported** from `@objectstack/spec/shared` and `@objectstack/spec/ui`, unchanged. After this change no schema in the package composes `HttpRequestSchema`; that orphan is recorded, not acted on here. + - **Ledger:** `view.list.data` and `view.form.data` stay `live` (their surviving arms are read) and carry the arm's retirement record. + - **Docs and the published skill** no longer teach a view-level `api` source; both point at the page element binding. + - **No deprecation window**, per the project's startup-stage posture. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, and `objectstack-ai/cloud`'s own view metadata was not readable from this change's session; a cloud view carrying an `api` source is refused at parse with the prescription above. + + +- 76815b9: feat(spec)!: a `controlled_by_parent` object derives its master from exactly one required `master_detail` — the parse refuses anything else, and the runtime's two fallback tiers retire (#23100) + + Clause-②: no (narrowing) + + ADR-0055 §1 says a `controlled_by_parent` object "must declare exactly one required `master_detail` field … Validation error otherwise (fail closed)". Until now neither the parse nor the runtime kept it. `ObjectSchema` accepted such an object with no master at all. The security runtime derived a master through a three-tier fallback the record never stated: a required `master_detail`, then any `master_detail`, then a required `lookup`, taking the first match by field order within a tier. The maintainer ruling (letter B) enforces the record, under ADR-0049 (enforce-or-remove). + + **BREAKING — what moves for consumers.** + + - **`@objectstack/spec`.** `ObjectSchema` (raw `.parse()` / `.safeParse()`, `ObjectSchema.create()`, `defineStack`, `os validate` and the metadata save door) refuses an object declaring `sharingModel: 'controlled_by_parent'` unless exactly one of its fields is a `master_detail` declared `required: true`. With none, the issue sits at `sharingModel` and names each field that relied on a retired tier. With two or more, it sits at `fields` and names them. A plain object literal is judged like a raw parse: an omitted `required` on its `master_detail` is refused, while `ObjectSchema.create()` keeps forcing an omitted `required` to `true`. The step-18 semantic migration entry `cbp-master-relation-tiers-retired` carries the prescription for `os migrate meta`. The step-18 conversion `cbp-master-detail-required-defaulted` covers the one lossless shape: a `controlled_by_parent` object whose one `master_detail` omits `required`, which Studio's field designer writes. It gets `required: true`, exactly what `ObjectSchema.create()` does. It runs on stored rows, built artifacts and `os migrate meta --from 17`. A live authoring parse still refuses the shape and names the fix. + - **`@objectstack/plugin-security`.** The relation resolver derives a master from that one field and nothing else. An object that declares a non-required `master_detail` only, a required `lookup` only, or two required `master_detail` fields resolves no master. Every read of it is denied, and every write and insert is refused with `422 INVALID_METADATA`. The plugin reports such an object once per object (`[security/controlled-by-parent] … resolves no master relation`), naming the shape it relied on. The `422` message now says "not exactly one required master_detail field". + - **`@objectstack/lint`.** `security-controlled-by-parent-no-relation` now refuses the non-required `master_detail` and required `lookup` shapes as well. `security-controlled-by-parent-ambiguous-relation` reports two or more required `master_detail` fields as "no master is derived", since the runtime no longer picks one by field order. Two required `lookup` fields are now a no-relation object, not a tie. `security-master-detail-ungranted` no longer treats a lookup-bound `controlled_by_parent` object as a detail. + + **FROM → TO — HotCRM's three objects.** These are the only producers measured on the retired third tier: the read-only HotCRM clone at `0f84c9c`, 8 `controlled_by_parent` objects, 5 of them already on one required `master_detail`. HotCRM's pinned engine is unaffected until it bumps past the release that carries this change. + + | object | FROM | TO | + |---|---|---| + | `crm_campaign_member` | `crm_campaign: Field.lookup('crm_campaign', { required: true })` | `crm_campaign: Field.masterDetail('crm_campaign', { required: true, deleteBehavior: 'restrict' })` | + | `crm_event_attendee` | `crm_event: Field.lookup('crm_event', { required: true })` | `crm_event: Field.masterDetail('crm_event', { required: true, deleteBehavior: 'restrict' })` | + | `crm_article_feedback` | `crm_knowledge_article: Field.lookup('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | `crm_knowledge_article: Field.masterDetail('crm_knowledge_article', { required: true, deleteBehavior: 'cascade' })` | + + `deleteBehavior: 'restrict'` keeps what the two lookups do today: a campaign or an event with members or attendees cannot be deleted. Without it a `master_detail` cascades, and a detail's rows are deleted with their master. An object whose rows must outlive their parent is not a detail. It leaves `controlled_by_parent` for `private`, `public_read` or `public_read_write`. + + **Remedy, in one line.** On every object with `sharingModel: 'controlled_by_parent'`, declare exactly one `master_detail` field with `required: true`, pointing at the master. Every other parent is a `lookup`. + + **Unchanged.** + + - An object with one required `master_detail` resolves and derives exactly as before, chain walk included. So does a required `master_detail` beside other `lookup` fields, or beside a `master_detail` that is not required. Every in-tree `controlled_by_parent` object is in that set: `examples/app-crm` (1), `examples/app-showcase` (2), the CLI's eval corpus (4) and the dogfood fixtures. + - Stored metadata whose one `master_detail` merely omits `required` keeps serving, converted as above. A stored row in any other retired shape is not rewritten: it still registers, under its `[metadata_spec_invalid]` diagnostic, and is denied until it is corrected and saved. Those shapes are an explicit `required: false`, two masters, or a required `lookup`. None of them is converted: turning a `lookup` into a `master_detail` changes what deleting the master does to every row, choosing between two masters is the pick ADR-0055 §1 refuses, and leaving `controlled_by_parent` is the author's decision. + - No export, signature or error code is added or removed. + + +- 4d1e4a8: feat(spec)!: a page action row's `onSuccess` is `ActionSchema`'s own post-success block, so its `navigate` is judged by the url scheme set, and the row's executor key refuses `'navigation'`, the runner's alias of `'url'` + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on two keys of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings. + + The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). Each forwards its action keys to the console's action runner. Two of those keys still let a row carry a target the runner refuses at the click: + + - **`onSuccess`** was any value (`z.unknown()`). The runner reads only `ActionSchema`'s post-success block, `{ navigate, openIn }`, and classifies `navigate` with the url scheme set. So `onSuccess: { navigate: 'javascript:alert(1)' }` parsed clean, and so did a misspelled key or a callback shape, which the runner gives no reading at all. The rows now hold `ActionSchema.onSuccess`'s own block, the same instance by reference, so a row's verdict on any `onSuccess` value is the action's: the scheme set on `navigate`, the rename prescriptions for `url` / `redirectUrl` / `to` / `route` / `path` / `target` / `navigateTo` / `href`, and the closed `openIn` with its `'self'` default. + - **The executor key** (`actionType` on a node, `type` on a member) is a free string, and the rows judged a url `target` only when it read `'url'`. The runner also dispatches `'navigation'`, its local alias of `'url'`, to the same navigator, so `actionType: 'navigation'` with `target: 'javascript:…'` parsed clean. The url executor now has one spelling on the rows: `'navigation'` is refused at the executor key, with the rename onto `'url'`. The key otherwise stays open: a handler a host registers under another name (`navigate_create`, `navigate_edit`, …) is still dispatched by name. + + A page's `properties` bag is outside the stack parse. `os validate` judges it through its props gate, which parses the bag against these same rows, so it reports each refusal at the row's own path with the same text, as a `component-props-invalid` finding (an error, like every finding of that gate). A stored page still saves and loads. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `onSuccess: { navigate: 'javascript:…' }` (or `data:`, `tel:`, any other scheme) | an `https://` page or an in-app route, or drop the `onSuccess` — no hop runs a script or renders content | + | `onSuccess: { navigate: '//cdn.example.com/x' }` | `'https://cdn.example.com/x'`, or `'/x'` for an in-app path | + | `onSuccess: { navigate: 'tasks/${result.id}' }` (a bare relative template) | the rooted route, `'/tasks/${result.id}'`, or a `./` path | + | `onSuccess: { navigate: '' }`, or `onSuccess: {}` | the address the hop was meant to open, or drop the `onSuccess` | + | `onSuccess: { url: '/x' }` (or `redirectUrl`, `to`, `route`, `path`, `target`, `navigateTo`, `href`) | `onSuccess: { navigate: '/x' }` | + | `onSuccess: { navigate: '/x', openIn: 'new-tab' }` | `openIn: 'newTab'` — the block's spelling, not the top-level url switch's | + | `onSuccess: { type: 'notify', … }`, an array of callbacks, or a bare string | drop it: the runner reads only the navigation block, so these never ran | + | `actionType: 'navigation'` on an `action:button` / `action:icon` | `actionType: 'url'`, with the same `target` and `openIn` — `os migrate meta --write` rewrites it, and a stored row is rewritten when it is read | + | `type: 'navigation'` on an `action:group` / `action:menu` member | `type: 'url'`, with the same `target` and `openIn` — `os migrate meta --write` rewrites it, and a stored row is rewritten when it is read | + + **The one-line fix: give every page action row's `onSuccess` the action's block, `{ navigate, openIn? }`, with a `navigate` that is an `http(s)://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or a template that opens with `${` — and spell the url executor `'url'`.** The rename from `'navigation'` is lossless on a row: the runner sends both spellings to one navigator, and the alias's own keys (`to`, `redirect`, `external`, `newTab`, `replace`) are not row keys. The executor rename is mechanical: `os migrate meta --from 17 --write` performs it through the ADR-0087 D2 conversion `page-action-row-navigation-executor-to-url`, which is retired from the load path, so an author meets the refusal while stored rows and migrated sources are rewritten. It lists the D3 entries `ui-page-action-row-navigation-executor-refused` (which names that conversion) and `ui-page-action-row-on-success-shape-closed`. An `onSuccess` rewrite stays by hand, because which address a refused `navigate` meant is the author's call. + + **What an author now sees.** `os validate`'s props gate reports a refused `navigate` at `properties.onSuccess.navigate` (or `properties.actions.N.onSuccess.navigate` for a member) with the action's own refusal: ``An action's `onSuccess.navigate` opens with `javascript:`, a scheme a url target never opens.``, followed by the reason and the admitted set. The alias is reported at `properties.actionType` (or `properties.actions.N.type`): ``An `action:button`'s `actionType: 'navigation'` is the console runner's local alias of `'url'`, refused on page action rows since @objectstack/spec 18, and the url executor has one spelling.``, followed by the rename. A row's parsed `onSuccess` now carries `openIn: 'self'` when it was omitted, as an action's does; the runner reads an omitted `openIn` the same way. + + **Measured producers: none refused.** No page action row in this repository or in objectui (at its pin `1fb1ced3`) writes an `onSuccess` outside the block or the executor `'navigation'`. objectui's row tests write `{ navigate: '/app/records/${result.id}' }`, admitted, and every `type: 'navigation'` there is an `element:button` inline action or a runner test, not a page action row. Host-registered executor names are kept: this package's own released guidance prescribes `actionType: 'navigate_edit'` on an `action:button`. +- 4cc3ff9: feat(spec): `record:related_list` declares its parent record with a binding of its own, `parentDataSource`, beside the child-row `dataSource` (#23134) + + Clause-②: yes (widening) + + - **What is new.** `ComponentPropsMap['record:related_list']` (`RecordRelatedListProps`) accepts `parentDataSource`: the PARENT record the list hangs off, bound by a query. Its shape is the element data-source object binding `{ object, view?, filter?, sort?, limit? }` (`ElementDataSourceSchema`, referenced, not copied), the same binding `dataSource` takes on every page component. A binding the data-source shape refuses (no `object`, an unknown key, the MongoDB-style `filter` record, the `api` arm) is refused here too, at `parentDataSource`. + - **The single-record rule.** The parent is the FIRST row of the bound set by the binding's `sort`; declare `limit: 1` to say one row is meant. When no row answers, the block renders a loud absence, never an empty list. `relationshipValueField` is read on that row. This is the rule `record:details`, `record:highlights` and `record:quick_actions` already take for their own `dataSource`. + - **The fallback.** Without `parentDataSource`, the record page's record context remains the parent, as before. Every related list that parsed before parses unchanged. + - **`dataSource` keeps its meaning.** The node-level `dataSource` on `record:related_list` is still the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`) and is never read as the parent. + - **Planned, not yet rendered.** The key's describe opens with `[PLANNED — not yet rendered]`: no objectui renderer reads it yet. objectui reads it once a release carrying this key is installable there; until then a related list on an `app` page still finds no parent. + - **Refused elsewhere.** Every other `ComponentPropsMap` row refuses `parentDataSource` as an unknown key, and `os validate` reports it as `component-props-unknown-key`. + + ```ts + { + type: 'record:related_list', + properties: { + objectName: 'contact', + relationshipField: 'account_id', + parentDataSource: { + object: 'account', + filter: [{ field: 'is_primary', operator: 'equals', value: true }], + limit: 1, + }, + }, + } + ``` +- 107853c: `IAIConversationService.get` / `list` and `IAIService.listPendingActions` take the caller's execution context as an optional trailing parameter (ADR-0096 D5) + + Clause-②: yes (widening: an optional parameter on three published store-contract methods) + + - **FROM → TO.** + - `get(conversationId)` → `get(conversationId, context?: ExecutionContext)` + - `list(options?)` → `list(options?, context?: ExecutionContext)` + - `listPendingActions?(filter?)` → `listPendingActions?(filter?, context?: ExecutionContext)` + - **What an implementation does with it.** When a caller supplies `context`, the implementation passes it unchanged as the `context` of every engine read the method makes (`BaseEngineOptions.context`), so the read runs as that caller. Under ADR-0096 D5 strict mode an engine read with no principal is refused with 403, so without a caller an implementation can only read as system. The type and the position are the ones `ISharingService`, `IApprovalService`, `IShareLinkService` and `IAnalyticsService` already use. + - **Nothing to migrate.** The parameter is optional. A caller that omits it gets the read the implementation made before, and an implementation written against the shorter signatures still satisfies both interfaces. To read as the caller, pass the request's `ExecutionContext` and forward it to your engine reads. + - `list`'s `options.userId` stays a filter on the conversation's owner. It is not the caller's identity. + - Whether an ordinary member is admitted to their own rows depends on the stored objects' access and sharing, which these contracts do not declare. +- 543b33d: fix(metadata-protocol)!: a metadata save or delete that names no package addresses the row of the one package the item belongs to, and a name two or more packages hold is refused `409 RESOURCE_CONFLICT`; the delete door can name its package (ADR-0048, ADR-0070) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A `sys_metadata` row is keyed by its package (ADR-0048), so one item can hold one row per package. A save that names a package (`?package=` on the REST door, `packageId` in process) writes that package's row. A save that named none always wrote the package-unbound row, even for an item a package owns, and a delete named no package at all: it removed whichever row of the item the store returned first. The draft save already resolved this: a draft that names no package is stored in the package of the item's active row. + + **FROM.** Save an item into a package (`PUT /api/v1/meta/app/my_app?package=com.acme.app`), then save it again without `?package=`: the second save answered `200` "Saved app 'my_app' (env-wide, state=active)" and wrote a second, package-less row beside the package's row. The item read kept serving the package's row, so nothing the save carried took effect, and a console "Disable" reported success while the app stayed enabled. The same held for the API, the CLI and agents, and for an overlay row bound to a code package. A `DELETE /api/v1/meta/app/my_app` of an item two packages hold removed one of the two rows, the other was then served, and the receipt said "it no longer exists". Uninstalling a package, or discarding its drafts, deleted each of its items by name the same way, so it could remove another package's row of the same name and leave its own. + + **TO.** A direct save, or a delete, that names no package resolves its address from the item's rows (the active rows for a save and a delete, the draft rows for a draft discard): + + - **Bound to one package.** The save writes that package's row, exactly as a save naming the package does, and the item read serves it next; the delete removes that row. The receipts name the package: "Saved app 'my_app' (env-wide, package=com.acme.app, state=active)" and "Deleted app 'my_app' (package=com.acme.app) — it no longer exists." Every check on the row the save writes reads that package: its stored lock, a stored credential carried forward, the optimistic lock, and the writable-base rule. The item read that names no package serves that row's `version`, so a read followed by a save or a delete with the served `If-Match` is accepted. + - **Bound to two or more packages.** The save or delete is refused `409 RESOURCE_CONFLICT` and changes nothing. The message names the packages and the remedy: name one with `?package=`. + - **Bound to none.** The save writes, and the delete removes, the package-unbound row, as before, and the receipts read as before. + + The delete door gains the address a save has: `DELETE /api/v1/meta/:type/:name?package=` (`packageId` in process, `DeleteMetaItemRequestSchema.packageId`) removes that package's row and no other; `all` and an empty value name no package, as on the save door. A repeated `?package=` is refused `400`. A delete receipt now says when another row of the item is still stored, naming it, instead of claiming the item is gone or back at its artifact default. Uninstalling a package and discarding its drafts name the package, so each removes only its own rows. + + **Also refused now**, each as the same save naming the package is refused: a package-less save of an item held by a read-only package (a code package's runtime-only item, or an installed `system`/`cloud` package's) answers `422 WRITABLE_PACKAGE_REQUIRED`; and a package-less save asserting a create (`If-None-Match: *`) of an item a package holds answers `409 METADATA_CONFLICT`. + + `packageId: null` in process names no package too, as the request contracts state. The server-side rewrites keep the row they name: `os migrate meta --stored --apply` and the stored flow credential move pass `null` for a package-unbound row they rewrite, and that row is still the one written; package duplication names its target package. + + **Unchanged.** A save or a delete that names a package exactly. A draft save, which keeps its own inheritance. An item a code package ships with no stored row: the package-less save still writes the package-unbound overlay, and the read serves it. Package-less rows already stored beside a package's row stay as they are; none is converted. A package-less delete of such an item removes the package's row and its receipt says the package-less row remains. + + **Remedy.** For an item two packages hold, send the save or the delete with `?package=` naming the package whose row it edits or removes. A receipt now names the package when the write addressed a package's row; a client that compares receipt text should expect `, package=` after `env-wide` on a save and ` (package=)` after the item on a delete. + + `@objectstack/spec`: the `SaveMetaItemRequestSchema.packageId` description now states what an `active` save that names no package writes, where it said the env-local overlay; `DeleteMetaItemRequestSchema` declares the optional `packageId`. `@objectstack/rest`: `package` joins the reset door's closed query set (`META_ITEM_RESET_PARAMS`). +- b529a5c: feat(spec)!: a page action row's url `target` is judged on the executor the console's action runner actually runs, so a row with no executor key whose `name` is `url` or `navigation` has its `target` judged by the url scheme set + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on one key of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings. + + The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). The console's action runner dispatches a row on `type || actionType || name`, and every row forwards its executor key (`actionType` on a node, `type` on a member) as `type` beside its `name`. So a row with no executor key runs the handler its `name` names, and `name: 'url'` or `name: 'navigation'` runs the url navigator. The rows judged `target` by the url scheme set only when the executor KEY read `'url'`, so such a row parsed with a target the click refuses. + + The rows now judge `target` against the row's effective executor: the executor key, or, when that key is omitted or empty, the `name`, exactly as the runner resolves it. When it resolves to the url navigator, `target` is judged by the same url scheme set as an `actionType: 'url'` row, with no second grammar. A declared executor key still outranks the `name`. `name` is not refused as an executor spelling, and a row whose `name` names any other handler (`navigate_edit`, `convert_lead`, …) is untouched. An `actionType: 'url'` row's refusal is unchanged. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `{ name: 'url', target: 'javascript:…' }` with no `actionType` (or no `type` on a member), or `data:…`, `tel:…` or any other scheme | an `https://` page, a `mailto:` address, or an in-app path that starts with `/` or `./`; or, for a button meant to run something, the executor that does the work (`actionType: 'script'`, `'flow'`, `'api'`) | + | the same with `name: 'navigation'` | the same; the `name` is the runner's url alias, so the target is judged as a url | + | `{ name: 'url', target: 'forecast' }`, `'../forecast'`, `'?tab=2'` | the rooted in-app path, `'/forecast'`, or a `./` path; or declare the executor the row was meant to run | + | `{ name: 'url', target: '//cdn.example.com/x' }` | `'https://cdn.example.com/x'`, or `'/x'` for an in-app path | + | `{ name: 'url', target: '' }` | the address the button was meant to open | + + **The one-line fix: give every page action row that has no executor key and is named `url` or `navigation` an `http(s)://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or a template that opens with `${` as its `target`, or declare the executor key it was meant to run.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-name-leg-url-target-judged`; the rewrite is by hand, because which address a refused target meant is the author's call. + + **What an author now sees.** `os validate`'s props gate reports the refusal at the row's `target` (`properties.target`, or `properties.actions.N.target` for a member). For example, `{ name: 'url', target: 'javascript:alert(1)' }` on an `action:button` gets: ``An `action:button`'s `target` (`name: 'url'` and no `actionType`, so the runner runs the url navigator) opens with `javascript:`, a scheme a url target never opens.``, followed by the reason and the admitted set. Every admitted target parses unchanged. + + **Measured producers: none.** No page action row in this repository, in objectui (at its pin `1fb1ced36243` and its main `866763bd`) or in hotcrm (at `0f84c9c`) is named `url` or `navigation`. +- 1623faf: feat(spec)!: a page action row's `onSuccess` is refused beside a console runner built-in other than `api` / `script`, and beside a row with no executor; `ActionSchema`'s `onSuccess` refusal now states its true reason + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on one key of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings. + + The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). Each forwards `onSuccess` to the console's action runner, which performs the post-success hop after every executor that succeeds. `ActionSchema` refuses `onSuccess` beside any `type` but `api` and `script`; the rows judged it beside no executor at all. They now carry the action's rule on the vocabulary the action's closed `ActionType` can see, read against the row's effective executor (the executor key, or the row's `name` when that key is omitted or empty, as the runner resolves it): + + - refused beside the runner built-ins `url`, `navigation`, `modal`, `flow` and `form`; + - refused on a row with no effective executor (no executor key, or an empty one, and no `name`); + - accepted beside `api` and `script`, and beside any other name: that is a handler a host registers, the runner hops after it too, and only the host knows whether its handler navigates. + + `ActionSchema`'s own reach is unchanged (`url`, `modal`, `flow` and `form` are refused, as before). Its message used to say that on those types the key "would parse clean and never run" and that "no renderer reads a post-success hop today". Both were false: the runner hops after every executor. The action and the rows now print one refusal text with the true reason for each executor. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `{ actionType: 'url', target: '/tasks', onSuccess: { navigate: '/done' } }` (or `type: 'url'` on a member, or `name: 'url'` with no executor key) | the destination in `target`, the tab in `openIn`, and no `onSuccess`: the url action is itself the navigation, so the hop navigated a second time | + | the same with `navigation` | the same, with `actionType: 'url'` (the alias is refused at the executor key as well) | + | `{ actionType: 'form', target: 'task_form', onSuccess: … }` | no `onSuccess`; declare where a submitted form goes in the form view's `submitBehavior` | + | `{ actionType: 'modal', … , onSuccess: … }` or `{ actionType: 'flow', … , onSuccess: … }` | no `onSuccess`, or move the hop onto an `api` or `script` action, whose result it reads | + | `{ label: 'Go', onSuccess: { navigate: '/tasks' } }` (no executor key and no `name`) | declare the executor whose success the hop follows, `actionType: 'api'` or `actionType: 'script'` (`type` on a member), or drop `onSuccess` | + + **The one-line fix: keep `onSuccess` only on a page action row whose executor is `api`, `script` or a host-registered handler, and declare that executor.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-on-success-beside-executor-refused`; the rewrite is by hand, because where the hop belongs is the author's call. + + **What an author now sees.** `os validate`'s props gate reports the refusal at the row's `onSuccess` (`properties.onSuccess`, or `properties.actions.N.onSuccess` for a member). For example, `{ actionType: 'url', target: '/tasks', onSuccess: { navigate: '/done' } }` on an `action:button` gets: ``An `action:button`'s `onSuccess` (`actionType: 'url'`) would navigate a second time: a `url` action is itself the navigation, and the console's action runner performs the post-success hop after every executor that succeeds.``, followed by what the hop is declared beside and the prescription. The same row without `onSuccess` parses unchanged. + + **Measured producers.** None in this repository or in hotcrm (at `0f84c9c`). In objectui (at its pin `1fb1ced36243` and its main `72594f29`), one test row with no executor and no `name` carries `onSuccess` and is asserted accepted; its remedy is to declare `actionType: 'api'` on that row. A host-handler row (`actionType: 'run'`) with `onSuccess` stays accepted. +- c857a93: feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as `system`, in both directions, with the prescription (ADR-0138 D2b) + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0138 D2b decides that no anonymous door triggers an elevated flow directly. An `authRequired: false` endpoint of `type: 'flow'` whose `target` flow declares `runAs: 'system'` ran every anonymous request's data steps as the system principal, past row-level security, with the endpoint's rate limit as the only brake. The runtime's own elevated-start refusal covers only self-triggered flow types (`autolaunched`, `record_change`, `schedule`), so a `system` flow of type `api` or `screen` behind such an endpoint executed elevated. Publish now refuses the pair, in both directions, because either item can be the last one edited: the endpoint's publish reads its target's `runAs`, and a flow's publish that sets `runAs: 'system'` reads the anonymous endpoints that target it. Each refusal names the endpoint and the flow and carries the prescription. + + **What is refused, and where.** Every publish door that holds the pair calls one judge, `anonymousElevatedFlowPairs` in `@objectstack/spec/api`: + + - **The stack parse** (`defineStack`, `objectstack validate`, `objectstack build`, the artifact load): a stack whose anonymous flow endpoint names one of its own flows declaring `runAs: 'system'`. One zod issue per pair, `code: 'custom'` at `apis[i].authRequired`; `defineStack` throws its schema refusal, `STACK_SCHEMA_INVALID` (422), carrying the issue. + - **The package publish** (`MetadataManager.publishPackage`, `POST /api/v1/packages/:id/publish`): an `api` item of the package naming a `system` flow anywhere in the manager's index, reported at the endpoint; and a `flow` item of the package declaring `runAs: 'system'` that an anonymous endpoint outside the package names, reported at the flow. The package publishes nothing (`success: false`, the pair in `validationErrors`), and `validate: false` does not turn it off. + - **A flow published through the metadata API** (`saveMetaItem` in publish mode, so `PUT /api/v1/meta/flow/:name`; a draft's publish, `POST /api/v1/meta/flow/:name/publish`; and `POST /api/v1/packages/:id/publish-drafts`): a flow going `active` with `runAs: 'system'` while a declared anonymous flow endpoint names it. `INVALID_METADATA` (422), `issues[0].path: 'runAs'`, nothing written; a publish-drafts batch aborts on it. A draft save is not judged (a draft does not execute; its publish is), and neither is a `migrate-stored` re-save of a row that already exists. + + **Unchanged.** Each item alone publishes as before: an authenticated endpoint on a `system` flow, an anonymous endpoint on a `user` flow (or one that leaves `runAs` at its default), and an anonymous `object_operation` endpoint are all accepted. A stored flow row that already pairs this way keeps its bytes and keeps executing as published; its next publish is refused. A guest-written row that fires a record-change flow is outside the refusal: the record leaves that indirect path undecided, because its trigger is the write, not the door. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` | `apis: [{ name: 'acme_intake_api', type: 'flow', target: 'acme_intake', authRequired: false, rateLimit: { enabled: true, windowMs: 60000, maxRequests: 100 }, … }]` beside `flows: [{ name: 'acme_intake', runAs: 'system', … }]` accepted; every anonymous request ran `acme_intake` as the system principal | refused, `STACK_SCHEMA_INVALID` 422, the issue at `apis[0].authRequired` naming both items and the prescription | + | package publish | a package carrying either half of the pair, with the other half anywhere in the manager's index, published | refused, `success: false`, the pair reported at the package's own item | + | metadata API flow publish | `PUT /api/v1/meta/flow/acme_intake` with `runAs: 'system'` while an anonymous endpoint names it saved and armed it | refused, `INVALID_METADATA` 422, the issue at `runAs`, nothing written | + + **The one-line fix: serve the caller another way — an authenticated endpoint (`authRequired: true`) for a partner that can hold a credential, the flow's signed inbound hook (ADR-0041: a start node with `triggerType: 'api'`, a `hookId` and a `secret`, which may keep `runAs: 'system'`) for a partner webhook, or a public form (a form view whose `sharing` sets `allowAnonymous: true`) for anonymous intake.** `runAs: 'automation'` joins that list when ADR-0073 M2 adds the value. + + No D2 conversion rewrites it: which channel replaces the anonymous endpoint depends on who the caller is, and whether the flow needs elevation is a judgment about its data steps, so no conversion can choose a posture for the author. Its D3 record is the semantic entry `api-endpoint-anonymous-elevated-flow-refused` (protocol 18). + + **`@objectstack/spec`.** The judge and its two refusal sentences join `@objectstack/spec/api`: `anonymousElevatedFlowPairs`, `anonymousFlowDoorTarget`, `anonymousEndpointElevatedFlowMessage` (the endpoint direction), `elevatedFlowAnonymousEndpointsMessage` (the flow direction) and `validateAnonymousElevatedFlows` (the stack's issues), with the types `AnonymousFlowDoorCandidate`, `ElevatedFlowCandidate` and `AnonymousElevatedFlowPair`. `ObjectStackDefinitionSchema` runs the judge beside the per-endpoint gates. The `runAs` description on the flow schema, which prescribed `system` for every user-less trigger, `api` included, now names the exception and what serves that caller. + + **`@objectstack/metadata`.** `MetadataManager.publishPackage` runs the judge over the package's `api` and `flow` items against the index the endpoint matcher serves from. + + **`@objectstack/metadata-protocol`.** The flow publish doors run the judge against the declared endpoints: the engine registry's `api` items and the `metadata` service's. + + **Who is affected, measured.** No in-repository stack pairs an anonymous flow endpoint with a `system` flow: `examples/` declares no `authRequired: false` endpoint, and every anonymous flow endpoint in the test fixtures names a `runAs: 'user'` flow. The partner-webhook example, `showcase_inbound_task_webhook`, takes the signed inbound hook with `runAs: 'system'` and is untouched. +- 345d1be: fix(spec,plugin-sharing)!: `sys_record_share` no longer offers a `guest` recipient, which no grant ever honoured (ADR-0138 D8) + + Clause-②: no (narrowing: a declared recipient value no grant honours is refused) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + A record share to the anonymous principal was declared in two places and honoured in none. Both declarations are gone: + + - `RecordShareRecipientType` (`@objectstack/spec/contracts`) is now `'user' | 'group' | 'position' | 'unit_and_subordinates'`. Passing `recipientType: 'guest'` to `ISharingService.grant`, or typing a `RecordShare.recipient_type` as `'guest'`, is a compile error. + - The `recipient_type` select on `sys_record_share` (`@objectstack/plugin-sharing`) offers those same four options, and its `guest` label is gone from the four translation bundles. + + Why: the grant refused `guest` (ADR-0078), and no gate reads such a row, because the read and write gates match `recipient_type: 'user'` only. ADR-0090 D11 routes guest access through the guest position and share links, so a record share to the guest had nothing to gain. The sharing-rule vocabulary retired the same recipient in v17. + + What a caller sees now: + + - **The grant door** (`POST /data/:object/:id/shares`, or `grant()` from untyped code). A body naming `recipientType: 'guest'` is still refused with `VALIDATION_FAILED` (`400`), as before. The sentence now says the value was removed and that guest access flows through the guest position and share links. It used to say that sharing rules deliver non-`user` recipients, which was never true of a guest. To let a guest reach a record, use a public form or a share link. + - **The row door** (a system-context engine write: a seed, a plugin, a migration script). A row carrying `recipient_type: 'guest'` is now refused with `VALIDATION_FAILED`, an `invalid_option` error on `recipient_type` that lists the four values a row may carry. Before this change such a write stored a row that looked granted and enforced nothing. + - **Unchanged:** every `user` grant, the other three row values, the sharing-rule service, and the gates. A `guest` row that a system write stored before this release is read by no gate, both before and after. +- 430e5f5: fix(security)!: under a walled posture every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes. ADR-0131 D12 item 7: a walled deployment is "one environment, one schema, one catalog … The metadata-authoring capability (`manage_metadata` / `studio.access`) is what headquarters IT holds and plant admins do not." Item 8: a plant "cannot create a position, a permission set, a capability, a template or any metadata … A plant that needs its own catalog is given its own environment." D11: the rules are identical in every posture; only whether the wall is enforced differs. + + **Why.** The environment definition ledger carries no organization column, so a write lands for every organization. Measured on booted `isolated` and `group` stacks before this change: an organization owner granted `manage_metadata` inside their own organization created an object, a view and a flow at `/api/v1/meta`, edited, published, rolled back and reset the operator's view, ran `POST /api/v1/meta/_migrate-stored`, passed all thirteen `/api/v1/packages` write doors, installed, upgraded and uninstalled packages through `install-local`, and created a flow at `/api/v1/automation`, each with `2xx`; another organization's owner read every result. The security catalog was refused at these doors already; every other type was not. + + **FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner): + + - `PUT`, `DELETE`, `POST …/publish` and `POST …/rollback` of `/api/v1/meta/:type/:name` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for a type outside the security catalog (`object`, `view`, `flow`, `app`, `dashboard`, `translation` and every other type): **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher answers `403 PERMISSION_DENIED`). + - Every `/api/v1/packages` write on the runtime dispatcher — create, the manifest `PATCH`, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, `commits/:commitId/revert`, duplicate, adopt-orphans and delete — for a package carrying no position, permission set or capability: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before anything is read or written. The refusal no longer reads the package's items. + - `POST /api/v1/marketplace/install-local` (install and upgrade) and `DELETE /api/v1/marketplace/install-local/:id` (uninstall) on `@objectstack/cloud-connection`, for a package carrying no catalog item: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN`. The refusal's `error.details` no longer carries `catalogItems`; it carries `manifestId`. + - `POST /api/v1/automation`, `PUT /api/v1/automation/:name`, `DELETE /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone`: **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the automation service is touched. + - `POST /api/v1/meta/_migrate-stored`, the install-wide rewrite of the stored metadata, preview and apply alike: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` on `@objectstack/rest` and `403 PERMISSION_DENIED` on the runtime dispatcher, before a row is read. + - Every one of these refusals carries one sentence: it names the written type (`a package` at the package doors, `the environment's stored metadata` at the rewrite), the posture and "ADR-0131 D3, D12 items 7–8, D11", and points at an environment of the organization's own. The catalog refusals that shipped before keep their envelopes and now read the same sentence. + - The activation refusals under a wall (`POST /api/v1/automation/:name/toggle`, `POST /api/v1/actions/_activation/…`, `POST /api/v1/security/_activation/permission/…`) no longer recommend cloning a flow or a permission set, or authoring a sibling action, since those writes are the operator's under a wall too; they name the platform operator. + + **Unchanged.** Reads. Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows. The platform operator and the engine's own writes (seeders, installers, migrations) write as before. Under `single` (and with no `tenancy` service), a `manage_metadata` holder writes exactly as before. + + **The remedy.** The platform operator authors the environment's metadata, in Studio or through a managed package; an organization that needs metadata of its own is given an environment of its own. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-environment-metadata-writes-retired`. `walledCatalogWriteVerdict` (`@objectstack/metadata-core`) now judges every type alike: a custom door that writes environment metadata asks it after its capability gate, with no `isSecurityCatalogType` pre-filter. +- 14a13bc: fix(metadata-protocol)!: the metadata save, restore and publish doors refuse a permission set whose body `packageId` disagrees with the package it is saved into, and the audience-binding suggestion reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the package the set is saved into: the stored row's `package_id`, served as `_packageId`. A body `packageId` that named another package was accepted and then silently overridden, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **FROM.** A permission set saved and published into package `app.crm` with `packageId: 'app.other'` in its body answered `200` and `published`, and was owned by `app.crm` everywhere it was read. A set saved into no package with `packageId: 'app.other'` was stored as an environment-authored set, while the audience-binding suggestion reader offered it, accepted it and derived it into `everyone` as `app.other`'s. + + **TO.** + + - **`@objectstack/metadata-protocol`.** The save door (`PUT /api/v1/meta/permission/:name`, draft and publish mode, and every in-process caller) the restore doors (`POST /api/v1/meta/permission/:name/rollback` and a commit revert's restore limb) and the draft promotion (`POST /api/v1/meta/permission/:name/publish` and the package publish) refuse a permission set whose body `packageId` is not the package the row is written into, with `400 VALIDATION_ERROR`, the code and status the doors already answer for a body `name` that disagrees with its row. `issues[]` carries the key at `packageId`. A body `packageId` on a set saved into no package is refused too, the empty string included: a present key declares a package-shipped set. The message names both values and both remedies. The package judged is the one the write lands in: a draft saved without naming a package lands in the package of the item's active row, and is judged against it. A package publish that meets such a draft (one stored before this change) aborts the whole batch, `outcome: 'refused'`, with the draft in `failed[]` under `VALIDATION_ERROR`. A rollback to a stored version that carries such a body (one written before this change) is refused, and a commit revert reports it in `failed[]` under the same code; the remedy there is to save the set again with `packageId` dropped or set to its package, instead of restoring that version. Nothing is written, published or restored. Package duplication copies a set whose body names the source package as naming the target package. `os migrate meta --stored --apply` records a stored set it rewrites, and the door refuses, as that row's failure. + - **`@objectstack/plugin-security`.** The audience-binding suggestion reader takes a set's owner from its binding only, never from its body's `packageId`. A set bound to no package has no owner there, so no suggestion is created for it. An acceptance recorded under the package its body named is withheld from `everyone` at the next derivation. The withholding is reported at `warn`, naming the set and its reason. Accepting a pending suggestion of that kind is refused. + - **`@objectstack/spec`.** The `PermissionSetSchema.packageId` description states the refusal. The step-18 semantic migration entry `permission-set-package-id-binding-mismatch-refused` carries the upgrade sweep. + + **Unchanged.** A body `packageId` equal to the package the set is saved into is accepted, and an absent one is accepted everywhere. Stored rows are not rewritten. A stored set whose body disagrees with its binding keeps loading and keeps reading as its binding, and is refused on its next save or publish, as is a restore of a stored version that carries one. An installed package's own sets are owned by the package that ships them, as before. + + **Remedy, in one line.** Drop `packageId` from the permission set's body, or save the set into the package it names (`?package=` on the REST door, `packageId` in process). +- ca0bdbc: A builtin flow node's `config` value that carries a `{token}` is judged for its type at the build doors like any other value: `get_record` with `limit: '{x}'` is refused at `nodes.N.config.limit` ("expected number, received string") by `FlowSchema.parse`, `defineStack`, `objectstack validate`, `objectstack compile`, the metadata save door and `AutomationEngine.registerFlow`, where it used to register and then fail every run that reached the node. + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`). + + **Why.** The build doors' value arm (`flowNodeConfigRefusals`) never refused a value carrying a `{token}` for its type, because the flow interpolator decided what the run would parse there. The interpolator and every call site it had are deleted on this line, and every builtin executor parses its config as authored (an `http` node after its credential channel's `signingSecret`), so the run's contract sees the token the build's sees and refuses it at every run. The value arm passed exactly those values. + + **What is refused now.** A token-carrying value at a builtin key its executor contract refuses it at: a number or boolean key (`get_record` `limit`, `loop` `maxIterations`, `update_record` and `delete_record` `multi`, a screen field's `min`, `max` or `required`, a `try_catch` `retry.maxRetries`), a key with a declared set (`notify` `severity`, `screen` `mode`), a record key written as one token (a CRUD `filter: '{f}'`), and a rule finding the token used to hide (a `notify` `templateData` with no `template`). Each is `node-config-refused-by-contract`, `params: { nodeType, key }`, anchored at the key, in the contract's own words — `defineStack` throws `STACK_SCHEMA_INVALID` (422) at `flows.N.nodes.M.config.`. + + **What does not change.** + + - A token at a plain string key (`outputVariable`, `flowName`, `objectName`) parses and registers as before: the contract takes a string there. + - A token at a value slot (a CRUD `fields` value, a callee `input`, a screen's `defaults`, `defaultValue` or `recordId`, a `loop` or `map` `collection`, a `notify` `recipients`, `templateData` or `sourceId`, an `http` `body`, a filter operand) or at a text slot (a `notify` `title`, `message` or `actionUrl`, a screen `title` or `description`, an `http` `url` or header value) is refused, as before, by that slot's own judge at `registerFlow` and `objectstack validate`, once, with the slot's label and remedy: the value arm exempts each finding that judge owns, so no token is reported twice. + - At a key its executor reads as written (a `notify` `sourceObject`, `actorId` or `payload`; an `http` `method`, `durable`, `timeoutMs` or whole `headers` map) the key is refused once, in the literal's own words, as before. + - An `http` `signingSecret` is still never judged at the build doors: the credential channel may supply what the run signs with. + + ## FROM → TO + + | you wrote | write instead | what changes | + |:--|:--|:--| + | `get_record` `limit: '{page.size}'` | `limit: 25` | refused at `config.limit` at every build door, where it registered and failed every run | + | `loop` `maxIterations: '{cap}'` | `maxIterations: 50` | the same, at `config.maxIterations` | + | `update_record` `multi: '{bulk}'` | `multi: true` | the same, at `config.multi` | + | a screen field `min: '{m}'` | `min: 1` | the same, at `config.fields.N.min` | + | `try_catch` `retry: { maxRetries: '{n}' }` | `retry: { maxRetries: 3 }` | the same, at `config.retry.maxRetries` | + | `notify` `severity: '{level}'` | `severity: 'warning'` | the same, at `config.severity` | + + **The one-line fix: write the literal the key declares.** None of these keys is evaluated per run, so no envelope replaces the token there. + + **Who is affected, measured.** A census of every builtin node literal whose `config` evaluates statically — 1010 nodes, 244 of them carrying a `{…}` token, across `examples/`, `packages/` and `apps/` in this repository at the base and hotcrm at `0f84c9c` — finds no verdict this change moves (a control node with `limit: '{x}'` moves, so the census can see one). No published docs page and no `@objectstack/lint` rule says a token at a typed key passes the build. + + ### The kit + + - **The judge.** `flowNodeConfigRefusals`'s builtin value arm (`flow-node-config-refusals.ts`): the token carve-out (`carriesInterpolationToken`) is deleted. In its place the arm asks the value-slot judge (`flowNodeValueTemplateRefusals`) and the text-slot judge (`textSlotTemplateRefusal`) which paths they refuse and exempts those, and reports a key whose contract marks its refusal `READ_AS_WRITTEN_PARAM` in that refusal's words alone. + - **ADR-0087.** No new entry: the step-18 D3 entry `flow-builtin-node-config-values-refused` and its rationale fragment now state the class this change judges. +- 572ecfb: fix: `os migrate meta --stored` lists every stored row that protocol 18 refuses and no conversion rewrites. Before, the preview counted each such row as already on protocol. + + Clause-②: yes (widening: `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec`, `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` and `StoredFlowCanonicalization.todos?` in `@objectstack/metadata-protocol`, each a new output member; nothing narrows, and no door accepts or refuses anything it did not before) + + Three step-18 retirements refuse a stored shape that has no lossless rewrite. Upgrading a v17 database left those rows refused at load or at boot, and the stored preview read them as `canonical`. Each shape now has exactly one fate: **refused and listed**. The preview and `--apply` print one `TODO` line per site under its row. The line names the step-18 entry that refuses the shape, the site, and the choice the owner has to make. Nothing is rewritten, and the run's verdict does not change, because no run of the pass could clear these rows. + + **#23269, a flow value slot carrying a `{…}` token, including a `get_record` / `update_record` / `delete_record` filter operand: refuse and list.** A whole-path token maps mechanically to `{ dialect: 'cel', source: '' }`, but the rewrite is not lossless. A token the template resolved to nothing (an erased condition, so the node was refused) is a CEL fault on an absent root, and a flow variable named like a query placeholder used to shadow it. ADR-0087 D2 conversions run on the load path and must be lossless, so this one is not a D2. The chain also has no operator-applied rewrite of stored rows separate from the load path: `--stored` writes back the read path's own pass, and `--from 17` rewrites authored sources, not rows. So the flow is listed by flow, node and path (`flow-value-slot-template-dialect-refused`), and `registerFlow` goes on refusing it until the value is rewritten as its CEL envelope. A flow already in the CEL form, or using a query placeholder such as `{current_user_id}`, is not listed. + + **#23270, a `controlled_by_parent` object without exactly one required `master_detail`: refuse and list, by object (`cbp-master-relation-tiers-retired`).** No shape here converts losslessly. The upgrade never picks between two masters, either both required or neither. An explicit `required: false` is a contradiction the builder refuses. Turning a required `lookup` into a `master_detail` changes what deleting the master does. An object with no master has nothing to convert. Leaving `controlled_by_parent` changes who can read the rows, so that is the owner's choice and never a silent rewrite. The listing reads the same predicate `ObjectSchema` refuses with, so the preview and the parse refuse one set. The one lossless shape, a single `master_detail` with `required` omitted, is still converted by `cbp-master-detail-required-defaulted` and is not listed. + + **#23271, a view whose `data` binds the retired view-level API source: refuse and list, by view (`view-data-api-provider-retired`).** There is no mechanical move. The replacement is a page component's `dataSource`, so a page must exist to carry it. Deleting the view's `data` would make the view query its object instead. Views in every stored body spelling are covered, and so are an object's own `listViews`. The line shows the request URL and method and withholds every other request key, so a credential stored in a view's headers never reaches the report. A view on the `object`, `value` or `schema` provider is not listed. + + How it is wired: `applyConversions` reports these shapes through its existing `onTodo` sink, after the conversions run. Each TODO's `conversionId` is the id of the D3 entry that refuses the shape, so the published `ConversionTodoNotice.conversionId` now names either a conversion or a D3 entry. The published flow-canonicalization result gains one member, a widening in three packages: + + - `@objectstack/spec`: the contract `IAutomationService.canonicalizeStoredFlow(...)` declares an optional `todos?: ConversionTodoNotice[]` beside `notices` and `conflicts`. An implementation that reports none still conforms. + - `@objectstack/service-automation`: `AutomationEngine.canonicalizeStoredFlow(...)` returns `todos`. `registerFlow` ignores them, because its own refusal names the same sites. + - `@objectstack/metadata-protocol`: `StoredFlowCanonicalization` gains the matching optional `todos?`, and the stored pass lists a flow row's TODOs the way it lists any other row's. + + The authoring funnel and the read seams pass no sink, so nothing changes for them. +- 707b3cd: feat(cli,spec): `os migrate organization-ownership --fate-supplement FILE`. A host declares the ADR-0131 D10 fates of the objects it provides, and the ceremony plans and applies them as it does the platform's own rows + + Clause-②: yes (widening) + + The v18 organization-ownership ceremony refuses any platform-prefixed table that carries `organization_id` when its inventory gives that table no fate. That is right for the platform's own objects, but a host's objects have no row in the inventory, so every database a host runs was refused. The host now declares those fates itself, in one JSON file, and passes the file to the command. **Adoption: a host passes its supplement through the flag; with no flag, the ceremony behaves as before.** + + - **The file is declared in the spec.** `OrganizationOwnershipSupplementSchema` (`@objectstack/spec/system`) is a JSON array of `OrganizationOwnershipSupplementEntrySchema` rows. Each row gives `object`, `fate` (`column-drop`, `mirror-deletion`, `attribution` or `report`), `citation`, and only the fields its fate reads: `anchors` (required for `attribution`), `mirror`, `departures`, `categories`, `notNullExempt` and `note`. Every shape refuses an unknown key, and a key written on the wrong fate is refused rather than ignored. + - **Both modes read it.** The plan plans each row on the same terms as a platform row, and `--apply` performs it the same way. A row whose table is not on the database being planned is listed `absent`, so one supplement serves both a control-plane database and a tenant database. + - **It is refused, never half-read.** The command refuses a missing file, a file that is not JSON, a row the schema refuses, two rows for one object, or a row naming an object the platform inventory already gives a fate. The refusal comes before the database is opened, inside the `--json` envelope (`plan_refused` / `apply_refused`), and names the file, the row index and the object. A row naming one of the cloud-provided objects that the inventory lists with no fate (`cloudCarried`) replaces that placeholder row. + - **Nothing is read without the flag:** no environment variable and no conventional path. A host table with no declared fate is still refused by name, and the refusal now names `--fate-supplement FILE` as the remedy. + - The supplement's rows are part of the plan's `inventoryDigest`, so the apply's checkpoint is keyed on the supplement as well. + + ```bash + os migrate organization-ownership --fate-supplement host-fates.json --out plan.json + os migrate organization-ownership --fate-supplement host-fates.json --apply --backup-taken + ``` +- 5e725b8: A server-side producer can store a file and attach it to a record: `IFileAttachmentService.attachFile`, registered by `StorageServicePlugin` as the kernel service `file-attachments` (`FILE_ATTACHMENT_SERVICE`), writes the bytes, one `sys_file` row and one `sys_attachment` row linking it to the parent, under the checks every other attachment of that parent meets + + Clause-②: yes (widening: `@objectstack/spec/contracts` gains `IFileAttachmentService`, `AttachFileInput`, `AttachedFile` and `FILE_ATTACHMENT_SERVICE`, and `StorageServicePlugin` registers and declares one more service) + + - **The call.** `attachFile({ parentObject, parentId, organizationId, filename, mimeType, data })` answers `{ fileId, attachmentId, size }`. `organizationId` is required: the organization the caller acts in, or `null` for none. It acts as the system, for a producer with no browser upload in front of it (inbound mail first). Reach it with `ctx.getService(FILE_ATTACHMENT_SERVICE)` once the kernel has bootstrapped. + - **What it runs.** Each check is the one the rest of the platform already runs, not a copy: + - The `max_upload_mb` storage setting, through the same verdict the REST upload doors ask. Over it, the call is refused `413 PAYLOAD_TOO_LARGE` before anything is stored. + - The parent's organization. A parent walled into another organization than `organizationId` is refused `404 RECORD_NOT_FOUND`, as a missing parent is. Both rows are written in the parent's organization, or in the caller's for a global parent. + - Every gate on the `sys_attachment` insert, among them the `enable.files` opt-in: a parent object without it is refused `403 FILES_DISABLED`. + - Before the kernel has bootstrapped, or with no data engine, the call is refused `503 SERVICE_UNAVAILABLE`. A malformed input is refused `400 VALIDATION_ERROR`. + - **No refusal leaves a committed `sys_file`.** The row is written `pending` before the bytes, and turns `committed` only in the same transaction as the `sys_attachment` insert. A refused call removes the bytes and the row before it throws. If the process dies mid-call, the pending-upload retention reclaims both. + - `@objectstack/service-storage`: the upload doors' size refusal moved from the route module into `storage-limits.ts`, beside the limit it enforces, so the doors and the verb read one status, one code and one sentence. The doors answer exactly as before. +- eeb95f7: fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3) + + Clause-②: yes (narrowing) + + + + **BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes. + + ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got. + + **What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door: + + - **The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it. + - **Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key. + + The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted. + + **Readers.** `@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report. + + **Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten. + + ## FROM → TO + + | door | FROM | TO | + |:--|:--|:--| + | `defineStack` / `objectstack validate` / `objectstack build` | `defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm` | refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 | + | `ObjectQL.registerApp` | the same manifest registered the set under `com.acme.crm`, silently | `400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed | + | an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key | + + **Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again. +- d89f491: A stored flow the automation engine refuses is no longer served as running: the flow meta read serves the engine's verdict beside the item as `registration`, an envelope sibling of `lock`, and the flow list serves it as `registrations` keyed by flow name, so a refused flow reads `{ state: 'refused', reason: { code?, message } }` beside its stored `status`, which stays as authored. + + Clause-②: yes (widening) + + - **The shape** (`@objectstack/spec`). `FlowRegistrationSchema` is a closed set: `registered` (the engine holds the flow), `refused` (it refused the stored flow and holds nothing under the name; `reason` carries the engine's own `message` and, when the thrown error set one, its `code`, verbatim), and `unregistered` (it holds nothing and refused nothing: a read during boot, between a save and the engine following it, or after a deliberate unregister). `GetMetaItemResponseSchema` gains `registration` and `GetMetaItemsResponseSchema` gains `registrations`, both optional. New exports: `FlowRegistrationSchema`, `FlowRegistrationRefusalSchema` and the types `FlowRegistration` and `FlowRegistrationRefusal`. `IAutomationService` gains the optional member `getFlowRegistration(name)`. + - **Where the verdict comes from** (`@objectstack/service-automation`). The `kernel:ready` cold-boot bind records each flow it refuses, with what the engine threw, after it withdraws the flow; the `metadata:reloaded` re-sync and the metadata-change sync record a refusal that leaves the name unregistered. A later successful registration of the name, or its withdrawal, ends the record. `AutomationEngine` answers `getFlowRegistration(name)` and takes `recordFlowRefusal(name, error)`. The refusal's warn line, its level and the withdrawal are unchanged. + - **Where it is served** (`@objectstack/metadata-protocol`). The protocol reads the engine through the kernel's service table by name, as it already reads the engine's stored-flow canonicalizer; it does not import the automation service. The field is absent where no automation service declares the member, on every type other than `flow`, on a draft read and on the execution face the engine binds from: absence means no engine judged the flow, never `registered`. The verdict is the engine of the process that answered the read. + - **The default door** (`@objectstack/rest`, `@objectstack/metadata-protocol`, `@objectstack/spec`). `GET /api/v1/meta/flow/:name` on a default deployment (`metadata.enableCache: true`) is answered by the cached item branch, which now carries `registration` too. `getMetaItemCached` returns it beside `data`, read live from its delegate read on every call, and `GetMetaItemCachedResponseSchema` declares it. It is never part of the ETag, so a cached read that carries it is never answered `304 Not Modified`: a judged flow is always served in full. The cached branch's other envelope keys are unchanged. +- dc39326: fix: `os migrate meta --stored` lists the two families of stored rows it still read as on protocol: a page component whose `data` prop binds the retired view-level API source, and every other site `registerFlow` refuses in a stored flow the parse accepts. + + Clause-②: yes (widening: `ConversionTodoNotice.conversionId` in `@objectstack/spec` may now name `registerFlow`, the refusing door; `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec` and `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` now also carry each site flow registration refuses; the `onTodo` sink of `applyConversions` now also receives a page component bound to the retired API source; nothing narrows, and no door accepts or refuses anything it did not before) + + **A page component bound to the retired view-level API source: refuse and list, by page and component (`view-data-api-provider-retired`).** `object-grid`, `object-map`, `object-gantt` and `object-tree` take a view data source in their `data` prop, so the step-18 retirement of the `provider: 'api'` arm reaches them through the same schema. The listing judges each stored page component with its own props row, the one the component-props gate parses, and lists it only when that row refuses the binding with the retirement's prescription; a component whose `data` is a row array, a custom type and a component's own `dataSource` binding are not listed. The line names the page, the component and its position, shows the request URL and method, and withholds every other request key. Nothing is rewritten: deleting `data` would make the component query its object instead. Left as stored, the page still loads and saves, because the page parse does not judge a component's properties, while `os validate`, `os build` and `os lint` refuse the binding in source. The D3 entry `view-data-api-provider-retired` now names these components in its scope. + + **A stored flow refused by `registerFlow` after the parse: refuse and list, by site (`registerFlow`).** The stored pass ran only one of registration's judges, the value-slot one. `canonicalizeStoredFlow` now runs registration's whole validation without registering: every check `registerFlow` runs once the parse has accepted the flow, through the one list `registerFlow` itself throws from. Each refused site is a `TODO registerFlow` line under its row, in the check's own words: a text slot still on the single-brace dialect or with a hole that does not compile, a decision's `mode`, a malformed node or edge predicate, a plugin node's undeclared config key, an `api` flow with no secret. A check added to registration later is listed the same way, with no second change. A site the chain already lists under a step-18 entry is not repeated. What the parse itself refuses, the builtin config contracts' value arm among it, is not a TODO: `canonicalizeStoredFlow` throws it, and the preview reports the row among those that could not be rewritten, in the parse's words. + + `registerFlow` refuses exactly what it refused before, with the same message: its checks now return their refusal instead of throwing it, and `registerFlow` throws the first one. The doors that canonicalize a flow without registering it (the metadata save door, a package duplication) read no `todos`, and the stored pass's run of the checks logs nothing, so they behave as before. +- 775ed65: feat(spec)!: a page action row declares `ActionSchema`'s own `operation` enum and refuses `onSuccess` beside `operation: 'update'`, with the action's text + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on two keys of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings. + + The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). Each forwards `operation` to the console's action runner, which reads it before any executor: `operation: 'update'` performs the declarative write through the platform action route and runs no executor of its own. The rows typed `operation` as an open value and never carried `ActionSchema`'s refusal of `onSuccess` beside `operation: 'update'`, so a row with an `api` or `script` executor, the write and a hop parsed while the same action was refused. Now: + + - a row's `operation` is `ActionSchema`'s own enum (one member, `'update'`); `'delete'` and `'custom'` get the action's "bulk-def operation with no row-level form" refusal, and any other value is refused at `operation`; + - a row's `onSuccess` beside `operation: 'update'` is refused whatever executor the row names (or none), with ONE text shared with `ActionSchema`: the hop would follow the write, whose success carries no result `${result.*}` could read. + + `ActionSchema`'s reach is unchanged. Its message for the pairing used to open "`onSuccess` is not defined for an `operation: 'update'` action yet"; it now states what the runner does (reads `operation` before any executor, writes, then hops) and keeps the prescription "Drop `onSuccess`". + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `{ actionType: 'api', operation: 'update', patch: { status: 'done' }, onSuccess: { navigate: '/done' } }` (or `type` on a member, or no executor) | the same row without `onSuccess`: the runner performs the write before any executor, so the hop followed a write whose success declares no result | + | `{ operation: 'delete', … }` / `{ operation: 'custom', … }` on a row | no `operation`: a row delete is the object's own delete affordance, and a row is already the action a bulk def's `'custom'` would name | + | any other `operation` value | `operation: 'update'` with a `patch`, or no `operation` (the runner ran nothing for it) | + + **The one-line fix: on a page action row, write `operation` only as `'update'`, and never beside `onSuccess`.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-operation-declared`; the rewrite is by hand. + + **What an author now sees.** `os validate`'s props gate reports the refusal at the row's `operation` or `onSuccess` (`properties.…`, or `properties.actions.N.…` for a member). For example, `{ actionType: 'api', operation: 'update', patch: { status: 'done' }, onSuccess: { navigate: '/done' } }` on an `action:button` gets: ``An `action:button`'s `onSuccess` (`operation: 'update'`) would follow a result the contract does not declare: the console's action runner reads `operation` before any executor, …``. The same row without `onSuccess` parses unchanged. + + **Measured producers.** None in this repository. In objectui (at its pin `1fb1ced36243` and its main `b9814b3`), the action renderers' own tests carry `operation: 'update'` rows without `onSuccess`, which stay accepted; no row carries another `operation` value or the pairing. objectui's metadata-admin form already clears `onSuccess` when an author switches an action into an update. + +### Patch Changes + +- dac99c2: feat(plugin-security)!: the permission-set projection, its seed-name lookup and refusal sink are deleted (ADR-0131 D3, C3 stage 7a) + + Clause-②: no (narrowing) + + + + **BREAKING** (retired exports of `@objectstack/plugin-security`), graded `minor` on the v18 prerelease line. + + Stage 8 stopped registering the `sys_permission_set` row object, the `permission` mutation projector and the data-door write-through. This change deletes the modules behind them: `permission-set-projection`, `seed-name-lookup` and `seed-refusal-sink`. A permission set is written through the metadata door alone (`PUT /api/v1/meta/permission/:name`), and the security catalog serves it by name. + + What a consumer sees: + + - FROM `createPermissionSetWriteThrough(deps)` TO nothing: no `sys_permission_set` data-door write exists to redirect. Write the set through the metadata door. Delete the call. + - FROM `registerPermissionSetProjection(protocol, deps)` TO nothing: no row mirrors a definition. Delete the call. + - FROM `reconcilePermissionSetProjection(...)` TO nothing: there is no projection to reconcile at boot. Delete the call. + - FROM `projectPermissionMutation(...)` TO nothing. Delete the call. + - FROM `upsertEnvPermissionSet(...)` TO `PUT /api/v1/meta/permission/:name`, or the metadata protocol's `saveMetaItem({ type: 'permission', name, item })`. + - FROM `permissionSetBodyFromRow(row)` TO `permissionSetDefinitionFromRow(row)` from `@objectstack/core`: the same mapping, for converting a stored `sys_permission_set` row into its definition. + - FROM `permissionSetRowFields(definition)` TO nothing: no row is written. Delete the call. + - FROM the types `PermissionSeedOutcome`, `ProjectionDeps`, `ProjectionReconcileOutcome` and `WriteThroughDeps` TO nothing. Delete the import. + + Unchanged: the security catalog's echo of a metadata-door permission set or position on an environment kernel, which lives in its own module since #23308. The packaged-permission-set lock still refuses a metadata-door save of a set a code package ships (`403 NOT_OVERRIDABLE`), and the boot still reports environment overlays that shadow such a set. `OverlayDetectionOptions.logger` now asks only for `warn`, the one channel the reading reports on. + + **`@objectstack/spec`** (patch): the `permission.tabPermissions` liveness entry no longer cites the deleted projection, and the liveness README's registered-but-unbound `permission-set-projection` proof row is retired with the invariant it described (no `@proof` tag carries it any more). +- 7958bdf: No flow position reads the single-brace `{…}` template dialect any more, and its resolver is deleted (ADR-0032 Sequencing 6: "delete the single-brace resolver"). `AutomationEngine.evaluateCondition` evaluates every condition as CEL: a condition carrying a `{var}` hole is refused as the CEL brace trap, naming its source, and a `template` or `flow` envelope is refused before evaluation, where the deleted path substituted the variables and compared the text. A literal in a value slot is handed on as written: a `Date` in a code-defined flow reaches the write as the `Date`, where the deleted interpolator wrote `{}`. + + Clause-②: no (narrowing) + + + + **BREAKING**: a narrowing of a published runtime method and of published lint exports, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`). + + **Why.** ADR-0032 Decision 3 keeps one template delimiter, `{{ }}`, and its Sequencing 6 deletes the single-brace resolver. The earlier changes on this line retired the dialect position by position — every value slot, the text slots, each CRUD filter operand — so the resolver's last readers were two calls that saw literals only and a condition path that only a direct call reached. + + **What changes.** + + - **Conditions.** `AutomationEngine.evaluateCondition(condition, variables, context?)` reads every condition as CEL — bare text, an envelope with `dialect: 'cel'`, or one naming no dialect. The template path it kept for a string carrying a `{var}` hole is deleted: such a string is refused as a CEL fault that names its source and says not to wrap field references in `{…}`, and an envelope with `dialect: 'template'` or `dialect: 'flow'` is refused before evaluation. `registerFlow` already refused both at every condition slot (a start node's `condition`, an edge `condition`, a `decision` branch's `expression`, a screen field's `visibleWhen`), so no registered flow changes. What changes is a direct caller: a plugin calling the method, or `@objectstack/verify`'s `stack.automation.evaluateCondition`, which hands the condition over as given. + - **Value-slot literals.** A literal in a value slot — a `fields`, `input` or `templateData` value, an `assignment` value in any of its three shapes, a `notify` recipient, an `http` `body`, a screen's `defaults`, `defaultValue` or `recordId` — is handed on as a copy of what was written (arrays and plain objects copied, every other value as it is). The deleted interpolator walked every object's own entries, so a `Date` written in a code-defined flow reached the write as `{}`; it reaches it as the `Date`. + - **`@objectstack/lint`.** `validateFlowTemplatePaths` reads the `{{ }}` holes of a node's strings and no single-brace token; a CRUD node's `filter` is not scanned, since each operand is a value slot that renders no template. So `flow-template-unknown-field` and `flow-template-lookup-traversal` report only `warning` now: their `error` at a filter operand, and every finding on a single-brace token, are gone, and the value-slot and text-slot judges refuse those tokens at the same doors as `expression-invalid`. Messages and hints spell a hole `{{ record.x }}`. `flow-double-brace-interpolation` and `flow-bare-dollar-reference`, at a string outside every value slot, text slot and declared literal (a plugin node's own config, a `connector_action` input), say the value is read as written, where they prescribed a `{var}` token nothing reads. The lint's mirror of the retired grammar is removed with its exports. + - **`@objectstack/spec`.** The `FlowSchema` example's `connector_action` input holds a literal, since nothing reads a token there. The upgrade guide's step-18 text-slot entry says no other flow string reads the single-brace dialect, where it said every other one kept it, and its value-slot entry names every value slot the retirement reached on this line, where it named only the `assignment` values and the CRUD `fields` map. + + ## FROM → TO + + | you wrote | write instead | what changes | + |:--|:--|:--| + | `engine.evaluateCondition('{amount} > 100', vars)` | `engine.evaluateCondition('amount > 100', vars)` | the template substituted text and compared it; CEL compares the values and fails the run on an absent variable | + | `"{status} == 'open'"`, `'{status} == open'` | `"status == 'open'"` | a string literal is quoted | + | `{ dialect: 'template', source: "{flag} == true" }`, `dialect: 'flow'` | `{ dialect: 'cel', source: 'flag == true' }` | — | + | `classifyFlowTemplateToken(inner)` (`@objectstack/lint`) | `valueSlotTemplateRefusals(value)` / `textSlotTemplateRefusal(text)` (`@objectstack/spec/automation`), which refuse a token with the spelling that replaces it | the grammar it classified is read by nothing | + | `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` | the CEL stdlib: `isoDate(today())`, `isoDatetime(now())`, `round`, `floor`, `ceil`, `abs`, `min`, `max` | — | + | `FlowTemplateTokenVerdict` | nothing: the judges' reading of the retired grammar is internal to `@objectstack/spec` | — | + + **The one-line fix: write a condition as CEL, without `{…}` around a reference.** + + **Who is affected, measured.** In this repository at the base: no braced condition and no `template` or `flow` condition envelope in `examples/`, the docs or any package source; the deleted path's own pins in `@objectstack/service-automation` (`engine.test.ts`, `nested-region-parity.test.ts`) and `@objectstack/verify`'s door test are re-pinned to the refusal here. No authored `Date` literal in a flow was found; a compiled artifact serialises one to text, so only a flow handed to `registerFlow` in code carried one. Neither repository imports the removed lint exports. hotcrm at `0f84c9c` carries no braced condition and no `connector_action` input; its flows still author the single-brace dialect at value and text positions earlier changes on this line refuse, which its own upgrade migrates. + + ### The kit + + - **The engine.** `AutomationEngine.evaluateCondition` (`@objectstack/service-automation`): the template path, its hole discriminator and its text comparison are deleted. The value-slot resolvers' literal arm and the `assignment` executor copy a literal (`copyLiteral`, `builtin/value-slot-map.ts`). The single-brace interpolator (`interpolate`, `interpolateString`, `resolveToken`, its function table and `FlowExpressionFunctionError`), which the package did not export, is deleted with its tests. + - **The lint.** `validateFlowTemplatePaths` and the two wrong-syntax hints (`lint-flow-patterns.ts`); `classifyFlowTemplateToken`, `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` and `FlowTemplateTokenVerdict` are removed. + - **The verify handle.** `stack.automation.evaluateCondition`'s documentation states the engine's reading: CEL, a `{var}` hole refused. + - **ADR-0087.** No new entry: the step-18 D3 entries `flow-value-slot-template-dialect-refused` and `flow-text-slot-single-brace-refused` record the retirement. +- 777eed2: The `mailAssociation` liveness row is `live`: the mailbox service's matcher reads the key, so authoring `mailAssociation: true` on an email field no longer draws the "no mailbox service reads this yet" author warning + + Clause-②: no (dropping an author warning widens no accept set and no export: `FieldSchema` accepts `mailAssociation` exactly as before; only the warning the shipped liveness ledger carried is gone) + + - `liveness/field.json`: the row moves from `planned` with `authorWarn` and `authorHint` to `live`, citing `MailRecordMatcher` in `@objectstack/service-mailbox` as its reader. The `field` state counts move from 94 live and 4 planned to 95 live and 3 planned. +- c056979: Retirement prescriptions for shapes `@objectstack/spec` 17.7.0 still accepts now name 18 as the release that removed them + + Clause-②: no + + 17.7.0, the newest 17.x release (npm `latest`), still accepts every shape in the two tables below. No 17.x release removes them: the 18 line does. `18.0.0-next.0` is the first release that refuses the shapes in this first table. Their refusals, and their `[REMOVED]` JSON Schema descriptions, named a different release: + + | Surface | Was | Now | + | --- | --- | --- | + | `element:number` `object` / `filter`; `element:record_picker` and `element:repeater` `object` / `filter` / `sort` / `limit` | removed in @objectstack/spec 17 | removed in @objectstack/spec 18 | + | `object-grid` `defaultFilters` | removed in @objectstack/spec 17 | removed in @objectstack/spec 18 | + | `email_template.category`, `email_template.isSystem` | removed in @objectstack/spec 17 | removed in @objectstack/spec 18 | + | a package manifest's `permissions` written as a list of permission strings | removed in @objectstack/spec 17 | removed in @objectstack/spec 18 | + | `public` in `StorageScope` | removed in @objectstack/spec 17.8.0 | removed in @objectstack/spec 18 | + | `MetadataTypeRegistryEntry.allowOrgOverride` → `allowEnvironmentOverlay` | renamed (no release named) | renamed in @objectstack/spec 18 | + + The `18.0.0-next.0` release notes quoted the two `email_template` prescriptions with the earlier version, 17; from this release on they name 18. + + `18.0.0-next.0` still accepts the shapes in this second table, so the first release that refuses them is the one after it. Their refusals named 17, or no release at all: + + | Surface | Was | Now | + | --- | --- | --- | + | `view.data.provider: 'api'` | removed in @objectstack/spec 17 | removed in @objectstack/spec 18 | + | `'navigation'` as a page action row's executor (`actionType` on `action:button` / `action:icon`, member `type` on `action:group` / `action:menu`) | no release named | refused on page action rows since @objectstack/spec 18 | + | a `controlled_by_parent` object with no `master_detail` declared `required: true` | no release named | the one-required-master rule holds since @objectstack/spec 18 | + + One prescription was wrong the other way. 17.4.0 accepts the sub-day `TimeUpdateInterval` members `second`, `minute` and `hour`, and 17.5.0 refuses them, but the refusal said "retired in protocol 18". It now says "removed in @objectstack/spec 17.5.0". + + The `os migrate meta --from 17` sentence in each prescription is unchanged and was already right: `--from 17` replays the protocol-18 conversions. A prescription for a key that a 17.x release already refuses keeps naming 17. Text only: no key, value or shape is accepted or refused differently. +- 4f68ba1: Every finding of the component-props gate is now an error at `objectstack validate`, `objectstack build` and `objectstack lint`. A page component whose `properties` carry a key its `ComponentPropsMap` row does not declare, a value that row rejects, or no `dataSource.object` binding where the element reads only that binding drew a warning: the command printed "✓ Validation passed" and built the artifact, and the renderer ignored the key or drew nothing. Each is now a `component-props-unknown-key` or `component-props-invalid` error, and the stack does not pass until it is fixed. A retired prop was already refused, and still is. + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` and `component-props-unknown-key` rules of `objectstack validate`, `objectstack build` and `objectstack lint`). + + **What is refused now.** Every verdict of the gate: + + - `component-props-unknown-key`: a key the component's props row does not declare, at the bag's own level, on a strict object or union arm below it, or on a strip-mode object below it. + - `component-props-invalid`: a value the props row rejects (a wrong type, a missing required prop, a closed-set refusal such as a url action's `target` scheme or a `record:details` section's `count`, a retired enum value), and a missing `dataSource.object` on `element:record_picker`, `element:number` and `element:repeater`. + + **Not judged here.** The runtime publish door (the Studio, REST `/meta`, MCP) does not run this rule, so a page saved there is not refused for any of these. `defineStack` and the stack schema keep the open `properties` bag as written. A component `type` that `ComponentPropsMap` carries no row for (a `custom.*` or plugin type) is not judged. + + `ComponentPropsSeverity`, the type `@objectstack/lint` exports for these findings, narrows from `'error' | 'warning'` to `'error'`. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `page:header` `properties: { titel: 'Deals' }` | `properties: { title: 'Deals' }`, the declared spelling the error names | + | `record:related_list` `properties: { limit: 'five' }` | `properties: { limit: 5 }`, the type the error names | + | `element:number` with no `dataSource` | `dataSource: { object: 'deal' }` on the component node, beside `type` | + | a key the component does not read | nothing: delete the key | + + **The one-line fix: apply what the error names.** An unknown-key error names the declared spelling when one is close, a value error names what the row expects, and the binding error names where the object goes. If the component really does read the key or value, the props row is what is wrong: correct the declaration in `@objectstack/spec`, not the page. + + The `@objectstack/spec` change is text only: the reason of each D3 migration entry named above said the gate reported its refusal as an advisory finding, and now says it is an error. + + **Who is affected, measured.** At objectstack `8323cad97`, this rule ran at its own input tier over `examples/app-crm`, `app-todo`, `app-multi-package` and `app-showcase`, and over the eight shipped pages outside them (`@objectstack/platform-objects` ×3, `@objectstack/plugin-approvals`, `packages/qa/downstream-contract`, `@objectstack/cloud-connection` ×2, `@objectstack/mcp`): 37 pages, 177 components judged. It also ran over `objectstack-ai/hotcrm` at `0f84c9c72`: 8 pages, 50 components judged. Result: 0 findings of every class in both. A planted undeclared key and a planted wrong-typed prop each drew one finding on every population. The `create-objectstack` template authors no page. Deployed metadata and other repositories were not measured. +- b4d573a: fix(spec,core): the rolling `last_7_days`, `last_30_days` and `last_90_days` presets close on today, at the spec's prescription table, the save door's refusal and the analytics preset arm + + Clause-②: no (no accept set widens: the three rolling windows gain the inclusive `{today}` end every calendar preset already has) + + The three rolling windows were the open arm of `DATE_RANGE_PRESET_MACRO_WINDOWS`: `['{30_days_ago}', null]`, prescribed as `{ $gte: '{30_days_ago}' }`. A `$gte`-only window has no upper bound, so on a column that carries future dates (a close date, a due date) "last 30 days" also counted every later day. Measured on a `date` field with one row dated today−5 and one dated today+5: that window counted 2, while the console's preset lowering and the analytics preset arm counted 1. + + - `DATE_RANGE_PRESET_MACRO_WINDOWS` now closes every rolling window on `{today}`: `last_7_days` is `['{7_days_ago}', '{today}']`, and `last_30_days` and `last_90_days` follow the same shape. That is the inclusive last-day end every calendar preset already uses, and the extent the console's lowering already selects. The table's declared type narrows to match, `readonly [start: string, end: string]` (it was `end: string | null`). A reader that compared `end` with `null` still compiles; that branch can no longer be taken. + - The save door's refusal of a bare preset comparand (`bareDateRangePresetComparandMessage`, and the `filter-preset-comparand` lint verdict that quotes it) now prescribes the closed window. For `last_30_days` it says: write `{ $between: ['{30_days_ago}', '{today}'] }`, where it used to say `{ $gte: '{30_days_ago}' }`. + - `@objectstack/core`'s `resolveAnalyticsDateRangePreset`, the preset arm behind `timeDimensions[].dateRange`, selects the same extent. A rolling window now stops before tomorrow begins in the reference timezone, exactly as `today` does, with `endExclusive: true`. It used to end at the reference instant, so it dropped rows stamped later today. Its ends keep this module's own "the day the window stops before" spelling (`tomorrow`). Every preset this resolver returns is now `endExclusive: true`, and a caller's explicit `[a, b]` window keeps its inclusive reading, as before. + - The semantic migration entry `filter-preset-ordering-comparand-refused`, which `spec-changes.json` and `os migrate meta` carry, now names `{ $between: ["{30_days_ago}", "{today}"] }` as the window for `last_30_days` in its replacement, where it named `{ $gte: "{30_days_ago}" }`. + + If you followed the old refusal and authored `{ $gte: '{N_days_ago}' }` for a rolling preset, add `$lte: '{today}'`, or write the `$between` pair above, to stop counting future-dated rows. +- 6673c80: feat(cli)!: the v18 upgrade carries stored comment reactions — `os migrate comment-reactions` turns each reaction stored in the retired `sys_comment.reactions` column into a `sys_comment_reaction` record, and `os migrate apply --allow-destructive` refuses to drop that column until the conversion is verified + + Clause-②: no (narrowing) — `os migrate apply --allow-destructive` now refuses the `sys_comment.reactions` drop it accepted before, until a verified conversion is recorded; the conversion itself only writes records of an existing object, so nothing widens. + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + **What the upgrade now carries.** A database upgraded from 17.x keeps `sys_comment.reactions` and every emoji reaction stored in it (schema sync is additive). Those reactions are no longer lost. `os migrate comment-reactions` converts them into records: + + - **One record per stored reaction.** Each (comment, emoji, user) in a stored map becomes one `sys_comment_reaction` row with the same comment, emoji and user, in the comment's organization, owned by the user who reacted. That user can remove it like any reaction made on v18. A user id listed twice under one emoji is one reaction. + - **Nothing is guessed.** A stored value that is not JSON or not a map of emoji to user-id lists, an entry that is not a user id, an emoji longer than a reaction record holds, and a user id that names no `sys_user` row are each listed in the report with their comment id. Nothing is written for them. The readable entries of the same comment still convert. + - **Idempotent.** A reaction that already has its record is skipped, so a second run writes nothing, and re-running finishes an interrupted run. + - **Dry run by default.** `--apply` writes the records, reads the column back, and records the run in `sys_migration`. The run is recorded as verified only when every readable reaction has its record and no stored value is left unreadable. `--json` gives the full report. A run that lists an unreadable value or fails a write exits `1`, and so does an `--apply` run whose read-back finds anything unconverted. + + **The column drop waits for the conversion (the narrowing).** Until a verified run is recorded, `os migrate apply --allow-destructive` refuses to drop `sys_comment.reactions` and exits `1` (`--json`: `message: "refused_comment_reactions_unconverted"`, with the recorded run under `conversion`). Nothing else is applied on that run. This also covers the boot drift line, which names that command as the remedy for the orphaned column. What to do when it refuses: run the conversion below until it reports the run verified, then re-run the apply. + + **`os migrate apply --json` exits `1` when the `sys_account.issuer` retirement pre-flight refuses.** Its two refusals, `refused_account_issuer_collisions` and `refused_account_issuer_preflight_unreadable`, emitted their document with exit code `0`, so a caller gating on the status read a refused apply as a successful one. They now exit `1`, as the same refusals do without `--json`. The document is unchanged. + + **Upgrade order:** take a backup, run `os migrate comment-reactions`, then `os migrate comment-reactions --apply`. If it lists unreadable values, correct each one in the database so it reads as a map of emoji to user ids, or clear it if it holds nothing to keep, and run `--apply` again. Once the run is verified, run `os migrate apply --allow-destructive` to drop the column. Run these with the flags and environment the deployment boots with (an auth secret set), so the audit plugin's objects are composed. + + This supersedes the "stored reactions are not migrated" sentence in the entry that retired the column. `@objectstack/spec`: the protocol-18 semantic entry `sys-comment-reactions-column-retired` now names the conversion in its replacement, reason and acceptance criteria, and orders it before the drop. `os migrate meta` and the upgrade guide carry that text. Its id and surface are unchanged. +- e4ec6ec: fix(spec): the two step-18 flow-binding D3 entries name only the reads a renamed variable has on protocol 18, a text-slot hole and a CEL expression + + Clause-②: no + + The `replacement` text of two ADR-0087 D3 semantic entries, `flow-binding-name-dollar-refused` and `flow-binding-variable-dollar-name-refused`, listed three ways a flow reads the old variable name, for the author to rename. Protocol 18 has only the first two. The protocol upgrade guide and `spec-changes.json` are generated from these entries, so they now say what the two entries' release notes already say. + + | FROM | TO | + |:--|:--| + | Rename every read of the old name with it — a text-slot hole, a CEL expression, a single-brace token in a value position | Rename every read of the old name with it — a text-slot hole, a CEL expression | + + No protocol-18 value position reads a flow variable through a single-brace `{…}` token. A value slot takes a CEL value envelope or a literal, and a `{…}` token there is refused. The one `{…}` string a value slot hands on is a CRUD `filter` operand's whole-string query placeholder (`{current_user_id}`, `{today}`): the query engine reads it, and it names no flow variable. A text slot reads `{{ }}` holes. + + Text only. No schema, door, conversion or entry id changes, and each entry keeps its `fromVersion`, `toVersion` and every other field. +- 325a92e: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `b9814b334d8a`. Four descriptions this pin makes true change with it. + + Clause-②: no + + The four descriptions, each now read by the console at this pin: + + - `EmptyStateSchema.link`, the one link an empty state carries, no longer opens with `[PLANNED — not yet rendered]`. The console draws it in a list's empty state (the `grid` and `gallery` visualizations) and in an `object-grid` block's empty state, and follows it through its action runner (objectui#12270). + - A list page's own `interfaceConfig.emptyState` no longer opens with `[PLANNED — not yet rendered]`. The console's list page prefers it, whole, over its source view's empty state (objectui#12270). + - A `record:details` section's `count` no longer opens with `[EXPERIMENTAL — not enforced]`. The record detail page fills the heading's `{{ count }}` hole (objectui#12192). + - The `object-grid` block's `emptyState` description names `link` among the members the grid draws. + + The two liveness-ledger notes that excluded `emptyState.link` and `interfaceConfig.emptyState` from their rows' `live` verdict now say both are covered. + + Every asserting citation (55, across 2047 spec sources) was mapped through the objectui diff `1fb1ced36243..b9814b334d8a`: 397 paths over 36 commits. Where a cited file changed, each cited line was re-read and its new number recorded; none changed what it reads. The moved files are `ObjectGrid.tsx`, `data-table.tsx`, `ListView.tsx`, `ObjectCalendar.tsx`, the `en` / `zh` / `de` packs, `record-details.tsx`, `record-highlights.tsx`, `record-quick-actions.tsx`, `data-objectstack/src/index.ts`, `objectql.ts`, `plugin-kanban/src/index.tsx`, `plugin-grid/src/index.tsx`, `plugin-calendar/src/index.tsx` and `block-types.ts`. Every other record cites only files that are byte-identical across the hop, and each gains a hop sentence that says so. Three quoted anchors of the `map` block record moved and were re-pointed; they are `case 'map':` in `ObjectView.tsx`, and `ObjectMapConfigSchema` and `LIST_VIEW_LOCAL_OVERRIDES` in `objectql.zod.ts`. + + The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces every `1fb1ced36243` number. The corpus is now 8743 tracked files. A superset of 98 names reads 0: the 13 lifecycle, 31 metrics and 37 tracing exports, and 17 further names the entries count (their retired key names and the two logging defs). The unrelated `Span` 519 and `SpanSchema` 57 are unchanged. The lit controls moved with the corpus, for example `objectstack` from 18601 to 18854, `timeout` from 1700 to 1750 and `tenant` from 1415 to 1420. + + No key, default, enum member or export moves. + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index 73244d5e854..cb041e5bf97 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 98767fdc4be..8f0e3de7f5f 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/trigger-api +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Major Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 407a77a34a4..321bdb1caac 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 0c20a2968ea..44bf72cd6be 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/plugin-trigger-record-change +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 47c7aba6c74..fb477eccce7 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": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 94072983226..72e5011a9be 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/plugin-trigger-schedule +## 18.0.0-next.1 + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [777eed2] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/metadata-core@18.0.0-next.1 + ## 18.0.0-next.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index b5fadcac341..c19d19d3de5 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack \u2014 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 6864fbebb5f..818b1d15ecf 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/types +## 18.0.0-next.1 + +### Minor Changes + +- 3c1a046: fix: a route's catch-all `500` answers its own generic text, never the caught error's message, and `@objectstack/types` exports the writer that does it, `sendServerFault` + + Clause-②: yes (widening) — `@objectstack/types` gains one export, `sendServerFault`; nothing is removed, renamed or narrowed, and no response field changes shape. + + A declared 5xx does not carry the text of the error it caught: that text can name server internals, such as a path, a host or a table. The REST layer already withheld it. The storage, settings and i18n route families and the share-link routes answer in the nested `{ success: false, error: { code, message } }` envelope and did not: their catch-all `500` sent the caught error's message to the caller. + + - **What a caller now reads.** The same `status` and `code` as before, with the route's own generic text as `error.message`: `Internal error` on the storage and i18n routes, the route's message on the settings routes (`Failed to list manifests`, `Failed to read namespace`, `Failed to write namespace`, `Action failed`), and the route's `Failed to …` message on the share-link routes. On the storage routes this includes the metadata store's outage sentence, which an operator now reads in the server log. + - **What the operator reads.** The original error, stack included, in one `[5xx]` server-fault line. The storage byte routes, which already withheld the text, logged two lines per fault and now log one. A line names the mounted route pattern when the route passes one, never the request URL. + - **Unchanged.** Every `4xx` answer. The settings `SETTINGS_CRYPTO_UNAVAILABLE` refusal, which keeps its prescription. The authorization-store outage `503`, which keeps its declared code and message. The storage `501 NOT_IMPLEMENTED` answers. On the share-link routes, any error that declares its own `status` keeps the envelope it declares. + - **`sendServerFault(res, status, code, message, thrown, request?)`** (new, `@objectstack/types`): writes `message` in the declared envelope, and hands `thrown` to the server-fault log with its stack, as the fault's one line. Use it for an arm that answers a caught fault. Keep `sendError` for a `4xx` and for a `5xx` whose text you wrote for the reader. `request` is the mounted route pattern, never the request URL. + +### Patch Changes + +- Updated dependencies [dac99c2] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [777eed2] +- Updated dependencies [5f37d36] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [c857a93] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [5e725b8] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [775ed65] +- Updated dependencies [e4ec6ec] +- Updated dependencies [325a92e] + - @objectstack/spec@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 37aace6f219..25d14653666 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index 0e13e2b5b17..7ab91ef5194 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,169 @@ # @objectstack/verify +## 18.0.0-next.1 + +### Minor Changes + +- 06dbc4e: fix(verify)!: `runDashboardWindowProofs` takes no options, so the dashboard date-window run can no longer be pinned to an instant the server does not resolve on, and a run that crosses UTC midnight after it starts reports `not-run` instead of a false red + + `runDashboardWindowProofs(stack, token, config)` loses its fourth parameter, and `DashboardWindowProofOptions` (its only member was `now`) is retired. The server resolves a dashboard's date preset per request on its own clock, so any `now` other than the current instant made every window read `contradicted`: the option could only produce false reds, and nothing passed it. + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. + + - FROM `DashboardWindowProofOptions` TO nothing (no replacement): drop the fourth argument, so `runDashboardWindowProofs(stack, token, config, { now })` becomes `runDashboardWindowProofs(stack, token, config)`. + + Fix: the presets resolve when the run starts, and each count measure places its fixture rows on the day it is probed. A run that crossed UTC midnight between the two judged that measure against the previous day's edges and could read it `contradicted`. That measure is now `not-run`, and its detail names both days; the widget reads `inconclusive`. The guard that catches a day turning while a measure is being probed is unchanged. + +### Patch Changes + +- 7958bdf: No flow position reads the single-brace `{…}` template dialect any more, and its resolver is deleted (ADR-0032 Sequencing 6: "delete the single-brace resolver"). `AutomationEngine.evaluateCondition` evaluates every condition as CEL: a condition carrying a `{var}` hole is refused as the CEL brace trap, naming its source, and a `template` or `flow` envelope is refused before evaluation, where the deleted path substituted the variables and compared the text. A literal in a value slot is handed on as written: a `Date` in a code-defined flow reaches the write as the `Date`, where the deleted interpolator wrote `{}`. + + Clause-②: no (narrowing) + + + + **BREAKING**: a narrowing of a published runtime method and of published lint exports, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`). + + **Why.** ADR-0032 Decision 3 keeps one template delimiter, `{{ }}`, and its Sequencing 6 deletes the single-brace resolver. The earlier changes on this line retired the dialect position by position — every value slot, the text slots, each CRUD filter operand — so the resolver's last readers were two calls that saw literals only and a condition path that only a direct call reached. + + **What changes.** + + - **Conditions.** `AutomationEngine.evaluateCondition(condition, variables, context?)` reads every condition as CEL — bare text, an envelope with `dialect: 'cel'`, or one naming no dialect. The template path it kept for a string carrying a `{var}` hole is deleted: such a string is refused as a CEL fault that names its source and says not to wrap field references in `{…}`, and an envelope with `dialect: 'template'` or `dialect: 'flow'` is refused before evaluation. `registerFlow` already refused both at every condition slot (a start node's `condition`, an edge `condition`, a `decision` branch's `expression`, a screen field's `visibleWhen`), so no registered flow changes. What changes is a direct caller: a plugin calling the method, or `@objectstack/verify`'s `stack.automation.evaluateCondition`, which hands the condition over as given. + - **Value-slot literals.** A literal in a value slot — a `fields`, `input` or `templateData` value, an `assignment` value in any of its three shapes, a `notify` recipient, an `http` `body`, a screen's `defaults`, `defaultValue` or `recordId` — is handed on as a copy of what was written (arrays and plain objects copied, every other value as it is). The deleted interpolator walked every object's own entries, so a `Date` written in a code-defined flow reached the write as `{}`; it reaches it as the `Date`. + - **`@objectstack/lint`.** `validateFlowTemplatePaths` reads the `{{ }}` holes of a node's strings and no single-brace token; a CRUD node's `filter` is not scanned, since each operand is a value slot that renders no template. So `flow-template-unknown-field` and `flow-template-lookup-traversal` report only `warning` now: their `error` at a filter operand, and every finding on a single-brace token, are gone, and the value-slot and text-slot judges refuse those tokens at the same doors as `expression-invalid`. Messages and hints spell a hole `{{ record.x }}`. `flow-double-brace-interpolation` and `flow-bare-dollar-reference`, at a string outside every value slot, text slot and declared literal (a plugin node's own config, a `connector_action` input), say the value is read as written, where they prescribed a `{var}` token nothing reads. The lint's mirror of the retired grammar is removed with its exports. + - **`@objectstack/spec`.** The `FlowSchema` example's `connector_action` input holds a literal, since nothing reads a token there. The upgrade guide's step-18 text-slot entry says no other flow string reads the single-brace dialect, where it said every other one kept it, and its value-slot entry names every value slot the retirement reached on this line, where it named only the `assignment` values and the CRUD `fields` map. + + ## FROM → TO + + | you wrote | write instead | what changes | + |:--|:--|:--| + | `engine.evaluateCondition('{amount} > 100', vars)` | `engine.evaluateCondition('amount > 100', vars)` | the template substituted text and compared it; CEL compares the values and fails the run on an absent variable | + | `"{status} == 'open'"`, `'{status} == open'` | `"status == 'open'"` | a string literal is quoted | + | `{ dialect: 'template', source: "{flag} == true" }`, `dialect: 'flow'` | `{ dialect: 'cel', source: 'flag == true' }` | — | + | `classifyFlowTemplateToken(inner)` (`@objectstack/lint`) | `valueSlotTemplateRefusals(value)` / `textSlotTemplateRefusal(text)` (`@objectstack/spec/automation`), which refuse a token with the spelling that replaces it | the grammar it classified is read by nothing | + | `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` | the CEL stdlib: `isoDate(today())`, `isoDatetime(now())`, `round`, `floor`, `ceil`, `abs`, `min`, `max` | — | + | `FlowTemplateTokenVerdict` | nothing: the judges' reading of the retired grammar is internal to `@objectstack/spec` | — | + + **The one-line fix: write a condition as CEL, without `{…}` around a reference.** + + **Who is affected, measured.** In this repository at the base: no braced condition and no `template` or `flow` condition envelope in `examples/`, the docs or any package source; the deleted path's own pins in `@objectstack/service-automation` (`engine.test.ts`, `nested-region-parity.test.ts`) and `@objectstack/verify`'s door test are re-pinned to the refusal here. No authored `Date` literal in a flow was found; a compiled artifact serialises one to text, so only a flow handed to `registerFlow` in code carried one. Neither repository imports the removed lint exports. hotcrm at `0f84c9c` carries no braced condition and no `connector_action` input; its flows still author the single-brace dialect at value and text positions earlier changes on this line refuse, which its own upgrade migrates. + + ### The kit + + - **The engine.** `AutomationEngine.evaluateCondition` (`@objectstack/service-automation`): the template path, its hole discriminator and its text comparison are deleted. The value-slot resolvers' literal arm and the `assignment` executor copy a literal (`copyLiteral`, `builtin/value-slot-map.ts`). The single-brace interpolator (`interpolate`, `interpolateString`, `resolveToken`, its function table and `FlowExpressionFunctionError`), which the package did not export, is deleted with its tests. + - **The lint.** `validateFlowTemplatePaths` and the two wrong-syntax hints (`lint-flow-patterns.ts`); `classifyFlowTemplateToken`, `FLOW_TEMPLATE_DATE_FUNCTIONS`, `FLOW_TEMPLATE_VALUE_FUNCTIONS` and `FlowTemplateTokenVerdict` are removed. + - **The verify handle.** `stack.automation.evaluateCondition`'s documentation states the engine's reading: CEL, a `{var}` hole refused. + - **ADR-0087.** No new entry: the step-18 D3 entries `flow-value-slot-template-dialect-refused` and `flow-text-slot-single-brace-refused` record the retirement. +- b4d573a: fix(spec,core): the rolling `last_7_days`, `last_30_days` and `last_90_days` presets close on today, at the spec's prescription table, the save door's refusal and the analytics preset arm + + Clause-②: no (no accept set widens: the three rolling windows gain the inclusive `{today}` end every calendar preset already has) + + The three rolling windows were the open arm of `DATE_RANGE_PRESET_MACRO_WINDOWS`: `['{30_days_ago}', null]`, prescribed as `{ $gte: '{30_days_ago}' }`. A `$gte`-only window has no upper bound, so on a column that carries future dates (a close date, a due date) "last 30 days" also counted every later day. Measured on a `date` field with one row dated today−5 and one dated today+5: that window counted 2, while the console's preset lowering and the analytics preset arm counted 1. + + - `DATE_RANGE_PRESET_MACRO_WINDOWS` now closes every rolling window on `{today}`: `last_7_days` is `['{7_days_ago}', '{today}']`, and `last_30_days` and `last_90_days` follow the same shape. That is the inclusive last-day end every calendar preset already uses, and the extent the console's lowering already selects. The table's declared type narrows to match, `readonly [start: string, end: string]` (it was `end: string | null`). A reader that compared `end` with `null` still compiles; that branch can no longer be taken. + - The save door's refusal of a bare preset comparand (`bareDateRangePresetComparandMessage`, and the `filter-preset-comparand` lint verdict that quotes it) now prescribes the closed window. For `last_30_days` it says: write `{ $between: ['{30_days_ago}', '{today}'] }`, where it used to say `{ $gte: '{30_days_ago}' }`. + - `@objectstack/core`'s `resolveAnalyticsDateRangePreset`, the preset arm behind `timeDimensions[].dateRange`, selects the same extent. A rolling window now stops before tomorrow begins in the reference timezone, exactly as `today` does, with `endExclusive: true`. It used to end at the reference instant, so it dropped rows stamped later today. Its ends keep this module's own "the day the window stops before" spelling (`tomorrow`). Every preset this resolver returns is now `endExclusive: true`, and a caller's explicit `[a, b]` window keeps its inclusive reading, as before. + - The semantic migration entry `filter-preset-ordering-comparand-refused`, which `spec-changes.json` and `os migrate meta` carry, now names `{ $between: ["{30_days_ago}", "{today}"] }` as the window for `last_30_days` in its replacement, where it named `{ $gte: "{30_days_ago}" }`. + + If you followed the old refusal and authored `{ $gte: '{N_days_ago}' }` for a rolling preset, add `$lte: '{today}'`, or write the `$between` pair above, to stop counting future-dated rows. +- Updated dependencies [dac99c2] +- Updated dependencies [5c9bfb0] +- Updated dependencies [66949b7] +- Updated dependencies [7958bdf] +- Updated dependencies [87e902d] +- Updated dependencies [3d830b7] +- Updated dependencies [777eed2] +- Updated dependencies [8258181] +- Updated dependencies [ed03834] +- Updated dependencies [5f37d36] +- Updated dependencies [8024e12] +- Updated dependencies [1912af4] +- Updated dependencies [76815b9] +- Updated dependencies [4d1e4a8] +- Updated dependencies [d4aaeaf] +- Updated dependencies [4cc3ff9] +- Updated dependencies [c056979] +- Updated dependencies [4f68ba1] +- Updated dependencies [3490bf5] +- Updated dependencies [920bc12] +- Updated dependencies [107853c] +- Updated dependencies [543b33d] +- Updated dependencies [b529a5c] +- Updated dependencies [1623faf] +- Updated dependencies [b4d573a] +- Updated dependencies [3c1a046] +- Updated dependencies [6f4c518] +- Updated dependencies [08b38cb] +- Updated dependencies [bae5841] +- Updated dependencies [917b1a0] +- Updated dependencies [d9d7edd] +- Updated dependencies [d45d0e9] +- Updated dependencies [37510aa] +- Updated dependencies [c857a93] +- Updated dependencies [01d5034] +- Updated dependencies [f24574a] +- Updated dependencies [e790cfc] +- Updated dependencies [345d1be] +- Updated dependencies [430e5f5] +- Updated dependencies [09ef77c] +- Updated dependencies [5d3018d] +- Updated dependencies [404fc47] +- Updated dependencies [14a13bc] +- Updated dependencies [ca0bdbc] +- Updated dependencies [572ecfb] +- Updated dependencies [707b3cd] +- Updated dependencies [6673c80] +- Updated dependencies [24e064f] +- Updated dependencies [5e725b8] +- Updated dependencies [6d96721] +- Updated dependencies [eeb95f7] +- Updated dependencies [d89f491] +- Updated dependencies [dc39326] +- Updated dependencies [28e294f] +- Updated dependencies [775ed65] +- Updated dependencies [02a691a] +- Updated dependencies [e4ec6ec] +- Updated dependencies [be50633] +- Updated dependencies [325a92e] + - @objectstack/plugin-security@18.0.0-next.1 + - @objectstack/spec@18.0.0-next.1 + - @objectstack/platform-objects@18.0.0-next.1 + - @objectstack/core@18.0.0-next.1 + - @objectstack/objectql@18.0.0-next.1 + - @objectstack/plugin-sharing@18.0.0-next.1 + - @objectstack/service-settings@18.0.0-next.1 + - @objectstack/service-automation@18.0.0-next.1 + - @objectstack/service-mailbox@18.0.0-next.1 + - @objectstack/rest@18.0.0-next.1 + - @objectstack/runtime@18.0.0-next.1 + - @objectstack/plugin-auth@18.0.0-next.1 + - @objectstack/plugin-approvals@18.0.0-next.1 + - @objectstack/types@18.0.0-next.1 + - @objectstack/service-storage@18.0.0-next.1 + - @objectstack/service-datasource@18.0.0-next.1 + - @objectstack/plugin-webhooks@18.0.0-next.1 + - @objectstack/metadata@18.0.0-next.1 + - @objectstack/service-messaging@18.0.0-next.1 + - @objectstack/mcp@18.0.0-next.1 + - @objectstack/plugin-audit@18.0.0-next.1 + - @objectstack/plugin-email@18.0.0-next.1 + - @objectstack/plugin-hono-server@18.0.0-next.1 + - @objectstack/service-analytics@18.0.0-next.1 + - @objectstack/service-cache@18.0.0-next.1 + - @objectstack/service-job@18.0.0-next.1 + - @objectstack/service-package@18.0.0-next.1 + - @objectstack/service-queue@18.0.0-next.1 + - @objectstack/service-realtime@18.0.0-next.1 + - @objectstack/service-sms@18.0.0-next.1 + - @objectstack/trigger-api@18.0.0-next.1 + - @objectstack/trigger-record-change@18.0.0-next.1 + - @objectstack/trigger-schedule@18.0.0-next.1 + - @objectstack/plugin-pinyin-search@18.0.0-next.1 + ## 18.0.0-next.0 ### Minor Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 0a201a468cc..d65063a5200 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "18.0.0-next.0", + "version": "18.0.0-next.1", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module", diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index 91b6835f845..8656a3c4adf 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -12,7 +12,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: ai tags: agent, tool, skill, knowledge, llm, embedding, mcp --- diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index ff1cf589c7a..ae08f1a575e 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -14,7 +14,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: api tags: rest, endpoint, auth, realtime, server --- diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 91b00246055..5a3535eebd7 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: automation tags: flow, workflow, trigger, approval, state-machine, scheduled, webhook --- diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 104c4b80992..5223fcfaac0 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -15,7 +15,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: data tags: object, field, validation, index, relationship, hook, schema, permission, rls, security, seed, fixture --- diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 4e6118cc27c..8a4651e9c0a 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/formula 18.x (CEL) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: expression tags: cel, formula, predicate, condition, validation, visibility, seed-dynamic --- diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index aec1a03531e..f96b448575c 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: i18n tags: i18n, translation, locale, l10n, bundle, coverage --- diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index ba917ef6773..8aec97d58ea 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -17,7 +17,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/core 18.x (Zod v4 schemas), Node 22+ metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: platform tags: project, defineStack, driver, hono, plugin, kernel, service, requires, capability, DI, lifecycle, cli, deploy, ops --- diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index 726c15b8551..8cedad0b4ec 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -11,7 +11,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: query tags: query, filter, sort, paginate, aggregate, ObjectQL, full-text --- diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 4c168fcf230..34d51ffdde7 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -18,7 +18,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: ui tags: view, app, page, dashboard, report, chart, action, widget, doc --- diff --git a/skills/objectstack-upgrade/SKILL.md b/skills/objectstack-upgrade/SKILL.md index 1d282c4e041..3eb70dbc929 100644 --- a/skills/objectstack-upgrade/SKILL.md +++ b/skills/objectstack-upgrade/SKILL.md @@ -16,7 +16,7 @@ license: Apache-2.0 compatibility: Requires `@objectstack/spec` and `@objectstack/cli` at the TARGET major; the chain replays from the spec's `MIGRATION_SUPPORT_FLOOR`. No network access required. metadata: author: objectstack-ai - version: "18.0.0-next.0" + version: "18.0.0-next.1" domain: process tags: upgrade, migration, protocol, major, retired-keys, tombstone, conversions, validate, report ---