From 5273dc58b2925ff52c155d24f0e3edf0ea44d44e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:05:47 +0000 Subject: [PATCH] chore: version packages --- .changeset/24757-retire-position-name-fold.md | 31 - .../24768-not-null-refusal-every-door.md | 26 - .../24814-action-copy-template-slots.md | 24 - .changeset/25069-automation-webhooks-row.md | 10 - .../25190-retire-render-template-export.md | 18 - .changeset/25337-spec-book-team-add-member.md | 12 - .../25337-team-add-member-binds-team.md | 10 - .../25347-rename-organization-auth-feature.md | 12 - .../25347-rename-organization-single.md | 14 - .../25442-verify-readme-scheduled-work.md | 12 - .changeset/25458-navigate-console-route.md | 10 - .changeset/25459-form-entry-pack-keys.md | 31 - .changeset/25486-unknown-resource-404.md | 28 - ...25491-import-inline-mapping-entry-rules.md | 26 - .changeset/25495-org-default-currency.md | 14 - .../25510-template-baseline-exact-pins.md | 14 - .changeset/25511-hide-empty-describe.md | 17 - .../25514-artifact-flow-dataset-refs.md | 19 - .../25529-oidc-userinfo-locale-claim.md | 13 - .../25549-external-share-followup-wording.md | 11 - ...5555-localization-input-organization-id.md | 23 - .../25561-analytics-admission-bridge-503.md | 18 - .../25561-analytics-read-gate-fault-503.md | 21 - .../25571-notify-email-field-delivery.md | 19 - ...5572-public-form-submit-carries-no-file.md | 24 - .changeset/25573-exclude-submitter-runtime.md | 18 - .changeset/25575-modify-all-allow-delete.md | 31 - .changeset/25576-admin-rescue-comment-i18n.md | 10 - .changeset/25577-import-applyon-matchkeys.md | 31 - .changeset/25580-flow-required-permissions.md | 22 - .../25598-escalation-dead-target-refused.md | 21 - .../25598-sla-reassign-target-resolves.md | 17 - .changeset/25608-retire-package-upgrade.md | 35 - .../25609-approval-exclude-submitter.md | 14 - .../25610-notify-email-field-recipient.md | 29 - .../25611-inline-transform-null-sentinel.md | 8 - .changeset/25629-org-logo-zh-label.md | 10 - .changeset/25631-dependency-advisories.md | 26 - .../25634-pinyin-reconcile-off-readiness.md | 19 - .../25635-exclude-submitter-auto-approve.md | 17 - .../25639-lockstep-exact-internal-deps.md | 14 - .changeset/25649-bound-action-schema-parse.md | 27 - .../25650-anonymous-form-file-refusal.md | 27 - ...25652-translation-positions-permissions.md | 23 - ...3-app-automation-identity-write-refusal.md | 26 - ...erited-elevation-identity-write-refusal.md | 23 - ...-body-identity-assignment-write-refused.md | 28 - .../25666-skills-bootstrap-exact-pins.md | 9 - .../25668-claim-by-row-stored-expression.md | 16 - .changeset/25671-skills-protocol-caret.md | 10 - .changeset/25672-search-readable-fields.md | 10 - .changeset/25675-api-flow-secret-ref.md | 33 - .changeset/25675-skills-secret-ref-row.md | 11 - .changeset/25680-action-copy-refusal.md | 32 - .changeset/25681-repeater-untyped-json.md | 18 - .changeset/25681-skills-repeater-untyped.md | 12 - .../25683-inline-help-default-locale.md | 10 - ...25692-file-reference-claim-read-verdict.md | 21 - ...25693-unclaimed-file-organization-scope.md | 21 - .../25694-anonymous-write-carries-no-file.md | 19 - .../25696-notify-recipients-describe.md | 17 - ...709-action-copy-holeless-dollar-refusal.md | 24 - .../25711-console-built-in-workspace.md | 15 - .../25712-renderer-runs-one-sdui-parser.md | 30 - .changeset/25712-sdui-parser-one-body.md | 37 - .../25744-object-own-action-schema-parse.md | 27 - ...notify-written-address-no-email-channel.md | 21 - .changeset/25759-mcp-stateless-get-405.md | 21 - ...760-controlled-by-parent-owner-may-edit.md | 18 - .changeset/25761-seed-column-apply-on.md | 33 - ...ublished-package-default-permission-set.md | 11 - .changeset/25777-demo-staff.md | 21 - .changeset/25778-dataset-duration-measure.md | 20 - .changeset/25779-seed-files.md | 39 - .../25783-console-open-metadata-cost.md | 13 - .../25783-rest-meta-app-execution-face.md | 13 - .changeset/25785-i18n-translations-etag.md | 12 - .changeset/25786-grouping-order-by.md | 16 - .../25787-activity-update-names-fields.md | 12 - .changeset/25788-translation-reports.md | 29 - .../25790-spec-liveness-seam-citations.md | 12 - ...5792-console-dependency-missing-refusal.md | 16 - .../25792-dependency-declared-co-ownership.md | 34 - .../25792-pre-install-dependency-check.md | 24 - .../25792-retire-dependency-spellings.md | 29 - .changeset/25800-flow-action-on-success.md | 31 - .changeset/25801-flow-screen-file-field.md | 24 - ...2-invitation-role-label-warn-and-expiry.md | 22 - .changeset/25808-now-anchored-comparand.md | 36 - .changeset/25809-flow-write-refusal-locale.md | 14 - .../25811-spec-shared-chunk-comments.md | 12 - .../25814-dataset-filter-object-field.md | 19 - .../25818-aggregate-query-shape-refusals.md | 14 - .../25820-automation-run-doors-flow-scope.md | 20 - .changeset/25821-trigger-params-object.md | 23 - .changeset/25822-locale-tag-shape.md | 28 - ...25824-admin-create-user-password-length.md | 24 - .changeset/25831-object-ui-18-1-to-merge.md | 78 -- .changeset/25832-objectui-skill-in-catalog.md | 12 - .changeset/25858-grid-group-order-by.md | 14 - .../25860-lookup-subtitle-name-field.md | 14 - .../25866-field-permission-row-condition.md | 20 - .../25866-studio-field-permission-when.md | 13 - .changeset/25871-zh-logout-label.md | 10 - .../25874-form-refusal-no-field-toast.md | 15 - .changeset/25875-audit-log-date-picker.md | 10 - .changeset/25876-nav-group-child-count.md | 15 - .changeset/25877-approval-override-menu.md | 16 - .changeset/25883-pencil-focus-reveal.md | 12 - .changeset/25884-grid-date-absolute.md | 18 - .../25885-register-field-error-legal.md | 12 - .changeset/25886-invitation-arrival-copy.md | 16 - .../25887-replayed-build-result-card.md | 16 - .../25888-related-list-view-all-filter.md | 16 - .changeset/25889-map-style-failure-notice.md | 14 - .../25890-pending-changes-generated-group.md | 17 - .../25891-marketplace-unreachable-copy.md | 12 - .changeset/25892-studio-nav-visible-noise.md | 14 - .../25893-system-overview-dataset-labels.md | 15 - .../25894-no-drafts-request-without-grant.md | 13 - .../25895-related-empty-fold-path-terminal.md | 13 - .../25897-record-read-mode-wraps-text.md | 13 - .changeset/25898-list-empty-state-access.md | 20 - .changeset/25899-monaco-self-host.md | 18 - ...25900-notify-recipients-email-field-row.md | 14 - .changeset/25901-inline-view-label.md | 13 - .changeset/25905-cli-hook-body-provenance.md | 10 - .../25905-runtime-hook-body-provenance.md | 17 - .../25906-cube-qualified-measure-filter.md | 21 - .changeset/25907-empty-sum-order-window.md | 13 - .../25909-position-description-column.md | 12 - .changeset/25911-studio-rail-labels-fold.md | 10 - .../25915-app-no-access-shared-state.md | 10 - .../25915-preview-draft-shared-states.md | 10 - .../25916-console-leftover-strings-round-3.md | 19 - .changeset/25917-react-adapter-datasource.md | 40 - .changeset/25921-delete-dead-i18n-keys.md | 17 - ...2-app-shell-retire-unreferenced-exports.md | 47 - .../25923-ui-types-mirrors-passthrough.md | 14 - .../25926-retire-node-visibility-dialects.md | 26 - ...-import-users-temporary-password-length.md | 21 - .../25933-state-machine-board-drop-gate.md | 15 - .changeset/25933-state-machine-form-select.md | 16 - .changeset/25934-reader-record-lock.md | 14 - .changeset/25937-user-cell-sys-user.md | 15 - .../25938-flow-failed-record-save-order.md | 14 - .changeset/25942-bulk-confirm-holes.md | 14 - .changeset/25942-bulk-confirm-param-holes.md | 12 - .changeset/25955-group-section-true-only.md | 12 - .changeset/25959-activity-fold.md | 16 - ...967-builtin-permission-set-descriptions.md | 14 - .changeset/25970-compare-to-all-time.md | 12 - .changeset/25976-run-door-input-shape.md | 25 - ...990-notify-both-channels-unheld-address.md | 15 - .changeset/25992-objectview-chrome-split.md | 12 - .../25992-objectview-header-list-url-split.md | 12 - .changeset/25992-objectview-helpers-split.md | 12 - ...92-objectview-identity-navigation-split.md | 12 - .../25992-objectview-live-data-split.md | 12 - .changeset/25992-objectview-options-split.md | 12 - ...92-objectview-page-header-overlay-split.md | 12 - ...-objectview-tab-bar-header-chrome-split.md | 12 - ...92-objectview-toolbar-affordances-split.md | 12 - .../25994-objectgrid-diagnostics-hook.md | 12 - .changeset/25994-objectgrid-helpers-split.md | 12 - .changeset/25994-objectgrid-props-split.md | 14 - .../25999-flow-string-list-stored-string.md | 10 - .changeset/26001-console-map-style-url.md | 26 - .../26020-signed-out-invitation-preview.md | 18 - .../26022-retire-position-grant-via-name.md | 24 - .changeset/26031-not-null-drift-every-door.md | 20 - ...6035-console-drift-remedy-console-build.md | 13 - .../26043-unreadable-file-refused-at-field.md | 21 - .../26046-released-file-keeps-record-rule.md | 21 - .../26094-built-in-permission-set-labels.md | 12 - .changeset/26095-license-files.md | 23 - .changeset/26095-publishing-metadata.md | 88 -- .../26126-console-version-define-test-leak.md | 10 - .changeset/26130-dev-compiles-at-boot.md | 13 - ...31-console-chunk-membership-attribution.md | 10 - .../26133-import-duplicate-match-key.md | 21 - .changeset/26136-objectos-cloud-naming.md | 20 - .changeset/26146-liveness-every-depth.md | 13 - .changeset/26153-body-refusal-log-once.md | 14 - .changeset/26161-own-row-unreact-door.md | 19 - .../26163-map-style-runtime-fallback.md | 24 - .../26167-activity-summary-reader-locale.md | 14 - .../26172-record-save-toast-names-record.md | 16 - .../26180-pending-approver-record-read.md | 21 - ...6181-approval-notification-record-title.md | 10 - .../26182-related-list-empty-actions-new.md | 13 - .changeset/26183-one-app-address.md | 16 - .../26187-permission-matrix-record-scope.md | 16 - .../26187-position-permission-preview.md | 19 - .../26188-hierarchy-scope-without-resolver.md | 29 - .../26189-delegation-scope-violation-code.md | 22 - .../26190-delegation-scope-refusal-reason.md | 20 - .changeset/26191-delegate-companion-grants.md | 14 - .changeset/26192-position-create-live.md | 13 - .changeset/26193-absent-create-bit-denies.md | 17 - ...194-field-type-picker-enter-first-match.md | 10 - ...26200-console-csp-storage-upload-origin.md | 14 - ...6205-core-one-declaration-per-condition.md | 14 - .changeset/26206-cjs-entry-points.md | 12 - .changeset/26206-cli-esm-entries.md | 10 - .../26208-node10-resolution-unsupported.md | 26 - .../26209-create-in-repo-tsconfig-base.md | 14 - .changeset/26228-objectos-cloud-residue.md | 18 - .../26232-template-locale-reserved-channel.md | 22 - .../26242-serve-dev-compiles-at-boot.md | 13 - .changeset/26243-port-flag-reaches-config.md | 12 - .../26270-dev-port-shift-reaches-config.md | 12 - .changeset/26276-ui-package-test-vitest.md | 30 - .changeset/26304-screen-field-record-link.md | 23 - .../26306-filter-builder-spec-row-value.md | 25 - .changeset/26307-bulk-confirm-locale.md | 12 - ...6314-record-anchored-approval-lock-read.md | 18 - .changeset/26332-create-in-repo-typescript.md | 14 - .changeset/26346-reaction-unreact-door.md | 15 - ...26348-filter-builder-boolean-list-entry.md | 19 - .changeset/26349-package-bound-wiring.md | 12 - .../26418-toast-clears-drawer-actions-row.md | 12 - .changeset/8133-spec-shared-chunks.md | 14 - .changeset/8740-upsert-nothing-to-merge.md | 20 - .../business-unit-member-title-names.md | 12 - .changeset/console-b41ed7f68956.md | 30 - ...onsole-document-content-security-policy.md | 24 - .changeset/create-user-organization-join.md | 21 - .changeset/default-org-owner-currency.md | 8 - .changeset/react-pages-default-off.md | 21 - .changeset/spec-form-object-name-shared.md | 10 - content/docs/deployment/self-hosting.mdx | 8 +- content/docs/releases/index.mdx | 2 +- content/docs/upgrading.mdx | 2 +- docker/README.md | 8 +- examples/app-crm/CHANGELOG.md | 74 ++ examples/app-crm/package.json | 2 +- examples/app-multi-package/CHANGELOG.md | 62 ++ examples/app-multi-package/package.json | 2 +- examples/app-showcase/CHANGELOG.md | 88 ++ examples/app-showcase/package.json | 2 +- examples/app-todo/CHANGELOG.md | 89 ++ examples/app-todo/package.json | 2 +- examples/embed-objectql/CHANGELOG.md | 72 ++ examples/embed-objectql/package.json | 2 +- packages/adapters/hono/CHANGELOG.md | 50 + packages/adapters/hono/package.json | 2 +- packages/apps/account/CHANGELOG.md | 86 ++ packages/apps/account/package.json | 2 +- packages/apps/setup/CHANGELOG.md | 86 ++ packages/apps/setup/package.json | 2 +- packages/apps/studio/CHANGELOG.md | 86 ++ packages/apps/studio/package.json | 2 +- packages/cli/CHANGELOG.md | 520 ++++++++++ packages/cli/package.json | 2 +- packages/client-react/CHANGELOG.md | 81 ++ packages/client-react/package.json | 2 +- packages/client/CHANGELOG.md | 91 ++ packages/client/package.json | 2 +- packages/cloud-connection/CHANGELOG.md | 166 ++++ packages/cloud-connection/package.json | 2 +- .../connectors/connector-imap/CHANGELOG.md | 25 + .../connectors/connector-imap/package.json | 2 +- .../connectors/connector-mcp/CHANGELOG.md | 80 ++ .../connectors/connector-mcp/package.json | 2 +- .../connectors/connector-openapi/CHANGELOG.md | 80 ++ .../connectors/connector-openapi/package.json | 2 +- .../connectors/connector-rest/CHANGELOG.md | 80 ++ .../connectors/connector-rest/package.json | 2 +- .../connectors/connector-slack/CHANGELOG.md | 80 ++ .../connectors/connector-slack/package.json | 2 +- packages/console/CHANGELOG.md | 81 ++ packages/console/package.json | 2 +- packages/core/CHANGELOG.md | 274 ++++++ packages/core/package.json | 2 +- packages/create-objectstack/CHANGELOG.md | 41 + packages/create-objectstack/package.json | 2 +- .../src/templates/blank/package.json | 18 +- packages/drivers/driver-memory/CHANGELOG.md | 83 ++ packages/drivers/driver-memory/package.json | 2 +- packages/drivers/driver-mongodb/CHANGELOG.md | 109 +++ packages/drivers/driver-mongodb/package.json | 2 +- packages/drivers/driver-sql/CHANGELOG.md | 126 +++ packages/drivers/driver-sql/package.json | 2 +- .../drivers/driver-sqlite-wasm/CHANGELOG.md | 82 ++ .../drivers/driver-sqlite-wasm/package.json | 2 +- packages/drivers/driver-turso/CHANGELOG.md | 92 ++ packages/drivers/driver-turso/package.json | 2 +- packages/formula/CHANGELOG.md | 115 +++ packages/formula/package.json | 2 +- packages/lint/CHANGELOG.md | 421 ++++++++ packages/lint/package.json | 2 +- packages/mcp/CHANGELOG.md | 127 +++ packages/mcp/package.json | 2 +- packages/metadata-core/CHANGELOG.md | 144 +++ packages/metadata-core/package.json | 2 +- packages/metadata-fs/CHANGELOG.md | 25 + packages/metadata-fs/package.json | 2 +- packages/metadata-protocol/CHANGELOG.md | 263 +++++ packages/metadata-protocol/package.json | 2 +- packages/metadata/CHANGELOG.md | 104 ++ packages/metadata/package.json | 2 +- packages/objectql/CHANGELOG.md | 281 ++++++ packages/objectql/package.json | 2 +- packages/observability/CHANGELOG.md | 74 ++ packages/observability/package.json | 2 +- packages/platform-objects/CHANGELOG.md | 171 ++++ packages/platform-objects/package.json | 2 +- packages/plugins/embedder-openai/CHANGELOG.md | 74 ++ packages/plugins/embedder-openai/package.json | 2 +- .../plugins/knowledge-memory/CHANGELOG.md | 81 ++ .../plugins/knowledge-memory/package.json | 2 +- .../plugins/knowledge-ragflow/CHANGELOG.md | 81 ++ .../plugins/knowledge-ragflow/package.json | 2 +- packages/plugins/organizations/CHANGELOG.md | 104 ++ packages/plugins/organizations/package.json | 2 +- .../plugins/plugin-approvals/CHANGELOG.md | 176 ++++ .../plugins/plugin-approvals/package.json | 2 +- packages/plugins/plugin-audit/CHANGELOG.md | 146 +++ packages/plugins/plugin-audit/package.json | 2 +- packages/plugins/plugin-auth/CHANGELOG.md | 244 +++++ packages/plugins/plugin-auth/package.json | 2 +- packages/plugins/plugin-dev/CHANGELOG.md | 138 +++ packages/plugins/plugin-dev/package.json | 2 +- packages/plugins/plugin-email/CHANGELOG.md | 129 +++ packages/plugins/plugin-email/package.json | 2 +- .../plugins/plugin-hono-server/CHANGELOG.md | 115 +++ .../plugins/plugin-hono-server/package.json | 2 +- .../plugins/plugin-pinyin-search/CHANGELOG.md | 104 ++ .../plugins/plugin-pinyin-search/package.json | 2 +- packages/plugins/plugin-security/CHANGELOG.md | 266 +++++ packages/plugins/plugin-security/package.json | 2 +- packages/plugins/plugin-sharing/CHANGELOG.md | 120 +++ packages/plugins/plugin-sharing/package.json | 2 +- packages/plugins/plugin-webhooks/CHANGELOG.md | 99 ++ packages/plugins/plugin-webhooks/package.json | 2 +- packages/qa/dogfood/CHANGELOG.md | 150 +++ packages/qa/dogfood/package.json | 2 +- packages/qa/downstream-contract/CHANGELOG.md | 62 ++ packages/qa/downstream-contract/package.json | 2 +- packages/qa/http-conformance/CHANGELOG.md | 18 + packages/qa/http-conformance/package.json | 2 +- packages/rest/CHANGELOG.md | 249 +++++ packages/rest/package.json | 2 +- packages/runtime/CHANGELOG.md | 394 ++++++++ packages/runtime/package.json | 2 +- packages/sdui-parser/CHANGELOG.md | 75 ++ packages/sdui-parser/package.json | 2 +- .../services/service-analytics/CHANGELOG.md | 195 ++++ .../services/service-analytics/package.json | 2 +- .../services/service-automation/CHANGELOG.md | 238 +++++ .../services/service-automation/package.json | 2 +- packages/services/service-cache/CHANGELOG.md | 81 ++ packages/services/service-cache/package.json | 2 +- .../service-cluster-redis/CHANGELOG.md | 76 ++ .../service-cluster-redis/package.json | 2 +- .../services/service-cluster/CHANGELOG.md | 94 ++ .../services/service-cluster/package.json | 2 +- .../services/service-datasource/CHANGELOG.md | 128 +++ .../services/service-datasource/package.json | 2 +- packages/services/service-i18n/CHANGELOG.md | 83 ++ packages/services/service-i18n/package.json | 2 +- packages/services/service-job/CHANGELOG.md | 86 ++ packages/services/service-job/package.json | 2 +- .../services/service-knowledge/CHANGELOG.md | 80 ++ .../services/service-knowledge/package.json | 2 +- .../services/service-mailbox/CHANGELOG.md | 100 ++ .../services/service-mailbox/package.json | 2 +- .../services/service-messaging/CHANGELOG.md | 95 ++ .../services/service-messaging/package.json | 2 +- .../services/service-package/CHANGELOG.md | 108 +++ .../services/service-package/package.json | 2 +- packages/services/service-print/CHANGELOG.md | 91 ++ packages/services/service-print/package.json | 2 +- packages/services/service-queue/CHANGELOG.md | 86 ++ packages/services/service-queue/package.json | 2 +- .../services/service-realtime/CHANGELOG.md | 86 ++ .../services/service-realtime/package.json | 2 +- .../services/service-settings/CHANGELOG.md | 101 ++ .../services/service-settings/package.json | 2 +- packages/services/service-sms/CHANGELOG.md | 88 ++ packages/services/service-sms/package.json | 2 +- .../services/service-storage/CHANGELOG.md | 235 +++++ .../services/service-storage/package.json | 2 +- packages/skills/CHANGELOG.md | 99 ++ packages/skills/package.json | 2 +- packages/spec/CHANGELOG.md | 915 ++++++++++++++++++ packages/spec/package.json | 2 +- packages/triggers/trigger-api/CHANGELOG.md | 115 +++ packages/triggers/trigger-api/package.json | 2 +- .../trigger-record-change/CHANGELOG.md | 86 ++ .../trigger-record-change/package.json | 2 +- .../triggers/trigger-schedule/CHANGELOG.md | 91 ++ .../triggers/trigger-schedule/package.json | 2 +- packages/types/CHANGELOG.md | 117 +++ packages/types/package.json | 2 +- packages/verify/CHANGELOG.md | 186 ++++ packages/verify/package.json | 2 +- skills/objectstack-ai/SKILL.md | 2 +- skills/objectstack-api/SKILL.md | 2 +- skills/objectstack-automation/SKILL.md | 2 +- skills/objectstack-data/SKILL.md | 2 +- skills/objectstack-formula/SKILL.md | 2 +- skills/objectstack-i18n/SKILL.md | 2 +- skills/objectstack-platform/SKILL.md | 2 +- skills/objectstack-query/SKILL.md | 2 +- skills/objectstack-ui/SKILL.md | 2 +- skills/objectstack-upgrade/SKILL.md | 2 +- skills/objectui/SKILL.md | 2 +- ui/apps/console/CHANGELOG.md | 136 +++ ui/apps/console/package.json | 2 +- ui/packages/app-shell/CHANGELOG.md | 802 +++++++++++++++ ui/packages/app-shell/package.json | 2 +- ui/packages/auth/CHANGELOG.md | 110 +++ ui/packages/auth/package.json | 2 +- ui/packages/cli/CHANGELOG.md | 122 +++ ui/packages/cli/package.json | 2 +- ui/packages/collaboration/CHANGELOG.md | 67 ++ ui/packages/collaboration/package.json | 2 +- ui/packages/components/CHANGELOG.md | 227 +++++ ui/packages/components/package.json | 2 +- ui/packages/core/CHANGELOG.md | 202 ++++ ui/packages/core/package.json | 2 +- ui/packages/create-plugin/CHANGELOG.md | 43 + ui/packages/create-plugin/package.json | 2 +- ui/packages/data-objectstack/CHANGELOG.md | 147 +++ ui/packages/data-objectstack/package.json | 2 +- ui/packages/fields/CHANGELOG.md | 178 ++++ ui/packages/fields/package.json | 2 +- ui/packages/i18n/CHANGELOG.md | 220 +++++ ui/packages/i18n/package.json | 2 +- ui/packages/layout/CHANGELOG.md | 150 +++ ui/packages/layout/package.json | 2 +- ui/packages/mobile/CHANGELOG.md | 53 + ui/packages/mobile/package.json | 2 +- ui/packages/permissions/CHANGELOG.md | 122 +++ ui/packages/permissions/package.json | 2 +- ui/packages/plugin-ai/CHANGELOG.md | 77 ++ ui/packages/plugin-ai/package.json | 2 +- ui/packages/plugin-calendar/CHANGELOG.md | 108 +++ ui/packages/plugin-calendar/package.json | 2 +- ui/packages/plugin-charts/CHANGELOG.md | 86 ++ ui/packages/plugin-charts/package.json | 2 +- ui/packages/plugin-chatbot/CHANGELOG.md | 152 +++ ui/packages/plugin-chatbot/package.json | 2 +- ui/packages/plugin-dashboard/CHANGELOG.md | 169 ++++ ui/packages/plugin-dashboard/package.json | 2 +- ui/packages/plugin-designer/CHANGELOG.md | 181 ++++ ui/packages/plugin-designer/package.json | 2 +- ui/packages/plugin-detail/CHANGELOG.md | 220 +++++ ui/packages/plugin-detail/package.json | 2 +- ui/packages/plugin-editor/CHANGELOG.md | 83 ++ ui/packages/plugin-editor/package.json | 2 +- ui/packages/plugin-form/CHANGELOG.md | 165 ++++ ui/packages/plugin-form/package.json | 2 +- ui/packages/plugin-gantt/CHANGELOG.md | 166 ++++ ui/packages/plugin-gantt/package.json | 2 +- ui/packages/plugin-grid/CHANGELOG.md | 266 +++++ ui/packages/plugin-grid/package.json | 2 +- ui/packages/plugin-kanban/CHANGELOG.md | 122 +++ ui/packages/plugin-kanban/package.json | 2 +- ui/packages/plugin-list/CHANGELOG.md | 175 ++++ ui/packages/plugin-list/package.json | 2 +- ui/packages/plugin-map/CHANGELOG.md | 122 +++ ui/packages/plugin-map/package.json | 2 +- ui/packages/plugin-markdown/CHANGELOG.md | 71 ++ ui/packages/plugin-markdown/package.json | 2 +- ui/packages/plugin-report/CHANGELOG.md | 84 ++ ui/packages/plugin-report/package.json | 2 +- ui/packages/plugin-timeline/CHANGELOG.md | 163 ++++ ui/packages/plugin-timeline/package.json | 2 +- ui/packages/plugin-tree/CHANGELOG.md | 166 ++++ ui/packages/plugin-tree/package.json | 2 +- ui/packages/plugin-view/CHANGELOG.md | 154 +++ ui/packages/plugin-view/package.json | 2 +- ui/packages/providers/CHANGELOG.md | 59 ++ ui/packages/providers/package.json | 2 +- ui/packages/react-runtime/CHANGELOG.md | 52 + ui/packages/react-runtime/package.json | 2 +- ui/packages/react/CHANGELOG.md | 264 +++++ ui/packages/react/package.json | 2 +- ui/packages/types/CHANGELOG.md | 172 ++++ ui/packages/types/package.json | 2 +- ui/packages/vscode-extension/CHANGELOG.md | 17 + ui/packages/vscode-extension/package.json | 2 +- 485 files changed, 17016 insertions(+), 4402 deletions(-) delete mode 100644 .changeset/24757-retire-position-name-fold.md delete mode 100644 .changeset/24768-not-null-refusal-every-door.md delete mode 100644 .changeset/24814-action-copy-template-slots.md delete mode 100644 .changeset/25069-automation-webhooks-row.md delete mode 100644 .changeset/25190-retire-render-template-export.md delete mode 100644 .changeset/25337-spec-book-team-add-member.md delete mode 100644 .changeset/25337-team-add-member-binds-team.md delete mode 100644 .changeset/25347-rename-organization-auth-feature.md delete mode 100644 .changeset/25347-rename-organization-single.md delete mode 100644 .changeset/25442-verify-readme-scheduled-work.md delete mode 100644 .changeset/25458-navigate-console-route.md delete mode 100644 .changeset/25459-form-entry-pack-keys.md delete mode 100644 .changeset/25486-unknown-resource-404.md delete mode 100644 .changeset/25491-import-inline-mapping-entry-rules.md delete mode 100644 .changeset/25495-org-default-currency.md delete mode 100644 .changeset/25510-template-baseline-exact-pins.md delete mode 100644 .changeset/25511-hide-empty-describe.md delete mode 100644 .changeset/25514-artifact-flow-dataset-refs.md delete mode 100644 .changeset/25529-oidc-userinfo-locale-claim.md delete mode 100644 .changeset/25549-external-share-followup-wording.md delete mode 100644 .changeset/25555-localization-input-organization-id.md delete mode 100644 .changeset/25561-analytics-admission-bridge-503.md delete mode 100644 .changeset/25561-analytics-read-gate-fault-503.md delete mode 100644 .changeset/25571-notify-email-field-delivery.md delete mode 100644 .changeset/25572-public-form-submit-carries-no-file.md delete mode 100644 .changeset/25573-exclude-submitter-runtime.md delete mode 100644 .changeset/25575-modify-all-allow-delete.md delete mode 100644 .changeset/25576-admin-rescue-comment-i18n.md delete mode 100644 .changeset/25577-import-applyon-matchkeys.md delete mode 100644 .changeset/25580-flow-required-permissions.md delete mode 100644 .changeset/25598-escalation-dead-target-refused.md delete mode 100644 .changeset/25598-sla-reassign-target-resolves.md delete mode 100644 .changeset/25608-retire-package-upgrade.md delete mode 100644 .changeset/25609-approval-exclude-submitter.md delete mode 100644 .changeset/25610-notify-email-field-recipient.md delete mode 100644 .changeset/25611-inline-transform-null-sentinel.md delete mode 100644 .changeset/25629-org-logo-zh-label.md delete mode 100644 .changeset/25631-dependency-advisories.md delete mode 100644 .changeset/25634-pinyin-reconcile-off-readiness.md delete mode 100644 .changeset/25635-exclude-submitter-auto-approve.md delete mode 100644 .changeset/25639-lockstep-exact-internal-deps.md delete mode 100644 .changeset/25649-bound-action-schema-parse.md delete mode 100644 .changeset/25650-anonymous-form-file-refusal.md delete mode 100644 .changeset/25652-translation-positions-permissions.md delete mode 100644 .changeset/25653-app-automation-identity-write-refusal.md delete mode 100644 .changeset/25653-app-hook-inherited-elevation-identity-write-refusal.md delete mode 100644 .changeset/25653-hook-action-body-identity-assignment-write-refused.md delete mode 100644 .changeset/25666-skills-bootstrap-exact-pins.md delete mode 100644 .changeset/25668-claim-by-row-stored-expression.md delete mode 100644 .changeset/25671-skills-protocol-caret.md delete mode 100644 .changeset/25672-search-readable-fields.md delete mode 100644 .changeset/25675-api-flow-secret-ref.md delete mode 100644 .changeset/25675-skills-secret-ref-row.md delete mode 100644 .changeset/25680-action-copy-refusal.md delete mode 100644 .changeset/25681-repeater-untyped-json.md delete mode 100644 .changeset/25681-skills-repeater-untyped.md delete mode 100644 .changeset/25683-inline-help-default-locale.md delete mode 100644 .changeset/25692-file-reference-claim-read-verdict.md delete mode 100644 .changeset/25693-unclaimed-file-organization-scope.md delete mode 100644 .changeset/25694-anonymous-write-carries-no-file.md delete mode 100644 .changeset/25696-notify-recipients-describe.md delete mode 100644 .changeset/25709-action-copy-holeless-dollar-refusal.md delete mode 100644 .changeset/25711-console-built-in-workspace.md delete mode 100644 .changeset/25712-renderer-runs-one-sdui-parser.md delete mode 100644 .changeset/25712-sdui-parser-one-body.md delete mode 100644 .changeset/25744-object-own-action-schema-parse.md delete mode 100644 .changeset/25758-notify-written-address-no-email-channel.md delete mode 100644 .changeset/25759-mcp-stateless-get-405.md delete mode 100644 .changeset/25760-controlled-by-parent-owner-may-edit.md delete mode 100644 .changeset/25761-seed-column-apply-on.md delete mode 100644 .changeset/25773-published-package-default-permission-set.md delete mode 100644 .changeset/25777-demo-staff.md delete mode 100644 .changeset/25778-dataset-duration-measure.md delete mode 100644 .changeset/25779-seed-files.md delete mode 100644 .changeset/25783-console-open-metadata-cost.md delete mode 100644 .changeset/25783-rest-meta-app-execution-face.md delete mode 100644 .changeset/25785-i18n-translations-etag.md delete mode 100644 .changeset/25786-grouping-order-by.md delete mode 100644 .changeset/25787-activity-update-names-fields.md delete mode 100644 .changeset/25788-translation-reports.md delete mode 100644 .changeset/25790-spec-liveness-seam-citations.md delete mode 100644 .changeset/25792-console-dependency-missing-refusal.md delete mode 100644 .changeset/25792-dependency-declared-co-ownership.md delete mode 100644 .changeset/25792-pre-install-dependency-check.md delete mode 100644 .changeset/25792-retire-dependency-spellings.md delete mode 100644 .changeset/25800-flow-action-on-success.md delete mode 100644 .changeset/25801-flow-screen-file-field.md delete mode 100644 .changeset/25802-invitation-role-label-warn-and-expiry.md delete mode 100644 .changeset/25808-now-anchored-comparand.md delete mode 100644 .changeset/25809-flow-write-refusal-locale.md delete mode 100644 .changeset/25811-spec-shared-chunk-comments.md delete mode 100644 .changeset/25814-dataset-filter-object-field.md delete mode 100644 .changeset/25818-aggregate-query-shape-refusals.md delete mode 100644 .changeset/25820-automation-run-doors-flow-scope.md delete mode 100644 .changeset/25821-trigger-params-object.md delete mode 100644 .changeset/25822-locale-tag-shape.md delete mode 100644 .changeset/25824-admin-create-user-password-length.md delete mode 100644 .changeset/25831-object-ui-18-1-to-merge.md delete mode 100644 .changeset/25832-objectui-skill-in-catalog.md delete mode 100644 .changeset/25858-grid-group-order-by.md delete mode 100644 .changeset/25860-lookup-subtitle-name-field.md delete mode 100644 .changeset/25866-field-permission-row-condition.md delete mode 100644 .changeset/25866-studio-field-permission-when.md delete mode 100644 .changeset/25871-zh-logout-label.md delete mode 100644 .changeset/25874-form-refusal-no-field-toast.md delete mode 100644 .changeset/25875-audit-log-date-picker.md delete mode 100644 .changeset/25876-nav-group-child-count.md delete mode 100644 .changeset/25877-approval-override-menu.md delete mode 100644 .changeset/25883-pencil-focus-reveal.md delete mode 100644 .changeset/25884-grid-date-absolute.md delete mode 100644 .changeset/25885-register-field-error-legal.md delete mode 100644 .changeset/25886-invitation-arrival-copy.md delete mode 100644 .changeset/25887-replayed-build-result-card.md delete mode 100644 .changeset/25888-related-list-view-all-filter.md delete mode 100644 .changeset/25889-map-style-failure-notice.md delete mode 100644 .changeset/25890-pending-changes-generated-group.md delete mode 100644 .changeset/25891-marketplace-unreachable-copy.md delete mode 100644 .changeset/25892-studio-nav-visible-noise.md delete mode 100644 .changeset/25893-system-overview-dataset-labels.md delete mode 100644 .changeset/25894-no-drafts-request-without-grant.md delete mode 100644 .changeset/25895-related-empty-fold-path-terminal.md delete mode 100644 .changeset/25897-record-read-mode-wraps-text.md delete mode 100644 .changeset/25898-list-empty-state-access.md delete mode 100644 .changeset/25899-monaco-self-host.md delete mode 100644 .changeset/25900-notify-recipients-email-field-row.md delete mode 100644 .changeset/25901-inline-view-label.md delete mode 100644 .changeset/25905-cli-hook-body-provenance.md delete mode 100644 .changeset/25905-runtime-hook-body-provenance.md delete mode 100644 .changeset/25906-cube-qualified-measure-filter.md delete mode 100644 .changeset/25907-empty-sum-order-window.md delete mode 100644 .changeset/25909-position-description-column.md delete mode 100644 .changeset/25911-studio-rail-labels-fold.md delete mode 100644 .changeset/25915-app-no-access-shared-state.md delete mode 100644 .changeset/25915-preview-draft-shared-states.md delete mode 100644 .changeset/25916-console-leftover-strings-round-3.md delete mode 100644 .changeset/25917-react-adapter-datasource.md delete mode 100644 .changeset/25921-delete-dead-i18n-keys.md delete mode 100644 .changeset/25922-app-shell-retire-unreferenced-exports.md delete mode 100644 .changeset/25923-ui-types-mirrors-passthrough.md delete mode 100644 .changeset/25926-retire-node-visibility-dialects.md delete mode 100644 .changeset/25932-import-users-temporary-password-length.md delete mode 100644 .changeset/25933-state-machine-board-drop-gate.md delete mode 100644 .changeset/25933-state-machine-form-select.md delete mode 100644 .changeset/25934-reader-record-lock.md delete mode 100644 .changeset/25937-user-cell-sys-user.md delete mode 100644 .changeset/25938-flow-failed-record-save-order.md delete mode 100644 .changeset/25942-bulk-confirm-holes.md delete mode 100644 .changeset/25942-bulk-confirm-param-holes.md delete mode 100644 .changeset/25955-group-section-true-only.md delete mode 100644 .changeset/25959-activity-fold.md delete mode 100644 .changeset/25967-builtin-permission-set-descriptions.md delete mode 100644 .changeset/25970-compare-to-all-time.md delete mode 100644 .changeset/25976-run-door-input-shape.md delete mode 100644 .changeset/25990-notify-both-channels-unheld-address.md delete mode 100644 .changeset/25992-objectview-chrome-split.md delete mode 100644 .changeset/25992-objectview-header-list-url-split.md delete mode 100644 .changeset/25992-objectview-helpers-split.md delete mode 100644 .changeset/25992-objectview-identity-navigation-split.md delete mode 100644 .changeset/25992-objectview-live-data-split.md delete mode 100644 .changeset/25992-objectview-options-split.md delete mode 100644 .changeset/25992-objectview-page-header-overlay-split.md delete mode 100644 .changeset/25992-objectview-tab-bar-header-chrome-split.md delete mode 100644 .changeset/25992-objectview-toolbar-affordances-split.md delete mode 100644 .changeset/25994-objectgrid-diagnostics-hook.md delete mode 100644 .changeset/25994-objectgrid-helpers-split.md delete mode 100644 .changeset/25994-objectgrid-props-split.md delete mode 100644 .changeset/25999-flow-string-list-stored-string.md delete mode 100644 .changeset/26001-console-map-style-url.md delete mode 100644 .changeset/26020-signed-out-invitation-preview.md delete mode 100644 .changeset/26022-retire-position-grant-via-name.md delete mode 100644 .changeset/26031-not-null-drift-every-door.md delete mode 100644 .changeset/26035-console-drift-remedy-console-build.md delete mode 100644 .changeset/26043-unreadable-file-refused-at-field.md delete mode 100644 .changeset/26046-released-file-keeps-record-rule.md delete mode 100644 .changeset/26094-built-in-permission-set-labels.md delete mode 100644 .changeset/26095-license-files.md delete mode 100644 .changeset/26095-publishing-metadata.md delete mode 100644 .changeset/26126-console-version-define-test-leak.md delete mode 100644 .changeset/26130-dev-compiles-at-boot.md delete mode 100644 .changeset/26131-console-chunk-membership-attribution.md delete mode 100644 .changeset/26133-import-duplicate-match-key.md delete mode 100644 .changeset/26136-objectos-cloud-naming.md delete mode 100644 .changeset/26146-liveness-every-depth.md delete mode 100644 .changeset/26153-body-refusal-log-once.md delete mode 100644 .changeset/26161-own-row-unreact-door.md delete mode 100644 .changeset/26163-map-style-runtime-fallback.md delete mode 100644 .changeset/26167-activity-summary-reader-locale.md delete mode 100644 .changeset/26172-record-save-toast-names-record.md delete mode 100644 .changeset/26180-pending-approver-record-read.md delete mode 100644 .changeset/26181-approval-notification-record-title.md delete mode 100644 .changeset/26182-related-list-empty-actions-new.md delete mode 100644 .changeset/26183-one-app-address.md delete mode 100644 .changeset/26187-permission-matrix-record-scope.md delete mode 100644 .changeset/26187-position-permission-preview.md delete mode 100644 .changeset/26188-hierarchy-scope-without-resolver.md delete mode 100644 .changeset/26189-delegation-scope-violation-code.md delete mode 100644 .changeset/26190-delegation-scope-refusal-reason.md delete mode 100644 .changeset/26191-delegate-companion-grants.md delete mode 100644 .changeset/26192-position-create-live.md delete mode 100644 .changeset/26193-absent-create-bit-denies.md delete mode 100644 .changeset/26194-field-type-picker-enter-first-match.md delete mode 100644 .changeset/26200-console-csp-storage-upload-origin.md delete mode 100644 .changeset/26205-core-one-declaration-per-condition.md delete mode 100644 .changeset/26206-cjs-entry-points.md delete mode 100644 .changeset/26206-cli-esm-entries.md delete mode 100644 .changeset/26208-node10-resolution-unsupported.md delete mode 100644 .changeset/26209-create-in-repo-tsconfig-base.md delete mode 100644 .changeset/26228-objectos-cloud-residue.md delete mode 100644 .changeset/26232-template-locale-reserved-channel.md delete mode 100644 .changeset/26242-serve-dev-compiles-at-boot.md delete mode 100644 .changeset/26243-port-flag-reaches-config.md delete mode 100644 .changeset/26270-dev-port-shift-reaches-config.md delete mode 100644 .changeset/26276-ui-package-test-vitest.md delete mode 100644 .changeset/26304-screen-field-record-link.md delete mode 100644 .changeset/26306-filter-builder-spec-row-value.md delete mode 100644 .changeset/26307-bulk-confirm-locale.md delete mode 100644 .changeset/26314-record-anchored-approval-lock-read.md delete mode 100644 .changeset/26332-create-in-repo-typescript.md delete mode 100644 .changeset/26346-reaction-unreact-door.md delete mode 100644 .changeset/26348-filter-builder-boolean-list-entry.md delete mode 100644 .changeset/26349-package-bound-wiring.md delete mode 100644 .changeset/26418-toast-clears-drawer-actions-row.md delete mode 100644 .changeset/8133-spec-shared-chunks.md delete mode 100644 .changeset/8740-upsert-nothing-to-merge.md delete mode 100644 .changeset/business-unit-member-title-names.md delete mode 100644 .changeset/console-b41ed7f68956.md delete mode 100644 .changeset/console-document-content-security-policy.md delete mode 100644 .changeset/create-user-organization-join.md delete mode 100644 .changeset/default-org-owner-currency.md delete mode 100644 .changeset/react-pages-default-off.md delete mode 100644 .changeset/spec-form-object-name-shared.md diff --git a/.changeset/24757-retire-position-name-fold.md b/.changeset/24757-retire-position-name-fold.md deleted file mode 100644 index b9181367378..00000000000 --- a/.changeset/24757-retire-position-name-fold.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/core': patch ---- - -fix(plugin-security)!: a permission set that shares a held position's name is no longer granted; declare it in the position's `permissionSets` (#24757) - -Clause-②: yes (narrowing: a holder of position `P` is no longer granted the permission set named `P` unless `P`'s definition names it in `permissionSets`; widened: `@objectstack/core`'s `UserAuthzGrants` and `ResolvedAuthzContext` gain the additive field `directPermissions`) -Metadata-protocol: none -Ruling-ref: #13419 ruling B (`5472608110`); 「甲」 as amended (`6303865025`); order and unlock on #24757 (`6290781571`, `6392096931`); #25799 seat ruling A (claims `6414302166`, `6414315157`) - -ADR-0131 D3/D4: a position grants only the permission sets its definition's `permissionSets` names — the binding is written, not inferred. The security plugin's set resolution still requested every held position's NAME as a permission-set name too (the position-name fold), so a set that happened to share a position's name was granted to its holders without anyone writing the binding. #13419 ruling B retires that channel; HotCRM declared its bindings first (objectstack-ai/hotcrm#2200, released in hotcrm `v6.0.0`). - -What changes: - -- `resolvePermissionSetsForContext` requests only the context's `permissions` and the baseline. A held position's name reaches no set. `dropDeactivatedFoldedSets`, the second activation read that only the fold needed, is gone. -- The `position_name_fold_grant` warning keeps its event name and fields and now says the opposite: the same-named set is **not** granted, and how to grant it (`permissionSets: ['P']`) or to rename one of the two. Its payload carries `granted: false`. It is still logged once per position name per process. -- `describePositionGrants` lists only the sets a position's definition names, so it no longer serves `via: 'name'`. The `'name'` member of `PositionGrantVia` (`@objectstack/spec`) and its UI badge are retired separately in #26022; until then the value is published but never served. -- The delegated-administration gate judges an assignment of position `P` on the sets `P`'s definition names only. A set that only shares `P`'s name is no longer walked for the allowlist, the containment check or the organization boundary, because no assignment grants it. -- `explainAccess` attributes a set to `position:P` when `P`'s definition names it, and says `direct grant` otherwise. When the position definitions cannot be read it says so, rather than guessing either. A set held both through the user's own grant row and through a held position's definition reads `direct grant + position:P`, so revoking the position is not mistaken for removing the access (#25799). -- `@objectstack/core`: `UserAuthzGrants` and `ResolvedAuthzContext` gain `directPermissions: string[]`, the names conferred by the user's own `sys_user_permission_set` rows that apply in the active organization (a set a position definition also names included, a set only a definition names excluded). It is additive and grants nothing; the explainer reads it. Code that builds either object by hand must now set it (`directPermissions: []` when there are no grant rows). -- The lint gate `check-position-name-fold-loaders` and the census script `measure-position-name-fold-census` are deleted. They guarded the fold's only route, which no longer exists. - -**BREAKING** for an environment that relied on a shared name to grant a set. - -- **Before.** A user holding position `sales_manager` was granted the permission set `sales_manager` whether or not the position's definition named it. -- **Now.** That user is granted only the sets the `sales_manager` definition lists in `permissionSets`. The first resolution that would have folded the set logs `position_name_fold_grant` with `granted: false`, naming the position and the fix. -- **Fix.** Name the set in the position: `permissionSets: ['sales_manager']`. If the shared name is a coincidence, nothing needs to change. -- **Cloud ordering.** An environment running HotCRM must be on the HotCRM release that declares its bindings (`v6.0.0` or later) before it takes this release. - - diff --git a/.changeset/24768-not-null-refusal-every-door.md b/.changeset/24768-not-null-refusal-every-door.md deleted file mode 100644 index 4d78d94f689..00000000000 --- a/.changeset/24768-not-null-refusal-every-door.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/types': patch -'@objectstack/rest': patch ---- - -fix(objectql,types,rest)!: a database NOT NULL refusal on a declared field is the record validator's `ValidationError` at every door, named by the field's label - -A field declared `storage: { notNull: true }` without `required` is ADR-0113's engine-populated column: the record validator judges `required` only, so a write with no value for it reaches the driver and the database refuses it. The engine let the driver's error out, and each HTTP door classified it from the driver's text its own way. For the same refused write, the runtime dispatcher's `/data` POST and PATCH answered `500 INTERNAL_ERROR`, the actions door answered `400 VALIDATION_ERROR` with the driver's text and `declaredCode: 'SQLITE_CONSTRAINT_NOTNULL'`, and `RestServer` answered `400 VALIDATION_FAILED` relabelled by a second object read on its own `POST` / `PATCH` only. - -Clause-②: yes (narrowing: `@objectstack/types` adds the export `notNullViolationSite` and retires `notNullRefusalSite` and `relabelNotNullRefusal`; the HTTP doors' answer for this refusal moves to the record validator's envelope) -Metadata-protocol: none - - - -**BREAKING** (an export retirement and a wire-answer change), shipped as `patch`: no metadata protocol kind moves. - -- **`@objectstack/objectql`.** The write doors' driver exits (single-row and batch insert, the autonumber retry, by-id and predicate update) translate a driver NOT NULL refusal on a field the object's field map holds into the record validator's own `ValidationError` (`code: 'VALIDATION_FAILED'`), built by the validator's own builder with the validator's message context: one field error, `code: 'required'`, the field named by its label (translation bundle, then declared `label`, then API name) in the caller's locale, with the sentence the validator gives the same verb (`required` on insert, `required_cleared` on update). The driver's error rides `cause`, and the doors' log line still carries the database's own diagnosis. Left as the driver threw it: a column no field declares, a refusal the driver attributes to another table, and a NOT NULL code whose column the driver did not name. `required` and `storage.notNull` stay apart (ADR-0113 D1 / D2): no pre-write check reads `storage.notNull`. -- **Every door answers it alike**, because every door already answers that class alike: the dispatcher `/data` POST and PATCH now answer `400 VALIDATION_FAILED` (was `500 INTERNAL_ERROR`), the actions door `400 VALIDATION_FAILED` with the field envelope (was `400 VALIDATION_ERROR` with the driver's text), and `RestServer`'s `POST` / `PATCH` and its `/data/:object/batch` rows answer the validator's envelope and sentence. Import rows, the public form submit and MCP write through the same engine doors and answer the same class through the same arms; they were not driven separately. On `RestServer` the body is now exactly the validator's `required` shape: `fields[0]` carries `label`, there is no `hint`, and a `PATCH` writing null reads the clear-out sentence. -- **`@objectstack/types`.** New export `notNullViolationSite(error)`: the column (and table, where the dialect names it) of a driver's NOT NULL refusal, read from the error's codes (better-sqlite3 `SQLITE_CONSTRAINT_NOTNULL`; libsql `extendedCode` / `rawCode: 1299`; node-postgres `23502` with its `column` / `table` fields; mysql2 `ER_BAD_NULL_ERROR` / `errno: 1048`), then its text (all sql.js gives), then one step down its `cause` chain. An error whose code names another refusal is not read for NOT NULL text. `mapDataError`'s NOT NULL branch is unchanged, and answers the column no field declares. -- **`@objectstack/rest`.** The `/data` create and update doors' NOT NULL relabel (`not-null-refusal-label.ts`) retires: every path it served is an engine write. - -Migration, for a TypeScript caller of the retired exports: - -- FROM `notNullRefusalSite(answer)` TO `notNullViolationSite(error)`: ask the driver's error, not `mapDataError`'s answer. -- FROM `relabelNotNullRefusal(answer, { message, declaresNotNull })` TO nothing (no replacement): write through the engine, which answers a declared field's NOT NULL refusal as a labelled `ValidationError` itself. diff --git a/.changeset/24814-action-copy-template-slots.md b/.changeset/24814-action-copy-template-slots.md deleted file mode 100644 index 0bbbe212b0a..00000000000 --- a/.changeset/24814-action-copy-template-slots.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor -'@objectstack/cli': minor -'@objectstack/platform-objects': minor -'@objectstack/plugin-approvals': minor ---- - -feat(spec,lint): an action's `confirmText`, `successMessage` and `errorMessage` are `template` slots over the roots the console's runner binds - -Clause-②: yes (widening) -Metadata-protocol: semantic - -- **Before.** On the page action rows (`action:button`, `action:icon`, an `action:group` / `action:menu` member) the three copy keys were plain `z.string()`s, and on a registered action `confirmText` / `successMessage` were `I18nLabel`s. None declared a hole. The console filled `${…}` in them (`${record.name}` in a confirm, `${result.email}` in a success toast), and the platform's own sentences were written that way. -- **Now.** Each is an ADR-0032 `template` slot (Decisions 2–3), the same mechanism as a bulk action's `confirmText`. A bare string is shorthand for `{ dialect: 'template', source }`, and the envelope is accepted too. Holes are `{{ path }}` and `{{ path | formatter[:arg] }}`, with formatters from the template engine's whitelist and no logic inside a hole. They read a closed scope, the one the console's action runner binds, declared in `@objectstack/spec/ui`: - - `confirmText`: `record`, the record the action runs against, and `params` only when the action's `params` is an object of static values (`actionConfirmTemplateRoots(action)`). An input list binds nothing, because its values are collected after the confirmation. - - `successMessage`: `result`, the payload the handler answered, then `record` (`ACTION_SUCCESS_TEMPLATE_ROOTS`). - - `errorMessage`, on the rows only: no root (`ACTION_ERROR_TEMPLATE_ROOTS`). The runner shows it as written. -- **Parsed shape.** A parsed copy key is now the `{ dialect: 'template', source }` envelope rather than a string. A reader of the parsed or served definition takes the text from `source` (`templateSlotSource` in `@objectstack/spec/automation` reads both forms). The console pinned by this release reads both. A registered action keeps its inline locale map, and a blank string still parses as itself. -- **Judged at the doors, as warnings.** `objectstack validate`, `objectstack compile` and the publish gate (on `action`, `object` and now `page` writes) judge every sentence through `closedScopeTemplateRefusal`, the template judge's closed-scope entry, then compile it as a template. A hole over a root the runner does not bind, a `${…}` or single-brace `{x}` token, a hole holding logic or an unknown formatter, and a blank sentence are each named. Each locale of an inline map is judged on its own. Every finding is a **warning**, an advisory at the publish gate and never a refusal, so no command's exit code changes: each of these texts parsed while the keys were plain strings. -- **`${…}` is prescribed its hole.** The judge reads a `${…}` token with its `$`, so `Delete ${record.name}?` is told to become `Delete {{ record.name }}?`. It no longer says `Delete ${{ record.name }}?`. A slot with no root names every hole and token in it as literal text. -- **The platform's sentences.** The `invite_user` success toast on `sys_invitation`, `sys_member`, `sys_organization` and `sys_user` (`Invitation sent to {{ result.email }}`) and the nine `sys_approval_request` decision toasts (`Approved "{{ result.request.record_title }}"`, …) now use `{{ }}` holes, in every shipped locale. The console fills them the same way. -- **Translation and extraction.** `translateAction` answers the authored envelope when no translation applies, and a translated sentence as a string. `os i18n extract` seeds an envelope-form `confirmText` / `successMessage` exactly like the bare string. -- **Unchanged.** `ActionSchema.errorMessage` and `outcomeMessages` are untouched (`outcomeMessages` still reads `${result.*}`), and so are `emptyText` and every other key. Producers measured at `e649c2d91e`: 13 authored copy sentences carried `${…}` (the 13 above) and none carried `{{`. After this change none carries `${…}`, and none in `examples/**`. diff --git a/.changeset/25069-automation-webhooks-row.md b/.changeset/25069-automation-webhooks-row.md deleted file mode 100644 index 59b70bc0ac9..00000000000 --- a/.changeset/25069-automation-webhooks-row.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the objectstack-automation capability table says a declared webhook or auto-launched flow is refused without its `requires` pair, not silently undelivered - -Clause-②: no -Metadata-protocol: none - -The 〈Prerequisite — declare the capabilities your nodes need〉 table in `objectstack-automation/SKILL.md` said that without `webhooks` in `requires` "nothing [is] delivered outbound", and that without `triggers` "flows register, never fire". Neither is what `defineStack` does: a declared webhook while `requires` omits `webhooks`, `realtime` or both is refused with `STACK_WEBHOOK_CAPABILITY_REQUIRED` (422), and an auto-launched flow while `requires` omits `triggers`, `automation` or both is refused with `STACK_TRIGGER_CAPABILITY_REQUIRED` (422) — at `os validate`, `os build`, `os dev` and `os serve` alike. The two rows now name the pair each surface owes and the refusal code; the lead-in says the refusal is at load time and that neither token implies the other. The `requires: [...]` example block under the table is folded into the lead-in to keep the skill within its token ceiling. diff --git a/.changeset/25190-retire-render-template-export.md b/.changeset/25190-retire-render-template-export.md deleted file mode 100644 index 682d045fc41..00000000000 --- a/.changeset/25190-retire-render-template-export.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/plugin-email': patch ---- - -fix(plugin-email)!: retire `renderTemplate`, the render export that takes an email template body with no save moment - -The hole-placement rule for mail templates is judged where a template is authored or published: `os validate`, `os build`, `os lint` and the runtime publish gate. The `renderTemplate` function on the package entry took a template body inline and rendered its html face at once, with no save moment, so the rule could not see it. It leaves the published entry of `@objectstack/plugin-email`. Nothing in this repository imported it from that entry. The renderer stays inside the package: `services.email` still renders every stored `email_template` through it, unchanged. - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** (a runtime export is removed). It ships as `patch` because the metadata protocol does not move. There is no staged window. - -- FROM `renderTemplate` TO an `email_template` item for the name and locale, rendered by `services.email` through `renderTemplate({ template, data, locale })` (render only) or `sendTemplate` (render and send). - -Unchanged: `IEmailService.renderTemplate` (the render-by-name method of the same name), `requireVars`, `htmlToText` and the template loader; and how every stored template renders. diff --git a/.changeset/25337-spec-book-team-add-member.md b/.changeset/25337-spec-book-team-add-member.md deleted file mode 100644 index f89baca4c99..00000000000 --- a/.changeset/25337-spec-book-team-add-member.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): book the team-side Add Member action under the `organization` feature gate - -Clause-②: no -Metadata-protocol: none - -`PUBLIC_AUTH_FEATURES.organization.gatedInputs` now lists `sys_team.actions.add_team_member`, right after `sys_team.actions.remove_team`. `@objectstack/platform-objects` declares that action on `sys_team` (Add Member opened from a team, which binds the team) with `requiresFeature: 'organization'`, and the booking keeps the two sides agreeing: every action gated on a feature is listed under it, and every listed input carries the gate. The membership-list copy, `sys_team_member.actions.add_team_member`, keeps its own row unchanged. - -What to do: nothing. No key, value or predicate changes; the list gains one entry. diff --git a/.changeset/25337-team-add-member-binds-team.md b/.changeset/25337-team-add-member-binds-team.md deleted file mode 100644 index 613eb84832e..00000000000 --- a/.changeset/25337-team-add-member-binds-team.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -feat: Add Member opened from a team now binds that team, so the dialog asks only for the user - -Clause-②: no -Metadata-protocol: none - -In Setup → Teams, Add Member lived only on the team-membership list's toolbar. No team is in hand there, so the dialog asked the administrator to pick the team again. `sys_team` now also declares `add_team_member`, on the team row (`list_item`) and the team record header (`record_header`). It calls the same `POST /api/v1/auth/organization/add-team-member` endpoint behind the same gates (the `organization` feature and the `add_team_member` membership grade). The team's id rides as `teamId` from the row it is opened on, and the dialog asks only for the user. The membership-list copy is unchanged and still asks for both. diff --git a/.changeset/25347-rename-organization-auth-feature.md b/.changeset/25347-rename-organization-auth-feature.md deleted file mode 100644 index 48e4f4bd78e..00000000000 --- a/.changeset/25347-rename-organization-auth-feature.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec): `PUBLIC_AUTH_FEATURES` books Rename Organization under the organization capability - -Clause-②: yes -Metadata-protocol: none - -The `sys_organization.actions.rename_organization` row moves from `multiOrgEnabled.gatedInputs` to `organization.gatedInputs`, matching the action's new `requiresFeature: 'organization'` in `@objectstack/platform-objects`. A `single` deployment, where `multiOrgEnabled` is false and the organization capability is on, now offers it. No other row moves. - -Ruling 6384629949 on #25347. diff --git a/.changeset/25347-rename-organization-single.md b/.changeset/25347-rename-organization-single.md deleted file mode 100644 index ae9ad3baabc..00000000000 --- a/.changeset/25347-rename-organization-single.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -feat(platform-objects): a single-organization deployment can rename its organization - -Clause-②: yes -Metadata-protocol: none - -- **`sys_organization`'s Rename Organization** (name and logo, never the slug) is now gated on the organization capability instead of `multiOrgEnabled`. Under the `single` posture it was the one organization's only possible name door, and it was hidden: the generic edit keeps `name` readonly and every other organization action is multi-organization only. It is still offered only to the organization's owners and admins, and better-auth's `organization/update` still decides at the door. -- **Edit Organization** (which also sends the slug), Create, Delete, Switch and Leave stay multi-organization only. Under `multi` nothing changes. -- **`sys_organization.name`'s help** now says the name is changed with Rename Organization, in all four bundles. - -Ruling 6384629949 on #25347. diff --git a/.changeset/25442-verify-readme-scheduled-work.md b/.changeset/25442-verify-readme-scheduled-work.md deleted file mode 100644 index 613aa2b33aa..00000000000 --- a/.changeset/25442-verify-readme-scheduled-work.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/verify': patch ---- - -docs(verify): the README says how a local stack arms scheduled flows - -Clause-②: no -Metadata-protocol: none - -The README's `flows.run(…, { system: true })` passage now says that this call works with scheduled work off. It also says that a local stack only arms time-triggered flows on their own cadence when `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true` is set; package-authored scheduled work is off by default in every posture. - -What to do: nothing. Only the packaged README changes. diff --git a/.changeset/25458-navigate-console-route.md b/.changeset/25458-navigate-console-route.md deleted file mode 100644 index 522873e21d9..00000000000 --- a/.changeset/25458-navigate-console-route.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): `onSuccess.navigate` and a `type: 'url'` action's `target` say that an in-app path is an app-qualified console route (`/apps/APP/OBJECT/record/ID`) (#25458) - -Clause-②: no -Metadata-protocol: none - -Wording only; no schema accepts or refuses anything new. The describe text of `ActionSchema.onSuccess.navigate` and of `ActionSchema.target` said "an in-app path starting with / or ./" and never said which path. They now say that an in-app path is a console route, written without the console's mount prefix, and that a record is `/apps/APP/OBJECT/record/ID`. `APP` is the app's route segment (ADR-0048): its package id, and its `name` also resolves. No app-relative form is declared and no `${ctx.*}` member names the current app, so write the app segment. The docblock example `/apps/crm/tasks/${result.id}` becomes `/apps/crm/tasks/record/${result.id}`, the route it was only redirected to. diff --git a/.changeset/25459-form-entry-pack-keys.md b/.changeset/25459-form-entry-pack-keys.md deleted file mode 100644 index 31ddd1656cd..00000000000 --- a/.changeset/25459-form-entry-pack-keys.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/cli': minor -'@objectstack/lint': minor ---- - -feat(spec,cli,lint): a form section entry's `label`, `placeholder` and `helpText` are translatable at `objects.._sections.
.fields.` - -Clause-②: yes (widening) -Metadata-protocol: added - -A form section entry (`{ field, label, placeholder, helpText }` in a form view's `sections[].fields`) can override the object field's copy for that form. Before this change a translation pack had no key for those three strings, so a form's entry overrides stayed in the authoring language for every reader, the anonymous `GET /forms/:slug` visitor included. - -- **`@objectstack/spec`:** the `_sections.
` node of an object translation gains `fields`, keyed by the entry's `field`, each carrying `label`, `placeholder` and `helpText` (strict; `help`, `hint` and `tooltip` are refused with the rename to `helpText`). `translateView` overlays them onto a served form view's `config.sections[].fields[]` under the section rules: the key is the section's `name` and the entry's `field`, only copy the entry authors is translated, a missing key keeps the authored string, and a string edited on top of the packaged view keeps the edit (ADR-0029 D9.2a). An entry in a section with no `name`, and a bare field-name entry, have no key. -- **`@objectstack/cli`:** `os i18n extract` scaffolds the key for every entry string a form view's named section authors, and `os i18n check` / `os lint` coverage report a locale that leaves one untranslated, in the `section` bucket. -- **`@objectstack/lint`:** `translation-target-unknown` reports a `_sections.
.fields.` key that no form view section of that name declares as an entry object. - -Example (`zh-CN` bundle): - -```ts -objects: { - crm_case: { - _sections: { - submitter: { - label: '关于您', - fields: { contact_name: { label: '您的姓名', placeholder: '张三', helpText: '与证件一致' } }, - }, - }, - }, -} -``` diff --git a/.changeset/25486-unknown-resource-404.md b/.changeset/25486-unknown-resource-404.md deleted file mode 100644 index 22413fd7b10..00000000000 --- a/.changeset/25486-unknown-resource-404.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/service-datasource': patch ---- - -fix(rest): an unknown resource answers `404 RESOURCE_NOT_FOUND` at the package draft and commit-trail doors, the flow runs list and delete, and the datasource by-name write doors, as their read siblings already did - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** (a served-set narrowing) for a caller that sends one of these doors a package id, flow name or datasource name that does not exist. Five of them used to answer such a request `200` with an empty result. All eight now refuse it. - -Eight doors answered a request about a resource that does not exist as if it were an empty or a bad request, while their sibling doors answered `404` for the same name. An SDK or AI caller could not tell "no such thing" from "nothing yet". Each now answers `404 RESOURCE_NOT_FOUND`, with the status, code and message its sibling gives for the same name. The answer before and after, per door: - -- `POST /api/v1/packages/:id/publish-drafts`, for a package the registry does not hold: `200 { success: false, outcome: 'nothing_to_publish' }` → `404 RESOURCE_NOT_FOUND "Package '' not found"`. That is the answer of `GET /api/v1/packages/:id`. -- `POST /api/v1/packages/:id/discard-drafts`: `200 { success: true, discardedCount: 0 }` → the same `404`. -- `GET /api/v1/packages/:id/commits`: `200 { commits: [] }` → the same `404`. -- `GET /api/v1/automation/:name/runs`, for a flow the engine does not hold: `200 { runs: [], hasMore: false }` → `404 RESOURCE_NOT_FOUND "Flow '' not found"`. That is the answer of `POST /api/v1/automation/:name/trigger`. -- `DELETE /api/v1/automation/:name`, for a flow that neither the engine nor the metadata store holds: `200 { name, deleted: true }` → the same `404`. -- `PATCH /api/v1/datasources/:name`, for a datasource that does not exist: `400 DATASOURCE_ADMIN_ERROR "Datasource '' not found."` → `404 RESOURCE_NOT_FOUND 'Datasource "" does not exist.'`. That is the answer of `GET /api/v1/datasources/:name`, word for word. -- `DELETE /api/v1/datasources/:name`: the same `400` → the same `404`. -- `POST /api/v1/datasources/:name/migrate-credential`: the same `400` → the same `404`. - -Unchanged: a resource that exists keeps its answer. An empty page for a flow that has not run, `nothing_to_publish` for a package with nothing pending, and `400 DATASOURCE_ADMIN_ERROR` for a code-defined datasource all stay as they were. Drafts or commits the store holds for an id the registry no longer lists are still published, discarded or listed. A stored flow the engine does not hold is still deleted. A failed ADR-0045 visibility flip still answers `200` with its `unhideError`. - -**What changes for you.** `ObjectStackClient` already throws on any non-2xx answer. `packages.publishDrafts`, `packages.discardDrafts`, `packages.listCommits`, `automation.listRuns` and `automation.delete` now reject with the not-found message for an unknown id, instead of resolving an empty result. Treat that rejection as "no such resource". An embedder calling `DatasourceAdminService.updateDatasource`, `removeDatasource` or `migrateCredential` directly now gets an error carrying `code: 'RESOURCE_NOT_FOUND'` and `status: 404`. Its message is now `Datasource "" does not exist.`. diff --git a/.changeset/25491-import-inline-mapping-entry-rules.md b/.changeset/25491-import-inline-mapping-entry-rules.md deleted file mode 100644 index 1eb40bf1d88..00000000000 --- a/.changeset/25491-import-inline-mapping-entry-rules.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest)!: an inline import mapping entry's `transform`, `defaultValue` and `required` are applied, not dropped - -`POST /api/v1/data/:object/import`, the async import job and plugin-auth's admin user import (`POST /api/v1/auth/admin/import-users`) all parse their body through the same request parser, which accepts `mapping` as a `FieldMappingEntry[]`, whose entries carry `transform`, `defaultValue` and `required`. The request parser kept only the column each entry names with `sourceField` and the field it names with `targetField` and dropped the rest, so a request asking for `transform: 'uppercase'` validated, ran, and stored the cell unchanged. Each entry's rules now run on its column before the cells are coerced: - -- `trim`, `uppercase` and `lowercase` rewrite a string cell. `none` and `lookup` copy it; a `lookup` cell is resolved to a record id by the import's reference resolution, as before. -- `defaultValue` fills a cell that is blank (absent, empty, whitespace or one of the request's `nullValues`). -- `required: true` refuses the request with `400 MISSING_REQUIRED_FIELD` when a row still leaves that column blank, naming the column and the rows. The refusal comes before any row is written, on the dry run and the commit alike. -- `transform: 'date_format'` refuses the request with `400 UNSUPPORTED_TRANSFORM`: an entry carries no date format for it to apply. A cell mapped to a date or datetime field is already parsed as a date, so drop the transform. A transform name outside the declared set is refused the same way. - -A plain `{ sourceField, targetField }` entry, and the compact `{ column: field }` record, map columns exactly as before. - -Clause-②: no (narrowing) — the import doors (the sync import, the async import job and plugin-auth's admin user import) refuse two request shapes they used to run: an inline mapping entry declaring `transform: 'date_format'` (or a transform name outside the declared set), and an entry marked `required: true` whose column a row leaves blank. Nothing is widened. - -Metadata-protocol: none (a REST import request's handling; nothing a metadata author writes changes) - - - -**BREAKING** for a caller that relied on either request running. - -- **Before.** An inline entry's `transform`, `defaultValue` and `required` were ignored; the import ran with the rename alone. -- **Now.** They are applied; `date_format` and a blank `required` cell refuse the request with `400`. -- **Fix.** Drop `transform: 'date_format'` from the entry (the date field's own parse still applies), and fill a required column or give its entry a `defaultValue`. diff --git a/.changeset/25495-org-default-currency.md b/.changeset/25495-org-default-currency.md deleted file mode 100644 index 27e24af2ecb..00000000000 --- a/.changeset/25495-org-default-currency.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -Clause-②: no -Metadata-protocol: none - -A new organization now gets its default currency from its creator's language or region. When an organization is created through `POST /api/v1/auth/organization/create`, the server reads the creator's stored language (`sys_user.locale`), or else the creating request's `Accept-Language`. It writes that region's ISO 4217 currency as the organization's `localization.currency`: `zh-CN` gives `CNY`, `ja` gives `JPY`, `en-GB` gives `GBP`. The value is an ordinary tenant setting, so Setup → Localization shows it and can change it. Amounts in `currencyMode: 'dynamic'` fields then show that currency instead of a plain number. - -When there is nothing to derive from (no stored language, no `Accept-Language`, or a tag with no country such as `es-419`), nothing is written and the setting stays unset, as before. There is no fallback to the `default_country` or `locale` platform defaults. A value already set on any rung (the organization's own, a global row, or an `OS_LOCALIZATION_CURRENCY` override) is never overwritten. A host's own `onOrganizationCreated` callback still runs, before this step. - -The `os dev` seed does the same for its admin's workspace from the host's POSIX locale (`LC_ALL`, then `LC_MONETARY`, then `LANG`), beside its time-zone seeding. `C`, `POSIX`, and a host with no locale set write nothing. - -The region-to-currency table is a checked-in snapshot of CLDR 48 `currencyData`. diff --git a/.changeset/25510-template-baseline-exact-pins.md b/.changeset/25510-template-baseline-exact-pins.md deleted file mode 100644 index b32fad5058e..00000000000 --- a/.changeset/25510-template-baseline-exact-pins.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'create-objectstack': patch ---- - -fix(create-objectstack): the bundled blank template pins every `@objectstack/*` dependency at the exact release version - -Clause-②: no -Metadata-protocol: none - -The `blank` template's committed `package.json` now lists each `@objectstack/*` package at the exact version of the `create-objectstack` release that ships it (`18.1.0`, a prerelease verbatim), instead of `^18.0.0`. A scaffolded project already received exact pins, because the scaffolder rewrites these entries at scaffold time; the template a reader opens in the tarball or on GitHub now says the same thing, which is what the v18 docs prescribe ("Pin exact versions"). - -The template manifest's `specVersion` stays a range (`^18.0.0`): it names the `@objectstack/spec` line the template is authored against, not a build. The release-time version pass keeps both values current — the exact version in the dependencies, the major's range in `specVersion`. - -No action is needed in an existing project. diff --git a/.changeset/25511-hide-empty-describe.md b/.changeset/25511-hide-empty-describe.md deleted file mode 100644 index a4648f0199c..00000000000 --- a/.changeset/25511-hide-empty-describe.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -spec(ui): `record:details` section `hideEmpty: false` — in read-only mode an all-empty section renders its heading plus one fold row, and the `describe()` text says so - -Clause-②: no -Metadata-protocol: semantic - -The meaning of `hideEmpty: false` on a `record:details` section changes for a section whose fields are ALL empty (for example on a brand-new record): - -- **Read-only mode:** the section renders its heading plus one fold row that counts the empty fields and reveals them on request — the same fold a partly-filled section already uses. It no longer renders a column of empty rows. -- **Edit mode:** unchanged. Every field renders as an editable row. - -Nothing else changes. No key is added, removed or renamed; the shape and the default are the same. Leaving `hideEmpty` out (or writing `true`) still means an all-empty section renders nothing at all: no heading, no skeleton. A partly-filled section's empty rows are not governed by this key, in either value. Authored metadata needs no change: a section that writes `hideEmpty: false` to keep its heading on an empty record keeps it. - -The `describe()` text, which the reference docs and the JSON Schema read, now states this. Its lead changes from "Hide this section's empty fields" to "Whether this section disappears when ALL its fields are empty", because under the new reading both values hide an all-empty section's empty rows in read-only mode and the old lead no longer told them apart. The renderer change is in objectui's record detail section. diff --git a/.changeset/25514-artifact-flow-dataset-refs.md b/.changeset/25514-artifact-flow-dataset-refs.md deleted file mode 100644 index 48ef3f3bcb6..00000000000 --- a/.changeset/25514-artifact-flow-dataset-refs.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor ---- - -feat: a package's action can launch a flow, and its dashboard widget can bind a dataset, that another package of the same release artifact ships - -Clause-②: yes (widening) -Metadata-protocol: added - -One release artifact carries several packages (ADR-0130 D4). Two per-package checks judged a reference against one package and refused a target a sibling package provides. The artifact carries the target, and the runtime serves it. - -- **`@objectstack/spec`: `defineStack(config, { artifactFlows })`.** This new `DefineStackOptions` key lists flow names that the other packages of the artifact provide. A `type: 'flow'` action's `target` resolves against the stack's own `flows` plus these names. That covers a top-level action, an action authored on an object, and an inline page action. It works the way `artifactObjects` already does for permission grants and seed rows. - - The option widens a check that already runs. A stack that declares no flows of its own still leaves its flow targets unresolved (a plugin may serve them). - - `defineStack` records every target that resolved only through the list, as a non-enumerable mark on the stack it returns. `composeStacks` refuses the composition when no input defines that flow. The refusal uses the same `STACK_CROSS_REFERENCE_INVALID` envelope and the same per-finding message as the per-stack pass. An input that did not opt in cannot newly fail at composition. - - A flow that no package of the artifact defines is refused, as before. -- **`@objectstack/lint`: `widget-dataset-unknown`.** The rule resolves a widget's `dataset` against the datasets that every `packages[]` body declares, as well as the stack's own. This is the resolution context that `os build`, `os validate` and `os lint` already pass to each package's run. Dataset-level checks still judge only the package's own datasets. A dataset that no package declares is refused, as before. The runtime publish gate passes no `packages[]`, so its behaviour is unchanged. - -Fix for a module split such as HotCRM's: in the app package, write `defineStack(appConfig, { artifactFlows: revenue.flows?.map((f) => f.name) })` and compose both packages with `composeStacks([...], { manifest: 'preserve' })`. A cross-package dataset binding needs no change. diff --git a/.changeset/25529-oidc-userinfo-locale-claim.md b/.changeset/25529-oidc-userinfo-locale-claim.md deleted file mode 100644 index 7aa73059117..00000000000 --- a/.changeset/25529-oidc-userinfo-locale-claim.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -feat(plugin-auth): the platform OIDC provider emits the standard `locale` claim at userinfo, so an environment signing in through it follows the user's language - -Clause-②: yes -Metadata-protocol: none - -- **Before.** The OIDC provider plugin-auth composes (`oauthProvider`) passed no `customUserInfoClaims`, so `/oauth2/userinfo` never carried `locale`. An environment signing in through an ObjectStack OP (a hosted environment through the cloud) reads the claim off userinfo to set its JIT user's language (`idp-locale-claim.ts`), and so never received it. -- **Now.** Userinfo carries `locale` (OIDC Core §5.1) when the access token's scopes include `profile` and the user's `sys_user.locale`, read through the data engine in system context, is a BCP-47 tag the platform's column reader accepts (`normalizeRecipientLocale`). Otherwise the key is absent. Every ObjectStack OP emits it; there is no new configuration key, column or better-auth `additionalField`. -- **Userinfo only.** The ID token is unchanged: the provider keeps the standard profile claims out of it. -- **A failed read never fails userinfo.** The claim is omitted and the failure is logged once per request at `warn` through the host logger, naming the user. diff --git a/.changeset/25549-external-share-followup-wording.md b/.changeset/25549-external-share-followup-wording.md deleted file mode 100644 index f5e1a9f34ed..00000000000 --- a/.changeset/25549-external-share-followup-wording.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-sharing': patch -'@objectstack/spec': patch ---- - -docs: the sharing-rule pass note and the `ISharingService.grant` contract say what a share on a public object does - -Clause-②: no -Metadata-protocol: none - -A follow-up to the external-share change. The one-line `note` a sharing-rule pass carries when it skips recipients on a public object now also covers a record whose organization could not be resolved, which admits no recipient. The `ISharingService.grant` docblock no longer says a share on a public object enforces nothing: it is admitted for an `external` member of the record's organization. diff --git a/.changeset/25555-localization-input-organization-id.md b/.changeset/25555-localization-input-organization-id.md deleted file mode 100644 index c667997af77..00000000000 --- a/.changeset/25555-localization-input-organization-id.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/mcp': patch -'@objectstack/plugin-audit': patch -'@objectstack/plugin-hono-server': patch ---- - -refactor(core)!: `resolveLocalizationContext` takes the organization as `organizationId`; the `tenantId` key is retired - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a TypeScript caller of `resolveLocalizationContext` from `@objectstack/core`. The key that names the organization to localize for is renamed. There is no alias and no deprecation window. - -`ResolveLocalizationInput.tenantId` always meant the **organization** id: the settings service stores and reads `localization` under the organization (ADR-0131 D8), and the direct `sys_setting` fallback reads that organization's rows. The name did not say so, and an environment id passed there type-checked. It matched no organization's settings, so every non-UTC organization silently resolved its timezone as `UTC`, and the date boundaries cut from it moved with it. The key is now `organizationId`, and the resolver reads no other spelling at run time. - -- FROM `ResolveLocalizationInput.tenantId` TO `ResolveLocalizationInput.organizationId`: pass the caller's organization id under the new key, `resolveLocalizationContext({ ql, settings, organizationId, userId })`. Never pass an environment id. - -**Who moves.** A caller that builds the input as an object literal with `tenantId` gets a compile error (excess property) and makes the one-key fix above. A caller that builds it some other way, such as spreading an object that holds `tenantId` or passing a variable typed wider than the input, does not get that error: its `tenantId` is ignored and the resolver localizes as for a caller with no organization, so grep for `tenantId` beside `resolveLocalizationContext`. In this repository the four callers (`@objectstack/core`'s request execution context, `@objectstack/mcp`, `@objectstack/plugin-audit` and `@objectstack/plugin-hono-server`) are migrated here and behave as before. - -**Not moved.** The settings service's own scope argument, `{ tenantId, userId }` on `get` / `getMany`, keeps its spelling. It is a separate surface with its own callers. The resolver passes the organization id there as before. diff --git a/.changeset/25561-analytics-admission-bridge-503.md b/.changeset/25561-analytics-admission-bridge-503.md deleted file mode 100644 index f5d2444681f..00000000000 --- a/.changeset/25561-analytics-admission-bridge-503.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -fix(service-analytics)!: the analytics admission bridge answers `503 SERVICE_UNAVAILABLE`, not a denial's `403 PERMISSION_DENIED`, when the `security` service is missing or cannot be used - -`AnalyticsServicePlugin` bridges the object-level read admission to the `security` service. When that service cannot be used, the bridge refuses the query, fail-closed. That covers three cases: resolving the service throws, the service exposes neither `canReadObject` nor `explain`, or no `security` service was ever registered (`ObjectKernel` and `LiteKernel` throw on a lookup for a service nobody registered). Until now the refusal answered `403 PERMISSION_DENIED`, the same answer as a caller who may not read the object. The bridge reached no verdict, so the refusal now answers `503 SERVICE_UNAVAILABLE` naming the object, like the other analytics read gates that cannot decide. The cause stays in the server log. Fail-closed is unchanged: nothing is read, and nothing that was refused is now served. A real denial (`canReadObject` or `explain` answering false) still answers `403 PERMISSION_DENIED`, byte for byte. The bridge's `error` log line no longer says "A security service is wired on this deployment". That was untrue when no security service was ever registered. The line now says the `security` service is missing or cannot be used. The row-level read scope bridge's `error` line said the same untrue sentence and is corrected the same way; what that bridge answers does not change. - -Clause-②: no -Metadata-protocol: none - - - -**BREAKING** (the wire answer of one fault refusal changes; no request that was served is refused and none that was refused is served), shipped as `patch` because the metadata protocol does not move. - -- **Before:** with no `admitObjectRead` supplied, an analytics query on a deployment whose `security` service is broken, or was never registered, was refused `403 PERMISSION_DENIED`. That was the same code and status as a caller who genuinely may not read the object. -- **Now:** the same refusal answers `503 SERVICE_UNAVAILABLE` (`status: 503`, `object` naming the object). On `POST /api/v1/analytics/dataset/query` the body is `{"code":"SERVICE_UNAVAILABLE","error":"Internal server error"}`. Real denials still answer `403 PERMISSION_DENIED`, unchanged. -- **Fix:** a client that treated every analytics `403` as "no access" should treat `503 SERVICE_UNAVAILABLE` as a server-side fault and check the server log. A deployment that wants analytics to answer registers a `security` service (for example `plugin-security`), or its host supplies its own `admitObjectRead`. diff --git a/.changeset/25561-analytics-read-gate-fault-503.md b/.changeset/25561-analytics-read-gate-fault-503.md deleted file mode 100644 index 18d02b1faa2..00000000000 --- a/.changeset/25561-analytics-read-gate-fault-503.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-analytics': patch -'@objectstack/metadata-protocol': patch ---- - -fix(service-analytics)!: an analytics read gate that could not decide answers `503 SERVICE_UNAVAILABLE`, not the `403 PERMISSION_DENIED` of a real denial - -Three of the analytics door's refusals are operator-side faults: the object-level read admission provider threw, the field-level read permissions could not be resolved, or the API exposure declaration could not be read. Each refused the query, which is right, but each answered with the same body as a real denial, `403 PERMISSION_DENIED`. A caller could not tell "you may not read this" from "the server could not decide", except by parsing the English message. All three now answer `503 SERVICE_UNAVAILABLE`. That is an existing ADR-0112 code, and it is the spelling `@objectstack/core` already uses when the permission store is unreachable. The refusal names the object and nothing else; it names no gate, grant or existence fact. The cause stays in the server log, as before. Fail-closed is unchanged: nothing that was refused is now admitted. Real denials keep their answers byte for byte: `403 PERMISSION_DENIED` when the caller holds no read grant or reads a hidden field, and `404 OBJECT_API_DISABLED` / `405 OBJECT_API_METHOD_NOT_ALLOWED` for an unexposed object. `POST /api/v1/analytics/dataset/query` relays the declared fault as `503 SERVICE_UNAVAILABLE`, with the message withheld as for every declared server fault. It does not answer `500 ANALYTICS_QUERY_FAILED`. The cube query door relays the status the same way. - -The publish build probe (`@objectstack/metadata-protocol`) reads the code too. A widget query that fails with `SERVICE_UNAVAILABLE` now gets a `widget_query_failed` fix that names the server-side fault: the dataset is not the cause, and the server could not decide read access to the named object. Before, it fell to "Fix the dataset definition…", which is wrong for an outage. The finding's `code`, `severity` and `message` are unchanged. - -Clause-②: no -Metadata-protocol: none - - - -**BREAKING** (the wire answer of three fault refusals changes; no request that was served is refused and none that was refused is served), shipped as `patch` because the metadata protocol does not move. - -- **Before:** when the analytics read-admission provider threw, or the field-level read permissions or the API exposure declaration could not be resolved, the query was refused `403 PERMISSION_DENIED`. That was the same code and status as a caller who genuinely may not read the object. -- **Now:** the same refusals answer `503 SERVICE_UNAVAILABLE` (`status: 503`, `object` naming the object). On `POST /api/v1/analytics/dataset/query` the body is `{"code":"SERVICE_UNAVAILABLE","error":"Internal server error"}`. Real denials still answer `403 PERMISSION_DENIED` (and `404` / `405` for exposure), unchanged. -- **Fix:** a client that treated every analytics `403` as "no access" should treat `503 SERVICE_UNAVAILABLE` as a server-side fault: retry once the permission or exposure resolver is healthy, and check the server log for the cause. Do not show it as a missing grant. diff --git a/.changeset/25571-notify-email-field-delivery.md b/.changeset/25571-notify-email-field-delivery.md deleted file mode 100644 index 0f07f6c9eee..00000000000 --- a/.changeset/25571-notify-email-field-delivery.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -feat(service-automation)!: a `notify` node mails the address in an email field of the trigger record, and refuses a computed address no user holds - -Clause-②: no (narrowing: a recipient that the value expression of a `notify` node whose `channels` include `email` evaluates to an email address no user holds now fails the node at run time instead of being mailed) -Metadata-protocol: narrowed - -A `notify` node can now reach a person who is not a user, such as an applicant who came in through a public form. `recipients: { emailField: 'applicant_email' }` with `channels: 'email'` (the form `@objectstack/spec` declared earlier in this release, which until now failed the run at the node) mails the address held in that `email`-typed field of the record that triggered the flow. - -- **The address.** It is read from the stored record by id when the node runs, never from the record snapshot the run carries. It must be exactly one plain ASCII mailbox, `name@example.com`. A list, a display name, a separator, a line break or an audience prefix such as `role:` fails the node and nothing is sent, and so does an address the `email` field type accepts but this node does not: a non-ASCII character, a quoted local part or an IP-literal domain. So do an empty field, a record that is not stored and a run with no trigger record. The failure names the field and the record, never the value. -- **The send.** The title and message render as for any notify, the email goes through the deployment's outbound mail, and it is recorded like every notification: a `sys_notification` event and a `sys_notification_delivery` row whose recipient is the address. -- **Per-address limit.** One address receives at most 5 email-field sends per hour (`EMAIL_FIELD_SENDS_PER_WINDOW`, `EMAIL_FIELD_SEND_WINDOW_SECONDS`), across every flow, counted case-insensitively in the kernel `cache` service (per process when none is registered, said once). A sixth fails the node before anything is written. The limit is best-effort: when the counter faults, the send is admitted and a warning is logged once. -- **BREAKING (narrowed): a computed address the node mails must be a user's.** In a `notify` node whose `channels` include `email`, a recipient that a value expression (a CEL envelope) evaluates to and that reads as an email address, bare or as `user:ADDRESS`, now resolves to the user who holds it, and fails the node when no user holds it (or when no user directory can be read), with a message that points to `{ emailField }`. Before, such an address was mailed as it stood, so an expression over record text let whoever typed that text choose who the flow mailed. A node that does not name `email` (`inbox` alone, the messaging default) mails nobody and is unchanged. An address written out in the flow (`recipients: ['ops@example.com']`) is the author's constant and is sent as written, as before. -- **Who moves.** A flow that mails (`channels` include `email`) a recipient its expression evaluates to an address no user holds. Measured over `examples/**` at `47af3dd`: no `notify` node names the `email` channel, so no in-repo flow changes. (Of the 17 `recipients` envelopes, the app-showcase ones over `record.owner` / `ownerId` do evaluate to addresses, since that field holds the owner's email, but those nodes are `inbox` only.) Fix: give the address its own `notify` node with `recipients: { emailField: 'FIELD' }` and `channels: 'email'`, or give the person a user. -- **Dependency.** `@objectstack/plugin-auth` moves from a dev dependency to a dependency of `@objectstack/service-automation`: the limit counts through its `rate-limit-storage` fixed-window counter, the repository's one counter, as `@objectstack/service-sms` does. This is a new workspace edge, `service-automation` → `plugin-auth`, and no cycle: neither `plugin-auth` nor what it depends on (`service-messaging`, `rest`) depends on `service-automation`. - - diff --git a/.changeset/25572-public-form-submit-carries-no-file.md b/.changeset/25572-public-form-submit-carries-no-file.md deleted file mode 100644 index e1cfebe5890..00000000000 --- a/.changeset/25572-public-form-submit-carries-no-file.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/rest': patch -'@objectstack/service-storage': patch ---- - -fix(rest)!: an anonymous public-form submission carries no file - -`POST /api/v1/forms/:slug/submit` (and its environment-scoped twin) now refuses a submission that carries a value in a field whose declared type is a file type (`file`, `image`, `avatar`, `video` or `audio`, the spec's `FILE_REFERENCE_TYPES`). The refusal comes before anything is written. It is the door's existing envelope, `400 VALIDATION_FAILED`, with one `fields[]` entry per file field, coded `invalid_value` (the ADR-0114 field catalog) and naming the field by its label. A missing `required` field in the same submission is reported in the same envelope. An empty or absent file field submits exactly as before. Only the fields the form declares are judged, so the refusal names nothing the form does not already publish. - -This is the server half of the rule "an anonymous public-form submission carries no file". A visitor who needs to provide a file registers and signs in, then submits through a signed-in surface that uploads through the authenticated upload doors. The anonymous door has no upload door of its own, and none is added. The protocol half, refusing at publish a guest-facing form that declares a file field, is separate. This door also judges forms published before that refusal exists. - -`@objectstack/service-storage`: the file-reference claim path now looks a referenced file up within the organization the write acts in. It uses the same channel that already stamps a copied file for that organization, so the driver's tenant scope for the posture in force decides what the lookup sees. A referenced file id outside that scope is kept as written and is neither claimed nor copied, exactly as an id the platform does not manage. A write that carries no organization, and the `single` posture, look files up as before. - -Clause-②: no (narrowing) — the anonymous public-form submit door refuses a request shape it used to accept (a value in a declared file field), and the file-reference claim path reaches only files within the writing organization's tenant scope. Nothing is widened. - -Metadata-protocol: none (a request's handling at an anonymous door and a write path's file bookkeeping; nothing a metadata author writes changes. The publish-time refusal of a guest-facing form with a file field is the protocol half, and ships separately) - - - -**BREAKING** for a public form that collected files anonymously, shipped as `patch` because the metadata protocol does not move. - -- **Before.** A value in a file field the form declares was written with the anonymous submission. -- **Now.** That submission is refused with `400 VALIDATION_FAILED`, naming each file field that carries a value. A submission that leaves every file field empty is accepted as before. -- **Fix.** A public form that collected files: have the submitter register and sign in, then provide the file from their account. Remove the file field from the guest-facing form, or leave it for the visitor to leave empty. diff --git a/.changeset/25573-exclude-submitter-runtime.md b/.changeset/25573-exclude-submitter-runtime.md deleted file mode 100644 index e1748f4cb9f..00000000000 --- a/.changeset/25573-exclude-submitter-runtime.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch -'@objectstack/spec': patch ---- - -feat(plugin-approvals): an approval step's `excludeSubmitter: true` keeps the submitter from approving their own request - -Clause-②: no -Metadata-protocol: none - -- **Before.** `excludeSubmitter` was declared on the approval node config and its describe opened with `[PLANNED — not yet enforced]`: the approval engine parsed it and read nothing, so a category manager who submitted their own admission was step 1's `field` approver and approved it. -- **At the open.** The submitter (the run's trigger user, `sys_approval_request.submitter_id`) is taken off the resolved slate, whatever approver type produced them, by their user id or their account's email, an out-of-office hop that lands on them included. Everyone else stays on it, per_group tags included. A slate this empties takes the node's `onEmptyApprovers` route: the organization's administrators by default, the declared `fallbackApprovers` under `'fallback'`, or the `NO_APPROVERS` refusal under `'fail'`, which now names the exclusion. The fallback slate and the administrators' slate are screened the same way, so an organization whose only administrator is the submitter opens the request on the slate that names no one. -- **On the record.** A request whose slate lost its submitter carries a system note on its timeline naming the route it took (the remaining approvers, the fallback approvers, the administrators, or no one), rendered in the reader's language by the approvals door. The administrators' routing note says the only approver was the submitter instead of "no approver resolved to a person". -- **After the open.** An approval decided by the submitter is refused with `FORBIDDEN`, the privileged override included; their rejection stands. A reassign to the submitter, or to a position only they hold, is refused with `VALIDATION_FAILED`. An SLA escalation that reassigns leaves them off the new slate, and one whose only target is the submitter is not applied: the pending approvers keep the request and are told the SLA lapsed. -- **Spec.** The describe drops the `[PLANNED — not yet enforced]` marker and its last sentence; nothing else in the schema changes. -- **Unchanged.** With the option off or omitted (the default), every slate, timeline, decision, reassign and escalation behaves as before. - -What to do: nothing to migrate. A step that set `excludeSubmitter: true` now enforces it; an `expression` approver or validation rule added by hand to keep the submitter off can be removed. diff --git a/.changeset/25575-modify-all-allow-delete.md b/.changeset/25575-modify-all-allow-delete.md deleted file mode 100644 index b81946104d4..00000000000 --- a/.changeset/25575-modify-all-allow-delete.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor ---- - -feat(spec,lint)!: a permission set can no longer declare `allowDelete: false` beside `modifyAllRecords: true` - -`modifyAllRecords` (Modify All Records) is the write bypass for edit, delete and transfer, as Salesforce's Modify All is. The permission evaluator grants delete on the bypass bit and never reads `allowDelete`, so an object-permission entry declaring `allowDelete: false` beside it parsed, was stored, and let its holder delete every record. The runtime does not change. What changes is what an author can write: - -- `ObjectPermissionSchema` refuses an explicit `allowDelete: false` in an entry that sets `modifyAllRecords: true`, with one `custom` issue at `objects..allowDelete` (the wildcard `*` entry included). The message names the route for "edit every record, delete none". -- An entry that sets `modifyAllRecords: true` and omits `allowDelete` now parses to `allowDelete: true` instead of `false`. That is what the evaluator already granted, and it keeps the parse idempotent: the output of `definePermissionSet` still parses when `defineStack`, a build or a save parses it again. -- `os lint` reports the pair as `security-modify-all-delete-withheld` (error) on a raw config that has not been through the schema. `os explain security-modify-all-delete-withheld` prints the route. -- The response-side `EffectiveObjectPermissionSchema` is unchanged and still accepts the pair. - -"Edit every record, delete none" is a `criteria` sharing rule with `accessLevel: 'edit'` shared with the position that holds the set, beside `allowEdit: true` and `allowDelete: false` with no `modifyAllRecords`. An edit share opens update and never delete (ADR-0111 D3). The object needs an owner field and an owner-write default (`private` or `public_read`), and the rule's `condition` must be a field predicate every row satisfies, such as `record.id != ''`: a literal `true` lowers to an empty filter, which the sharing service never serves (it logs the rule as not served at boot). - -Stored rows and older built artifacts: every earlier parse filled an omitted `allowDelete` with `false`, so the pair also sits where nobody wrote it. The ADR-0087 D2 conversion `permission-modify-all-allow-delete-false-to-true` rewrites it to `true` when a stored row is rehydrated and when a built artifact inside its declared-floor window is loaded. That changes no access decision. It is retired from the load path, so a source that still declares the pair is refused, not rewritten. The D3 entry `permission-modify-all-allow-delete-false-refused` carries the judgment for `os migrate meta`. - -Clause-②: no (narrowing) — the object-permission schema refuses a shape it used to accept (`allowDelete: false` beside `modifyAllRecords: true`). Nothing is widened. - -Metadata-protocol: narrowed (an object-permission entry may no longer declare `allowDelete: false` beside `modifyAllRecords: true`) - -Ruling-ref: 6377890121 (letter A, under 6377842908); retriage 6382861731 - - - -**BREAKING** for an author who declared the pair. - -- **Before.** `{ allowEdit: true, allowDelete: false, modifyAllRecords: true }` parsed, and the holder could delete every record. -- **Now.** The entry is refused at `objects..allowDelete`. -- **Fix.** If the set should delete every record, delete `allowDelete` from the entry. If it should edit records it does not own but delete none, remove `modifyAllRecords` and add a sharing rule: `{ type: 'criteria', name, object, condition: "record.id != ''", accessLevel: 'edit', sharedWith: { type: 'position', value: '' } }`. diff --git a/.changeset/25576-admin-rescue-comment-i18n.md b/.changeset/25576-admin-rescue-comment-i18n.md deleted file mode 100644 index 5501a169484..00000000000 --- a/.changeset/25576-admin-rescue-comment-i18n.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@objectstack/plugin-approvals": patch ---- - -Clause-②: no -Metadata-protocol: none - -The note an `admin_rescue` approval request writes on its own timeline ("Routed to this organization's administrators because the approver slate named no one: …") is now read in the reader's language. The request's node-config snapshot stores the note as a code and its parameters (`__adminRescueNote`: the timeline row it explains, the abandoned approver slots, and why a declared `fallback` did not help), and `listActions` (`GET /api/v1/approvals/requests/:id/actions`) renders that row's `comment` through the plugin's `messages` catalog at the caller's locale, the way the approvals door already localizes a request's flow, step and object labels. The sentence and its clauses ship in `en`, `zh-CN`, `ja-JP` and `es-ES`. - -The stored `comment` is unchanged: it is still the English sentence, which a reader with no locale, an `en` reader, the generic data door and the action-history list views are served as before. Requests opened before this release carry no snapshot entry, so their note keeps showing its stored English; no migration is needed. diff --git a/.changeset/25577-import-applyon-matchkeys.md b/.changeset/25577-import-applyon-matchkeys.md deleted file mode 100644 index 8bbd64ee21a..00000000000 --- a/.changeset/25577-import-applyon-matchkeys.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/rest': minor ---- - -feat(spec,core,rest): an import column can be written only when its row is created (`applyOn`), and `matchFields` takes ordered key sets - -Clause-②: yes (widening) -Metadata-protocol: added - -Two additions to the import contract (`POST /data/:object/import` and the async import job), both honoured by the import runner: - -- **`FieldMappingEntry.applyOn: 'create' | 'always'`** (default `always`, today's behaviour). An inline mapping entry marked `create` is written when the import creates the row and left out when an `upsert` updates an existing one; an `update` import never writes it. One upsert can then be re-run safely: master-data columns refresh, while a column the app has changed since the first run (a status, a flag) keeps its current value. A create-only column is still read for matching, and the key a row was found by stays in that row's update. An `applyOn` value outside `create` / `always` refuses the request with `400 INVALID_REQUEST`. -- **`matchFields` accepts ordered key sets** (`string[][]`) beside one key set (`string[]`, unchanged). Each row is matched by the first set whose fields are all non-blank on it, and only by that set, so one sheet can identify domestic suppliers by `credit_code` and overseas ones by `country` + `registration_no`. A row blank on every set fails with `BLANK_MATCH_KEY` (`ok: false`, `action: 'failed'`) instead of being created or skipped; `skipBlankMatchKey: true` with key sets is refused with `400 INVALID_REQUEST`, as are malformed key sets (an empty set or name, or names mixed with sets). A set that matches more than one record fails the row with `AMBIGUOUS_MATCH`, as before. - -```json -{ - "writeMode": "upsert", - "matchFields": [["credit_code"], ["country", "registration_no"]], - "mapping": [ - { "sourceField": "Code", "targetField": "credit_code" }, - { "sourceField": "Country", "targetField": "country" }, - { "sourceField": "Reg. no.", "targetField": "registration_no" }, - { "sourceField": "Name", "targetField": "name" }, - { "sourceField": "Status", "targetField": "status", "applyOn": "create" } - ] -} -``` - -`@objectstack/spec` also exports `isMatchKeySets(matchFields)`, which tells the two `matchFields` spellings apart. `RunImportOptions` in `@objectstack/core` gains `createOnlyFields` and accepts both `matchFields` spellings. diff --git a/.changeset/25580-flow-required-permissions.md b/.changeset/25580-flow-required-permissions.md deleted file mode 100644 index aec4d4165c0..00000000000 --- a/.changeset/25580-flow-required-permissions.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor -'@objectstack/lint': minor -'@objectstack/platform-objects': minor ---- - -feat(automation): a flow declares who may start it — `requiredPermissions` on the flow, checked at every start door - -A flow can now carry `requiredPermissions: ['capability', …]`, the same value shape as an action's `requiredPermissions` (ADR-0066 D4). Every door that starts a flow by name checks it in one shared check: the trigger route (`POST /automation/:name/trigger`), an action of `type: 'flow'` (REST `POST /actions/:object/:action` and the MCP `run_action` bridge), and a declared endpoint of `type: 'flow'`. A caller that is not the system principal must hold every named capability (granted through a permission set's `systemPermissions`), or the door answers `403 PERMISSION_DENIED` and nothing runs. - -- **The action door inherits the flow's list.** An action that starts a gated flow is gated by the flow even when the action declares nothing, so the app states it once. An action that declares its own list keeps its own gate, which answers first, so a caller at that door needs both lists. -- **A sub-flow is not a door.** A parent flow's `subflow` node starts its child through the engine and is not checked again; the parent's own start was. -- **The elevated self-triggered rule still answers first.** Holding the capabilities does not let a member start a `runAs: 'system'` flow of type `autolaunched`, `record_change` or `schedule`. -- **Studio's flow form** offers the key (a tag list beside `runAs`). -- **`os validate`** judges the key's values the way it judges an action's: a capability no built-in, `defineCapability` or `systemPermissions` grant provides is reported as `capability-reference-unknown` at `flows[i].requiredPermissions` (`os explain capability-reference-unknown`). - -This is how to say who may start an elevated (`runAs: 'system'`) `screen` or `api` flow. An app that gates such a flow with a `decision` node on `current_user.positions` can move that gate to the flow's `requiredPermissions`; the positions array is not an authorization input. A flow that declares no `requiredPermissions` starts exactly as before. - -Clause-②: yes (widening) — a flow may declare a new key, `requiredPermissions`; a flow that does not declare it is admitted at every door exactly as before. - -Metadata-protocol: added (`FlowSchema.requiredPermissions`) diff --git a/.changeset/25598-escalation-dead-target-refused.md b/.changeset/25598-escalation-dead-target-refused.md deleted file mode 100644 index a73b1a53d78..00000000000 --- a/.changeset/25598-escalation-dead-target-refused.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an SLA escalation's `escalateTo` naming a `team:`, `department:`, `org_membership_level:` or `role:` address is refused at registration - -Clause-②: no (narrowing) — `ApprovalEscalationSchema.escalateTo` refuses values it used to accept. Nothing is widened. - -Metadata-protocol: narrowed (an approval node's `escalation.escalateTo` written as a `team:`, `department:`, `org_membership_level:` or `role:` address is refused) - - - -`escalateTo` was a bare string. The approvals service resolves it as a position machine name and falls back to the value as written, and a person acts on a pending slot only under their user id, their email or `position:` for a position they hold. So a `team:`, `department:` or `org_membership_level:` literal, or the `role:` spelling ADR-0090 D3 retired, names nobody under any escalation action: a `reassign` on timeout would hand the request to no one (the runtime now keeps the slate and notifies it instead), and a `notify` reaches no one through it. The schema accepted it and every build door exited 0. - -`ApprovalEscalationSchema` now refuses those four prefixes at `escalateTo`, under every action and with surrounding whitespace trimmed as the runtime trims it. The refusal names the value and the shapes that are accepted. It reaches every door that judges an approval node's config: `FlowSchema.parse` (`defineStack`, `defineFlow`, `objectstack validate` / `compile`, the metadata save door) and `AutomationEngine.registerFlow`. A user id, an email, a position machine name and `position:` stay accepted; whether they resolve is directory state, judged by the runtime on timeout. The D3 entry `approval-escalation-dead-target-refused` carries the judgment for `os migrate meta`; there is no D2 conversion, because only the author knows which position or person was meant. - -**BREAKING** for an author whose approval node escalates to a team, department, membership tier or `role:` address. - -- **Before.** `escalation: { timeoutHours: 24, action: 'reassign', escalateTo: 'team:night_shift' }` parsed, and on timeout the request was handed to no one. -- **Now.** The flow is refused at `nodes.N.config.escalation.escalateTo`, with a message naming the value and what to write instead. -- **Fix.** Name a target someone acts under: `escalateTo: 'night_shift_lead'` (a position its people hold), `'position:night_shift_lead'`, or a person's user id or email. Write `role:` as `position:`. diff --git a/.changeset/25598-sla-reassign-target-resolves.md b/.changeset/25598-sla-reassign-target-resolves.md deleted file mode 100644 index 0632cf75890..00000000000 --- a/.changeset/25598-sla-reassign-target-resolves.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -fix(plugin-approvals): an SLA escalation that reassigns to a target resolving to no one keeps the request on its approvers and notifies them, instead of stranding it on a slate nobody can decide - -Clause-②: no -Metadata-protocol: none - -An approval node's `escalation: { action: 'reassign', escalateTo }` expands `escalateTo` to the holders of that position, and when nobody holds it falls back to the value itself, so a config naming a specific user id keeps working. That fallback is now judged by the same rule the reassign door applies: a user id or email an account carries (and not a user of other organizations only), or `position:` for a position someone in the request's organization holds. - -- **A target that resolves** reassigns exactly as before. -- **A target that does not** (an unstaffed or mistyped position name, a user id with no account, a `team:` / `department:` / `org_membership_level:` / `role:` literal) no longer replaces the slate. The pending approvers are kept and receive `approval.sla_breached`, and the `escalate` audit row records `reassign → — not applied: `. The escalation still fires once. -- **The submitter's escalation notice** now names the action actually taken (`notify` when the reassign was not applied, or when no `escalateTo` was set), not the configured one. -- A directory read that fails during the sweep leaves the request untouched and unmarked, so the next sweep retries it. - -Nothing an author writes is refused that was accepted before. To have an overdue request move, name a staffed position or a person in the request's organization in `escalateTo`. diff --git a/.changeset/25608-retire-package-upgrade.md b/.changeset/25608-retire-package-upgrade.md deleted file mode 100644 index 505fbcceb2c..00000000000 --- a/.changeset/25608-retire-package-upgrade.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -**BREAKING** — the package upgrade model (`kernel/package-upgrade.zod.ts`: upgrade plan, metadata diff, impact level, pre-upgrade snapshot, upgrade and rollback request/response, phase vocabulary), the four `IPackageService` upgrade members and the `plan` member of `PackageUpgradeResponse` leave the public surface (#25608). - -A `major`-class change, recorded as `minor` under the launch-window convention. Maintainer ruling A (`6259332195`, item 3, 「其他同意」): "**Retired:** `packages/spec/src/kernel/package-upgrade.zod.ts` (plan, diff, conflict, snapshot, rollback; zero runtime consumers), with its ADR-0087 conversion." Done directly in 18.x, with no retirement stage; ADR-0049 enforce-or-remove. - -Clause-②: no (narrowing) -Metadata-protocol: narrowed -Ruling-ref: 6259332195 - -**Why.** `@objectstack/spec` declared a complete package upgrade lifecycle — plan the upgrade against a metadata diff, grade its impact, take a pre-upgrade snapshot, three-way merge the customer's customizations, roll back to the snapshot — and nothing on the platform implemented any of it. No provider implements `IPackageService`, so `planUpgrade`, `upgrade`, `rollback` and `getSnapshot` had no body anywhere; no route answers `UpgradePackageRequest` or `RollbackPackageRequest`; and no package, example, app or skill in this repository, in objectui or in hotcrm read any of the names. `UpgradePackageRequest.createSnapshot: true` and `mergeStrategy: 'three-way-merge'` read as protections the platform gave an operator's customizations, and it never did. - -### FROM → TO - -| removed | from | what to write instead | -| --- | --- | --- | -| `UpgradePlanSchema`, `MetadataDiffItemSchema`, `MetadataChangeTypeSchema`, `UpgradeImpactLevelSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no code ever generated an upgrade plan or diffed two package versions. | -| `UpgradeSnapshotSchema`, `RollbackPackageRequestSchema`, `RollbackPackageResponseSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no snapshot was ever taken. The package rollback the platform serves is the commit rollback, `POST /api/v1/packages/:id/rollback` with a `commitId`, answered by `RollbackToPackageCommitResponseSchema` (`@objectstack/spec/api`). | -| `UpgradePackageRequestSchema`, `UpgradePackageResponseSchema`, `UpgradePhaseSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no upgrade ever ran. Installing a new version over an installed package is a re-install: `POST /api/v1/packages` with `overwrite: true` (the SDK's `packages.install(manifest, { overwrite: true })`). | -| `IPackageService.planUpgrade`, `.upgrade`, `.rollback`, `.getSnapshot`, and their inputs `PlanUpgradeInput`, `ExecuteUpgradeInput`, `RollbackInput` | `@objectstack/spec/contracts` | delete the members from your implementation and the imports of the three inputs — the contract no longer carries them. | -| `PackageUpgradeResponse.data.plan` | `@objectstack/spec/api` | nothing — it carried the retired `UpgradePlan`. `PackageUpgradeRequestSchema` and `PackageUpgradeResponseSchema` themselves stay published, bound to no route. | - -**The one-line fix: delete every import of the names above and every read of `PackageUpgradeResponse.data.plan`.** The compiler finds them (`TS2305: Module '"@objectstack/spec/kernel"' has no exported member …`, and `TS2339` for `plan`). No behaviour is lost — nothing ever read or produced these shapes. - -**What stays.** The package model beside it — `ManifestSchema`, `InstalledPackageSchema`, `PackageArtifactSchema`, the install, uninstall, enable/disable and dependency-resolution members of `IPackageService` — is unchanged. The `UpgradePlan.estimatedDuration` tombstone leaves with its def's source; its `RETIRED_KEYS_BY_MAJOR[18]` entry and the D3 entry `kernel-package-lifecycle-durations-unit-in-key` stay as history. - -⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever parsed a retired shape or called a retired member, so every request answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately). - -⚠️ **The out-of-repo consumer population beyond objectui and hotcrm is NOT MEASURED.** Inside this repository the names occurred only in their declarations, their own tests, generated artifacts and prose; objectui (main and the pinned `.objectui-sha`) and hotcrm name none of them; `@objectstack/spec` is published, so readers elsewhere were not measured. - -The ADR-0087 D3 semantic entry `kernel-package-upgrade-family-retired` carries the judgement, and the ten defs are registered in `RETIRED_DEFS_BY_MAJOR[18]`: none of these shapes is a stack collection or a metadata type, so there is no source for a D2 conversion to rewrite and no carrier key for a tombstone. - - diff --git a/.changeset/25609-approval-exclude-submitter.md b/.changeset/25609-approval-exclude-submitter.md deleted file mode 100644 index 111856d8e0b..00000000000 --- a/.changeset/25609-approval-exclude-submitter.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an approval step's `excludeSubmitter` option, default off — declared ahead of the approval engine that honours it - -Clause-②: yes (widening) -Metadata-protocol: added - -An approval node's `config` accepts `excludeSubmitter: boolean`. Each approval step is an approval node, so this is the step-level option: when `true`, the request's submitter (the user whose action started the flow run, recorded on the approval request as `submitter_id`) is to be removed from the slate the node's `approvers` resolve to, whatever approver type produced them, so a manager who submits their own request cannot approve it. If that leaves nobody, the request takes the node's `onEmptyApprovers` route and never passes silently, and the approval record shows that the exclusion changed the route. - -- **Declared, not yet honoured.** This release carries the protocol half only. The approval engine parses the key with the rest of the node config and does not read it yet, so the submitter is NOT removed today. The key's describe opens with `[PLANNED — not yet enforced]` and says so. Keep any guard you rely on (an `expression` approver or a validation rule) until the approval engine honours the key. -- **Off by default, and absent when off.** The key is optional with no default: a node that does not author it parses to exactly the output it did before, so stored approval nodes and the config snapshot each request keeps are unchanged. -- **Step-level only.** It is one switch per node. Written on an approver row, on the flow node beside `config`, or on the flow, it is still refused as an unknown key. diff --git a/.changeset/25610-notify-email-field-recipient.md b/.changeset/25610-notify-email-field-recipient.md deleted file mode 100644 index ec7678fa98c..00000000000 --- a/.changeset/25610-notify-email-field-recipient.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor ---- - -feat(spec): a `notify` recipient form that is the address in an `email`-typed field of the triggering record - -A `notify` node's `recipients` takes a fourth form, as the whole value: `{ emailField: 'applicant_email' }`. It names an `email`-typed field of the record that triggered the flow, and the address that field holds is the recipient — for a person who is not a user, such as an applicant who came in through a public form. The field is named by its machine name, so neither an address written out nor an expression that builds one can parse as this form: a string in `recipients` stays a user id or an audience selector, and an envelope stays a CEL value. - -```ts -{ - type: 'notify', - config: { - recipients: { emailField: 'contact_email' }, - channels: 'email', - template: 'supplier.application_received', - }, -} -``` - -- The node names `email` as its only channel (`channels: 'email'` or `['email']`). An address that no user holds has no inbox, so a node with no `channels`, or one naming any other channel, is refused. -- The form is the whole `recipients` value, never a list entry. -- `@objectstack/lint` refuses, at `os validate`, `os lint`, `os build` and the runtime publish door for a flow, an email-field recipient whose field the trigger object does not declare (`notify-email-field-unknown`), one whose field is not `email`-typed (`notify-email-field-not-email`), and one in a flow that hands its run no record of a known object — a schedule, an inbound api call, a record trigger that names no object (`notify-email-field-no-trigger-record`). A trigger object another package declares is not judged. - -This change is the protocol half. Delivery — resolving the field to an address, the outbound mail configuration, the per-address rate limit and the messaging-log entry — ships in the same release in `@objectstack/service-automation`. - -Clause-②: yes (widening) - -Metadata-protocol: added diff --git a/.changeset/25611-inline-transform-null-sentinel.md b/.changeset/25611-inline-transform-null-sentinel.md deleted file mode 100644 index 3b3d6e79f3b..00000000000 --- a/.changeset/25611-inline-transform-null-sentinel.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -Clause-②: no -Metadata-protocol: none - -`POST /api/v1/data/:object/import`: a cell the request declares null through `nullValues` now stays blank whatever case `transform` its inline mapping entry declares. The blank test reads the raw cell and a blank cell is not transformed, so an `uppercase` or `lowercase` entry no longer rewrites a sentinel such as `n/a` into a stored value (`N/A`): the cell is stored unset, takes the entry's `defaultValue`, or is refused by `required` as blank. A non-blank cell is transformed as before. diff --git a/.changeset/25629-org-logo-zh-label.md b/.changeset/25629-org-logo-zh-label.md deleted file mode 100644 index 9041583b3ce..00000000000 --- a/.changeset/25629-org-logo-zh-label.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): the zh-CN label for an organization's logo reads 「徽标」, no longer 「标识」, which read as the slug (「标识符」) on the same record page and in the Rename Organization dialog - -Clause-②: no -Metadata-protocol: none - -`sys_organization.fields.logo.label` in the zh-CN bundle was 「标识」 ("identifier"), directly beside the slug's 「标识符」. The Rename Organization and Edit Organization dialogs take their `logo` param label from this field label, so a zh-CN admin read the logo URL input as the organization's identifier. ja-JP (「ロゴ」 / 「スラッグ」) and es-ES (「Logotipo」 / 「Slug」) had no such collision and are unchanged. diff --git a/.changeset/25631-dependency-advisories.md b/.changeset/25631-dependency-advisories.md deleted file mode 100644 index b20cbeeb1a5..00000000000 --- a/.changeset/25631-dependency-advisories.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/core': patch -'@objectstack/cli': patch -'@objectstack/runtime': patch -'@objectstack/objectql': patch -'@objectstack/metadata': patch -'@objectstack/metadata-core': patch -'@objectstack/metadata-protocol': patch -'@objectstack/rest': patch -'@objectstack/mcp': patch -'@objectstack/driver-turso': patch -'@objectstack/plugin-email': patch -'@objectstack/service-mailbox': patch ---- - -fix(deps): the declared `zod` floor moves to `^4.6.6`, past GHSA-mrhx-c7xc-m3w5, and `postal-mime` and `nodemailer` move to their patched releases - -Clause-②: no -Metadata-protocol: none - -- **`zod` `^4.6.5` → `^4.6.6`** in every package that depends on it. zod 4.6.5 and earlier can exhaust memory validating an oversized array against an array schema with no length limit ([GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5)). 4.6.6 also changed zod's `v4/core` type declarations, so a project that had already moved its own zod to 4.6.6 or later could get a second, older zod under these packages whose types do not assign to its own; with both ranges at `^4.6.6` the two can resolve to one copy. If your project pins zod below 4.6.6, raise it to `^4.6.6`. -- **`@objectstack/service-mailbox`: `postal-mime` `^4.0.5` → `^4.0.8`.** Decoding a header made of many joined RFC 2047 encoded words took quadratic time, so one crafted message could block the event loop for seconds ([GHSA-f2hq-32vh-53cc](https://github.com/advisories/GHSA-f2hq-32vh-53cc)). -- **`@objectstack/plugin-email`: `nodemailer` `^10.0.2` → `^10.1.4`**, for three moderate advisories fixed in 10.1.4 (GHSA-8c8m-p3m8-cjg8, GHSA-m943-m63m-w62w, GHSA-85x2-q946-cp5h). - -No API, schema or behaviour of these packages changes. The generated spec artifacts (JSON schemas, API surface, declaration map) are identical under zod 4.6.7. diff --git a/.changeset/25634-pinyin-reconcile-off-readiness.md b/.changeset/25634-pinyin-reconcile-off-readiness.md deleted file mode 100644 index 565da793fb3..00000000000 --- a/.changeset/25634-pinyin-reconcile-off-readiness.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-pinyin-search': patch ---- - -fix(plugin-pinyin-search): the boot companion pass no longer holds readiness, and an unchanged restart reads no rows - -Clause-②: yes -Metadata-protocol: none - -Before this change, `PinyinSearchPlugin` awaited a full walk of every row of every object that carries the `__search` companion inside `kernel:bootstrapped`. The kernel awaits that hook before `kernel:listening`, so the HTTP server opened only after the largest such table had been read. Every restart paid the same walk again, even when nothing had changed. In production, 6.7 M `sys_inbox_message` rows took about 4 min 50 s. - -Now: - -- The pass starts at `kernel:bootstrapped` and runs after readiness. Writes made through ObjectQL meanwhile are stamped by the companion hooks, as before. A failure is logged at `warn`. Plugin `destroy()` stops the pass between pages. -- When a walk of an object completes, the pass records it in one `sys_migration` receipt row (id `pinyin-search-companion`, `blocking: 0`, `verified_at: null`). A later boot reads only the rows written since that walk that still have no companion. On an unchanged table that is one query that returns no rows. A changed display field, or a kernel without `sys_migration`, falls back to the full walk. -- `rebuildSearchCompanion()` (`force: true`) still walks every row. Use it after an import that bypasses hooks and back-dates `updated_at`. -- `CompanionBackfillResult` gains `pages`, `CompanionBackfillOptions` gains `signal`, and `PinyinSearchPlugin` gains `whenReconciled()`, which resolves when the boot pass ends. - -What to do: nothing. The first boot after upgrading walks each companion-bearing object once more, after readiness. Later boots read only what changed. diff --git a/.changeset/25635-exclude-submitter-auto-approve.md b/.changeset/25635-exclude-submitter-auto-approve.md deleted file mode 100644 index 21568d23dac..00000000000 --- a/.changeset/25635-exclude-submitter-auto-approve.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an approval node refuses `excludeSubmitter: true` beside `onEmptyApprovers: 'auto_approve'` - -Clause-②: no (narrowing) -Metadata-protocol: narrowed -Ruling-ref: 6386627950 - -An approval node's `excludeSubmitter: true` removes the request's submitter from the slate its `approvers` resolve to. When that leaves nobody, the request takes the node's `onEmptyApprovers` route, and under `'auto_approve'` that route approves the request with no one deciding: a submitter who is the only approver would get their own request approved, the one outcome the option exists to prevent. The approval node config now refuses the pair at the parse, with one issue at `onEmptyApprovers` naming both keys, so `os validate`, `compile`, `defineStack`, the flow build judge and the metadata save door all refuse it before it is deployed. Choose a policy that never passes the request instead: `'admin_rescue'` (the default, which opens the request on the organization's administrators), `'fail'`, or `'fallback'` with a `fallbackApprovers` list naming who decides. - -- **Narrows nothing a released version accepted.** `excludeSubmitter` is new in this same release (its own changeset declares it), and no published `@objectstack/spec` carries it, so no package source or stored approval node written against a released version can hold the pair. -- **Only `true` arms it.** `onEmptyApprovers: 'auto_approve'` beside `excludeSubmitter: false`, or with the key omitted, parses as before and keeps its meaning. Every other policy parses beside `excludeSubmitter: true`. -- **The approval engine is unchanged.** Removing the submitter at run time is still to come; this change makes the contract refuse the one combination that would let an emptied slate pass. - - diff --git a/.changeset/25639-lockstep-exact-internal-deps.md b/.changeset/25639-lockstep-exact-internal-deps.md deleted file mode 100644 index 44a5496495f..00000000000 --- a/.changeset/25639-lockstep-exact-internal-deps.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/hono': patch -'@objectstack/plugin-dev': patch ---- - -Clause-②: no -Metadata-protocol: none - -`@objectstack/cli`, `@objectstack/hono` and `@objectstack/plugin-dev` now depend on the other `@objectstack/*` packages at exactly their own release version, not at a caret range. Twenty-one dependencies were published as `^`: eight of the CLI's (`core`, `driver-memory`, `driver-mongodb`, `driver-sql`, `driver-sqlite-wasm`, `objectql`, `observability`, `runtime`), the Hono adapter's `runtime`, and twelve of `plugin-dev`'s (`account`, `driver-memory`, `objectql`, `plugin-auth`, `plugin-hono-server`, `plugin-security`, `rest`, `runtime`, `service-i18n`, `service-realtime`, `service-storage`, `setup`). - -Every `@objectstack/*` package is released together on one version, so the range bought nothing. What it did was let an app that pins exact `@objectstack/*` versions still install a newer release of these packages, and of everything they depend on, through the CLI, which mixes two releases in one tree. They are now published exact, like the other `@objectstack/*` dependencies of these three packages. - -If you pinned exact versions and added `overrides` to hold these packages at your version, you can remove those overrides once you are on this release. Peer dependencies keep their caret ranges. diff --git a/.changeset/25649-bound-action-schema-parse.md b/.changeset/25649-bound-action-schema-parse.md deleted file mode 100644 index eb200f2c4a8..00000000000 --- a/.changeset/25649-bound-action-schema-parse.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql)!: a code package's action is stored as its `ActionSchema` parse, so `requiresFeature` takes effect on bound actions - -Clause-②: no (narrowing) — `SchemaRegistry.registerItem`, and with it `ObjectQL.registerApp` and every door that registers a code package's items, now refuses with `422 INVALID_METADATA` an `action` body that does not satisfy `ActionSchema`. Before, it registered that body as written. Nothing is widened. - -Metadata-protocol: none (`ActionSchema` is unchanged and no key is added, retired or re-shaped; every body the registry now refuses already fails the same schema at `defineStack`, `os validate` and the metadata save door) - - - -The schema registry now parses every `action` item a code package registers through `ActionSchema` before storing it. The stored item is the parsed body. Before this, an action that reached the registry without a parse was stored as authored. Examples are a programmatic `registerApp`, a nested plugin's manifest, a `strict: false` stack, and a raw plugin manifest. Such an action was then attached to its target object unparsed. Its `requiresFeature` (and `requiresMembershipReach`) was never lowered into `visible`, so the declared gate did nothing. A platform object's own actions were already lowered. - -What the parse does to such an action, as with any parsed action: - -- it lowers `requiresFeature` / `requiresMembershipReach` into `visible`; -- it fills schema defaults; -- it wraps a CEL string shorthand in `{ dialect, source }`. - -An action a strict `defineStack` already parsed is stored unchanged. - -**BREAKING** for a package that registers an action body `ActionSchema` does not accept. - -- **Before.** The registry stored such a body as written, and served it with any declared gate inert. -- **Now.** Registration is refused with `INVALID_METADATA` (422). The error names the action and its package, and `issues[]` gives the path and message of each failure. The registration that carried it fails. -- **Fix.** Correct the action as `issues[]` describes. An action that passes `os validate`, or a strict `defineStack`, is unaffected. diff --git a/.changeset/25650-anonymous-form-file-refusal.md b/.changeset/25650-anonymous-form-file-refusal.md deleted file mode 100644 index 02301ad385c..00000000000 --- a/.changeset/25650-anonymous-form-file-refusal.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -feat(lint)!: `objectstack validate`, `compile` and the publish gate refuse a form open to anonymous intake that declares a file field (#25650) - -Clause-②: no (narrowing: a form view open to anonymous intake whose sections declare a field of a `FILE_REFERENCE_TYPES` type is refused at `objectstack validate`, `compile` and the publish gate) -Metadata-protocol: narrowed -Ruling-ref: ruling C on #25572 (the maintainer, 「25619 C 25495 B」, recorded on PR #25619 with the seat record `6383110676`); triage `6385114404` - -A public form takes no attachment. A form view is open to anonymous intake when its `sharing` declares `enabled: true`, `allowAnonymous: true` and a `publicLink` (`anonymousFormIntakeCandidates`, `@objectstack/spec/ui`, the rule both anonymous doors serve from). Such a form collects the fields its sections declare. A visitor has no route to upload a file, so a declared `file`, `image`, `avatar`, `video` or `audio` field can never be filled. The new authoring rule `validatePublicFormFileFields` (rule id `public-form-file-field`, exported as `PUBLIC_FORM_FILE_FIELD`) refuses it, naming the field and the object. - -**BREAKING — what moves for authors.** - -- FROM: a public form whose `sections[].fields` (or legacy `groups[].fields`) name a file-typed field of the bound object published clean. TO: an error. `objectstack validate`, `compile` and `lint` fail with `public-form-file-field` at the field's entry (`views[0].formViews.apply.sections[1].fields[2]`), and an active `view` write answers `422 INVALID_METADATA` with that issue. The item is not stored. This covers `saveMetaItem` in publish mode (Studio, REST `/meta`, MCP) and the promotion of a draft (`publishMetaItem`). All three form shapes are judged: a view's default `form`, each `formViews` entry, and the flattened `config` of a `viewKind: 'form'` item. A bare field name and a `{ field }` entry are both read. - -**Fix (one line).** Take the file field off the public form. To collect a file from someone outside the organization, have them register and sign in, then fill a form that is not open to anonymous intake and declares the field. There is no guest upload. - -**Unchanged.** - -- The parse. No schema shape moves, and nothing is added to `@objectstack/spec`. A stored view carrying such a form keeps loading and serving until it is next saved, and that save is judged. Saving as a draft is still allowed; publishing the draft is judged. -- A form that is not open to anonymous intake (either switch off, or no `publicLink`) is not judged, whatever fields it declares. -- An `object` write is not judged by this rule: its snapshot carries no views. Changing a field's type to a file type under a published public form is caught by `objectstack validate` and `compile`, and by that view's next publish. -- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write. -- Measured before crossing: 0 producers declare such a form. `examples/app-crm` (through the built `@objectstack/lint`: 0 findings, 1 for a planted file field on its public form), `app-todo` and `app-multi-package` (0 public forms); `app-showcase`'s one public form, read in source (`inquiry.view.ts`, fields `name`, `email`, `company`, `message`: text and email); `@objectstack/platform-objects` (0 public forms); HotCRM at `a4b3560`, whose two public forms (`src/sales/views/lead.view.ts`, `src/service/views/case.view.ts`) declare text, email, url, select, currency, number and markdown fields only. - - diff --git a/.changeset/25652-translation-positions-permissions.md b/.changeset/25652-translation-positions-permissions.md deleted file mode 100644 index 067b0f679c9..00000000000 --- a/.changeset/25652-translation-positions-permissions.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/cli': minor -'@objectstack/lint': minor ---- - -feat(spec,cli): positions and permission sets are translatable — `positions.` and `permissions.` in the translation bundle - -Clause-②: yes (widening) -Metadata-protocol: added - -An app authored in one language ships its positions (`positions[].label`) and permission sets (`permissions[].label`) in that language. Before this change the translation bundle had no key for them: a bundle carrying `positions` was refused by `os validate` as an unrecognized key, so those labels stayed in the authoring language next to translated object, app and flow labels, and `os i18n check` could not report the gap. - -- **`@objectstack/spec`:** `TranslationData` (and the registered `translation` item and the platform bundle, which share its groups) gains `positions` and `permissions`, keyed by the position's and the permission set's `name`, each carrying `label` and `description` (strict: any other key is refused — `name` or `title` with a message pointing at `label`, and the group spellings `position`, `permission` and `permissionSets` with a message pointing at `positions` / `permissions`). Two new translators, `translatePosition` and `translatePermissionSet`, overlay them, and both are registered in the metadata-document dispatch, so `GET /meta/position` and `GET /meta/permission` — the lists the console's position and permission-set pickers draw — are served in the request's locale. A key the bundle does not carry keeps the authored string. -- **`@objectstack/lint`:** `translation-target-unknown` reports a `positions.` or `permissions.` key that names no position or permission set a package of the stack declares (the audience anchors `everyone` and `guest` always resolve), as an error with the nearest declared name. -- **`@objectstack/cli`:** `os i18n extract` scaffolds `positions..label` / `.description` and `permissions..label` / `.description` from the stack's `positions[]` and `permissions[]`, and `os i18n check` / `os lint` coverage report an untranslated one in its own bucket (`i18n/missing-position`, `i18n/missing-permission`). - -Example (`en` bundle of a Chinese-authored app): - -```ts -positions: { srm_legal_counsel: { label: 'Legal Counsel' } }, -permissions: { srm_contract_reviewer: { label: 'Contract Review', description: 'Reads and approves supplier contracts' } }, -``` diff --git a/.changeset/25653-app-automation-identity-write-refusal.md b/.changeset/25653-app-automation-identity-write-refusal.md deleted file mode 100644 index c302f915f48..00000000000 --- a/.changeset/25653-app-automation-identity-write-refusal.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/objectql': patch -'@objectstack/runtime': patch -'@objectstack/core': patch ---- - -fix(security): a hook that runs as system and an action body are refused writes to position holdings, permission-set grants, invitations and memberships, as a system flow already is - -Clause-②: no (narrowing: app-authored hook and action-script writes to the four identity objects are refused at run time, as #25251 did for flows; the seat runs the contract review before the queue) -Metadata-protocol: none - - - -**BREAKING** (a served-set narrowing) for app-authored automation that writes `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member` under system elevation. A census of the hooks and action bodies in this repository and in HotCRM found none that does. - -A `runAs: 'system'` flow has been refused these writes since 18.1. Two other kinds of app-authored automation run with the same elevation and were not: a hook that declares `runAs: 'system'`, and an action body (`type: 'script'`), which always runs elevated. Their writes skipped the delegated-administration gate (so a grant carried no `granted_by`) and the invitation door's placement and role-cap checks. Both are now refused the same way: - -- An insert, update, delete, transfer, restore or purge of one of the four objects, from a `runAs: 'system'` hook (a sandboxed body or an in-process handler) or from an action body (through `ctx.api` or `ctx.engine`), answers `403 PERMISSION_DENIED`. Nothing is written. A hook's refusal aborts the triggering write under the default `onError: 'abort'`. -- The message names the source and the fix. For position holdings and permission-set grants: set the hook's `runAs` to `'user'`, so the delegated-administration gate judges the user the hook acts for and records them as `granted_by`. An action body has no `runAs`, so its grant moves to a `runAs: 'user'` flow, which an action of type `flow` runs as the invoking user. For invitations and memberships: use the organization endpoints. - -Unchanged: reads of the four objects, and writes to every other object, from the same hook or body. A hook under `runAs: 'user'`, which the ordinary gates judge as its user. The platform's own writers (better-auth and its reconciliation, invitation placement, the seeders, any `sudo()`), which carry no app-automation mark. A system flow's refusal, word for word. - -**What changes for you.** A hook or action body that grants a position or a permission set under elevation now fails. Declare `runAs: 'user'` on the hook, or move an action body's grant into a `runAs: 'user'` flow. Invitations and memberships change through `POST /api/v1/auth/organization/invite-member`, `cancel-invitation`, `update-member-role` and `remove-member`. - -`@objectstack/core` exports the mark: `APP_AUTOMATION_ELEVATION`, `markAppAutomationElevation`, `appAutomationElevationOf` and `carryAppAutomationElevation`. diff --git a/.changeset/25653-app-hook-inherited-elevation-identity-write-refusal.md b/.changeset/25653-app-hook-inherited-elevation-identity-write-refusal.md deleted file mode 100644 index 3264dbf2e47..00000000000 --- a/.changeset/25653-app-hook-inherited-elevation-identity-write-refusal.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/plugin-security': patch ---- - -fix(security): an app-authored hook that inherits a system context, its `ctx.api.sudo()`, and an action it reaches through `execute()` are refused writes to position holdings, permission-set grants, invitations and memberships - -Clause-②: no (narrowing: an app-authored hook's inherited or sudo() elevation, and an action reached through ObjectRepository.execute(), are refused the four identity objects at run time, as a runAs: 'system' hook already is) -Metadata-protocol: none - - - -**BREAKING** (a served-set narrowing) for an app-authored hook that writes `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member` while elevated. A census of the hooks in this repository and in HotCRM found none that does. - -Since the previous release, a hook that declares `runAs: 'system'` and an action body are refused these writes. Three other ways app-authored code ran elevated were not: - -- a hook that declares no `runAs` and fires on a write made under a system context, which it inherits; -- a hook's in-process `ctx.api.sudo()`; -- an action reached through `ctx.api.object(name).execute(action)`. - -Each is now refused the same way: `403 PERMISSION_DENIED`, nothing written, and under the default `onError: 'abort'` the triggering write aborts. The message names the hook and the fix: set its `runAs` to `'user'` and leave `ctx.api.sudo()` out, or use the organization endpoints for invitations and memberships. - -A hook is app-authored when it is bound from metadata through the hook binder (the same hooks the `skipAutomations` opt-out skips). A hook a plugin registers in code with `engine.registerHook`, and platform code's own `sudo()`, keep writing these objects. Reads of the four objects, and writes to every other object, are unchanged. diff --git a/.changeset/25653-hook-action-body-identity-assignment-write-refused.md b/.changeset/25653-hook-action-body-identity-assignment-write-refused.md deleted file mode 100644 index 65ba18ca08f..00000000000 --- a/.changeset/25653-hook-action-body-identity-assignment-write-refused.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a `runAs: 'system'` hook or a `script` action whose sandboxed body writes an identity-assignment object is refused at save - -App-authored automation does not use elevation to write `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member`. The data middleware already refuses that write at run time with `PERMISSION_DENIED` (403), from a system flow run, a hook that runs as system and an action body. Until now every authoring door still accepted a hook or action whose body says so in writing, and the author learned otherwise only from a failed write. The parse now refuses it too: - -- `HookSchema` refuses a hook that declares `runAs: 'system'` and carries a sandboxed JS body (`language: 'js'`) whose `source` calls `insert`, `update`, `delete`, `updateMany`, `deleteMany` or `upsert` on `ctx.api.object('')` for one of the four objects, on the handle or on a name the body bound it to. -- `ActionSchema` refuses a `type: 'script'` action whose sandboxed JS body does the same. An action body always runs as system. -- One `custom` issue per object, at `body.source`, naming the object and the route: `runAs: 'user'` (a hook) or a `runAs: 'user'` flow (an action) for position holdings and permission-set grants, so the ADR-0090 D12 delegated-administration gate judges the user; the organization endpoints for invitations and memberships. It surfaces at `os validate`, at `defineStack` (`422 STACK_SCHEMA_INVALID`), at an artifact's parse and at the metadata save door (`422 INVALID_METADATA`). -- `Hook.runAs` now says that elevation does not reach identity assignment, as `FlowSchema.runAs` does. - -Unchanged: reads of the four objects, writes to every other object, a hook under `runAs: 'user'` or `'inherit'`, an expression (L1) body, a code `handler`, and the run-time refusal, which stays the enforcement for every elevated write. - -Clause-②: no (narrowing) — `HookSchema` and `ActionSchema` refuse a body they used to accept. Nothing is widened. - -Metadata-protocol: narrowed (a `runAs: 'system'` hook or a `script` action may no longer carry a sandboxed body that writes an identity-assignment object) - -Ruling-ref: 6384657105 (letter A); retriage 6431012701 - - - -**BREAKING** for an author whose system hook or action body writes one of the four objects. Such a write was already refused when the body ran. - -- **Before.** The hook or action saved, and its write failed at run time with `PERMISSION_DENIED`. -- **Now.** The hook or action is refused at save, at `body.source`. -- **Fix.** For a position holding or permission-set grant, set the hook's `runAs` to `'user'`, or move an action's grant to a flow whose `runAs` is `'user'` (an action of `type: 'flow'` runs it as the invoking user). For an invitation or membership, call `POST /api/v1/auth/organization/invite-member`, `cancel-invitation`, `update-member-role` or `remove-member`. diff --git a/.changeset/25666-skills-bootstrap-exact-pins.md b/.changeset/25666-skills-bootstrap-exact-pins.md deleted file mode 100644 index 07363593597..00000000000 --- a/.changeset/25666-skills-bootstrap-exact-pins.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the objectstack-platform bootstrap example pins every `@objectstack/*` dependency to one exact version - -Clause-②: no - -The 〈Complete Working Example〉 `package.json` in `objectstack-platform/references/bootstrap.md` gave `^17.0.0` for its five `@objectstack/*` packages, a major behind and a caret range the v18 docs advise against. It now gives `18.1.0` for each, the exact form `create-objectstack` writes, with all of them on one version. diff --git a/.changeset/25668-claim-by-row-stored-expression.md b/.changeset/25668-claim-by-row-stored-expression.md deleted file mode 100644 index c2e400329a4..00000000000 --- a/.changeset/25668-claim-by-row-stored-expression.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/organizations': patch ---- - -fix(plugin-security,organizations): the seed-ownership claims re-own row by row when a stored expression reads `owner_id` - -Clause-②: no -Metadata-protocol: none - -- **Before.** Both seed-ownership claims (`claimSeedOwnership` in `@objectstack/plugin-security`, `claimOrgSeedOwnership` in `@objectstack/organizations`) re-owned seeded rows with one `multi: true` predicate update. A stored computed field (a non-formula field with an `expression`) that reads `owner_id`, directly (`record.owner_id`) or one hop through it (`record.owner_id.name`), computes a different value on each row once the owner is set. One predicate update can store only one value, so the engine refused the write, and every seeded row of that object stayed ownerless. The boot logged only `claimSeedOwnership failed for …`, and the next pass sent the same write. -- **Now.** An object whose stored expressions read `owner_id`, directly, one hop through it, or through another stored field that does, is claimed with one write per row. Each write keeps the claim's own terms (`owner_id IS NULL` or the seed identity, and the organization in the multi-tenant claim) plus the row's id, so each row's stored value is computed from that row, and a row someone owned in the meantime is not taken from them. Reads come from the same member reader the engine uses (`analyzeRelationshipTraversals` in `@objectstack/formula`). An expression whose reads cannot be named also takes the row-by-row path. -- **Unchanged.** Every other object keeps the predicate write, and its paging. The engine's refusal of a predicate update that would store different values stays as it is. A row whose own write is refused stays ownerless and is reported with the claim's existing warning, and the object's other rows are still claimed. -- `@objectstack/organizations` now depends on `@objectstack/formula`. - -What to do: nothing. For an app whose stored expression reads `owner_id`, such as a title that includes the owner's name, the single-tenant claim hands the seeded rows to the platform admin at the next seed settle (`app:seeded`), on the next boot. The organization claim covers organizations whose owner is bound from this release on. It runs in the call that replays their seed. diff --git a/.changeset/25671-skills-protocol-caret.md b/.changeset/25671-skills-protocol-caret.md deleted file mode 100644 index 48bf74f9762..00000000000 --- a/.changeset/25671-skills-protocol-caret.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the objectstack-platform stack-config example declares `engines: { protocol: '^18' }`, the major the 18.x boot handshake accepts - -Clause-②: no -Metadata-protocol: none - -`objectstack-platform/SKILL.md` taught `engines: { protocol: '^17' }` in its `defineStack` manifest example while `PROTOCOL_VERSION` is `18.0.0`, so a config copied from the skill was refused at boot (`OS_PROTOCOL_INCOMPATIBLE`). The example, the eval that pins it (`evals/config-plugins-ops.json`, prompt and expected output together) and the `objectstack-upgrade` sentence describing what the scaffold writes now all say `'^18'`, matching `create-objectstack` and every example app. The `'^17'` → `--from 17` worked example in `objectstack-upgrade` is unchanged. diff --git a/.changeset/25672-search-readable-fields.md b/.changeset/25672-search-readable-fields.md deleted file mode 100644 index 137cb0bc191..00000000000 --- a/.changeset/25672-search-readable-fields.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a free-text `search` resolves only over the fields the caller may query - -Clause-②: yes (widening) -Metadata-protocol: none - -The server-resolved search set used to include fields the caller cannot read, so the field-read predicate guard refused the whole expanded `$or` with 403. On `sys_user`, `role`, `ban_reason` and `last_login_ip` broke every user and owner picker for non-admin members as soon as they typed. The engine now asks the registered security service's `getQueryableFields` (the complement of what the guard refuses) and withholds every other field from the resolved set, on `find`, `findOne`, `count` and `aggregate`. The guard is unchanged: an explicit `filter`, or a `searchFields` naming a hidden field, is still refused with 403. A search whose declared fields are all hidden from the caller answers the existing `400 INVALID_FIELD` empty-search-set refusal, never every row. diff --git a/.changeset/25675-api-flow-secret-ref.md b/.changeset/25675-api-flow-secret-ref.md deleted file mode 100644 index a6361beff59..00000000000 --- a/.changeset/25675-api-flow-secret-ref.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor -'@objectstack/service-automation': minor -'@objectstack/trigger-api': minor ---- - -feat(spec): an `api` flow's start node may name its inbound hook secret (`config.secretRef`) instead of carrying it - -An app package can now ship an inbound `api` flow without publishing its credential. The start node names the secret, the deployment fills that name, and the engine reads it at every post: - -```ts -{ - id: 'start', - type: 'start', - config: { triggerType: 'api', hookId: 'heartbeat', secretRef: 'SRM_HEARTBEAT_SECRET' }, -} -``` - -- The name resolves through the automation plugin's `credentialResolver`, the one a declarative connector's `auth.credentialRef` already uses: in the open tier it is the environment variable of that name (`process.env.SRM_HEARTBEAT_SECRET`); a host that wires a secrets service serves it from there. -- It is read per post, never cached, so a rotation applies to the next post. A post that arrives while the name has no value is refused `503 SERVICE_UNAVAILABLE` and is never verified; the server log names the flow and the reference, never a value. -- `FlowSchema` refuses a reference that is not a name (a letter or `_`, then letters, digits and `_`), one in the platform's `OS_` namespace (whoever signs the posts holds the hook secret, so a platform secret named there would be handed out with it), one beside a literal `secret`, and one on a flow that binds no inbound `api` trigger. -- `os validate` (`flow-api-trigger-secret-missing`) and the engine's registration check accept a usable reference in place of a literal, and the remedy they print now names `config.secretRef`. -- The reference is a name, not a credential: a flow read serves it as written. Saving a flow through the metadata API with a reference removes any hook secret the write-only flow credential store held for that start node. -- New exports from `@objectstack/spec/automation`: `ApiTriggerSecretRefSchema`, `apiTriggerSecretRefOf`, `apiTriggerSecretRefRefusals`, `API_TRIGGER_SECRET_REF_KEY`, `API_TRIGGER_SECRET_REF_RESERVED_PREFIX`. `AutomationEngineOptions` gains `credentialResolver`. - -A literal `config.secret` still works. To move a flow off one, replace `secret: ''` with `secretRef: 'YOUR_NAME'` and set `YOUR_NAME` in the deployment's environment. - -Clause-②: yes (widening) - -Metadata-protocol: added - -Ruling-ref: 6386630545 diff --git a/.changeset/25675-skills-secret-ref-row.md b/.changeset/25675-skills-secret-ref-row.md deleted file mode 100644 index 893a889cf7a..00000000000 --- a/.changeset/25675-skills-secret-ref-row.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill's inbound-hook table names `config.secretRef`, the form a package ships instead of a literal secret - -Clause-②: no - -Metadata-protocol: none - -The `objectstack-automation` skill's start-node table said an `api` flow needs a non-blank literal `secret`, or the flow is refused at registration. An `api` flow's start node may now name its secret instead, with `secretRef: 'NAME'`: the deployment fills that name (an environment variable in the open tier, never `OS_*`), and the engine reads it at every post, answering `503` while the name is unfilled. The row now says either one is required, and that a package ships `secretRef` and never commits a literal. The signature line names the `x-objectstack-signature` header the row used to carry. diff --git a/.changeset/25680-action-copy-refusal.md b/.changeset/25680-action-copy-refusal.md deleted file mode 100644 index faaa8223a4b..00000000000 --- a/.changeset/25680-action-copy-refusal.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/spec': minor ---- - -feat(lint)!: `objectstack validate`, `compile` and the publish gate refuse an action's `confirmText` / `successMessage` / `errorMessage` that is blank, names an unbound root, carries a single-brace token, holds logic in a hole, or carries a `${…}` token the runner shows raw (#25680) - -Clause-②: no (narrowing: an action's copy sentence that is blank, holes an unbound root, carries a single-brace or raw-shown `${…}` token, or holds logic in a hole is refused at `objectstack validate`, `compile` and the publish gate; it ships now under the amended 「甲」, #6015 `6303865025`, as #25205 did for the bulk slot) -Metadata-protocol: narrowed -Ruling-ref: `6365736632` (on #24814, the refusal half); triage `6386348015` - -An action's person-facing copy is an ADR-0032 `template` slot over the roots the console's runner binds (#24814): `confirmText` over `record` (and `params` beside static values), `successMessage` over `result` and `record`, `errorMessage` over none. That covers a page action row (`action:button`, `action:icon`, an `action:group` / `action:menu` member), a page component's inline `properties.action`, and a registered action (`stack.actions[]`, an object's `actions[]`). `validateStackExpressions`, the rule all three doors run, judges each sentence through `closedScopeTemplateRefusal`, then compiles it as a template. Its findings were warnings. They are now errors. This supersedes the warning-only behaviour #24814 described earlier in this release line; a refused finding no longer ends with "A warning, not a refusal". - -**BREAKING — what moves for consumers.** - -- FROM: a warning at `objectstack validate` and `compile`, and an entry on the publish gate's `advisories` channel while the action, object or page saved. TO: an error. Both commands fail, and an active `action`, `object` or `page` write answers `422 INVALID_METADATA` with an `expression-invalid` issue located at the key (`page 'P'.regions[0].components[1] (action:button) · confirmText`, `stack · action 'A' successMessage`, `… successMessage.zh-CN` for one value of a locale map). The item is not stored. This covers `saveMetaItem` in publish mode (Studio, REST `/meta`, MCP) and the promotion of a draft (`publishMetaItem`). -- The refused texts: a blank sentence (`''`, `' '`); a hole over a root the key does not bind (`{{ result.name }}` in a `confirmText`, any hole in an `errorMessage`); a single-brace `{x}` token; a hole holding logic or naming an unknown formatter (`{{ record.count * 2 }}`, `{{ result.email | shout }}`); and a `${…}` token the runner shows as written — one in an `errorMessage`, one in a sentence that also has a `{{ }}` hole or a `{x}` token, one over `params` in a `confirmText`, or one over any root but `result` in a `successMessage`. - -**Fix (one line).** Write the hole the finding prescribes (`Delete ${record.name}?` → `Delete {{ record.name }}?`), name a root the key binds, double a single-brace token, move logic out of the hole, or remove a blank key so the runner's default shows. - -**The holeless `${…}` sentence too.** `${record.*}` in a `confirmText` and `${result.*}` in a `successMessage`, in a sentence with no `{{ }}` hole and no other token, is refused as well: the console's runner shows it as written. Its own entry in this release says so (#25709). - -**Unchanged.** - -- The parse. Each copy key accepts what it accepted, a blank string included (the slot's blank-string arm stays). The boot load and the read path judge a stored row with this parse, so a stored page or action carrying a refused sentence keeps loading and serving until it is next saved, and that save is judged. Saving as a draft is still allowed; publishing the draft is judged. -- `ActionSchema.errorMessage` and `outcomeMessages` are not in this family and draw nothing. -- A sentence with no finding draws nothing at any door, in any form (the bare string, the `{ dialect: 'template', source }` envelope, an inline locale map). -- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write. -- No schema shape moves, and `@objectstack/spec` adds no export. The three copy describes now say the texts are refused. -- Measured before crossing, through the built `@objectstack/lint` at `6021b5e837`: 0 in-repo producers carry such a sentence. `examples/app-crm`, `app-showcase` (23 registered `successMessage`s, 29 pages), `app-todo` (2 `confirmText`, 16 `successMessage`) and `app-multi-package`, `@objectstack/platform-objects` (12 `confirmText`, 48 `successMessage`) and `@objectstack/plugin-approvals` (9 `successMessage`) draw 0 findings, against 1 for a planted unbound hole. HotCRM at `a4b3560` declares 1 action `confirmText` and 19 action `successMessage`s; judged through the same built pass, all 20 draw 0 findings, against an error for a planted `{count}`. - - diff --git a/.changeset/25681-repeater-untyped-json.md b/.changeset/25681-repeater-untyped-json.md deleted file mode 100644 index ddff5400ff0..00000000000 --- a/.changeset/25681-repeater-untyped-json.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor ---- - -feat(spec,lint): object-level `composite` / `repeater` are untyped JSON — a typed list is a child object - -Clause-②: yes (widening) -Metadata-protocol: semantic -Ruling-ref: 6386744473 - -On an object field, `composite` and `repeater` are untyped JSON containers of CMS lineage (a Strapi component, an ACF group or repeater). The `FieldType` comments used to promise "declared sub-fields", but `FieldSchema` has no member that declares them, so nothing gave the inner keys labels, types, options, required flags or translations, and nothing could validate or address one row. The comments and the field-types docs now say so, and point a list that needs typed columns to a child object: a `master_detail` field pointing back at the parent, with `inlineEdit: 'grid'` to edit the rows inline on the parent's form. - -The strict field schema already refused `fields`, `subFields` and `columns` on a field, but named only the key. The refusal now names the route: a typed list is a child object (`columns` also names the live `lookupColumns`, `relatedListColumns` and `inlineColumns`). This changes the message, not what parses. - -New `@objectstack/lint` rule `field/embedded-sub-fields` (error): a `composite` / `repeater` object field carrying `fields`, `subFields` or `columns`. It fires on the doors that never parse (`os lint` on a raw object-literal config, `defineStack(x, { strict: false })`, the metadata-generation rubric); every parsed door refuses the field first. `os explain field/embedded-sub-fields` prints the reasoning. A form view's `fields` sub-rows and the Studio metadata forms are not judged. - -What to do: nothing for a field that declares no sub-field key — the type is unchanged. A field that carried one never parsed under `defineStack`; model the list as a child object. diff --git a/.changeset/25681-skills-repeater-untyped.md b/.changeset/25681-skills-repeater-untyped.md deleted file mode 100644 index dda4366e972..00000000000 --- a/.changeset/25681-skills-repeater-untyped.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): object-level `composite` / `repeater` are untyped JSON — a typed list is a child object - -Clause-②: no -Metadata-protocol: none - -The `objectstack-data` field-types table said a `composite` carries "declared sub-fields". An object field has no member that declares them: `fields`, `subFields` and `columns` are refused on the field. The two rows now say the types are untyped JSON and that a list needing typed columns is a child object with a `master_detail` field (`inlineEdit: 'grid'` edits it inline on the parent's form). - -What to do: nothing. No key, value or behaviour changes. diff --git a/.changeset/25683-inline-help-default-locale.md b/.changeset/25683-inline-help-default-locale.md deleted file mode 100644 index 3851ca432f8..00000000000 --- a/.changeset/25683-inline-help-default-locale.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os i18n check` reads a field's `inlineHelpText` as the default-locale text of its `help` key - -Clause-②: no -Metadata-protocol: none - -The field help key (`objects.OBJECT.fields.FIELD.help`) took its inline source from `help` then `description` only, while `FieldSchema` spells help text `inlineHelpText`. An app that authored `inlineHelpText` and translated `help` into one locale had its default locale reported as missing a translation. The inline source is now `help`, then `inlineHelpText`, then `description`, like an authored label; a non-default locale with no `help` is still reported as a gap. diff --git a/.changeset/25692-file-reference-claim-read-verdict.md b/.changeset/25692-file-reference-claim-read-verdict.md deleted file mode 100644 index 060682ab9c1..00000000000 --- a/.changeset/25692-file-reference-claim-read-verdict.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(storage)!: a record write claims or copies only a file its caller may read - -The file-reference claim path (ADR-0104 D3: a file field holds a `sys_file` id, at most one record's field owns a file, and writing an id another record owns copies the bytes into a fresh file the writing record owns) now asks, before it copies or claims a referenced file, whether the write's caller may read that file. The question is the download door's own verdict, asked under the write's caller context: the uploader, a reader of the one record whose field owns the file (its object's `fileAccessDelegate` where one is declared), a member of the organization for a file no record owns, a reader of a parent record for an attachments-scope file, anyone for a `public_read` file. The door and the claim path now call one judgement, so one file has one access rule. - -A file the caller may not read is answered as the organization-scoped lookup answers a file it cannot see: the value is kept as written, nothing is claimed or copied, and constraint verdicts no longer run ahead of the read check. A copy this write made is claimed by the same write without a second question. System writes and context-less programmatic calls have no caller to ask and reference files as before; so does a kernel with no `auth` service, where the download door binds no gate. An authorization-store outage fails the write with its declared envelope instead of reading as either answer. - -Clause-②: no (narrowing) — a record write reaches fewer files: a file its caller may not read is no longer copied or claimed for it. Nothing that was refused becomes accepted. - -Metadata-protocol: none (a write path's file bookkeeping; nothing a metadata author writes changes) - - - -**BREAKING** for a write that referenced a file its caller may not read, shipped as `patch` because the metadata protocol does not move. - -- **Before.** The claim path copied or claimed a referenced file without asking whether the write's caller may read it. -- **Now.** Both happen only when the caller may read the file under the download door's rule. Otherwise the record keeps the id as written and owns nothing: the file's readers are unchanged, and the written record's readers gain no access to it through that record. -- **Fix.** Reference a file as a caller who may read it (its uploader, or a reader of the record that owns it), or upload the file again. A server-side process that copies files between records on purpose runs as a system write. diff --git a/.changeset/25693-unclaimed-file-organization-scope.md b/.changeset/25693-unclaimed-file-organization-scope.md deleted file mode 100644 index 9edeac8b840..00000000000 --- a/.changeset/25693-unclaimed-file-organization-scope.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(storage)!: an unclaimed file is served only within its organization - -The two download doors (`GET /api/v1/storage/files/:fileId/url` and `GET /api/v1/storage/files/:fileId`) now serve a file with neither an attachments scope nor a field owner (an upload no record has claimed yet, or one whose record let it go) only within its organization. The signed-in check comes first, as before. The download authorizer then judges the file as every other organization-scoped row is read: the organization wall's verdict for the tenancy posture in force, and the driver's tenant scope for the reach. A caller outside that reach is refused `403 FILE_DOWNLOAD_DENIED`, the status and code the doors already give a file the caller may not read, with a message that names no record. No error code is added. - -What does not change: the uploader is still served their own upload, as for every other private file. Under the `single` posture every signed-in caller is served as before. A file marked `acl: 'public_read'` stays anonymous. Attachments-scope and field-owned files keep their parent-derived verdicts. An anonymous caller still gets `401 AUTH_REQUIRED`. A kernel with no auth service still serves these files without a check. - -Clause-②: no (narrowing) — the download doors serve an unclaimed file to fewer callers under a walled posture (`isolated`, `group`); nothing is widened. - -Metadata-protocol: none (how two storage download doors authorize a request; nothing a metadata author writes changes) - - - -**BREAKING** for a walled deployment (`isolated` or `group` posture) whose users download unclaimed files from outside the file's organization, shipped as `patch` because the metadata protocol does not move. - -- **Before.** The download doors did not apply the organization wall to an unclaimed file. -- **Now.** A signed-in caller is served an unclaimed file when they uploaded it, or when the file is in their organization's reach under the posture in force (their active organization under `isolated`, any organization they belong to under `group`). Anyone else gets `403 FILE_DOWNLOAD_DENIED`. A caller with no organization under a walled posture is in no organization's reach. -- **Fix.** Download the file while acting in its organization. Have a record claim it (a file field or an attachment), so that the record's access decides. Or, for a file that must be readable by everyone, mark its stored file record `acl: 'public_read'`. diff --git a/.changeset/25694-anonymous-write-carries-no-file.md b/.changeset/25694-anonymous-write-carries-no-file.md deleted file mode 100644 index 536812b0740..00000000000 --- a/.changeset/25694-anonymous-write-carries-no-file.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(storage)!: an anonymous write carries no file - -Ruling C ("an anonymous submission carries no file") now holds at every anonymous write door of ADR-0138 D2, not only at the public form door. A record write whose caller has no user, such as the guest an `authRequired: false` endpoint executes as under the grants a deployment binds to the guest, may not put a value in a file field (`file`, `image`, `avatar`, `video`, `audio`). It is refused before anything is written, with the form door's envelope: `400 VALIDATION_FAILED`, one `fields[]` entry per refused field carrying the ADR-0114 field code `invalid_value` and the field's label. An empty or absent file field is written as before. Signed-in callers, system writes and context-less programmatic writes are unchanged. No error code is added. - -Clause-②: no (narrowing) — an anonymous write that carried a value in a file field is refused. Nothing that was refused becomes accepted. - -Metadata-protocol: none (a write path's file rule; nothing a metadata author writes changes) - - - -**BREAKING** for an anonymous write that carried a file value, shipped as `patch` because the metadata protocol does not move. - -- **Before.** Only the public form door refused a file value from an anonymous caller. -- **Now.** Every write whose caller has no user is refused with `400 VALIDATION_FAILED` when a file field carries a value. -- **Fix.** Leave file fields empty in an anonymous write. A file is provided by a signed-in caller, through the authenticated upload doors. diff --git a/.changeset/25696-notify-recipients-describe.md b/.changeset/25696-notify-recipients-describe.md deleted file mode 100644 index 3e6ce36e3df..00000000000 --- a/.changeset/25696-notify-recipients-describe.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -docs(spec): a `notify` node's `recipients` names an email address written out as an author constant, not an audience selector, and states the computed-address and email-field behaviour the runtime makes - -Clause-②: no -Metadata-protocol: semantic -Ruling-ref: 6386629237 - -The `recipients` text of a `notify` node config now says what an email address in `recipients` means, and what the notify node does with one. That text is the field's `describe()` (which the reference docs and the JSON Schema read), the union's description, and the sentence a refused `recipients` value leads with. - -- **Written out as a string, it is an author constant.** `recipients: 'ops@example.com'` is no audience selector. It is sent as written: to the user who holds that address, or else, on the `email` channel, to the address itself. Nothing about it changes; the text no longer calls it an audience selector. The audience selectors are the prefixed forms, such as `team:ID`. -- **An address a CEL envelope evaluates to is not one.** In a node whose `channels` include `email`, a computed recipient that reads as an address, bare or as `user:ADDRESS`, must belong to a user. One a user holds resolves to that user. One that no user holds fails the node, and the message points to `{ emailField }`. A node that does not name `email` (`inbox` alone, the default) mails nobody, so its computed recipients go to the messaging service as before. The screen is scoped to nodes that include the `email` channel (confirmed by the maintainer). -- **`{ emailField }` is delivered.** The text no longer says delivery is not live. The address is read from the stored trigger record when the node runs. It must be one plain ASCII mailbox, or the node fails and nothing is sent. One address receives at most 5 such emails per hour; the limit is best-effort. - -No key, shape, default or accepted value changes. Everything that parsed still parses, and authored metadata needs no change. diff --git a/.changeset/25709-action-copy-holeless-dollar-refusal.md b/.changeset/25709-action-copy-holeless-dollar-refusal.md deleted file mode 100644 index 6a52d2c57c8..00000000000 --- a/.changeset/25709-action-copy-holeless-dollar-refusal.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/spec': minor ---- - -feat(lint)!: `objectstack validate`, `compile` and the publish gate refuse a holeless `${record.*}` `confirmText` and a holeless `${result.*}` `successMessage` on an action, the one action-copy finding that was still a warning (#25709) - -Clause-②: no (narrowing: an action's `confirmText` / `successMessage` sentence with no `{{ }}` hole whose tokens are `${record.*}` / `${result.*}` is refused at `objectstack validate`, `compile` and the publish gate instead of saving with a warning) -Metadata-protocol: narrowed - -The console's action runner no longer fills `${…}` in an action's copy: a sentence with no `{{ }}` hole is shown as written, `${…}` included (objectui#13476, in the `ui/` tree). The doors kept one `${…}` shape a warning while the runner still filled it: `Delete ${record.name}?` in a `confirmText`, `Invitation sent to ${result.email}` in a `successMessage`. The person reading the dialog or toast now sees that token raw, so the doors refuse it like every other `${…}` token. - -**BREAKING — what moves for consumers.** - -- FROM: a warning at `objectstack validate` and `compile`, and an entry on the publish gate's `advisories` channel while the action, object or page saved. TO: an error. Both commands fail, and an active `action`, `object` or `page` write answers `422 INVALID_METADATA` with an `expression-invalid` issue located at the key. The item is not stored. The finding no longer ends with "A warning, not a refusal, for now". -- `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` (`@objectstack/spec/ui`) and `dollarOnlyTokenRoots` (`@objectstack/spec/automation`), which reached `main` after `@objectstack/spec@18.1.0` and were never published, are withdrawn before this release. The `confirmText` and `successMessage` describes no longer say the shape is a warning. - -**Fix (one line).** Write the hole the finding prescribes: `Delete ${record.name}?` → `Delete {{ record.name }}?`, `Invitation sent to ${result.email}` → `Invitation sent to {{ result.email }}`. - -**Unchanged.** The parse: each copy key accepts what it accepted, so a stored row with such a sentence keeps loading until its next save, and that save is judged. Saving as a draft is still allowed. `outcomeMessages` and `ActionSchema.errorMessage` are not in this family. `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write. - -**Measured before crossing**, by walking every action-copy sentence reachable from each module's exports on this branch's base `38050ced` and through the built `@objectstack/lint`: `examples/app-crm` (2 `successMessage`, 2 `errorMessage`), `app-todo` (1 `confirmText`, 8 `successMessage`), `app-showcase` (6 `confirmText`, 24 `successMessage`), `app-multi-package` (none), `@objectstack/platform-objects` and `@objectstack/plugin-approvals` carry 0 sentences with a `${…}` token and 0 of the holeless shape, and the four example stacks draw 0 copy findings. A planted `Delete ${record.name}?` `confirmText` on each example stack draws the finding. - - diff --git a/.changeset/25711-console-built-in-workspace.md b/.changeset/25711-console-built-in-workspace.md deleted file mode 100644 index 0e2463eef14..00000000000 --- a/.changeset/25711-console-built-in-workspace.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/console': patch -'@objectstack/cli': patch ---- - -build(console): `@objectstack/console` is built in this workspace from `ui/apps/console`, and the Console no longer arrives through a pinned objectui commit - -Clause-②: no -Metadata-protocol: none - -- **The package.** `@objectstack/console` ships the same `dist/` it shipped at 18.1.0: the built Console (`index.html`, `assets/`, the manifests it served). Two files leave the tarball: `dist/.objectui-sha`, which named the objectui commit the Console was built from, and `dist/.objectstack-injection.json`, which recorded the spec and client dists injected into that build. Both described a pin that no longer exists: the Console is built from the `ui/` tree of the same commit as the framework, so its version is the framework's version. -- **The CLI.** When a framework checkout has no built Console, `objectstack serve` / `objectstack dev` and the 503 at `/_console/` now name `pnpm console:build`, which builds `ui/apps/console` and copies its dist into `packages/console`. The remedy used to be `pnpm objectui:build`, which cloned objectui at the pinned commit. -- **One version.** The `@object-ui/*` packages and the private VS Code extension join the `@objectstack/*` fixed group, so every public package of both scopes publishes at one version and the extension ships at it. - -What to do: nothing as a consumer; install `@objectstack/console` as before. In a framework checkout, run `pnpm console:build` where you ran `pnpm objectui:build`. diff --git a/.changeset/25712-renderer-runs-one-sdui-parser.md b/.changeset/25712-renderer-runs-one-sdui-parser.md deleted file mode 100644 index 3a7068a98cb..00000000000 --- a/.changeset/25712-renderer-runs-one-sdui-parser.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/console': patch -'@object-ui/layout': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-timeline': patch -'@object-ui/plugin-tree': patch -'@object-ui/react': patch ---- - -refactor(ui)!: the renderer runs `@objectstack/sdui-parser`, and `@object-ui/sdui-parser` is deleted (#25712) - -Clause-②: yes (narrowing: a `type-mismatch` or `member-type-mismatch` the html-tier compile used to grade `warning` is now an `error`, so a page that carried one now refuses to compile instead of rendering with the value dropped) -Metadata-protocol: none - -The renderer and the save gate each ran their own copy of the SDUI parser. The previous change under this card made `@objectstack/sdui-parser` the one body. This change moves every `@object-ui/*` package, the console and the examples onto it, and deletes `ui/packages/sdui-parser`. There is no forwarding release of `@object-ui/sdui-parser`: it leaves the fixed version group, and the 18.1 line on npm is its last. - -- **Dependencies.** Each package above now depends on `@objectstack/sdui-parser` where it depended on `@object-ui/sdui-parser`. The console's `gen-manifest.ts` moved from the deleted package to `ui/apps/console/scripts/`. -- **Severity, the one narrowing.** The renderer's copy graded a `type-mismatch` as an `error` only beside an `enum` arm, and as a `warning` otherwise. The kept rule grades every `type-mismatch` and `member-type-mismatch` an `error`, which is what the save gate already did. So an html-tier page whose attribute does not fit the declared type now shows the compile-error block naming the prop, where it rendered the block with that value dropped. Example: `` (a string where `columns` is declared `array`). A page the save gate accepted is not affected, because the save gate already refused it. -- **Body-dialect messages.** The retired `body` child-list key's `unknown-prop` message now ends `("body" is its retired spelling)`, and its `not-a-container` message drops the `(objectui#6771)` parenthetical. - -Migration: none for a page the save gate accepted. A page that relied on the warning has a value the declared type does not take: write it in the declared type, for example `columns={['name','amount']}`. - - diff --git a/.changeset/25712-sdui-parser-one-body.md b/.changeset/25712-sdui-parser-one-body.md deleted file mode 100644 index 18a7d5f1ccb..00000000000 --- a/.changeset/25712-sdui-parser-one-body.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/sdui-parser': patch ---- - -feat(sdui-parser)!: one parser body: the renderer copy's additions join `@objectstack/sdui-parser` (#25712) - -Clause-②: yes (narrowing: `validateTree` now also judges the nodes held in a manifest entry's declared `slots`, so a caller whose manifest carries `slots` gets diagnostics on slot-held nodes it did not get before) -Metadata-protocol: none - -Two copies of this parser existed: this package, which the save gate runs, and `@object-ui/sdui-parser`, which the renderer runs. `check:sdui-lockstep` held them to one grammar and one code set, and they drifted everywhere else. This change makes this package the one body. The renderer moves onto it and the second copy is deleted in follow-up changes under the same card. - -**Added (from the renderer's copy).** - -- Node-slot validation (objectui#11170). `ManifestComponent.slots` lists the positions besides `children` where a renderer reads nodes, such as `trigger`, `items[].content` and `regions[].components`. `validateTree` descends each one the way it descends `children`. `manifestFromConfigs` gains a `slotsFor` option that writes the list, and a manifest built without it carries none, so the walk follows `children` alone. -- The retired `body` child-list key (objectui#6771). Before, `body` drew the bare `unknown-prop`. Now the message names `children`. A `body` child list under a component that takes no children draws `not-a-container`, the code the `children` spelling draws. Both remain warnings. Exported as `checkRetiredBodyDialect` and `RETIRED_CHILD_LIST_KEY`. -- html-tier provenance (objectui#4000). `parseJsx` stamps every element it builds with an enumerable symbol-keyed marker, `Symbol.for('@object-ui/sdui-parser.html-tier-node')`. `JSON.stringify` and `Object.keys` do not see it. Exported as `HTML_TIER_NODE`, `isHtmlTierNode` and `markHtmlTierNode`. The registry key keeps its original package name on purpose, so a node either copy stamped reads the same. -- More exports: - - `acceptsChildren`, `CHILD_LIST_KEY`, `SDUI_BASE_PROPS`, `SduiBaseProp` and `SduiBasePropScope`; - - `assertFullyLoaded`, the manifest generators' guard against lazy stubs, with `RegistryConfigLike.lazy`; - - `NodeSlotDeclarationLike`. -- The generated `.d.ts` header gains `/* eslint-disable */` and names this package. - -**Renamed, with the old names kept.** `ParsedSchemaNode` is the parsed node and `ManifestValidationResult` is `validateTree`'s result. These are the names the renderer's copy took so it would not collide with `@object-ui/types`' `SchemaNode` or the spec's `ValidationResult` (objectui#6349, objectui#3161). `SchemaNode` and `ValidationResult` stay exported as `@deprecated` aliases of them, so nothing exported is removed. - -**Unchanged.** - -- Every `type-mismatch` and `member-type-mismatch` is still an `error`. The renderer's copy graded them `error` only beside an `enum` arm; this package's rule is the one kept. -- `unconsumed-widget-option` is still emitted as an inline literal. -- The grammar, the 25 diagnostic codes and the containment predicate. - -**BREAKING** (a narrowing of `validateTree` for a manifest that carries `slots`), shipped as `patch` because no metadata protocol kind moves. The save gate's verdicts do not move, and that was measured: the manifest that `objectstack validate`, `build`, `lint` and `serve` read is the tracked `sdui.manifest.json`, which `@objectstack/console` ships. It carries `slots` on 0 of its 106 entries, because its generator calls `manifestFromConfigs` without `slotsFor`. So no save that was accepted before is refused now. The walk has work only for a caller that builds its manifest with `slotsFor`, and today that is the renderer, whose own copy already walks the slots. - -Migration, for a TypeScript caller: none is required. To move off the deprecated names, change `SchemaNode` to `ParsedSchemaNode` and `ValidationResult` to `ManifestValidationResult`. - -`body-dialect.ts` and `provenance.ts` moved in from the MIT-licensed `@object-ui/sdui-parser` and keep that licence. Each carries the notice, and the package now ships `LICENSE-MIT`. - - diff --git a/.changeset/25744-object-own-action-schema-parse.md b/.changeset/25744-object-own-action-schema-parse.md deleted file mode 100644 index c9db87197b4..00000000000 --- a/.changeset/25744-object-own-action-schema-parse.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql)!: an object's own actions are stored as their `ActionSchema` parse, so `requiresFeature` takes effect on a raw manifest's object actions - -Clause-②: no (narrowing: an object's own action that fails `ActionSchema` is refused at the runtime registration door, as #25649 did for standalone actions) - -Metadata-protocol: none - - - -The schema registry now parses an object's own `actions` through `ActionSchema` when a code package registers the object (`SchemaRegistry.registerObject`), and stores the parsed actions. A standalone `action` item has been stored this way already; both now go through one parse. Before this, an object that reached the registry without a parse kept its actions as authored. Examples are a raw plugin manifest, a `strict: false` stack, and a programmatic `registerApp`. Such an object was served with its actions unparsed: a declared `requiresFeature` (or `requiresMembershipReach`) was never lowered into `visible`, so the gate did nothing. - -What the parse does to such an action, as with any parsed action: - -- it lowers `requiresFeature` / `requiresMembershipReach` into `visible`; -- it fills schema defaults; -- it wraps a CEL string shorthand in `{ dialect, source }`. - -An object a strict `defineStack` or `ObjectSchema.create` already parsed is stored unchanged. A registration with no package, and a tenant-authored body (the metadata save door's write-through, the boot rehydration of stored rows), is not judged here. - -**BREAKING** for a package that registers an object whose own action `ActionSchema` does not accept. - -- **Before.** The registry stored such an object as written, and served the action with any declared gate inert. -- **Now.** The object's registration is refused with `INVALID_METADATA` (422), and nothing of it is stored. The error names the action, its object and its package, and `issues[]` gives the path (`actions.N.KEY`) and message of each failure. -- **Fix.** Correct the action as `issues[]` describes. An object that passes `os validate`, or a strict `defineStack`, is unaffected. diff --git a/.changeset/25758-notify-written-address-no-email-channel.md b/.changeset/25758-notify-written-address-no-email-channel.md deleted file mode 100644 index ad13039ef67..00000000000 --- a/.changeset/25758-notify-written-address-no-email-channel.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation)!: a `notify` node that does not name the `email` channel fails on an address no user holds, written out or computed, instead of reporting success - -Clause-②: no (narrowing: a `notify` node whose `channels` do not include `email`, and whose `recipients` hold an email address no user holds, written out or evaluated by a value expression, bare or as `user:ADDRESS`, now fails the node at run time instead of reporting success) -Metadata-protocol: none - -An email address written out in `recipients` is the author's constant and is sent as written, and only the email channel sends to an address itself. The inbox delivers to a user, and so does the sms channel (or to a literal phone number). So on a node whose `channels` do not include `email` (the inbox, the default, or `inbox` or `sms` named), an address reaches someone only when a user holds it, wherever the address came from. - -- **BREAKING (run time): such a node now fails when no user holds the address, written out or computed.** Before, the address went on as it stood: the inbox wrote a `sys_inbox_message` row whose `user_id` was the address, which no inbox reads, and the node reported success. Now the node fails before anything is written, and the message names the node, each such recipient and the fix. A written-out address is named; one a value expression (a CEL envelope) evaluated to is named by the expression's source, never by its value. `user:ADDRESS` fails the same way, because `user:` takes a user id and the address is handed on as one with no lookup. With no data engine to look the address up, or a lookup that fails, the node fails too. -- **Unchanged.** An address a user holds, written bare, still reaches that user. A node whose `channels` include `email` sends a written-out address as written, as before. On a node whose `channels` include `email`, a computed address no user holds still fails through the ruling-B screen, pointing to `{ emailField }`. The node never adds the `email` channel its author did not declare. -- **Who moves.** A flow whose `notify` node does not name `email` and writes out an address no user holds, or whose recipient expression evaluates to one (for example a record field holding an address that is not a user's). Fix: add `email` to the node's `channels` to mail a written-out address, or name a user: a user id, an audience selector such as `role:NAME`, or the address that user holds, written bare. A computed address of a person who is not a user goes to its own node with `recipients: { emailField: 'FIELD' }` and `channels: 'email'`. -- **Who moves in this repository (app-showcase).** - - `alert_exec` (flow `showcase_project_escalation`) wrote out `exec@example.com`, which no user holds, on the inbox. It now notifies `role:owner`. - - `notify_cleared` (flow `showcase_invoice_signoff`, the seeded Invoice Dual Sign-off demo) notified `record.owner` on the inbox. The seeded invoices' owners (`ada@`, `linus@` and `grace@example.com`) are deliberately not users: they are the fixture of the ADR-0055 isolation demo, where an operator signs up as one of them, so provisioning them would delete that demo. Approving a seeded invoice would now have failed the run at this node. It now notifies `role:owner`, the Default Organization's admin on a dev boot, who also holds the finance and legal positions that clear the request. `approval-resume-relation-expand.test.ts` approves the seeded INV-1010 through it. - - Its other inbox nodes whose expressions read `record.owner`, `record.assignee` or `ownerId` (projects, tasks) evaluate to addresses its seeds provision as users (pinned by `inert-wirings.test.ts`), so they keep delivering. - - Four nodes write out `admin@objectos.ai` on the inbox: `digest` (`showcase_scheduled_digest`), `log_fail` (`showcase_project_escalation`), `notify_done` (`showcase_release_signoff`) and `report` (`showcase_inquiry_purge`). On a dev boot, where `plugin-auth` seeds that admin, they deliver as before. On a boot without that user they now fail loud, where before they wrote an inbox row nobody reads. - - diff --git a/.changeset/25759-mcp-stateless-get-405.md b/.changeset/25759-mcp-stateless-get-405.md deleted file mode 100644 index 8a1e8dbd763..00000000000 --- a/.changeset/25759-mcp-stateless-get-405.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/mcp': patch -'@objectstack/runtime': patch ---- - -fix(mcp,runtime)!: the stateless `/api/v1/mcp` surface answers `GET` and `DELETE` with `405` and `Allow: POST`, so an MCP client stops re-opening a GET stream that ends at once - -Clause-②: yes (narrowing) -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the MCP HTTP transport (`MCPServerRuntime.handleHttpRequest`, `@objectstack/mcp`), and on the standalone server's `/api/v1/mcp` route (`createDispatcherPlugin`, `@objectstack/runtime`). - -- **Before.** The transport is stateless (no session id) and closes its per-request server when the request ends. A `GET` still reached the SDK's stream handler, which answered `200 text/event-stream` and then lost the stream when the server closed. An MCP client reads a GET stream that ends as a dropped connection and opens another one: claude-code re-opened it 2–3 times a second for as long as a session stayed connected. A `DELETE` answered `200` and ended nothing, because no session exists. Every other verb answered `405` with `Allow: GET, POST, DELETE`. -- **Now.** Every verb but `POST` that reaches the transport (`OPTIONS` with CORS off; with CORS on the CORS middleware's `204` answers first on both doors) answers `405` with `Allow: POST` and the JSON-RPC body `{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed."},"id":null}`, before a server is built. This is the Streamable HTTP transport's answer for a server that offers no GET stream and does not end sessions. The MCP SDK client treats a `405` on GET as "no stream offered" and does not retry it. -- **The standalone server.** `createDispatcherPlugin` mounts `/api/v1/mcp` for `GET`, `POST` and `DELETE`, and now also declares it a path whose domain decides every verb (the declaration `/mcp/skill` and `/keys` already carry). Before, a `PUT`, a `PATCH` or an `OPTIONS` with CORS off matched no route there and the HTTP adapter answered it itself: `405` with `Allow: DELETE, GET, HEAD, POST` and a `METHOD_NOT_ALLOWED` envelope, to an anonymous caller too. Now those verbs reach the dispatcher like the mounted ones: an anonymous caller gets `401`, a signed-in one the transport's `405` with `Allow: POST`. A `createHonoApp` host already handed every verb to the dispatcher, so both doors now answer every verb alike. -- **Unchanged.** `POST` (JSON-RPC: `initialize`, `tools/list`, `tools/call`, …) answers exactly as before. An anonymous caller is refused `401` before any verb answer, on every verb and on both doors. -- **Who sees it.** A client that opens the GET stream on `/api/v1/mcp` now gets `405` instead of a `200` stream that ends at once. Nothing was ever sent on that stream. On the standalone server, an anonymous `PUT`, `PATCH` or `OPTIONS` (CORS off; with CORS on the CORS middleware's `204` answers `OPTIONS` first on both doors) on `/api/v1/mcp` now gets `401` instead of `405`. - -**The remedy.** None needed for an MCP client: it stops opening the stream. A probe or health check that read `GET /api/v1/mcp` as `200` should send a `POST initialize` instead. diff --git a/.changeset/25760-controlled-by-parent-owner-may-edit.md b/.changeset/25760-controlled-by-parent-owner-may-edit.md deleted file mode 100644 index a1ce87fc539..00000000000 --- a/.changeset/25760-controlled-by-parent-owner-may-edit.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-security': minor -'@objectstack/lint': minor ---- - -feat(spec,plugin-security,lint): a `controlled_by_parent` detail may opt its rows' owners into updating them with read access to the master — `sharing.controlledByParent.ownerMayEdit: true` - -Clause-②: yes (widening) — a detail row's owner now passes the master-detail write gate with READ on the master, on an object that declares the opt-in; everyone else, and every other shape of write, still needs master EDIT. The default is unchanged. -Metadata-protocol: added (`ObjectSchema.sharing.controlledByParent.ownerMayEdit`, a per-object opt-in read only under `sharingModel: 'controlled_by_parent'`) -Ruling-ref: 6398566164 (letter 1) - -ADR-0055 §3 had one rule for every write of a `controlled_by_parent` detail: EDIT access to the master record. Salesforce's Activities under OWD = Controlled by Parent let the owner of an activity edit it while merely reading its account; the platform could not express that, and an authored `owner_id == current_user.id` update policy on the detail could not either — the detail's own policies compose AND with the master gate, so it only narrowed the master's editors. - -- **Spec.** `ObjectSchema` gains `sharing: { controlledByParent: { ownerMayEdit: boolean } }`, both blocks closed. `ownerMayEdit: true` is refused at parse on any object that is not a `sharingModel: 'controlled_by_parent'` detail, at `sharing.controlledByParent.ownerMayEdit`. The reader `controlledByParentOwnerMayEdit(schema)` (`@objectstack/spec/data`) fails closed on every shape but the parsed `true`. -- **Gate.** On an opted-in detail, a by-id `update` by the caller whose `userId` is the stored row's `owner_id` (the registry's injected per-record owner) passes step 2.8 when the caller can READ the master row, asked of the read door under the caller's own context. Insert, delete, a predicate (`multi: true`) update and a delegated (on-behalf-of) write keep the master-edit rule; a body that re-points the master reference still asks EDIT on the master it names; a row with no owner admits nobody. The detail's own object grant and row-level policies run before the leg, unchanged. An opt-in on an object whose schema carries no `owner_id` column (`ownership` other than `'user'`) is inert and reported once per object at `warn` by the relation resolver. -- **Lint.** `os lint` reports `security-controlled-by-parent-owner-policy-without-opt-in` (warning) for an `update` or `all` row-level policy whose `using` or `check` keys a field on the caller (`owner_id == current_user.id`, either operand order, the SQL `=` spelling included) on a `controlled_by_parent` detail that has not declared the opt-in. `os explain security-controlled-by-parent-owner-policy-without-opt-in` prints why such a policy can only narrow, and what the opt-in changes. -- **ADR-0055** §3 is amended (2026-10-26, #25760) with the opt-in, its boundaries, and what stays out of scope. diff --git a/.changeset/25761-seed-column-apply-on.md b/.changeset/25761-seed-column-apply-on.md deleted file mode 100644 index e7f203f5c5e..00000000000 --- a/.changeset/25761-seed-column-apply-on.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/skills': minor ---- - -feat(spec,metadata-protocol): a seed dataset column can be written only when its row is created (`columns.FIELD.applyOn: 'create'`) - -Clause-②: yes -Metadata-protocol: added - -A seed is replayed on every dev-server boot and every package re-publish, and an `upsert` replay re-asserts every column a record declares on the row it matched. That reverts the columns an app's own jobs write after the first seed (a status a payment job advances, a flag a renewal job sets), so a re-run undid the jobs' work with nothing in the log. - -A seed dataset now takes a per-column write policy, keyed by field name: - -```ts -defineSeed(Instalment, { - externalId: 'code', - columns: { - status: { applyOn: 'create' }, - is_expiring: { applyOn: 'create' }, - }, - records: [{ code: 'INS-001', amount: 1200, status: 'pending', is_expiring: false }], -}); -``` - -- **`applyOn: 'create'`** writes the column when the seed creates the row and leaves it out of every write to a row the seed matched by its `externalId`: an `upsert` replay over an existing row, and every row of an `update` dataset. The matched row keeps the value it holds, while the `always` columns are refreshed. A row whose only differences are create-only columns is not written at all. A create-only reference column on a matched row is not resolved and not deferred to pass 2, so nothing later in the load writes it back. A column of the `externalId` is the key the row was matched by and stays in its update. `insert`, `ignore` and `replace` never update a matched row, so nothing changes on those modes. -- **`applyOn: 'always'`** (the default, and every column without a `columns` entry) is the behaviour before this change. -- The values, the default and the meaning are those of the import door's `FieldMappingEntry.applyOn`. Both doors now read one enum, exported as `ColumnApplyOnSchema` from `@objectstack/spec/data`. `SeedColumnSchema` (with `SeedColumn` / `SeedColumnParsed`) is the per-column shape. -- A `columns` key must name a column the object has: a declared field, or a system column the platform injects on it such as `owner_id`. `defineSeed` refuses one that does not when it runs (and at compile time for an object literal), naming the key with a near miss. The seed loader logs an `error` naming the column, once per dataset, whenever it loads a seed that names one, since a stored seed never passes through `defineSeed`. The rows still load. -- `SeedSchema` refuses an unknown key on a column (`unrecognized_keys`), and a dataset-level `applyOn` is pointed at `columns`. - -`@objectstack/skills` only regenerates its schema reference indexes, which now list `SeedColumnSchema` and `ColumnApplyOnSchema`. diff --git a/.changeset/25773-published-package-default-permission-set.md b/.changeset/25773-published-package-default-permission-set.md deleted file mode 100644 index f11ef8c16ae..00000000000 --- a/.changeset/25773-published-package-default-permission-set.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch -'@objectstack/spec': patch ---- - -docs(skills, spec): state that every published package declares exactly one anchor-safe `isDefault: true` permission set - -Clause-②: no -Metadata-protocol: none - -`objectstack-data/rules/security.md` said what `isDefault: true` means (the `everyone` baseline, ADR-0090 D5) but not that a published package must declare one. Its `isDefault` bullet now states the requirement as a check an author can run: exactly one such set, anchor-safe, meaning `describeAnchorForbiddenBits(set, 'everyone')` from `@objectstack/spec/security` returns `null`; otherwise lint rule `security-anchor-high-privilege` and the binding refuse it. It also says what happens without one (invited members get nothing until an admin assigns a set to each) and that no gate counts the sets. The Marketplace Protocol header in `marketplace.zod.ts`, which the generated marketplace reference page renders, gains a matching "What a published package declares" section. No lint rule or schema changes. To stay within the skill file's token ceiling, three source-path asides, a section reference and one sentence are trimmed in the same file. diff --git a/.changeset/25777-demo-staff.md b/.changeset/25777-demo-staff.md deleted file mode 100644 index a643b7541ab..00000000000 --- a/.changeset/25777-demo-staff.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-auth': minor -'@objectstack/runtime': minor -'@objectstack/cli': minor ---- - -feat: `demoStaff` — an application declares the demo accounts its seed data is dealt to, and the development boot creates them - -Clause-②: yes -Metadata-protocol: added - -A stack may now declare `demoStaff: [{ name, email, position?, permissionSets? }]`. On a local development boot (`os dev`), `@objectstack/plugin-auth` creates each entry right after the development admin, through the same sign-up pipeline, with the development seed password (`os dev --admin-password`, default `admin123`); assigns the declared position and permission sets in the Default Organization (an account the membership policy left outside it is reported and gets none); and the application's seed then runs, so a seed row naming one of these people (`owner_id: 'Legal Counsel 1'`) resolves on the first boot. Idempotent: an address that already has an account is left exactly as it is, and only its missing assignments are added. - -Development only. `os build` leaves the key out of the artifact (`objectstack.json`), so a deployment booted from one has nothing to provision. Every other boot creates nothing and says why, with an ADR-0112 code: a boot that is not `NODE_ENV=development`, or a migrate-and-exit run (`PERMISSION_DENIED`, logged at `info` — the intended outcome there); and, on a development boot, logged at `warn` because the developer loses the accounts: a walled tenancy posture or an auth base URL that is not a loopback address (`PERMISSION_DENIED`), a database where nothing but a demo account could sign in, so a demo account would be promoted to platform admin (`PRECONDITION_REQUIRED`), or a credential store that could not be read (`SERVICE_UNAVAILABLE`). - -`defineStack` refuses an entry that names a position or permission set the stack does not declare (a platform set such as `admin_full_access` is never one), and two entries with one name or one address (`STACK_CROSS_REFERENCE_INVALID`). - -`@objectstack/runtime`: when the boot will create demo accounts, `AppPlugin` runs its inline seed at `kernel:bootstrapped` instead of inside `start()`. The seed source is declared as before, so the settlement tally and `app:seeded` behave as they do for a seed that overran its budget. - -What to do: nothing. A stack that declares no `demoStaff` boots, seeds and builds exactly as before. An application that creates its demo accounts with its own script can declare them here and delete the script. diff --git a/.changeset/25778-dataset-duration-measure.md b/.changeset/25778-dataset-duration-measure.md deleted file mode 100644 index ee39dfe19c7..00000000000 --- a/.changeset/25778-dataset-duration-measure.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': minor -'@objectstack/lint': minor -'@objectstack/platform-objects': minor -'@object-ui/app-shell': minor ---- - -feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record - -Clause-②: yes (widening) -Metadata-protocol: added - -- **`@objectstack/spec`**: `DatasetMeasureSchema` gains an optional `duration: { from, to, unit }`, in place of `field`. `from` and `to` name two `datetime` fields of the dataset's own object by their bare names, `unit` is `seconds`, `minutes`, `hours` or `days`, and the measure's `aggregate` is `avg`, `min`, `max` or `sum` of `to` minus `from`. A relationship path, an expression or the row wildcard in either column, the same column twice, another unit, a counting aggregate, or a `field`, `derived` or `currency` beside it is refused at parse. New exports: `DatasetMeasureDurationSchema`, `DatasetDurationUnit`, `DATASET_DURATION_UNIT_SECONDS`, `DATASET_DURATION_AGGREGATES`, `DATASET_DURATION_FIELD_TYPE` and the `DatasetMeasureDuration` / `DatasetDurationUnit` types. -- **`@objectstack/service-analytics`**: the dataset compile refuses a duration column declared anything but `datetime` (`400 DATASET_INVALID`, naming the column and its type). The native-SQL strategy computes the duration as one statement on SQLite, PostgreSQL and MySQL, in double precision on each; SQLite and PostgreSQL are tested live to answer the same number: a record missing either instant is left out, a `to` before its `from` counts as negative, and over a group with no such record `sum` answers `0` and the others `null`. Measure filters, derived measures, ordering and the draft preview serve it. A query the native strategy does not serve (a time dimension bucketed by `granularity`, a field-to-field filter, an object served through engine middleware or an external datasource, a non-SQL datasource) is refused with `400 INVALID_FIELD` naming the measure. Its two columns pass the field-level read gate like any measure's field. The temporal half of the aggregate × field-type refusal now names the duration measure instead of a stored "days open" number. -- **`@objectstack/lint`**: `os validate` refuses a duration column not declared `datetime` with the new gating rule `measure-duration-field-type-refused` (at `datasets[N].measures[M].duration.from` / `.to`), with an `os explain` entry. `dataset-field-unknown` now also reports a duration column the object does not declare. -- **`@objectstack/platform-objects`**: the dataset form's new **Duration** row is translated in every shipped locale. -- **`@object-ui/app-shell`**: changing a dataset's base object in the console's dataset inspector now clears a duration measure along with the other measures that name the old object's fields, instead of keeping it as if it were a plain row count. - -What to do: nothing. A dataset without `duration` parses, compiles and answers exactly as before. diff --git a/.changeset/25779-seed-files.md b/.changeset/25779-seed-files.md deleted file mode 100644 index b27ca2073d3..00000000000 --- a/.changeset/25779-seed-files.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/service-storage': minor -'@objectstack/runtime': minor -'@objectstack/cli': minor ---- - -feat(spec,metadata-protocol,service-storage,runtime,cli)!: a seed record can carry a file the package ships, `seedFile(path)`, and a file field's id that names no stored file is refused - -Clause-②: yes (narrowing) -Metadata-protocol: narrowed - - - -A `file`, `image`, `avatar`, `video` or `audio` field stores the id of a stored file, and only an upload made one, so a seed could not fill a required file field at all. A seed record now names the file by the path the package ships it at: - -```ts -import { defineSeed, seedFile } from '@objectstack/spec/data'; - -defineSeed(ContractVersion, { - externalId: 'code', - records: [ - { code: 'NDA-001-V1', file: seedFile('fixtures/nda-v1.pdf') }, - { code: 'NDA-002-V1', file: seedFile('fixtures/nda-v2', { name: 'NDA v2.pdf', contentType: 'application/pdf' }) }, - ], -}); -``` - -- **The path** is relative to the directory holding `objectstack.config.ts`, uses `/`, and stays inside it: an absolute path, a drive letter, a `\`, or an empty, `.` or `..` segment is refused at parse. A `multiple: true` file field takes an array of `seedFile(...)` values. `defineSeed` refuses `seedFile(...)` in a field that does not store a file. -- **`os build` / `os compile`** reads each referenced file and writes its bytes into the dataset's new `files` map (`{ contentType, size, sha256, data }`, base64), so the compiled artifact carries them to every boot that loads it: `os dev`, `os serve`, a published package, an artifact URL. A path that names no file, or a file whose type is neither named with `contentType` nor known by its extension, fails the build, and `os validate` refuses the same paths. `os serve` / `os dev` booting a config rather than an artifact reads the files the same way, so both boots seed the same files. -- **The seed loader** stores the bytes through the new `field-files` service (`FIELD_FILE_SERVICE`, `IFieldFileService`, registered by `@objectstack/service-storage`), writes the file's id into the field, and makes the written record the file's owner, the (object, record, field) ownership an upload gets. Each row gets a file of its own. A replay that finds the row holding a file stored from the same path with the same bytes, name and type keeps it, so a restart does not store a new file for an unchanged fixture. A file stays `pending` until its row lands, so a file whose row is refused is reclaimed by the pending-upload retention. -- **Refused, by row, before the row is written**: a reference whose bytes the dataset does not carry, bytes that do not match their recorded size and SHA-256, a load with no `field-files` service, a field mixing `seedFile(...)` with other values, and several `seedFile(...)` values in a one-file field. Each is counted, reported in the load's errors and logged at `error`. -- **The inline boot seed** of an app whose datasets carry seed files runs at `kernel:bootstrapped` (as one with `demoStaff` already does), once the storage service has read its settings, so the files land in the store the deployment serves from. -- `SeedLoaderService` takes an optional fourth argument, `{ getFieldFiles }`; every platform caller passes the kernel's `field-files` service. - -**BREAKING** (a seed-load narrowing). A seed record whose file field holds an id-shaped string (letters, digits, `_` and `-`, at most 64 characters) that names no stored file in the row's organization is now refused, by row, with an error naming the field and the value. Before, it was written as given, and the field then read as holding no file. An external URL or an inline value is not an id and is written as before; so is any value on a deployment without `@objectstack/service-storage`. - -**What to do.** If a seed load now reports `… holds '', which names no stored file`, replace the value with the file the field should hold, `seedFile('')`, or remove it. diff --git a/.changeset/25783-console-open-metadata-cost.md b/.changeset/25783-console-open-metadata-cost.md deleted file mode 100644 index d7d9c665901..00000000000 --- a/.changeset/25783-console-open-metadata-cost.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch ---- - -perf(metadata-protocol,runtime): served metadata lists parse each unchanged document once, and the dispatcher's `/meta/app` reads objects on the execution face - -Clause-②: no -Metadata-protocol: none - -- **Diagnostics verdicts are memoized.** `computeMetadataDiagnostics` remembers a verdict per schema object and per document content (a SHA-256 of an encoding that keeps apart everything the parse keeps apart), so a served list of unchanged documents parses each one once instead of on every read. A replaced type schema starts from nothing, each caller gets its own copy, and a document with no faithful key is parsed every time as before. The served bytes are unchanged. Measured on the showcase app (108 objects, local, main-thread CPU per request): `GET /meta/object` 85.75 ms → 37 ms. -- **The dispatcher's `/meta/app`** (the `${prefix}/*` catch-all) reads each object's `enable` for its nav prune through `getMetaItemsForExecution` where the protocol has it, as `RestServer` does, and `getMetaItems` otherwise. -- **No public surface moves.** The built `dist/index.d.ts` of both packages is byte-identical at `cb0a2fcf` and at this head. diff --git a/.changeset/25783-rest-meta-app-execution-face.md b/.changeset/25783-rest-meta-app-execution-face.md deleted file mode 100644 index 083f84bdc04..00000000000 --- a/.changeset/25783-rest-meta-app-execution-face.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -perf(rest): `GET /meta/app` judges its navigation on the protocol's execution face, so it no longer schema-parses every object on each read - -Clause-②: yes (widening): the exported `RestProtocol` type gains one optional member, `getMetaItemsForExecution?`. Measured on the built `dist/index.d.ts` at `cb0a2fcf` (before) and at this head (after): the export list is unchanged (67 names), and outside doc comments the one changed line is that member. Every other member signature is byte-identical. The member is optional, so nothing a protocol must provide changes. - -Metadata-protocol: none - -- **What.** The nav-servability gate (#24031) reads each object's `enable` to prune the nav entries whose object refuses `list`. It read them through the served `getMetaItems`, which stamps `_diagnostics` on every object, so every `/meta/app` read ran a full schema parse of every object definition. `RestServer.metaListSource` now asks `getMetaItemsForExecution` for `object` when the protocol has it (the same list, without the serving decorations) and `getMetaItems` otherwise. The batch route's exposure gate and the by-name gate's list fallback read the same source. The `book` and `doc` lists stay on the served face. -- **Measured** on the showcase app (108 objects, local, main-thread CPU per request): `GET /meta/app` 73.25 ms → 16.75 ms. -- **Unchanged.** Every `/meta` answer: the same nav prune, and `GET /meta/object` still serves `_diagnostics` on every object. diff --git a/.changeset/25785-i18n-translations-etag.md b/.changeset/25785-i18n-translations-etag.md deleted file mode 100644 index 32515c31410..00000000000 --- a/.changeset/25785-i18n-translations-etag.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): `GET /i18n/translations/:locale` carries an `ETag` and `Cache-Control: private, no-cache`, and a matching `If-None-Match` is answered `304` - -Clause-②: no -Metadata-protocol: none - -- **Before.** The translation bundle (both spellings, `/i18n/translations/:locale` and `/i18n/translations?locale=`) answered every read with the whole locale bundle and neither header, so a browser re-downloaded it on every console open: 80–88 KB compressed on a hosted tenant. -- **Now.** The `200` carries a weak `ETag`, a SHA-256 of the exact envelope served (`locale`, `requestedLocale` and `translations`), computed on every read, and `Cache-Control: private, no-cache`. A request whose `If-None-Match` names that representation (a strong or weak tag, a comma list, or `*`) is answered `304` with no body and the same two headers. A bundle that changed since the validator was issued is served in full under a new validator. -- **Unchanged.** The `200` body is byte-for-byte the envelope it was. An anonymous caller is still refused `401` before any validator is read, and refusals carry neither header. `/i18n/locales` and `/i18n/labels/*` are untouched. diff --git a/.changeset/25786-grouping-order-by.md b/.changeset/25786-grouping-order-by.md deleted file mode 100644 index a7487a04520..00000000000 --- a/.changeset/25786-grouping-order-by.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': minor ---- - -feat(spec,lint): a list-view grouping level can order its groups by another field (`grouping.fields[].orderBy`) - -Clause-②: yes -Metadata-protocol: added - -- **Before.** A grouping level ordered its groups only by its own key. A period label grouped as text (`Nov 2026`, `Oct 2026`, `Q1 2027`, `Q4 2025`), so putting periods in time order took a second, outer grouping level on the period start, which showed two header rows for every group. -- **Now.** `GroupingFieldSchema` gains `orderBy`, the name of a field on the same object whose value orders this level's groups: `{ field: 'period_label', orderBy: 'period_start', order: 'desc' }`. When it is absent, or names the grouping field itself, the groups order by their own key as before. Nothing is defaulted in. -- **What the header query carries.** The consumer still sorts the header rows. `compileListViewGroupQuery` now adds the value to sort by: one `min` node over `orderBy` for `order: 'asc'`, or one `max` node for `'desc'`, under `min_` / `max_`. Each group sits where its first row would sit if the rows were sorted by `orderBy` in `order`. The grouping key stays the only `groupBy` column, so there is one header per label. A column summary that already declares the same `min` is the same node and the same column. In a multi-level grouping the outer level's value folds exactly from the leaf rows (the `min` of mins, the `max` of maxes). -- **`listViewGroupOrderColumn(grouping, level)`** (`@objectstack/spec/ui`) names the header-row column a level sorts by: the grouping field itself, or the order node's alias. A consumer reads this instead of rebuilding the alias. A group whose order value is `null` sorts last in both directions, and equal values fall back to the group key in the same direction. -- **Refused, at compile and at authoring.** An empty `orderBy` is refused by the schema (`too_small`), and a padded one, whitespace-only included, by name, as a padded `field` is, because the name is compiled verbatim. The compiler refuses a blank one in-process (`grouping_order_by_blank`, `INVALID_QUERY` / 400). An order alias that lands on a grouped field's own column is `alias_collision`. `os validate`, `os build` and the runtime save door judge `orderBy` the way the server doors do. An unknown name is `list-view-field-unknown`. A dotted path is `list-view-field-dotted`, because the header query compiles no join. A field whose type `min` / `max` cannot order on every backend is the new `list-view-group-order-unorderable` (`error`). That covers the string classes, whose order depends on the collation, the virtual `formula`, and every other type outside the numeric, temporal and boolean classes. Each of these is refused `400 INVALID_FIELD` on the server, so the grouped view would render no group: an unknown or dotted name by the data door's ingress (`findData`), and an unorderable type by `engine.aggregate`'s type door. -- **Unchanged.** `order` and `collapsed` keep their meaning and defaults. The row page query (`compileListViewGroupRowsQuery`) does not change. `EngineAggregateOptions` gains no `orderBy`, because the group sort stays the consumer's. That sort is right only over the whole header set, which is what the compiler asks for: a `limit` on the header query is sliced by `findData` before any sort, so a paged header set honours neither `order` nor `orderBy`, the limit already recorded on `GroupingConfigSchema`. The new key is purely additive, so a grouping declared today compiles to exactly the same query. diff --git a/.changeset/25787-activity-update-names-fields.md b/.changeset/25787-activity-update-names-fields.md deleted file mode 100644 index d841c72c239..00000000000 --- a/.changeset/25787-activity-update-names-fields.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): an activity entry for an update that moves no `trackHistory` field names the fields that changed - -Clause-②: no -Metadata-protocol: none - -The record Discussion feed wrote `Updated "