From 762f2e5bbfea5280d7f34d956d37d95d842a851e Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 15:22:46 +0200 Subject: [PATCH 01/11] chore: prepare native UltraModern migration Preserve merged cleanup while adopting the current framework and Effect cohort. Keep owner boundaries, native tooling and quality gates. Draft checkpoint: framework compatibility publication and end-to-end acceptance remain pending. Co-Authored-By: Claude Code --- .../workflows/ultramodern-workspace-gates.yml | 10 +- app/.mise.toml | 2 +- app/.modernjs/release-cohort.json | 114 +- app/.modernjs/ultramodern.json | 23 +- app/apps/shell-super-app/modern.config.ts | 3 +- .../module-federation.config.ts | 3 + app/apps/shell-super-app/package.json | 43 +- .../src/routes/[lang]/login/page.tsx | 11 +- .../src/routes/[lang]/page.tsx | 9 +- .../src/routes/[lang]/search/page.tsx | 13 +- .../src/routes/ultramodern-route-metadata.ts | 10 +- .../tests/unit/routes/home/page.test.tsx | 125 +- .../tests/unit/routes/login/page.test.tsx | 96 +- .../tests/unit/routes/search/page.test.tsx | 307 + app/apps/shell-super-app/tsconfig.json | 17 +- app/oxfmt.config.ts | 8 +- app/package.json | 35 +- app/packages/core-runtime/package.json | 8 +- .../core-runtime/src/actions/collector.ts | 2 +- .../core-runtime/src/actions/repository.ts | 7 +- .../auth/principal-administration-reads.ts | 4 +- .../workspace-environment-bootstrap.cjs | 16 +- .../workspace-environment-bootstrap.d.cts | 4 +- .../src/install/spicedb-database-config.ts | 12 +- .../src/modules/application-composition.ts | 2 +- .../src/modules/runtime-registration.ts | 2 +- .../core-runtime/src/outbox/poller.ts | 2 +- .../core-runtime/src/outbox/repository.ts | 2 +- .../core-runtime/src/search/persistence.ts | 10 +- .../src/search/projection-store.ts | 43 + .../core-runtime/src/search/projection.ts | 91 +- .../core-runtime/src/search/query-runtime.ts | 29 + .../src/testing/live-operations.ts | 6 +- .../integration/action-permission.test.ts | 66 +- .../integration/search-persistence.test.ts | 14 +- .../tests/unit/action-definition.test.ts | 2 +- .../tests/unit/action-identity.test.ts | 12 +- .../tests/unit/action-runtime.test.ts | 16 +- .../unit/commit-recovery-metadata.test.ts | 4 +- .../tests/unit/context-access.test.ts | 7 +- .../tests/unit/governed-read-http.test.ts | 2 +- .../http-principal-authentication.test.ts | 4 +- .../tests/unit/operation-context.test.ts | 6 +- .../tests/unit/outbox-definition.test.ts | 2 +- .../tests/unit/principal-management.test.ts | 13 +- .../tests/unit/read-runtime.test.ts | 8 +- .../tests/unit/scoped-transaction.test.ts | 2 +- .../tests/unit/search-ingestion.test.ts | 12 +- .../tests/unit/search-projection.test.ts | 200 +- .../unit/system-principal-context.test.ts | 10 +- .../tests/unit/tenant-module-state.test.ts | 6 +- .../gateway-principal-verifier/package.json | 4 +- app/packages/shared-contracts/package.json | 7 +- .../shared-contracts/src/gateway-context.ts | 2 +- app/packages/shared-contracts/tsconfig.json | 4 +- ...dule-federation__bridge-react@2.9.0.patch} | 138 - ...@module-federation__dts-plugin@2.9.0.patch | 26 + ...odule-federation__modern-js-v3@2.8.0.patch | 268 - ...odule-federation__modern-js-v3@2.9.0.patch | 268 + ...odule-federation__runtime-core@2.9.0.patch | 13 + .../@tanstack__router-core@1.171.21.patch | 39 - app/patches/msgpackr@2.1.0.patch | 30 + app/patches/zod@4.5.4.patch | 56 + app/pnpm-lock.yaml | 5953 ++++++++--------- app/pnpm-workspace.yaml | 156 +- .../audit-database-trust-boundaries.mts | 102 +- .../authorization/rollout-contract.mts | 5 +- .../check-database-access-boundaries.mts | 4 +- .../check-module-entrypoint-boundaries.mts | 9 +- app/scripts/check-ontos-module-contracts.mts | 4 +- .../check-ultramodern-i18n-boundaries.mts | 1 + .../database-trust-audit/collect-snapshot.mts | 579 +- app/scripts/database-trust-audit/report.mts | 2 +- app/scripts/ensure-local-environment.mts | 63 +- .../generate-ontos-module-contract.mts | 33 +- .../generate-outbox-worker-deployment.mjs | 53 +- app/scripts/generate-tanstack-routes.mts | 21 +- app/scripts/generated-module-api-boundary.mts | 6 +- app/scripts/install-zerops-node.sh | 4 +- app/scripts/local-environment-values.test.mts | 22 +- app/scripts/materialize-outbox-worker.mjs | 12 +- app/scripts/materialize-zerops-runtime.mjs | 2 +- .../microvertical-api-baseline-boundary.mts | 10 +- .../migrate-contacts-authorization.mts | 44 +- .../module-federation-bridge-boundary.mts | 93 + app/scripts/plan-deployment-impact.mts | 27 +- .../postgres/bootstrap-runtime-role.mts | 78 +- .../postgres/bootstrap-spicedb-database.mts | 104 +- app/scripts/proof-workerd-ssr.mts | 92 +- ...eport-fail-closed-authorization-impact.mts | 206 +- app/scripts/reset-workspace-dependencies.mjs | 15 +- app/scripts/run-zerops-migrator.mjs | 46 +- .../scaffolding/action-service/scaffold.mts | 5 +- app/scripts/scaffolding/action/scaffold.mts | 3 +- app/scripts/scaffolding/generator-adapter.mts | 25 +- .../microvertical-page/scaffold.mts | 3 +- .../scaffolding/module-api/scaffold.mts | 2 +- .../scaffolding/outbox-message/scaffold.mts | 1 + .../scaffolding/public-component/scaffold.mts | 2 +- app/scripts/scaffolding/report/scaffold.mts | 2 +- app/scripts/scaffolding/resource/scaffold.mts | 1 + .../search-provider-access/scaffold.mts | 8 +- .../scaffolding/search-provider/scaffold.mts | 2 +- app/scripts/scaffolding/shared.mts | 21 +- .../tests/module-contract-generator.test.mts | 787 ++- .../tests/resource-generator.test.mts | 2 + .../tests/retire-contribution.test.mts | 6 +- .../tests/scaffold-generators.test.mts | 119 +- app/scripts/setup-agent-reference-repos.mts | 762 +-- app/scripts/shared/ultramodern-command.mts | 6 +- app/scripts/shared/ultramodern-launch.mts | 4 +- .../shared/ultramodern-wrapper-source.mts | 4 +- app/scripts/tests/api-only-tooling.test.mts | 630 +- .../audit-database-trust-boundaries.test.mts | 147 +- .../authorization-rollout-contract.test.mts | 45 +- .../check-authorization-readiness.test.mts | 41 +- .../tests/database-access-boundaries.test.mts | 8 +- app/scripts/tests/locki-feature.test.mts | 74 +- .../migrate-contacts-authorization.test.mts | 61 +- .../tests/outbox-worker-delivery.test.mts | 74 +- .../protected-entrypoint-inventory.test.mts | 26 +- .../tests/quality-audit-model.test.mts | 22 +- .../quality-audit-runtime-model.test.mts | 9 +- .../tests/quality-audit-test-support.mts | 8 +- ...-fail-closed-authorization-impact.test.mts | 40 +- .../typecheck-project-references.test.mts | 153 +- ...tramodern-performance-readiness.config.mjs | 3 +- .../validate-ultramodern-workspace.mts | 176 +- app/scripts/verify-application-db-schema.mts | 63 +- app/topology/reference-topology.json | 4 +- .../api/party-registry-production-layers.ts | 2 + app/verticals/party-registry/modern.config.ts | 7 +- .../module-federation.config.ts | 19 +- app/verticals/party-registry/package.json | 48 +- .../src/routes/ultramodern-route-metadata.ts | 2 +- .../integration/governed-identity.test.ts | 2 + app/verticals/party-registry/tsconfig.json | 15 +- .../party-registry/tsconfig.mf-types.json | 10 +- app/zerops.yaml | 34 +- 139 files changed, 7750 insertions(+), 5794 deletions(-) create mode 100644 app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx create mode 100644 app/packages/core-runtime/src/search/projection-store.ts create mode 100644 app/packages/core-runtime/src/search/query-runtime.ts rename app/patches/{@module-federation__bridge-react@2.8.0.patch => @module-federation__bridge-react@2.9.0.patch} (51%) create mode 100644 app/patches/@module-federation__dts-plugin@2.9.0.patch delete mode 100644 app/patches/@module-federation__modern-js-v3@2.8.0.patch create mode 100644 app/patches/@module-federation__modern-js-v3@2.9.0.patch create mode 100644 app/patches/@module-federation__runtime-core@2.9.0.patch delete mode 100644 app/patches/@tanstack__router-core@1.171.21.patch create mode 100644 app/patches/msgpackr@2.1.0.patch create mode 100644 app/patches/zod@4.5.4.patch create mode 100644 app/scripts/module-federation-bridge-boundary.mts diff --git a/.github/workflows/ultramodern-workspace-gates.yml b/.github/workflows/ultramodern-workspace-gates.yml index fbc5d7094..57a0439d0 100644 --- a/.github/workflows/ultramodern-workspace-gates.yml +++ b/.github/workflows/ultramodern-workspace-gates.yml @@ -74,7 +74,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "26.5.0" + node-version: "26.7.0" - name: Setup mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 @@ -128,7 +128,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "26.5.0" + node-version: "26.7.0" - name: Setup mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 @@ -184,7 +184,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "26.5.0" + node-version: "26.7.0" - name: Setup mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 @@ -229,7 +229,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "26.5.0" + node-version: "26.7.0" - name: Setup mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 @@ -277,7 +277,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "26.5.0" + node-version: "26.7.0" - name: Setup mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 diff --git a/app/.mise.toml b/app/.mise.toml index 959880fc3..aa82777f4 100644 --- a/app/.mise.toml +++ b/app/.mise.toml @@ -2,5 +2,5 @@ pnpm = "npm:pnpm" [tools] -node = "26.5.0" +node = "26.7.0" pnpm = "11.25.0" diff --git a/app/.modernjs/release-cohort.json b/app/.modernjs/release-cohort.json index 56e219189..ac9360f52 100644 --- a/app/.modernjs/release-cohort.json +++ b/app/.modernjs/release-cohort.json @@ -2,17 +2,20 @@ "aliases": { "@modern-js/adapter-rstest": "@bleedingdev/modern-js-adapter-rstest", "@modern-js/app-tools": "@bleedingdev/modern-js-app-tools", + "@modern-js/app-tools-extensions": "@bleedingdev/modern-js-app-tools-extensions", "@modern-js/bff-core": "@bleedingdev/modern-js-bff-core", + "@modern-js/bff-effect": "@bleedingdev/modern-js-bff-effect", "@modern-js/bff-runtime": "@bleedingdev/modern-js-bff-runtime", "@modern-js/builder": "@bleedingdev/modern-js-builder", "@modern-js/code-tools": "@bleedingdev/modern-js-code-tools", - "@modern-js/create": "@bleedingdev/modern-js-create", "@modern-js/create-request": "@bleedingdev/modern-js-create-request", + "@modern-js/i18n-runtime-extensions": "@bleedingdev/modern-js-i18n-runtime-extensions", "@modern-js/i18n-utils": "@bleedingdev/modern-js-i18n-utils", "@modern-js/image": "@bleedingdev/modern-js-image", "@modern-js/main-doc": "@bleedingdev/modern-js-main-doc", "@modern-js/plugin": "@bleedingdev/modern-js-plugin", "@modern-js/plugin-bff": "@bleedingdev/modern-js-plugin-bff", + "@modern-js/plugin-bff-extensions": "@bleedingdev/modern-js-plugin-bff-extensions", "@modern-js/plugin-data-loader": "@bleedingdev/modern-js-plugin-data-loader", "@modern-js/plugin-i18n": "@bleedingdev/modern-js-plugin-i18n", "@modern-js/plugin-polyfill": "@bleedingdev/modern-js-plugin-polyfill", @@ -22,6 +25,7 @@ "@modern-js/prod-server": "@bleedingdev/modern-js-prod-server", "@modern-js/render": "@bleedingdev/modern-js-render", "@modern-js/runtime": "@bleedingdev/modern-js-runtime", + "@modern-js/runtime-extensions": "@bleedingdev/modern-js-runtime-extensions", "@modern-js/runtime-utils": "@bleedingdev/modern-js-runtime-utils", "@modern-js/sandpack-react": "@bleedingdev/modern-js-sandpack-react", "@modern-js/server": "@bleedingdev/modern-js-server", @@ -31,178 +35,210 @@ "@modern-js/server-utils": "@bleedingdev/modern-js-server-utils", "@modern-js/tsconfig": "@bleedingdev/modern-js-tsconfig", "@modern-js/types": "@bleedingdev/modern-js-types", + "@modern-js/ultramodern-create": "@bleedingdev/modern-js-ultramodern-create", + "@modern-js/ultramodern-sandpack-profile": "@bleedingdev/modern-js-ultramodern-sandpack-profile", "@modern-js/utils": "@bleedingdev/modern-js-utils" }, "packages": [ { "sourceName": "@modern-js/adapter-rstest", "targetName": "@bleedingdev/modern-js-adapter-rstest", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/app-tools", "targetName": "@bleedingdev/modern-js-app-tools", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/app-tools-extensions", + "targetName": "@bleedingdev/modern-js-app-tools-extensions", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/bff-core", "targetName": "@bleedingdev/modern-js-bff-core", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/bff-effect", + "targetName": "@bleedingdev/modern-js-bff-effect", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/bff-runtime", "targetName": "@bleedingdev/modern-js-bff-runtime", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/builder", "targetName": "@bleedingdev/modern-js-builder", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/code-tools", "targetName": "@bleedingdev/modern-js-code-tools", - "version": "3.8.2-ultramodern.12" - }, - { - "sourceName": "@modern-js/create", - "targetName": "@bleedingdev/modern-js-create", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/create-request", "targetName": "@bleedingdev/modern-js-create-request", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/i18n-runtime-extensions", + "targetName": "@bleedingdev/modern-js-i18n-runtime-extensions", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/i18n-utils", "targetName": "@bleedingdev/modern-js-i18n-utils", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/image", "targetName": "@bleedingdev/modern-js-image", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/main-doc", "targetName": "@bleedingdev/modern-js-main-doc", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin", "targetName": "@bleedingdev/modern-js-plugin", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-bff", "targetName": "@bleedingdev/modern-js-plugin-bff", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/plugin-bff-extensions", + "targetName": "@bleedingdev/modern-js-plugin-bff-extensions", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-data-loader", "targetName": "@bleedingdev/modern-js-plugin-data-loader", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-i18n", "targetName": "@bleedingdev/modern-js-plugin-i18n", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-polyfill", "targetName": "@bleedingdev/modern-js-plugin-polyfill", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-ssg", "targetName": "@bleedingdev/modern-js-plugin-ssg", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-styled-components", "targetName": "@bleedingdev/modern-js-plugin-styled-components", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/plugin-tanstack", "targetName": "@bleedingdev/modern-js-plugin-tanstack", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/prod-server", "targetName": "@bleedingdev/modern-js-prod-server", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/render", "targetName": "@bleedingdev/modern-js-render", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/runtime", "targetName": "@bleedingdev/modern-js-runtime", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/runtime-extensions", + "targetName": "@bleedingdev/modern-js-runtime-extensions", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/runtime-utils", "targetName": "@bleedingdev/modern-js-runtime-utils", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/sandpack-react", "targetName": "@bleedingdev/modern-js-sandpack-react", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/server", "targetName": "@bleedingdev/modern-js-server", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/server-core", "targetName": "@bleedingdev/modern-js-server-core", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/server-runtime", "targetName": "@bleedingdev/modern-js-server-runtime", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/server-runtime-extensions", "targetName": "@bleedingdev/modern-js-server-runtime-extensions", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/server-utils", "targetName": "@bleedingdev/modern-js-server-utils", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/tsconfig", "targetName": "@bleedingdev/modern-js-tsconfig", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/types", "targetName": "@bleedingdev/modern-js-types", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/ultramodern-create", + "targetName": "@bleedingdev/modern-js-ultramodern-create", + "version": "3.9.0-ultramodern.2" + }, + { + "sourceName": "@modern-js/ultramodern-sandpack-profile", + "targetName": "@bleedingdev/modern-js-ultramodern-sandpack-profile", + "version": "3.9.0-ultramodern.2" }, { "sourceName": "@modern-js/utils", "targetName": "@bleedingdev/modern-js-utils", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" } ], "release": { "tag": "latest", - "version": "3.8.2-ultramodern.12" + "version": "3.9.0-ultramodern.2" }, "schema": "bleedingdev.ultramodern.release-cohort", "schemaVersion": 1, "source": { - "commit": "69f2b5648e13a057261f22bb36cb2d8ca2d5962f", + "commit": "d2c75828230edf92775feca796c0960af754508f", "repository": "BleedingDev/ultramodern.js" } } diff --git a/app/.modernjs/ultramodern.json b/app/.modernjs/ultramodern.json index 84ea7e17d..0f732504a 100644 --- a/app/.modernjs/ultramodern.json +++ b/app/.modernjs/ultramodern.json @@ -3,7 +3,7 @@ "profile": "cloudflare-ssr-mf-effect-v1", "generator": { "package": "@modern-js/create", - "version": "3.8.1-ultramodern.2" + "version": "3.9.0-ultramodern.2" }, "workspace": { "packageScope": "app", @@ -12,13 +12,14 @@ "version": "11.25.0" }, "node": { - "version": "26.5.0", + "version": "26.7.0", "engineRange": ">=26" } }, "packageSource": { "strategy": "install", - "modernPackageVersion": "3.8.2-ultramodern.12", + "modernPackageVersion": "3.9.0-ultramodern.2", + "registry": "https://registry.npmjs.org", "aliasScope": "bleedingdev", "aliasPackageNamePrefix": "modern-js-" }, @@ -216,7 +217,11 @@ "exposes": [ "./PageContacts" ], - "ssr": true + "ssr": true, + "dts": { + "compilerInstance": "effect-tsgo", + "tsConfigPath": "./tsconfig.mf-types.json" + } }, "backendFederation": { "role": "microvertical-server", @@ -264,9 +269,9 @@ "ssr": { "workerEntry": ".output/server/index.mjs", "workerManifest": ".output/server/modern-worker-manifest.json", + "effectBffBundle": ".output/worker/__modern_bff_effect.js", "routeManifest": ".output/server/route.json", "ssrBundle": ".output/worker/index.js", - "effectBffBundle": ".output/worker/__modern_bff_effect.js", "assetsBinding": "ASSETS" }, "zephyr": { @@ -304,8 +309,8 @@ "compatibility": { "contractVersion": "microvertical-server-effect-v1", "packageName": "@app/party-registry", - "effectVersion": "4.0.0-beta.107", - "moduleFederationVersion": "2.8.0" + "effectVersion": "4.0.0-rc.112", + "moduleFederationVersion": "2.9.0" }, "cache": { "cloudflareSnapshot": "immutable", @@ -556,9 +561,9 @@ "ssr": { "workerEntry": ".output/server/index.mjs", "workerManifest": ".output/server/modern-worker-manifest.json", + "effectBffBundle": ".output/worker/__modern_bff_effect.js", "routeManifest": ".output/server/route.json", "ssrBundle": ".output/worker/index.js", - "effectBffBundle": ".output/worker/__modern_bff_effect.js", "assetsBinding": "ASSETS" }, "zephyr": { @@ -608,7 +613,7 @@ ] }, "tooling": { - "command": "modern-js-create ultramodern", + "command": "ultramodern-create ultramodern", "wrappers": { "validate": "scripts/validate-ultramodern-workspace.mts", "typecheck": "scripts/ultramodern-typecheck.mts", diff --git a/app/apps/shell-super-app/modern.config.ts b/app/apps/shell-super-app/modern.config.ts index c69a408be..2a84a3d0c 100644 --- a/app/apps/shell-super-app/modern.config.ts +++ b/app/apps/shell-super-app/modern.config.ts @@ -7,7 +7,7 @@ import { } from '../../packages/shared-contracts/tooling/modern-config.ts'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; -import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools'; +import { appTools, defineConfig, presetUltramodern, ultramodernReleaseEnvelopePlugin } from '@modern-js/app-tools'; import type { AppTools, AppToolsUserConfig, CliPlugin } from '@modern-js/app-tools'; import { getBuildConfigEnvironment, withBuildConfigEnvironment } from '@modern-js/app-tools/config'; import { bffPlugin } from '@modern-js/plugin-bff'; @@ -296,6 +296,7 @@ export default defineConfig( }, plugins: [ appTools(), + ultramodernReleaseEnvelopePlugin(), bffPlugin(), tanstackRouterPlugin(), i18nPlugin({ diff --git a/app/apps/shell-super-app/module-federation.config.ts b/app/apps/shell-super-app/module-federation.config.ts index d611290d2..19fb07685 100644 --- a/app/apps/shell-super-app/module-federation.config.ts +++ b/app/apps/shell-super-app/module-federation.config.ts @@ -112,6 +112,9 @@ const reactDomVersion = packageVersion('react-dom'); const moduleFederationConfig: Parameters[0] = createModuleFederationConfig({ + bridge: { + enableBridgeRouter: false, + }, dts: { consumeTypes: true, generateTypes: false, diff --git a/app/apps/shell-super-app/package.json b/app/apps/shell-super-app/package.json index 886d6949e..fba126087 100644 --- a/app/apps/shell-super-app/package.json +++ b/app/apps/shell-super-app/package.json @@ -9,9 +9,9 @@ }, "scripts": { "dev": "modern dev", - "build": "modern build && node ../../scripts/generate-public-surface-assets.mts --app shell-super-app --target dist && MODERNJS_DEPLOY=node modern deploy --skip-build", - "cloudflare:build": "MODERNJS_DEPLOY=cloudflare modern build && node ../../scripts/generate-public-surface-assets.mts --app shell-super-app --target cloudflare-dist && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build && node ../../scripts/verify-cloudflare-output.mts --app shell-super-app", - "cloudflare:deploy": "ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json", + "build": "modern build && node ../../scripts/generate-public-surface-assets.mts --app shell-super-app --target dist && cross-env MODERNJS_DEPLOY=node modern deploy --skip-build", + "cloudflare:build": "cross-env MODERNJS_DEPLOY=cloudflare modern build && node ../../scripts/generate-public-surface-assets.mts --app shell-super-app --target cloudflare-dist && cross-env MODERNJS_DEPLOY=cloudflare modern deploy --skip-build && node ../../scripts/verify-cloudflare-output.mts --app shell-super-app", + "cloudflare:deploy": "cross-env ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json", "cloudflare:preview": "pnpm run cloudflare:build && wrangler dev --config .output/wrangler.json", "cloudflare:proof": "node ../../scripts/proof-cloudflare-version.mts --app shell-super-app", "db:generate": "drizzle-kit generate --config drizzle.auth.config.ts", @@ -33,45 +33,46 @@ "@authzed/authzed-node": "1.6.1", "@better-auth/api-key": "1.7.2", "@better-auth/drizzle-adapter": "1.7.2", - "@effect/sql-pg": "4.0.0-beta.107", - "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12", - "@modern-js/plugin-i18n": "npm:@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12", - "@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12", - "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12", - "@module-federation/modern-js-v3": "2.8.0", - "@tanstack/react-router": "1.170.25", + "@effect/sql-pg": "4.0.0-rc.112", + "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2", + "@modern-js/plugin-i18n": "npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2", + "@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2", + "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2", + "@module-federation/modern-js-v3": "2.9.0", + "@tanstack/react-router": "1.170.33", "@techsio/ui-kit": "0.25.1", "better-auth": "1.7.2", "drizzle-orm": "1.0.0-rc.5-ab785fc", - "effect": "4.0.0-beta.107", - "i18next": "26.3.6", + "effect": "4.0.0-rc.112", + "i18next": "26.4.2", "jose": "6.2.5", "pg": "8.22.0", "react": "19.2.8", "react-dom": "19.2.8", - "react-router": "7.18.1" + "@effect/opentelemetry": "4.0.0-rc.112" }, "devDependencies": { "@cloudflare/workers-types": "5.20260810.1", - "@modern-js/adapter-rstest": "npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12", - "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12", + "@modern-js/adapter-rstest": "npm:@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2", + "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2", "@playwright/test": "1.61.0", "@rsbuild/plugin-tailwindcss": "^2.0.3", "@rstest/core": "0.11.10", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", - "@types/node": "^26.2.0", + "@types/node": "^26.4.1", "@types/pg": "8.20.0", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", "bun-types": "1.4.0", "drizzle-kit": "1.0.0-rc.5-ab785fc", "happy-dom": "20.8.3", - "tailwindcss": "^4.3.2", + "tailwindcss": "^4.3.3", "typescript": "7.0.2", - "wrangler": "4.110.0", - "zephyr-rspack-plugin": "1.2.4" + "wrangler": "4.116.0", + "zephyr-rspack-plugin": "1.2.4", + "cross-env": "10.1.0" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx b/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx index 3dc9036b8..9ec45012c 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx +++ b/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx @@ -1,16 +1,17 @@ -import { Effect, Match, Option, Predicate, Schema } from 'effect'; -import type { Cause } from 'effect'; -import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; +import { Link as LocalizedLink, useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { useNavigate } from '@modern-js/plugin-tanstack/runtime'; import { Button } from '@techsio/ui-kit/atoms/button'; import { Link } from '@techsio/ui-kit/atoms/link'; import { FormInput } from '@techsio/ui-kit/molecules/form-input'; import { Toaster, useToast } from '@techsio/ui-kit/molecules/toast'; +import { Effect, Match, Option, Predicate, Schema } from 'effect'; +import type { Cause } from 'effect'; import { useRef, useState } from 'react'; + +import { SignInPayloadSchema } from '../../../../shared/api.ts'; import { signIn } from '../../../api/auth-client.ts'; import type { ShellAuthenticationClientError } from '../../../api/auth-client.ts'; import { runBrowserEffect } from '../../../runtime/browser-effect-runtime.ts'; -import { SignInPayloadSchema } from '../../../../shared/api.ts'; import { UltramodernRouteHead } from '../../ultramodern-route-head'; interface LoginValidation { @@ -140,7 +141,7 @@ const LoginPage = () => {
- + {t('shell.login.back')}
diff --git a/app/apps/shell-super-app/src/routes/[lang]/page.tsx b/app/apps/shell-super-app/src/routes/[lang]/page.tsx index 513015a4c..6a0b0a858 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/page.tsx +++ b/app/apps/shell-super-app/src/routes/[lang]/page.tsx @@ -1,18 +1,19 @@ -import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; +import { Link as LocalizedLink, useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { useLoaderData } from '@modern-js/plugin-tanstack/runtime'; import { LinkButton } from '@techsio/ui-kit/atoms/link-button'; import { StatusText } from '@techsio/ui-kit/atoms/status-text'; -import type { HomePageModel } from './page.data.ts'; + import { AuthenticatedDashboardLayout } from '../shell-frame'; import { UltramodernRouteHead } from '../ultramodern-route-head'; import { useShellControls } from '../use-shell-controls.ts'; +import type { HomePageModel } from './page.data.ts'; interface HomeViewProps { readonly initialModel: HomePageModel; } export const HomeView = ({ initialModel }: HomeViewProps) => { - const { language, t } = useModernI18n(); + const { t } = useModernI18n(); const model = initialModel; const controls = useShellControls(model.state === 'authenticated' ? model : undefined); @@ -21,7 +22,7 @@ export const HomeView = ({ initialModel }: HomeViewProps) => { <>
- + {t('shell.auth.loginLink')}
diff --git a/app/apps/shell-super-app/src/routes/[lang]/search/page.tsx b/app/apps/shell-super-app/src/routes/[lang]/search/page.tsx index 00d2ccfda..98639c0ef 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/search/page.tsx +++ b/app/apps/shell-super-app/src/routes/[lang]/search/page.tsx @@ -1,10 +1,11 @@ /* eslint-disable no-negated-condition, unicorn/no-negated-condition -- Closed route states read most clearly as error-versus-ready branches. expires: 2026-12-31. */ -import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; +import { Link as LocalizedLink, useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { useLoaderData } from '@modern-js/plugin-tanstack/runtime'; import { Badge } from '@techsio/ui-kit/atoms/badge'; import { LinkButton } from '@techsio/ui-kit/atoms/link-button'; import { StatusText } from '@techsio/ui-kit/atoms/status-text'; import { Match } from 'effect'; + import type { ShellSearchResult } from '../../../../shared/api.ts'; import { ShellContentLayout } from '../../shell-content-layout.tsx'; import { useShellControls } from '../../use-shell-controls.ts'; @@ -16,7 +17,7 @@ const SearchResultItem = ({ readonly currentTenantId: string; readonly result: ShellSearchResult; }) => { - const { language, t } = useModernI18n(); + const { t } = useModernI18n(); const party = Match.value(result).pipe( Match.when({ kind: 'resource' }, () => null), Match.when({ kind: 'party' }, (partyResult) => partyResult), @@ -35,7 +36,13 @@ const SearchResultItem = ({ key={`${result.ref.moduleId}:${result.ref.resourceType}:${result.ref.resourceId}`} > {result.title} diff --git a/app/apps/shell-super-app/src/routes/ultramodern-route-metadata.ts b/app/apps/shell-super-app/src/routes/ultramodern-route-metadata.ts index 835a449a1..3476fff34 100644 --- a/app/apps/shell-super-app/src/routes/ultramodern-route-metadata.ts +++ b/app/apps/shell-super-app/src/routes/ultramodern-route-metadata.ts @@ -1,4 +1,4 @@ -// @generated by @modern-js/create. +// @generated by @modern-js/ultramodern-create. // Author route metadata in colocated src/routes/**/route.meta.ts files. // This compatibility manifest is regenerated from route-owned metadata. @@ -169,10 +169,6 @@ export const ultramodernLocalisedUrls = { cs: '/contacts', en: '/contacts', }, - '/hledat': { - cs: '/hledat', - en: '/search', - }, '/login': { cs: '/login', en: '/login', @@ -189,8 +185,4 @@ export const ultramodernLocalisedUrls = { cs: '/hledat', en: '/search', }, - '/zdroje/:moduleId/:resourceType/:resourceId': { - cs: '/zdroje/:moduleId/:resourceType/:resourceId', - en: '/resources/:moduleId/:resourceType/:resourceId', - }, } as const; diff --git a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx index 957199f87..4082659cc 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx @@ -3,7 +3,8 @@ import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Effect, Schema } from 'effect'; -import type { ReactNode } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; + import { AppIdSchema, GroupKeySchema, @@ -15,17 +16,72 @@ import { TenantAuthenticationRequiredProblemSchema, TenantIdSchema, } from '../../../../shared/api.ts'; -import { HomeView } from '../../../../src/routes/[lang]/page.tsx'; import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; +import { HomeView } from '../../../../src/routes/[lang]/page.tsx'; +import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; -const { navigateMock, runBrowserEffectMock, signOutMock, switchLegalEntityMock, switchTenantMock } = - rstest.hoisted(() => ({ +type LocalizedLinkDoubleProps = Omit, 'href'> & { + readonly children?: ReactNode; + readonly href?: string | undefined; + readonly params?: Readonly>; + readonly to: string; +}; + +const { + languageState, + localizedLinkCalls, + navigateMock, + runBrowserEffectMock, + signOutMock, + switchLegalEntityMock, + switchTenantMock, +} = rstest.hoisted(() => { + const recordedLinkCalls: { + href: string | undefined; + params: Readonly> | undefined; + to: string; + }[] = []; + return { + languageState: { current: 'en' }, + localizedLinkCalls: recordedLinkCalls, navigateMock: rstest.fn(), runBrowserEffectMock: rstest.fn(), signOutMock: rstest.fn(), switchLegalEntityMock: rstest.fn(), switchTenantMock: rstest.fn(), - })); + }; +}); + +const localisedUrlPatterns = new Map>>( + Object.entries(ultramodernLocalisedUrls).map( + ([canonicalPattern, localisedPatterns]): readonly [ + string, + Readonly>, + ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], + ), +); + +/** + * Resolves the destination the framework link would produce, using the + * application's own canonical-to-localised route map instead of a hand-written + * expectation, so the page is proven to hand over a language-agnostic target. + */ +const resolveLocalizedHref = ( + to: string, + params: Readonly> | undefined, + language: string, +): string => { + const canonicalPattern = to.replaceAll('$', ':'); + const localisedPattern = + localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; + const segments = localisedPattern + .split('/') + .filter(Boolean) + .map((segment) => + segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, + ); + return `/${[language, ...segments].join('/')}`; +}; const translations = new Map( Object.entries({ @@ -63,14 +119,20 @@ const translations = new Map( ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ - Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => ( - - {children} - - ), - useLocalizedLocation: () => ({ alternates: { cs: '/cs/', en: '/en/' }, canonical: '/en/' }), + Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { + localizedLinkCalls.push({ href, params, to }); + return ( + + {children} + + ); + }, + useLocalizedLocation: () => ({ + alternates: { cs: '/cs/', en: '/en/' }, + canonical: '/en/', + }), useModernI18n: () => ({ - language: 'en', + language: languageState.current, t: (key: string) => translations.get(key) ?? key, }), })); @@ -105,7 +167,7 @@ const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); -const authenticatedModel = (): HomePageModel => ({ +const authenticatedModel = (options?: { readonly moduleEnabled?: boolean }): HomePageModel => ({ contextState: 'authenticated', identity: { displayName: 'Ada Lovelace', @@ -124,7 +186,7 @@ const authenticatedModel = (): HomePageModel => ({ items: [ { appId: inventoryAppId, - enabled: true, + enabled: options?.moduleEnabled ?? true, groupKey: navigationGroupKey, href: '/modules/inventory.stock', label: 'Inventory', @@ -161,6 +223,8 @@ beforeEach(() => { afterEach(() => { cleanup(); + languageState.current = 'en'; + localizedLinkCalls.length = 0; rstest.clearAllMocks(); }); @@ -170,6 +234,34 @@ test('anonymous home exposes only the localized login action', () => { expect(screen.queryByRole('banner')).toBeNull(); }); +test('the anonymous login action hands a canonical target to the framework link', () => { + render(); + const loginCall = localizedLinkCalls.find((call) => call.to === '/login'); + expect(loginCall).toBeDefined(); + expect(loginCall?.params).toBeUndefined(); + expect(loginCall?.href).toBeUndefined(); +}); + +test('the anonymous login action resolves Czech from the same canonical target', () => { + languageState.current = 'cs'; + render(); + expect(localizedLinkCalls.map((call) => call.to)).toContain('/login'); + expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/cs/login'); +}); + +test('the unavailable dashboard exposes no navigable affordance', () => { + render(); + expect(screen.queryAllByRole('link')).toHaveLength(0); + expect(localizedLinkCalls).toHaveLength(0); +}); + +test('a disabled module affordance stays non-interactive text', () => { + render(); + expect(screen.queryByRole('link', { name: 'Inventory' })).toBeNull(); + expect(screen.getByText('Inventory')).toBeTruthy(); + expect(localizedLinkCalls.map((call) => call.to)).not.toContain('/modules/inventory.stock'); +}); + test('authenticated home renders server-composed navigation and selected legal context', () => { render(); expect(screen.getByRole('link', { name: 'Inventory' }).getAttribute('href')).toBe( @@ -219,7 +311,10 @@ test('logout clears the authenticated composition together', async () => { await user.click(screen.getByRole('button', { name: 'Ada Lovelace' })); await user.click(await screen.findByRole('menuitem', { name: 'Logout' })); await waitFor(() => - expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '/en/login' }), + expect(navigateMock).toHaveBeenCalledWith({ + reloadDocument: true, + to: '/en/login', + }), ); }); diff --git a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx index eae9573be..9529db26d 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx @@ -1,16 +1,67 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; -import { Effect, Redacted } from 'effect'; +import { toaster } from '@techsio/ui-kit/molecules/toast'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { toaster } from '@techsio/ui-kit/molecules/toast'; +import { Effect, Redacted } from 'effect'; +import type { ComponentProps, ReactNode } from 'react'; + import LoginPage from '../../../../src/routes/[lang]/login/page'; +import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; -const { navigateMock, runBrowserEffectMock, signInMock } = rstest.hoisted(() => ({ - navigateMock: rstest.fn(async () => {}), - runBrowserEffectMock: rstest.fn(), - signInMock: rstest.fn(), -})); +type LocalizedLinkDoubleProps = Omit, 'href'> & { + readonly children?: ReactNode; + readonly href?: string | undefined; + readonly params?: Readonly>; + readonly to: string; +}; + +const { languageState, localizedLinkCalls, navigateMock, runBrowserEffectMock, signInMock } = + rstest.hoisted(() => { + const recordedLinkCalls: { + href: string | undefined; + params: Readonly> | undefined; + to: string; + }[] = []; + return { + languageState: { current: 'en' }, + localizedLinkCalls: recordedLinkCalls, + navigateMock: rstest.fn(async () => {}), + runBrowserEffectMock: rstest.fn(), + signInMock: rstest.fn(), + }; + }); + +const localisedUrlPatterns = new Map>>( + Object.entries(ultramodernLocalisedUrls).map( + ([canonicalPattern, localisedPatterns]): readonly [ + string, + Readonly>, + ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], + ), +); + +/** + * Resolves the destination the framework link would produce, using the + * application's own canonical-to-localised route map instead of a hand-written + * expectation, so the page is proven to hand over a language-agnostic target. + */ +const resolveLocalizedHref = ( + to: string, + params: Readonly> | undefined, + language: string, +): string => { + const canonicalPattern = to.replaceAll('$', ':'); + const localisedPattern = + localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; + const segments = localisedPattern + .split('/') + .filter(Boolean) + .map((segment) => + segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, + ); + return `/${[language, ...segments].join('/')}`; +}; beforeEach(() => { runBrowserEffectMock.mockImplementation( @@ -43,6 +94,14 @@ const translations = new Map( ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ + Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { + localizedLinkCalls.push({ href, params, to }); + return ( + + {children} + + ); + }, useLocalizedLocation: () => ({ alternates: { cs: '/cs/login', @@ -51,7 +110,7 @@ rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ canonical: '/en/login', }), useModernI18n: () => ({ - language: 'en', + language: languageState.current, t: (key: string) => translations.get(key) ?? key, }), })); @@ -76,6 +135,8 @@ const renderLogin = () => render(); afterEach(() => { cleanup(); + languageState.current = 'en'; + localizedLinkCalls.length = 0; toaster.remove(); rstest.unstubAllGlobals(); rstest.clearAllMocks(); @@ -101,6 +162,25 @@ test('shows the required login controls through the UI kit', () => { ); }); +test('the back link hands the canonical home target to the framework link', () => { + renderLogin(); + + const homeCall = localizedLinkCalls.find((call) => call.to === '/'); + expect(homeCall).toBeDefined(); + expect(homeCall?.params).toBeUndefined(); + expect(homeCall?.href).toBeUndefined(); +}); + +test('the back link resolves Czech from the same canonical target', () => { + languageState.current = 'cs'; + renderLogin(); + + expect(localizedLinkCalls.map((call) => call.to)).toContain('/'); + expect(screen.getByRole('link', { name: '← Back to the home page' }).getAttribute('href')).toBe( + '/cs', + ); +}); + interface LoginValidationCase { readonly focus: 'login' | 'password' | 'submit'; readonly login: string; diff --git a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx new file mode 100644 index 000000000..0e8759484 --- /dev/null +++ b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx @@ -0,0 +1,307 @@ +import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; +import { cleanup, render, screen } from '@testing-library/react'; +import { Effect, Schema } from 'effect'; +import type { ComponentProps, ReactNode } from 'react'; + +import { + AppIdSchema, + GroupKeySchema, + LegalEntityIdSchema, + ModuleIdSchema, + PrincipalIdSchema, + ResourceIdSchema, + TenantIdSchema, +} from '../../../../shared/api.ts'; +import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; +import type { SearchPageModel } from '../../../../src/routes/[lang]/search/page.data.ts'; +import SearchPage from '../../../../src/routes/[lang]/search/page.tsx'; +import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; + +type LocalizedLinkDoubleProps = Omit, 'href'> & { + readonly children?: ReactNode; + readonly href?: string | undefined; + readonly params?: Readonly>; + readonly to: string; +}; + +const { + languageState, + localizedLinkCalls, + navigateMock, + runBrowserEffectMock, + signOutMock, + switchLegalEntityMock, + switchTenantMock, + useLoaderDataMock, +} = rstest.hoisted(() => { + const recordedLinkCalls: { + href: string | undefined; + params: Readonly> | undefined; + to: string; + }[] = []; + return { + languageState: { current: 'en' }, + localizedLinkCalls: recordedLinkCalls, + navigateMock: rstest.fn(async () => {}), + runBrowserEffectMock: rstest.fn(), + signOutMock: rstest.fn(), + switchLegalEntityMock: rstest.fn(), + switchTenantMock: rstest.fn(), + useLoaderDataMock: rstest.fn(), + }; +}); + +const localisedUrlPatterns = new Map>>( + Object.entries(ultramodernLocalisedUrls).map( + ([canonicalPattern, localisedPatterns]): readonly [ + string, + Readonly>, + ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], + ), +); + +/** + * Resolves the destination the framework link would produce, using the + * application's own canonical-to-localised route map instead of a hand-written + * expectation, so the page is proven to hand over a language-agnostic target. + */ +const resolveLocalizedHref = ( + to: string, + params: Readonly> | undefined, + language: string, +): string => { + const canonicalPattern = to.replaceAll('$', ':'); + const localisedPattern = + localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; + const segments = localisedPattern + .split('/') + .filter(Boolean) + .map((segment) => + segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, + ); + return `/${[language, ...segments].join('/')}`; +}; + +const translations = new Map( + Object.entries({ + 'shell.auth.identity.title': 'Authenticated identity', + 'shell.auth.logout.action': 'Logout', + 'shell.auth.logout.failed': 'Logout failed', + 'shell.dashboard.account.label': 'Account menu', + 'shell.dashboard.brand': 'OntOS', + 'shell.dashboard.header.label': 'Dashboard header', + 'shell.dashboard.legalEntity.accessibleLabel': 'Current legal entity', + 'shell.dashboard.navigation.home': 'Home', + 'shell.dashboard.navigation.label': 'Dashboard navigation', + 'shell.dashboard.sidebar.label': 'Dashboard sidebar', + 'shell.dashboard.tenant.accessibleLabel': 'Current tenant', + 'shell.dashboard.unavailable': 'Dashboard unavailable', + 'shell.modules.state.readOnly': 'Read only', + 'shell.search.empty': 'No results', + 'shell.search.label': 'Search this legal entity', + 'shell.search.selection_required': 'Select a legal entity first', + 'shell.search.submit': 'Search', + 'shell.search.title': 'Search', + 'shell.search.unavailable': 'Search unavailable', + }), +); + +rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ + Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { + localizedLinkCalls.push({ href, params, to }); + return ( + + {children} + + ); + }, + useLocalizedLocation: () => ({ + alternates: { cs: '/cs/hledat', en: '/en/search' }, + }), + useModernI18n: () => ({ + language: languageState.current, + t: (key: string) => translations.get(key) ?? key, + }), +})); + +rstest.mock('@modern-js/plugin-tanstack/runtime', () => ({ + useLoaderData: useLoaderDataMock, + useNavigate: () => navigateMock, +})); + +rstest.mock('../../../../src/api/auth-client.ts', () => ({ + signOut: signOutMock, + switchLegalEntity: switchLegalEntityMock, + switchTenant: switchTenantMock, +})); + +rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ + runBrowserEffect: runBrowserEffectMock, +})); + +const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( + '00000000-0000-4000-8000-000000000001', +); +const tenantId = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101'); +const legalEntityId = Schema.decodeUnknownSync(LegalEntityIdSchema)( + '00000000-0000-4000-8000-000000000201', +); +const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); +const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); +const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); +const plainResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit-1'); +const awkwardResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2'); + +const authenticatedShell = (): HomePageModel => ({ + contextState: 'authenticated', + identity: { + displayName: 'Ada Lovelace', + email: 'ada@example.test', + principalId, + tenantId, + }, + legalEntities: { + items: [{ legalEntityId, legalName: 'Alpha company' }], + state: 'available', + }, + navigation: { + items: [ + { + appId: inventoryAppId, + enabled: true, + groupKey: navigationGroupKey, + href: '/modules/inventory.stock', + label: 'Inventory', + moduleId: inventoryModuleId, + order: 10, + state: 'read_only', + unavailable: false, + writable: false, + }, + ], + state: 'available', + unavailableDeployments: [], + }, + selectedLegalEntityId: legalEntityId, + state: 'authenticated', + tenants: { + items: [{ name: 'Alpha tenant', tenantId }], + state: 'available', + }, +}); + +const readyModel = (resourceType: string, resourceId: typeof plainResourceId): SearchPageModel => ({ + query: 'unit', + response: { + partial: false, + results: [ + { + kind: 'resource', + ref: { moduleId: inventoryModuleId, resourceId, resourceType }, + title: 'Unit 1', + }, + ], + }, + shell: authenticatedShell(), + state: 'ready', +}); + +const resourceLinkCalls = () => + localizedLinkCalls.filter((call) => call.to.startsWith('/resources')); + +beforeEach(() => { + runBrowserEffectMock.mockImplementation( + async (effect: Effect.Effect) => await runEffectTestPromise(effect), + ); + signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); + switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId })); + switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId })); + useLoaderDataMock.mockReturnValue(readyModel('stock-item', plainResourceId)); +}); + +afterEach(() => { + cleanup(); + languageState.current = 'en'; + localizedLinkCalls.length = 0; + rstest.clearAllMocks(); +}); + +test('a search result hands the canonical resource route to the framework link', () => { + render(); + + const [resultCall] = resourceLinkCalls(); + expect(resourceLinkCalls()).toHaveLength(1); + expect(resultCall?.to).toBe('/resources/$moduleId/$resourceType/$resourceId'); + expect(resultCall?.params).toEqual({ + moduleId: 'inventory.stock', + resourceId: 'unit-1', + resourceType: 'stock-item', + }); + expect(resultCall?.href).toBeUndefined(); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/en/resources/inventory.stock/stock-item/unit-1', + ); +}); + +test('a search result resolves the Czech resource route from the same canonical target', () => { + languageState.current = 'cs'; + render(); + + expect(resourceLinkCalls()[0]?.to).toBe('/resources/$moduleId/$resourceType/$resourceId'); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/cs/zdroje/inventory.stock/stock-item/unit-1', + ); +}); + +test('resource path segments stay percent-encoded per segment', () => { + useLoaderDataMock.mockReturnValue(readyModel('stock item', awkwardResourceId)); + render(); + + expect(resourceLinkCalls()[0]?.params).toEqual({ + moduleId: 'inventory.stock', + resourceId: 'unit #1/2', + resourceType: 'stock item', + }); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/en/resources/inventory.stock/stock%20item/unit%20%231%2F2', + ); +}); + +test('an empty result set exposes no resource affordance', () => { + useLoaderDataMock.mockReturnValue({ + query: 'unit', + response: { partial: false, results: [] }, + shell: authenticatedShell(), + state: 'ready', + } satisfies SearchPageModel); + render(); + + expect(screen.getByText('No results')).toBeTruthy(); + expect(resourceLinkCalls()).toHaveLength(0); +}); + +test('an unavailable search exposes no resource affordance', () => { + useLoaderDataMock.mockReturnValue({ + query: 'unit', + shell: authenticatedShell(), + state: 'unavailable', + } satisfies SearchPageModel); + render(); + + expect(screen.getByText('Search unavailable')).toBeTruthy(); + expect(resourceLinkCalls()).toHaveLength(0); +}); + +test('a closed shell state exposes no navigable affordance at all', () => { + useLoaderDataMock.mockReturnValue({ + query: 'unit', + shell: { state: 'unavailable' }, + state: 'unavailable', + } satisfies SearchPageModel); + render(); + + expect(screen.getByText('Dashboard unavailable')).toBeTruthy(); + expect(screen.queryAllByRole('link')).toHaveLength(0); + expect(localizedLinkCalls).toHaveLength(0); +}); diff --git a/app/apps/shell-super-app/tsconfig.json b/app/apps/shell-super-app/tsconfig.json index 97d9677e4..e1f8a9596 100644 --- a/app/apps/shell-super-app/tsconfig.json +++ b/app/apps/shell-super-app/tsconfig.json @@ -8,27 +8,30 @@ "incremental": true, "noEmit": false, "outDir": "../../node_modules/.cache/tsgo/declarations/apps__shell-super-app", - "skipLibCheck": true, "tsBuildInfoFile": "../../node_modules/.cache/tsgo/apps__shell-super-app.tsbuildinfo", - "types": ["node", "bun-types/sqlite"] + "types": [ + "node", + "bun-types/sqlite" + ] }, "include": [ - "api", "src", "locales/**/*.json", "package.json", - "shared" + "shared", + "server", + "api" ], "references": [ - { - "path": "../../packages/core-runtime" - }, { "path": "../../packages/shared-contracts" }, { "path": "../../packages/shared-design-tokens" }, + { + "path": "../../packages/core-runtime" + }, { "path": "../../verticals/party-registry" } diff --git a/app/oxfmt.config.ts b/app/oxfmt.config.ts index 62dc36bb3..e4ebaf17f 100644 --- a/app/oxfmt.config.ts +++ b/app/oxfmt.config.ts @@ -2,7 +2,13 @@ import { defineConfig } from 'oxfmt'; import ultracite from 'ultracite/oxfmt'; export default defineConfig({ - extends: [ultracite], + ...ultracite, + printWidth: 100, + proseWrap: 'preserve', + trailingComma: 'all', + sortImports: false, + sortPackageJson: false, + sortTailwindcss: false, ignorePatterns: [ '.agents', '.codex/skills', diff --git a/app/package.json b/app/package.json index 6fa67c87e..7c6164614 100644 --- a/app/package.json +++ b/app/package.json @@ -34,8 +34,8 @@ "action:test:unit": "pnpm --filter @app/core-runtime action:test:unit", "action:test:integration": "pnpm --filter @app/core-runtime action:test:integration", "outbox:test": "pnpm --filter @app/core-runtime outbox:test:unit && pnpm --filter @app/core-runtime outbox:test:integration", - "build": "pnpm -r --filter \"./verticals/*\" run build && pnpm --filter \"./apps/shell-super-app\" run build && pnpm mf:types && pnpm performance:readiness", - "cloudflare:build": "pnpm -r --filter \"./verticals/*\" run cloudflare:build && pnpm --filter \"./apps/shell-super-app\" run cloudflare:build && ULTRAMODERN_MF_TYPES_ARCHIVE=dist-cloudflare/@mf-types.zip pnpm mf:types && pnpm cloudflare-output:verify && pnpm cloudflare:ssr-proof", + "build": "node ./scripts/ultramodern-typecheck.mts --build packages/shared-contracts/tsconfig.json && node ./scripts/ultramodern-typecheck.mts --build packages/shared-design-tokens/tsconfig.json && pnpm -r --filter \"./verticals/*\" run build && pnpm --filter \"./apps/shell-super-app\" run build && pnpm mf:types && pnpm performance:readiness", + "cloudflare:build": "node ./scripts/ultramodern-typecheck.mts --build packages/shared-contracts/tsconfig.json && node ./scripts/ultramodern-typecheck.mts --build packages/shared-design-tokens/tsconfig.json && pnpm -r --filter \"./verticals/*\" run cloudflare:build && pnpm --filter \"./apps/shell-super-app\" run cloudflare:build && pnpm mf:types --target cloudflare && pnpm cloudflare-output:verify && pnpm cloudflare:ssr-proof", "cloudflare:deploy": "pnpm -r --filter \"./verticals/*\" run cloudflare:deploy && pnpm --filter \"./apps/shell-super-app\" run cloudflare:deploy", "cloudflare:proof": "node ./scripts/proof-cloudflare-version.mts --out .codex/reports/cloudflare-version-proof/public-url-proof.json", "cloudflare-output:verify": "node ./scripts/verify-cloudflare-output.mts", @@ -94,30 +94,29 @@ "better-auth": "1.7.2", "drizzle-orm": "1.0.0-rc.5-ab785fc", "pg": "8.22.0", - "@effect/sql-pg": "4.0.0-beta.107" + "@effect/sql-pg": "4.0.0-rc.112" }, "devDependencies": { - "@effect/platform-node": "4.0.0-beta.107", - "@effect/tsgo": "0.19.0", + "@effect/platform-node": "4.0.0-rc.112", + "@effect/tsgo": "0.41.0", "@noble/hashes": "2.2.0", - "@modern-js/code-tools": "npm:@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12", + "@modern-js/code-tools": "npm:@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2", "@modern-js/codesmith": "2.6.9", - "@modern-js/create": "npm:@bleedingdev/modern-js-create@3.8.2-ultramodern.12", - "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12", - "@oxlint/plugins": "1.79.0", - "@types/node": "20.19.43", + "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2", + "@oxlint/plugins": "1.81.0", + "@types/node": "^26.4.1", "@types/pg": "8.20.0", "@typescript/native": "npm:typescript@7.0.2", "@typescript/native-preview": "npm:typescript@7.0.2", - "effect": "4.0.0-beta.107", + "effect": "4.0.0-rc.112", "esbuild": "0.28.1", "jose": "6.2.5", "lefthook": "^2.1.10", - "miniflare": "4.20260708.1", - "oxfmt": "0.64.0", - "oxlint": "1.79.0", + "miniflare": "4.20260730.0", + "oxfmt": "0.66.0", + "oxlint": "1.81.0", "oxc-parser": "0.147.0", - "ultracite": "7.10.7", + "ultracite": "7.11.0", "@nkzw/eslint-plugin": "2.0.0", "eslint-plugin-github": "6.1.2", "eslint-plugin-perfectionist": "5.10.1", @@ -127,7 +126,11 @@ "knip": "6.34.0", "jscpd": "5.1.2", "jsonc-parser": "3.3.1", - "fallow": "3.22.0" + "fallow": "3.22.0", + "@modern-js/ultramodern-create": "npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2", + "cross-env": "10.1.0", + "@effect/opentelemetry": "4.0.0-rc.112", + "eslint": "10.10.0" }, "engines": { "node": ">=26", diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json index 8fb9095af..9df17d216 100644 --- a/app/packages/core-runtime/package.json +++ b/app/packages/core-runtime/package.json @@ -33,14 +33,14 @@ }, "dependencies": { "@authzed/authzed-node": "1.6.1", - "@effect/platform-node": "4.0.0-beta.107", + "@effect/platform-node": "4.0.0-rc.112", "drizzle-orm": "1.0.0-rc.5-ab785fc", - "effect": "4.0.0-beta.107", + "effect": "4.0.0-rc.112", "pg": "8.22.0", - "@effect/sql-pg": "4.0.0-beta.107" + "@effect/sql-pg": "4.0.0-rc.112" }, "devDependencies": { - "@types/node": "^20.19.43", + "@types/node": "^26.4.1", "@types/pg": "8.20.0", "drizzle-kit": "1.0.0-rc.5-ab785fc" } diff --git a/app/packages/core-runtime/src/actions/collector.ts b/app/packages/core-runtime/src/actions/collector.ts index a8af4848a..d5883d5d7 100644 --- a/app/packages/core-runtime/src/actions/collector.ts +++ b/app/packages/core-runtime/src/actions/collector.ts @@ -335,7 +335,7 @@ export const createActionCollector = Effect.fail( invalidCollectorInput('The Domain Event payload violates its declared contract'), diff --git a/app/packages/core-runtime/src/actions/repository.ts b/app/packages/core-runtime/src/actions/repository.ts index 4a18837b3..81d70d1a8 100644 --- a/app/packages/core-runtime/src/actions/repository.ts +++ b/app/packages/core-runtime/src/actions/repository.ts @@ -859,8 +859,9 @@ export const makeActionRepository = (): ActionRepositoryService => { } if (input.evidence.outboxMessages.length > 0) { - const persistedOutboxMessages = yield* Effect.all( - input.evidence.outboxMessages.map((collected) => { + const persistedOutboxMessages = yield* Effect.forEach( + input.evidence.outboxMessages, + (collected) => { const persistedDomainEvent = persistedDomainEvents[collected.domainEventIndex]; if (persistedDomainEvent === undefined) { return Effect.fail( @@ -879,7 +880,7 @@ export const makeActionRepository = (): ActionRepositoryService => { tenantId: input.principal.tenantId, topic: collected.message.topic, }); - }), + }, { concurrency: 1 }, ); yield* transaction diff --git a/app/packages/core-runtime/src/auth/principal-administration-reads.ts b/app/packages/core-runtime/src/auth/principal-administration-reads.ts index 037b2e739..dcf1566ad 100644 --- a/app/packages/core-runtime/src/auth/principal-administration-reads.ts +++ b/app/packages/core-runtime/src/auth/principal-administration-reads.ts @@ -135,7 +135,7 @@ const services = ( }; }), Effect.flatMap((result) => - Schema.decodeUnknownEffect(ManagedResultJson)(result).pipe( + Schema.decodeEffect(ManagedResultJson)(result).pipe( Effect.mapError((cause) => readUnavailable('Managed identities are temporarily unavailable', cause), ), @@ -190,7 +190,7 @@ const services = ( nextOffset: rows.length > limit ? offset + limit : null, })), Effect.flatMap((result) => - Schema.decodeUnknownEffect(SelfResultJson)(result).pipe( + Schema.decodeEffect(SelfResultJson)(result).pipe( Effect.mapError((cause) => readUnavailable('Identity bindings are temporarily unavailable', cause), ), diff --git a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs index 41fe429d2..200a97cef 100644 --- a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs +++ b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs @@ -1,14 +1,22 @@ /* oxlint-disable typescript/consistent-return, typescript/no-unsafe-argument -- Existing compatibility boundary; expires: 2026-12-31. */ const { existsSync } = process.getBuiltinModule('node:fs'); const path = process.getBuiltinModule('node:path'); -const ambientEnvironmentDescriptor = Object.getOwnPropertyDescriptor(process, 'env'); +const ambientEnvironmentDescriptor = Object.getOwnPropertyDescriptor( + process, + 'env' +); const environmentValue = (name) => { const variableDescriptor = ambientEnvironmentDescriptor === undefined ? undefined - : Object.getOwnPropertyDescriptor(ambientEnvironmentDescriptor.value, name); - return variableDescriptor === undefined ? undefined : String(variableDescriptor.value); + : Object.getOwnPropertyDescriptor( + ambientEnvironmentDescriptor.value, + name + ); + return variableDescriptor === undefined + ? undefined + : String(variableDescriptor.value); }; const isAppWorkspace = (candidate) => @@ -39,7 +47,7 @@ const resolveAppWorkspaceRootSync = (startDirectory) => { */ const resolveWorkspaceEnvironmentSync = (candidates) => { const usableCandidates = candidates.filter( - (candidate) => candidate !== undefined && candidate.length > 0, + (candidate) => candidate !== undefined && candidate.length > 0 ); const APP_WORKSPACE_ROOT = usableCandidates diff --git a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts index 6e2d7451e..5c5b6e6a9 100644 --- a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts +++ b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts @@ -1,7 +1,9 @@ declare const bootstrapEnvironment: { readonly APP_ENV_PATH: string; readonly APP_WORKSPACE_ROOT: string; - readonly resolveAppWorkspaceRootSync: (startDirectory: string) => string | undefined; + readonly resolveAppWorkspaceRootSync: ( + startDirectory: string + ) => string | undefined; }; export = bootstrapEnvironment; diff --git a/app/packages/core-runtime/src/install/spicedb-database-config.ts b/app/packages/core-runtime/src/install/spicedb-database-config.ts index c3632e2a8..bb60a32b8 100644 --- a/app/packages/core-runtime/src/install/spicedb-database-config.ts +++ b/app/packages/core-runtime/src/install/spicedb-database-config.ts @@ -75,24 +75,22 @@ export const parseSpiceDbDatabaseBootstrapConfig = ( Schema.decodeUnknownResult(SpiceDbDatabaseBootstrapEnvironmentSchema)(environment), ); const admin = Result.getOrThrow( - Schema.decodeUnknownResult(PostgreSqlUrlSchema)(source.DATABASE_ADMIN_URL), + Schema.decodeResult(PostgreSqlUrlSchema)(source.DATABASE_ADMIN_URL), ); const spicedb = Result.getOrThrow( - Schema.decodeUnknownResult(PostgreSqlUrlSchema)(source.SPICEDB_DATABASE_URL), + Schema.decodeResult(PostgreSqlUrlSchema)(source.SPICEDB_DATABASE_URL), ); const pair = Result.getOrThrow( - Schema.decodeUnknownResult(SpiceDbDatabasePairSchema)({ + Schema.decodeResult(SpiceDbDatabasePairSchema)({ admin, spicedb, spicedbUser: decodeURIComponent( - Result.getOrThrow( - Schema.decodeUnknownResult(PercentEncodedUriComponentSchema)(spicedb.username), - ), + Result.getOrThrow(Schema.decodeResult(PercentEncodedUriComponentSchema)(spicedb.username)), ), }), ); const encodedPassword = Result.getOrThrow( - Schema.decodeUnknownResult(PercentEncodedUriComponentSchema)(pair.spicedb.password), + Schema.decodeResult(PercentEncodedUriComponentSchema)(pair.spicedb.password), ); return makeSpiceDbDatabaseBootstrapConfig({ diff --git a/app/packages/core-runtime/src/modules/application-composition.ts b/app/packages/core-runtime/src/modules/application-composition.ts index 57b4c0aa0..6150de334 100644 --- a/app/packages/core-runtime/src/modules/application-composition.ts +++ b/app/packages/core-runtime/src/modules/application-composition.ts @@ -393,7 +393,7 @@ export const validateApplicationCompositionCandidate = Effect.fnUntraced(functio ), ), ); - const observed = yield* Schema.decodeUnknownEffect(candidateEvidenceSchema)(evidence).pipe( + const observed = yield* Schema.decodeEffect(candidateEvidenceSchema)(evidence).pipe( Effect.catchTag('SchemaError', () => Effect.fail( new ApplicationCompositionValidationError({ diff --git a/app/packages/core-runtime/src/modules/runtime-registration.ts b/app/packages/core-runtime/src/modules/runtime-registration.ts index 179ea710c..889d20dfd 100644 --- a/app/packages/core-runtime/src/modules/runtime-registration.ts +++ b/app/packages/core-runtime/src/modules/runtime-registration.ts @@ -198,7 +198,7 @@ export const extractVerticalRuntimeSafeDescriptors = ( .map(({ descriptor }) => Object.freeze( Result.getOrThrow( - Schema.decodeUnknownResult(OntosActionContractSchema)({ + Schema.decodeResult(OntosActionContractSchema)({ actionKey: descriptor.actionKey, auditProfile: descriptor.auditProfile, entrypoint: descriptor.entrypoint, diff --git a/app/packages/core-runtime/src/outbox/poller.ts b/app/packages/core-runtime/src/outbox/poller.ts index ecb387bba..8b22f70e2 100644 --- a/app/packages/core-runtime/src/outbox/poller.ts +++ b/app/packages/core-runtime/src/outbox/poller.ts @@ -114,7 +114,7 @@ export const parseOutboxPollingConfig = ({ return decoded.pipe( Effect.flatMap((value) => - Schema.decodeUnknownEffect(ClaimOwner)(value.claimOwner).pipe( + Schema.decodeEffect(ClaimOwner)(value.claimOwner).pipe( Effect.map((claimOwner) => Object.freeze({ ...value, claimOwner })), ), ), diff --git a/app/packages/core-runtime/src/outbox/repository.ts b/app/packages/core-runtime/src/outbox/repository.ts index 930d64eec..56f72f947 100644 --- a/app/packages/core-runtime/src/outbox/repository.ts +++ b/app/packages/core-runtime/src/outbox/repository.ts @@ -121,7 +121,7 @@ const loadClaimCorrelationId = Effect.fnUntraced(function* loadClaimCorrelationI export const makeOutboxRepository = (executor: CoreDatabaseExecutor): OutboxRepositoryService => ({ claimNext: (registrations, claimOwner, now) => { if (registrations.length === 0) { - return Effect.succeed(Option.none()); + return Effect.succeedNone; } const byWorkerKey = new Map( registrations.map((registration) => [registration.descriptor.workerKey, registration]), diff --git a/app/packages/core-runtime/src/search/persistence.ts b/app/packages/core-runtime/src/search/persistence.ts index 1e1f9a7b7..1dfa53466 100644 --- a/app/packages/core-runtime/src/search/persistence.ts +++ b/app/packages/core-runtime/src/search/persistence.ts @@ -628,7 +628,7 @@ export const makePostgresCoreSearchProjectionStore = ( const transactionBody = (transaction: CoreTransaction) => transactionOperations.queryCandidatesTransaction(transaction, input); const documents = yield* runTransaction(transactionBody); - return yield* Schema.decodeUnknownEffect(Schema.Array(CoreSearchProjectionDocumentSchema))( + return yield* Schema.decodeEffect(Schema.Array(CoreSearchProjectionDocumentSchema))( documents, ).pipe(Effect.mapError(unavailable)); }); @@ -652,12 +652,8 @@ export const CoreSearchProjectionStoreLive = Layer.effect( }), ); -/** Fully composed production query layer; owner adapters never import Core database capabilities. */ +/** Query layer exposes its store requirement for composition at the application boundary. */ export const CoreSearchQueryRuntimeLive = Layer.effect( CoreSearchQueryRuntime, - Effect.gen(function* createCoreSearchQueryRuntimeLive() { - const database = yield* CoreDatabase; - const store = makePostgresCoreSearchProjectionStore(database); - return createCoreSearchQueryRuntime(store); - }), + createCoreSearchQueryRuntime, ); diff --git a/app/packages/core-runtime/src/search/projection-store.ts b/app/packages/core-runtime/src/search/projection-store.ts new file mode 100644 index 000000000..a664c89a9 --- /dev/null +++ b/app/packages/core-runtime/src/search/projection-store.ts @@ -0,0 +1,43 @@ +import { Context } from 'effect'; +import type { Effect, Schema } from 'effect'; + +import type { + CoreSearchProjectionDocument, + CoreSearchProjectionInvalid, + CoreSearchProjectionUnavailable, + CoreSearchQuery, +} from './projection.ts'; + +type UnparsedCoreSearchInput = typeof Schema.Unknown.Type; +type CoreSearchProjectionUnavailableInstance = InstanceType; + +export interface CoreSearchProjectionStoreService { + /** Applies one idempotent versioned lifecycle observation. */ + readonly apply: ( + input: UnparsedCoreSearchInput, + ) => Effect.Effect; + /** Candidate access is Core-private: the query runtime strips searchable evidence before return. */ + readonly queryCandidates: ( + input: CoreSearchQuery, + ) => Effect.Effect< + readonly CoreSearchProjectionDocument[], + CoreSearchProjectionUnavailableInstance + >; + /** + * Replaces one tenant/module/resource projection as one physical rebuild unit. Implementations + * must leave the prior unit intact when validation or persistence fails. + */ + readonly replace: ( + input: UnparsedCoreSearchInput, + ) => Effect.Effect; +} + +/** Production persistence implements this Core-owned port; business modules never own an index. */ +export class CoreSearchProjectionStore extends Context.Service< + CoreSearchProjectionStore, + CoreSearchProjectionStoreService +>()( + // Preserve the public Context identity after splitting the service into its owning module. + // @effect-diagnostics-next-line deterministicKeys:off + '@app/core-runtime/search/projection/CoreSearchProjectionStore', +) {} diff --git a/app/packages/core-runtime/src/search/projection.ts b/app/packages/core-runtime/src/search/projection.ts index e9a7872c8..2fd81af1e 100644 --- a/app/packages/core-runtime/src/search/projection.ts +++ b/app/packages/core-runtime/src/search/projection.ts @@ -1,5 +1,15 @@ import { createHash } from 'node:crypto'; -import { Clock, Context, DateTime, Effect, Option, Predicate, Result, Schema } from 'effect'; + +import { Clock, DateTime, Effect, Option, Predicate, Result, Schema } from 'effect'; + +import { CoreSearchProjectionStore } from './projection-store.ts'; +import type { CoreSearchProjectionStoreService } from './projection-store.ts'; +import type { CoreSearchQueryRuntimeService } from './query-runtime.ts'; + +export { CoreSearchProjectionStore } from './projection-store.ts'; +export { CoreSearchQueryRuntime } from './query-runtime.ts'; +export type { CoreSearchProjectionStoreService } from './projection-store.ts'; +export type { CoreSearchQueryRuntimeService } from './query-runtime.ts'; const boundedText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)); const stableKey = Schema.String.check( @@ -34,8 +44,16 @@ export const CoreSearchFacetSchema = Schema.Struct({ export type CoreSearchFacet = typeof CoreSearchFacetSchema.Type; export const CoreSearchMetadataFieldSchema = Schema.Union([ - Schema.Struct({ key: stableKey, kind: Schema.Literal('boolean'), value: Schema.Boolean }), - Schema.Struct({ key: stableKey, kind: Schema.Literal('string'), value: boundedText }), + Schema.Struct({ + key: stableKey, + kind: Schema.Literal('boolean'), + value: Schema.Boolean, + }), + Schema.Struct({ + key: stableKey, + kind: Schema.Literal('string'), + value: boundedText, + }), Schema.Struct({ key: stableKey, kind: Schema.Literal('strings'), @@ -126,7 +144,10 @@ export const CoreSearchProjectionReplacementSchema = Schema.Struct({ export type CoreSearchProjectionReplacement = typeof CoreSearchProjectionReplacementSchema.Type; export const CoreSearchProjectionMutationSchema = Schema.Union([ - Schema.Struct({ document: CoreSearchProjectionDocumentSchema, kind: Schema.Literal('upsert') }), + Schema.Struct({ + document: CoreSearchProjectionDocumentSchema, + kind: Schema.Literal('upsert'), + }), Schema.Struct({ kind: Schema.Literal('delete'), projectionVersion, @@ -160,52 +181,6 @@ export const CoreSearchProjectionUnavailable = 'CoreSearchProjectionUnavailable', projectionUnavailableFields, ); -type CoreSearchProjectionUnavailableInstance = InstanceType; - -export interface CoreSearchProjectionStoreService { - /** Applies one idempotent versioned lifecycle observation. */ - readonly apply: ( - input: UnparsedCoreSearchInput, - ) => Effect.Effect; - /** Candidate access is Core-private: the query runtime strips searchable evidence before return. */ - readonly queryCandidates: ( - input: CoreSearchQuery, - ) => Effect.Effect< - readonly CoreSearchProjectionDocument[], - CoreSearchProjectionUnavailableInstance - >; - /** - * Replaces one tenant/module/resource projection as one physical rebuild unit. Implementations - * must leave the prior unit intact when validation or persistence fails. - */ - readonly replace: ( - input: UnparsedCoreSearchInput, - ) => Effect.Effect; -} - -/** Production persistence implements this Core-owned port; business modules never own an index. */ -const defineContextService = Context.Service; -export const CoreSearchProjectionStore = defineContextService( - '@app/core-runtime/search/projection/CoreSearchProjectionStore', -); -type CoreSearchProjectionStorePort = - typeof CoreSearchProjectionStore extends Context.Service - ? Store - : never; - -export interface CoreSearchQueryRuntimeService { - readonly search: ( - input: UnparsedCoreSearchInput, - ) => Effect.Effect< - readonly CoreSearchProjectionHit[], - CoreSearchProjectionInvalid | CoreSearchProjectionUnavailableInstance - >; -} - -export const CoreSearchQueryRuntime = defineContextService( - '@app/core-runtime/search/projection/CoreSearchQueryRuntime', -); - const projectionUnitKeyCodec = Schema.fromJsonString( Schema.Tuple([Schema.String, Schema.String, Schema.String]), ); @@ -217,7 +192,10 @@ const normalize = (value: string): string => value.normalize('NFKC').toLocaleLow const invalid = (reason: string, cause?: unknown): CoreSearchProjectionInvalid => { if (cause === undefined) { - return new CoreSearchProjectionInvalid({ code: 'core_search_projection_invalid', reason }); + return new CoreSearchProjectionInvalid({ + code: 'core_search_projection_invalid', + reason, + }); } return new CoreSearchProjectionInvalid({ cause, @@ -656,9 +634,12 @@ const matchDocument = ( : { ...hit, matchedSubjectRef: alias.ref }; }; -export const createCoreSearchQueryRuntime = ( - store: CoreSearchProjectionStorePort, -): CoreSearchQueryRuntimeService => { +export const createCoreSearchQueryRuntime: Effect.Effect< + CoreSearchQueryRuntimeService, + never, + CoreSearchProjectionStore +> = Effect.gen(function* createCoreSearchQueryRuntimeService() { + const store = yield* CoreSearchProjectionStore; const search: CoreSearchQueryRuntimeService['search'] = Effect.fn( 'CoreSearchQueryRuntime.search', )(function* searchCoreSearchProjection(input: UnparsedCoreSearchInput) { @@ -694,4 +675,4 @@ export const createCoreSearchQueryRuntime = ( ); }); return Object.freeze({ search }); -}; +}); diff --git a/app/packages/core-runtime/src/search/query-runtime.ts b/app/packages/core-runtime/src/search/query-runtime.ts new file mode 100644 index 000000000..00d7118fa --- /dev/null +++ b/app/packages/core-runtime/src/search/query-runtime.ts @@ -0,0 +1,29 @@ +import { Context } from 'effect'; +import type { Effect, Schema } from 'effect'; + +import type { + CoreSearchProjectionHit, + CoreSearchProjectionInvalid, + CoreSearchProjectionUnavailable, +} from './projection.ts'; + +type UnparsedCoreSearchInput = typeof Schema.Unknown.Type; + +export interface CoreSearchQueryRuntimeService { + readonly search: ( + input: UnparsedCoreSearchInput, + ) => Effect.Effect< + readonly CoreSearchProjectionHit[], + CoreSearchProjectionInvalid | InstanceType + >; +} + +/** Core-owned query port returns hits without private searchable evidence. */ +export class CoreSearchQueryRuntime extends Context.Service< + CoreSearchQueryRuntime, + CoreSearchQueryRuntimeService +>()( + // Preserve the public Context identity after splitting the service into its owning module. + // @effect-diagnostics-next-line deterministicKeys:off + '@app/core-runtime/search/projection/CoreSearchQueryRuntime', +) {} diff --git a/app/packages/core-runtime/src/testing/live-operations.ts b/app/packages/core-runtime/src/testing/live-operations.ts index e30ebe475..3f43e9b72 100644 --- a/app/packages/core-runtime/src/testing/live-operations.ts +++ b/app/packages/core-runtime/src/testing/live-operations.ts @@ -374,9 +374,9 @@ const grantFixtureResourceAccess = Effect.fn('LiveOperations.grantResourceAccess /** Real Core persistence and SpiceDB. Call only against a disposable local database. */ const makeLiveOperationFixtureEffect = Effect.fn('LiveOperations.makeLiveOperationFixture')( function* makeLiveOperationFixtureEffect(input: LiveOperationFixtureConfiguration) { - const configuration = yield* Schema.decodeUnknownEffect( - LiveOperationFixtureConfigurationSchema, - )(input).pipe( + const configuration = yield* Schema.decodeEffect(LiveOperationFixtureConfigurationSchema)( + input, + ).pipe( Effect.mapError((cause) => fixtureFailure('Invalid live operation fixture configuration', cause), ), diff --git a/app/packages/core-runtime/tests/integration/action-permission.test.ts b/app/packages/core-runtime/tests/integration/action-permission.test.ts index a019fc5f1..eba157445 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -315,39 +315,36 @@ Effect.gen(function* preparePermissionFixture() { }).pipe(effectCallback, before); Effect.gen(function* cleanPermissionFixture() { - const relationshipCleanupExit = yield* Effect.exit( - Effect.all( - [...relationshipActionKeys].map((actionKey) => - promiseEffect( - adminClient.promises.deleteRelationships( - v1.DeleteRelationshipsRequest.create({ - relationshipFilter: v1.RelationshipFilter.create({ - optionalResourceId: toSpiceDbActionObjectId(actionKey), - resourceType: 'action', - }), - }), - ), - ), + // Preserve the adapter's eager starts and ordered awaiting before closing the client. + const actionCleanups: PromiseLike[] = []; + for (const actionKey of relationshipActionKeys) { + actionCleanups.push( + adminClient.promises.deleteRelationships( + v1.DeleteRelationshipsRequest.create({ + relationshipFilter: v1.RelationshipFilter.create({ + optionalResourceId: toSpiceDbActionObjectId(actionKey), + resourceType: 'action', + }), + }), ), - { discard: true }, - ).pipe( - Effect.andThen( - Effect.all( - [tenantId, otherTenantId].map((membershipTenantId) => - promiseEffect( - adminClient.promises.deleteRelationships( - v1.DeleteRelationshipsRequest.create({ - relationshipFilter: v1.RelationshipFilter.create({ - optionalResourceId: membershipTenantId, - resourceType: 'tenant', - }), - }), - ), - ), - ), - { discard: true }, - ), + ); + } + const tenantCleanups: PromiseLike[] = []; + for (const membershipTenantId of [tenantId, otherTenantId]) { + tenantCleanups.push( + adminClient.promises.deleteRelationships( + v1.DeleteRelationshipsRequest.create({ + relationshipFilter: v1.RelationshipFilter.create({ + optionalResourceId: membershipTenantId, + resourceType: 'tenant', + }), + }), ), + ); + } + const relationshipCleanupExit = yield* Effect.exit( + Effect.forEach(actionCleanups, promiseEffect, { discard: true }).pipe( + Effect.andThen(Effect.forEach(tenantCleanups, promiseEffect, { discard: true })), Effect.ensuring(Effect.sync(() => adminClient.close())), ), ); @@ -671,10 +668,9 @@ effectTest( ), transport: transport(key, moduleStateKey), }; - const results = yield* Effect.all( - [1, 2].map(() => - runWithLivePermission(database, (runtime) => Effect.flip(runtime.runAction(input))), - ), + const results = yield* Effect.forEach( + [1, 2], + () => runWithLivePermission(database, (runtime) => Effect.flip(runtime.runAction(input))), { concurrency: 'unbounded' }, ); const [invocation] = yield* database.executor diff --git a/app/packages/core-runtime/tests/integration/search-persistence.test.ts b/app/packages/core-runtime/tests/integration/search-persistence.test.ts index bcaa9a978..eeac80c06 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -12,7 +12,10 @@ import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; import { coreRelations } from '../../src/db/schema.ts'; import { makePostgresCoreSearchProjectionStore } from '../../src/search/persistence.ts'; -import { createCoreSearchQueryRuntime } from '../../src/search/projection.ts'; +import { + CoreSearchProjectionStore, + createCoreSearchQueryRuntime, +} from '../../src/search/projection.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; @@ -61,7 +64,9 @@ effectTest( NativeScope.provide(nativeDatabaseScope), ), }); - const search = createCoreSearchQueryRuntime(store); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store), + ); const partyDocument = (resourceId: string, projectionVersion: string, title: string) => ({ aliases: [ { @@ -226,8 +231,11 @@ effectTest( NativeScope.provide(nativeDatabaseScope), ), }); + const restartedSearch = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, restarted), + ); const floorSearch = () => - createCoreSearchQueryRuntime(restarted).search({ + restartedSearch.search({ includeArchived: false, moduleId: floorRef.moduleId, query: 'unseen', diff --git a/app/packages/core-runtime/tests/unit/action-definition.test.ts b/app/packages/core-runtime/tests/unit/action-definition.test.ts index 34b0e42e6..e4de87357 100644 --- a/app/packages/core-runtime/tests/unit/action-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/action-definition.test.ts @@ -174,7 +174,7 @@ void test('validates decoded DateTime and Option results through their encoded r archivedAt: Schema.OptionFromNullOr(Schema.DateTimeUtcFromString), createdAt: Schema.DateTimeUtcFromString, }); - const decoded = Schema.decodeUnknownSync(resultSchema)({ + const decoded = Schema.decodeSync(resultSchema)({ archivedAt: null, createdAt: '2026-09-07T10:30:00.000Z', }); diff --git a/app/packages/core-runtime/tests/unit/action-identity.test.ts b/app/packages/core-runtime/tests/unit/action-identity.test.ts index 9bcb80e3a..eb071c296 100644 --- a/app/packages/core-runtime/tests/unit/action-identity.test.ts +++ b/app/packages/core-runtime/tests/unit/action-identity.test.ts @@ -38,13 +38,13 @@ void test('identity administration and support starts declare independent tenant const originalPrincipalId = '00000000-0000-4000-8000-000000000003'; const managedPermissions = [ bindManagedApiKeyAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(bindManagedApiKeyAction.descriptor.payloadSchema)({ + Schema.decodeSync(bindManagedApiKeyAction.descriptor.payloadSchema)({ principalId, providerSubjectId: 'provider-key-id', }), ), changePrincipalStatusAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(changePrincipalStatusAction.descriptor.payloadSchema)({ + Schema.decodeSync(changePrincipalStatusAction.descriptor.payloadSchema)({ expectedStatus: 'active', newStatus: 'disabled', principalId, @@ -52,13 +52,13 @@ void test('identity administration and support starts declare independent tenant }), ), createNonHumanPrincipalAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(createNonHumanPrincipalAction.descriptor.payloadSchema)({ + Schema.decodeSync(createNonHumanPrincipalAction.descriptor.payloadSchema)({ displayName: 'Inventory service', kind: 'service', }), ), setManagedApiKeyBindingStatusAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ + Schema.decodeSync(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'disabled', @@ -78,7 +78,7 @@ void test('identity administration and support starts declare independent tenant }; assert.equal( recordSupportImpersonationAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ + Schema.decodeSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ ...supportPayload, checkpoint: 'requested', }), @@ -87,7 +87,7 @@ void test('identity administration and support starts declare independent tenant ); assert.equal( recordSupportImpersonationAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ + Schema.decodeSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ ...supportPayload, checkpoint: 'stopped', sessionRef: 'better-auth-session:safe-session-reference', diff --git a/app/packages/core-runtime/tests/unit/action-runtime.test.ts b/app/packages/core-runtime/tests/unit/action-runtime.test.ts index b692be9c3..ead04ba19 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -688,15 +688,13 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an const recoveryPrincipal = await runEffectTestPromise( supportRecoveryPrincipalContextResolverFromRepository({ load: () => - Effect.succeed( - Option.some({ - bindingPrincipalId: principal.principalId, - bindingTenantId: principal.tenantId, - principalKind: 'human' as const, - principalTenantId: principal.tenantId, - tenantId: principal.tenantId, - }), - ), + Effect.succeedSome({ + bindingPrincipalId: principal.principalId, + bindingTenantId: principal.tenantId, + principalKind: 'human' as const, + principalTenantId: principal.tenantId, + tenantId: principal.tenantId, + }), }).resolveStoppedImpersonation({ originalAuthBindingId: principal.authBindingId, originalPrincipalId: principal.principalId, diff --git a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts index a4a6b4929..df6ab95be 100644 --- a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts +++ b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts @@ -81,9 +81,7 @@ test('committed error schema requires and preserves the recovery invocation iden invocationId: '40000000-0000-4000-8000-000000000001', reason: 'This idempotency key already committed successfully', } as const; - const decoded = await runEffectTestPromise( - Schema.decodeUnknownEffect(ActionAlreadyCommitted)(encoded), - ); + const decoded = await runEffectTestPromise(Schema.decodeEffect(ActionAlreadyCommitted)(encoded)); assert.deepEqual( await runEffectTestPromise(decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted))), encoded, diff --git a/app/packages/core-runtime/tests/unit/context-access.test.ts b/app/packages/core-runtime/tests/unit/context-access.test.ts index 183c6114f..d4b5878e0 100644 --- a/app/packages/core-runtime/tests/unit/context-access.test.ts +++ b/app/packages/core-runtime/tests/unit/context-access.test.ts @@ -258,8 +258,9 @@ effectTest( ]; const [failingClient] = failures; assert.ok(failingClient); - yield* Effect.all( - failures.map((client) => + yield* Effect.forEach( + failures, + (client) => makeContextAccess(client) .legalEntities(input) .pipe( @@ -267,7 +268,7 @@ effectTest( assert.deepEqual(result, [{ decision: 'unavailable', key: legalEntityId }]), ), ), - ), + { concurrency: 1 }, ); assert.deepEqual( yield* makeContextAccess(failingClient).legalEntities({ diff --git a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts index 04b409b28..7d552775e 100644 --- a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts +++ b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts @@ -158,7 +158,7 @@ const registration = defineRead( () => ({ kind: 'module', moduleId: 'core.shell' }), ); -const principal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)({ +const principal = Schema.decodeSync(TrustedPrincipalContextSchema)({ authBindingId: '00000000-0000-4000-8000-000000000002', authContextRef: 'better-auth-session:governed-http-test', authMethod: 'session', diff --git a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts index dd5fec255..438c1f70c 100644 --- a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts +++ b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts @@ -27,7 +27,7 @@ const principal = { const verificationFailure = ( _tag: (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag'], ) => - Schema.decodeUnknownSync(OperationPrincipalVerificationErrorSchema)({ + Schema.decodeSync(OperationPrincipalVerificationErrorSchema)({ _tag, reason: 'Private verifier diagnostic', }); @@ -139,7 +139,7 @@ test('mounted HTTP authentication maps verifier classes, challenges unusable cre rawBody, expectedStatus === 401 ? authenticationProblem() : unavailableProblem(), ); - const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)(rawBody); + const body = yield* Schema.decodeEffect(ProblemResponseSchema)(rawBody); assert.ok( Schema.is( Schema.TaggedStruct( diff --git a/app/packages/core-runtime/tests/unit/operation-context.test.ts b/app/packages/core-runtime/tests/unit/operation-context.test.ts index decae9358..f68846b44 100644 --- a/app/packages/core-runtime/tests/unit/operation-context.test.ts +++ b/app/packages/core-runtime/tests/unit/operation-context.test.ts @@ -1,5 +1,5 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, Exit, Option, Schema, flow } from 'effect'; +import { DateTime, Effect, Exit, Schema, flow } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { supportRecoveryPrincipalContextResolverFromRepository } from '../../src/auth/support-recovery-principal-context.ts'; @@ -141,7 +141,7 @@ effectTest( kind: 'system' as const, principalStatus: 'active' as const, tenantStatus: 'active' as const, - }).pipe(Effect.map(Option.some)), + }).pipe(Effect.asSome), }).resolve({ principalId: principal.principalId, registration: registerSystemWorkload({ jobKey: 'operation-scope-test' }), @@ -190,7 +190,7 @@ effectTest( principalKind: 'human' as const, principalTenantId: principal.tenantId, tenantId: principal.tenantId, - }).pipe(Effect.map(Option.some)), + }).pipe(Effect.asSome), }).resolveStoppedImpersonation({ originalAuthBindingId: principal.authBindingId, originalPrincipalId: principal.principalId, diff --git a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts index eb441725f..c9159c9a3 100644 --- a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts @@ -75,7 +75,7 @@ void test( assert.equal('handler' in worker, false); assert.deepEqual(Object.keys(worker), ['descriptor']); - const payload = yield* Schema.decodeUnknownEffect(payloadSchema)({ messageKey: 'message-1' }); + const payload = yield* Schema.decodeEffect(payloadSchema)({ messageKey: 'message-1' }); yield* getOutboxWorkerHandler(worker)(payload, { attemptNumber: 1, claimId: 'claim-1', diff --git a/app/packages/core-runtime/tests/unit/principal-management.test.ts b/app/packages/core-runtime/tests/unit/principal-management.test.ts index 962c4df8f..6cdc60844 100644 --- a/app/packages/core-runtime/tests/unit/principal-management.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-management.test.ts @@ -170,8 +170,9 @@ effectTest( { bindingStatus: 'active', principalKind: 'human', principalStatus: 'active' }, { bindingStatus: 'active', principalKind: 'service', principalStatus: 'disabled' }, ] satisfies readonly ApiKeyBindingRecord[]; - yield* Effect.all( - records.map((record) => + yield* Effect.forEach( + records, + (record) => Effect.gen(function* rejectsIneligibleBindingTarget() { const error = yield* Effect.flip( setApiKeyBindingStatus({ @@ -185,7 +186,7 @@ effectTest( ); assert.equal(error._tag, 'IdentityTargetInvalidError'); }), - ), + { concurrency: 1 }, ); }), ); @@ -200,7 +201,7 @@ effectTest( inserted = value; return Option.some({ authBindingId }); }), - loadPrincipal: () => Effect.succeed(Option.some({ kind: 'service', status: 'active' })), + loadPrincipal: () => Effect.succeedSome({ kind: 'service', status: 'active' }), }); const result = yield* bindApiKey({ managed: true, @@ -221,8 +222,8 @@ effectTest( 'maps an existing API key binding to a lifecycle conflict', Effect.gen(function* mapsExistingBindingToConflict() { const transaction = repository({ - insertApiKeyBinding: () => Effect.succeed(Option.none()), - loadPrincipal: () => Effect.succeed(Option.some({ kind: 'service', status: 'active' })), + insertApiKeyBinding: () => Effect.succeedNone, + loadPrincipal: () => Effect.succeedSome({ kind: 'service', status: 'active' }), }); const error = yield* Effect.flip( diff --git a/app/packages/core-runtime/tests/unit/read-runtime.test.ts b/app/packages/core-runtime/tests/unit/read-runtime.test.ts index e4fca1acb..ba299f049 100644 --- a/app/packages/core-runtime/tests/unit/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/read-runtime.test.ts @@ -257,7 +257,7 @@ void test('uses each denying Policy reference own declared HTTP status', async ( ), ); assert.equal(error._tag, 'ReadPolicyDenied'); - assert.equal(Schema.decodeUnknownSync(ReadPolicyDenied)(error).httpStatus, denialStatus); + assert.equal(Schema.decodeSync(ReadPolicyDenied)(error).httpStatus, denialStatus); }), ); }); @@ -814,7 +814,7 @@ for (const scenario of [ void test('does not release generated search candidates denied by result-level authorization', async () => { const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ + const candidate = Schema.decodeSync(ResourceTargetSchema)({ moduleId: 'inventory.stock', resourceId: 'stock-1', resourceType: 'inventory.stock.item', @@ -856,7 +856,7 @@ void test('does not release generated search candidates denied by result-level a }); test('authorizes tenant-scoped Party search results without fabricating a Legal Entity', async () => { - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ + const candidate = Schema.decodeSync(ResourceTargetSchema)({ moduleId: 'party.registry', resourceId: 'party-1', resourceType: 'party.registry.party', @@ -900,7 +900,7 @@ test('authorizes tenant-scoped Party search results without fabricating a Legal }); test('fails closed when tenant-scoped Party result authorization becomes unavailable', async () => { - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ + const candidate = Schema.decodeSync(ResourceTargetSchema)({ moduleId: 'party.registry', resourceId: 'party-1', resourceType: 'party.registry.party', diff --git a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts index 833729e17..84bf9e750 100644 --- a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts @@ -59,7 +59,7 @@ void test('installs and verifies transaction-local scope and exposes no transact void test('fails closed when transaction settings do not match', async () => { const transaction = transactionService( () => Effect.void, - Effect.succeed(Option.some({ legal_entity_id: '', tenant_id: 'foreign' })), + Effect.succeedSome({ legal_entity_id: '', tenant_id: 'foreign' }), ); const error = await runEffectTestPromise( Effect.flip( diff --git a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts index 213b84191..90d843aee 100644 --- a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts +++ b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts @@ -8,6 +8,7 @@ import { makeCoreSearchIngestion, } from '../../src/search/ingestion.ts'; import { + CoreSearchProjectionStore, createCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, } from '../../src/search/projection.ts'; @@ -66,9 +67,11 @@ void test('declares one immutable Core registration for every closed Party lifec effectTest('ingests duplicate and out-of-order post-commit observations idempotently', () => { const store = makeInMemoryCoreSearchProjectionStore(); const ingestion = makeCoreSearchIngestion(store); - const runtime = createCoreSearchQueryRuntime(store); return Effect.gen(function* ingestObservationsIdempotently() { + const runtime = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store), + ); yield* ingestion.ingest(observation('2', 'Current title')); yield* ingestion.ingest(observation('2', 'Current title')); yield* ingestion.ingest(observation('1', 'Stale title')); @@ -129,10 +132,9 @@ effectTest('rejects undeclared topics and sequence/document identity mismatches' }, }, ]; - return Effect.all( - invalidObservations.map((invalidObservation) => - Effect.flip(ingestion.ingest(invalidObservation)), - ), + return Effect.forEach( + invalidObservations, + (invalidObservation) => Effect.flip(ingestion.ingest(invalidObservation)), { concurrency: 'unbounded' }, ).pipe( Effect.tap((failures) => diff --git a/app/packages/core-runtime/tests/unit/search-projection.test.ts b/app/packages/core-runtime/tests/unit/search-projection.test.ts index f3c19cb56..7273394c7 100644 --- a/app/packages/core-runtime/tests/unit/search-projection.test.ts +++ b/app/packages/core-runtime/tests/unit/search-projection.test.ts @@ -1,11 +1,21 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import test from 'node:test'; + +import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; import { Effect, Schema } from 'effect'; + import { + CoreSearchProjectionInvalid, + CoreSearchProjectionStore, + CoreSearchProjectionUnavailable, + CoreSearchQueryRuntime, createCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, } from '../../src/search/projection.ts'; +import type { + CoreSearchProjectionHit, + CoreSearchProjectionStoreService, +} from '../../src/search/projection.ts'; const effectTest = (name: string, body: () => Effect.Effect): void => { void test(name, () => { @@ -63,7 +73,9 @@ effectTest( 'a projection rebuild floor prevents unseen stale resources and rejects divergent equal-version rebuilds', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); const rebuild = { documents: [], moduleId: partyRef.moduleId, @@ -74,7 +86,11 @@ effectTest( return Effect.gen(function* testProjectionRebuildFloor() { yield* store.replace(rebuild); yield* store.apply({ document: party(), kind: 'upsert' }); - yield* store.replace({ ...rebuild, documents: [party()], rebuildVersion: '1' }); + yield* store.replace({ + ...rebuild, + documents: [party()], + rebuildVersion: '1', + }); assert.deepEqual( yield* runtime.search({ includeArchived: false, @@ -88,7 +104,10 @@ effectTest( yield* store.replace(rebuild); const divergent = yield* Effect.flip(store.replace({ ...rebuild, documents: [party()] })); assert.equal(divergent._tag, 'CoreSearchProjectionInvalid'); - yield* store.apply({ document: party({ projectionVersion: '3' }), kind: 'upsert' }); + yield* store.apply({ + document: party({ projectionVersion: '3' }), + kind: 'upsert', + }); yield* store.replace(rebuild); const searchResults = yield* runtime.search({ includeArchived: false, @@ -106,12 +125,18 @@ effectTest( 'Core Search identifies alias-only matches while canonical evidence takes precedence', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); return Effect.gen(function* testAliasMatches() { yield* store.apply({ document: party({ aliases: [ - { kind: 'resource', ref: aliasRef, searchableText: ['Former Company', 'Acme'] }, + { + kind: 'resource', + ref: aliasRef, + searchableText: ['Former Company', 'Acme'], + }, ], matchedRef: aliasRef, }), @@ -151,7 +176,9 @@ effectTest( }, ], }, - { temporalSearchableText: [{ validFrom: 'not-a-date', value: 'private' }] }, + { + temporalSearchableText: [{ validFrom: 'not-a-date', value: 'private' }], + }, { temporalSearchableText: [ { validFrom: '2026-02-01', validTo: '2026-02-01', value: 'private' }, @@ -164,7 +191,11 @@ effectTest( ref: aliasRef, searchableText: [], temporalSearchableText: [ - { validFrom: '2026-02-01', validTo: '2026-01-01', value: 'private' }, + { + validFrom: '2026-02-01', + validTo: '2026-01-01', + value: 'private', + }, ], }, ], @@ -183,10 +214,9 @@ effectTest( })), }, ]; - return Effect.all( - invalidEvidence.map((evidence) => - Effect.flip(store.apply({ document: party(evidence), kind: 'upsert' })), - ), + return Effect.forEach( + invalidEvidence, + (evidence) => Effect.flip(store.apply({ document: party(evidence), kind: 'upsert' })), { concurrency: 'unbounded' }, ).pipe( Effect.tap((failures) => @@ -204,7 +234,9 @@ effectTest( 'Core Search honors half-open evidence periods for canonical and subject aliases', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); return Effect.gen(function* testHalfOpenEvidencePeriods() { yield* store.apply({ document: party({ @@ -223,7 +255,10 @@ effectTest( }, ], temporalSearchableText: [ - { validFrom: '2026-02-01T00:00:00Z', value: 'current-private@example.test' }, + { + validFrom: '2026-02-01T00:00:00Z', + value: 'current-private@example.test', + }, { validFrom: '2000-01-01T00:00:00Z', validTo: '2100-01-01T00:00:00Z', @@ -259,7 +294,9 @@ effectTest( effectTest('Core Search rebuilds one owned projection atomically and isolates tenants', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); return Effect.gen(function* testOwnedProjectionRebuild() { yield* store.replace({ @@ -301,7 +338,13 @@ effectTest('Core Search rebuilds one owned projection atomically and isolates te assert.doesNotMatch(encodeJson(result), /private@example\.test/u); yield* store.replace({ - documents: [party({ archived: true, projectionVersion: '2', title: 'Replacement' })], + documents: [ + party({ + archived: true, + projectionVersion: '2', + title: 'Replacement', + }), + ], moduleId: 'party.registry', rebuildVersion: '2', resourceType: 'party.registry.party', @@ -322,7 +365,9 @@ effectTest( 'Core Search applies typed Legal Entity and role facets without returning match evidence', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); const counterpartyRef = { moduleId: 'party.registry', resourceId: '40000000-0000-4000-8000-000000000001', @@ -336,7 +381,13 @@ effectTest( archived: false, facets: [], matchedSubjectRef: aliasRef, - metadata: [{ key: 'current-roles', kind: 'strings', value: ['CUSTOMER', 'SUPPLIER'] }], + metadata: [ + { + key: 'current-roles', + kind: 'strings', + value: ['CUSTOMER', 'SUPPLIER'], + }, + ], projectionVersion: '1', ref: counterpartyRef, searchableText: ['Acme', 'private@example.test'], @@ -378,7 +429,13 @@ effectTest( archived: false, facets: [], matchedSubjectRef: aliasRef, - metadata: [{ key: 'current-roles', kind: 'strings', value: ['CUSTOMER', 'SUPPLIER'] }], + metadata: [ + { + key: 'current-roles', + kind: 'strings', + value: ['CUSTOMER', 'SUPPLIER'], + }, + ], ref: counterpartyRef, selectedLegalEntityId: legalEntityId, subjectRef: partyRef, @@ -416,7 +473,9 @@ effectTest( 'Core Search rejects malformed or cross-owner rebuild documents without partial replacement', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); return Effect.gen(function* testMalformedRebuildDocuments() { yield* store.replace({ documents: [party()], @@ -450,8 +509,13 @@ effectTest( effectTest('Core Search makes duplicate and out-of-order lifecycle observations harmless', () => { const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); - const versionTwo = party({ projectionVersion: '2', title: 'Current title' }); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); + const versionTwo = party({ + projectionVersion: '2', + title: 'Current title', + }); return Effect.gen(function* testDuplicateLifecycleObservations() { yield* store.apply({ document: versionTwo, kind: 'upsert' }); yield* store.apply({ document: versionTwo, kind: 'upsert' }); @@ -459,7 +523,11 @@ effectTest('Core Search makes duplicate and out-of-order lifecycle observations document: party({ projectionVersion: '1', title: 'Stale title' }), kind: 'upsert', }); - yield* store.apply({ kind: 'delete', projectionVersion: '3', ref: partyRef }); + yield* store.apply({ + kind: 'delete', + projectionVersion: '3', + ref: partyRef, + }); yield* store.apply({ document: versionTwo, kind: 'upsert' }); assert.deepEqual( @@ -474,3 +542,89 @@ effectTest('Core Search makes duplicate and out-of-order lifecycle observations ); }); }); + +effectTest('native projection services preserve keys and provided identity', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const runtime = runEffectTestSync( + createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)), + ); + return Effect.gen(function* testNativeServiceIdentity() { + assert.equal( + CoreSearchProjectionStore.key, + '@app/core-runtime/search/projection/CoreSearchProjectionStore', + ); + assert.equal( + CoreSearchQueryRuntime.key, + '@app/core-runtime/search/projection/CoreSearchQueryRuntime', + ); + assert.strictEqual(yield* CoreSearchProjectionStore, store); + assert.strictEqual(yield* CoreSearchQueryRuntime, runtime); + }).pipe( + Effect.provideService(CoreSearchProjectionStore, store), + Effect.provideService(CoreSearchQueryRuntime, runtime), + ); +}); + +effectTest('native projection services compose store writes with query reads', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testNativeProjectionComposition() { + const providedStore = yield* CoreSearchProjectionStore; + const runtime = yield* CoreSearchQueryRuntime; + yield* providedStore.apply({ document: party(), kind: 'upsert' }); + const hits = yield* runtime.search({ + includeArchived: false, + moduleId: partyRef.moduleId, + query: 'acme', + resourceType: partyRef.resourceType, + tenantId, + }); + assert.equal(hits.length, 1); + assert.deepEqual(hits[0]?.ref, partyRef); + }).pipe( + Effect.provideServiceEffect(CoreSearchQueryRuntime, createCoreSearchQueryRuntime), + Effect.provideService(CoreSearchProjectionStore, store), + ); +}); + +const searchThroughNativeService = ( + input: Parameters[0], +): Effect.Effect< + readonly CoreSearchProjectionHit[], + CoreSearchProjectionInvalid | InstanceType, + CoreSearchQueryRuntime +> => + Effect.gen(function* searchNativeProjection() { + const runtime = yield* CoreSearchQueryRuntime; + return yield* runtime.search(input); + }); + +effectTest('native projection services retain invalid and unavailable failures', () => { + const unavailable = new CoreSearchProjectionUnavailable({ + code: 'core_search_projection_unavailable', + reason: 'Projection test store unavailable', + }); + const store: CoreSearchProjectionStoreService = { + ...makeInMemoryCoreSearchProjectionStore(), + queryCandidates: () => Effect.fail(unavailable), + }; + return Effect.gen(function* testNativeProjectionFailures() { + const providedStore = yield* CoreSearchProjectionStore; + const invalid = yield* Effect.flip(providedStore.apply({ kind: 'invalid' })); + assert.ok(Schema.is(CoreSearchProjectionInvalid)(invalid)); + const invalidQuery = yield* Effect.flip(searchThroughNativeService({})); + assert.ok(Schema.is(CoreSearchProjectionInvalid)(invalidQuery)); + const failedQuery = yield* Effect.flip( + searchThroughNativeService({ + includeArchived: false, + moduleId: partyRef.moduleId, + query: 'acme', + resourceType: partyRef.resourceType, + tenantId, + }), + ); + assert.strictEqual(failedQuery, unavailable); + }).pipe( + Effect.provideServiceEffect(CoreSearchQueryRuntime, createCoreSearchQueryRuntime), + Effect.provideService(CoreSearchProjectionStore, store), + ); +}); diff --git a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts index 69e05a995..36038640c 100644 --- a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts +++ b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts @@ -1,6 +1,6 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import { Effect, Option, Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { TrustedPrincipalContextSchema } from '../../src/actions/principal-context.ts'; @@ -19,7 +19,7 @@ const resolverFor = (record: { readonly tenantStatus: 'active' | 'suspended'; }) => systemPrincipalContextResolverFromRepository({ - load: () => Effect.succeed(Option.some(record)), + load: () => Effect.succeedSome(record), }); void test('constructs one immutable trusted system context from a branded registration', async () => { @@ -41,7 +41,7 @@ void test('constructs one immutable trusted system context from a branded regist principalId, tenantId, }); - assert.deepEqual(Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context), context); + assert.deepEqual(Schema.decodeSync(TrustedPrincipalContextSchema)(context), context); assert.deepEqual(await runEffectTestPromise(decodeTrustedPrincipalContext(context)), context); await assert.rejects(runEffectTestPromise(decodeTrustedPrincipalContext({ ...context }))); }); @@ -141,7 +141,7 @@ void test('enforces mode-specific trusted context cross-field invariants', () => assert.doesNotThrow(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context)); } assert.throws(() => - Schema.decodeUnknownSync(TrustedPrincipalContextSchema)({ + Schema.decodeSync(TrustedPrincipalContextSchema)({ authContextRef: 'better-auth-api-key:key-id', authMethod: 'api_key', principalId, @@ -149,7 +149,7 @@ void test('enforces mode-specific trusted context cross-field invariants', () => }), ); assert.throws(() => - Schema.decodeUnknownSync(TrustedPrincipalContextSchema)({ + Schema.decodeSync(TrustedPrincipalContextSchema)({ authBindingId: binding, authContextRef: 'better-auth-session:nested', authMethod: 'support_impersonation', diff --git a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts index 9cc4ba77c..be99b39bd 100644 --- a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts +++ b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts @@ -43,7 +43,7 @@ void test('uses one canonical tenant module state schema', async () => { const decodedStates = await Promise.all( TENANT_MODULE_STATES.map( async (state) => - await runEffectTestPromise(Schema.decodeUnknownEffect(TenantModuleStateSchema)(state)), + await runEffectTestPromise(Schema.decodeEffect(TenantModuleStateSchema)(state)), ), ); assert.deepEqual(decodedStates, TENANT_MODULE_STATES); @@ -162,7 +162,7 @@ void test('declares the generated Core Action contract and bounded business payl assert.deepEqual( await runEffectTestPromise( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + Schema.decodeEffect(descriptor.payloadSchema)({ expectedState: 'inactive', moduleKey: 'testing.module', newState: 'active', @@ -178,7 +178,7 @@ void test('declares the generated Core Action contract and bounded business payl ); await assert.rejects( runEffectTestPromise( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + Schema.decodeEffect(descriptor.payloadSchema)({ moduleKey: 'testing.module', newState: 'active', reason: 'x'.repeat(501), diff --git a/app/packages/gateway-principal-verifier/package.json b/app/packages/gateway-principal-verifier/package.json index d2b6b20c1..1cebb50f3 100644 --- a/app/packages/gateway-principal-verifier/package.json +++ b/app/packages/gateway-principal-verifier/package.json @@ -14,10 +14,10 @@ "dependencies": { "@app/core-runtime": "workspace:*", "@app/shared-contracts": "workspace:*", - "effect": "4.0.0-beta.107", + "effect": "4.0.0-rc.112", "jose": "6.2.5" }, "devDependencies": { - "@types/node": "20.19.43" + "@types/node": "^26.4.1" } } diff --git a/app/packages/shared-contracts/package.json b/app/packages/shared-contracts/package.json index f7430c9dc..c37b09165 100644 --- a/app/packages/shared-contracts/package.json +++ b/app/packages/shared-contracts/package.json @@ -18,10 +18,11 @@ }, "dependencies": { "@app/core-runtime": "workspace:*", - "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12", - "effect": "4.0.0-beta.107" + "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2", + "effect": "4.0.0-rc.112", + "@effect/opentelemetry": "4.0.0-rc.112" }, "devDependencies": { - "@types/node": "20.19.43" + "@types/node": "^26.4.1" } } diff --git a/app/packages/shared-contracts/src/gateway-context.ts b/app/packages/shared-contracts/src/gateway-context.ts index 86da38575..3d5276f7a 100644 --- a/app/packages/shared-contracts/src/gateway-context.ts +++ b/app/packages/shared-contracts/src/gateway-context.ts @@ -236,7 +236,7 @@ export const issueGatewayContext = ( payload: GatewayContextRequest, options: GatewayContextClientOptions = {}, ): GatewayContextClientEffect => - Schema.decodeUnknownEffect(GatewayContextRequestSchema)(payload).pipe( + Schema.decodeEffect(GatewayContextRequestSchema)(payload).pipe( Effect.flatMap((decodedPayload) => gatewayContextClient.pipe( Effect.flatMap((client) => diff --git a/app/packages/shared-contracts/tsconfig.json b/app/packages/shared-contracts/tsconfig.json index 8c221943e..dad5c771d 100644 --- a/app/packages/shared-contracts/tsconfig.json +++ b/app/packages/shared-contracts/tsconfig.json @@ -9,7 +9,9 @@ "noEmit": false, "outDir": "../../node_modules/.cache/tsgo/declarations/packages__shared-contracts", "tsBuildInfoFile": "../../node_modules/.cache/tsgo/packages__shared-contracts.tsbuildinfo", - "types": ["node"] + "types": [ + "node" + ] }, "include": [ "src" diff --git a/app/patches/@module-federation__bridge-react@2.8.0.patch b/app/patches/@module-federation__bridge-react@2.9.0.patch similarity index 51% rename from app/patches/@module-federation__bridge-react@2.8.0.patch rename to app/patches/@module-federation__bridge-react@2.9.0.patch index d736aff33..0f8107f18 100644 --- a/app/patches/@module-federation__bridge-react@2.8.0.patch +++ b/app/patches/@module-federation__bridge-react@2.9.0.patch @@ -1,141 +1,3 @@ -diff --git a/dist/lazy-load-component-plugin-FKp6nQa-.js b/dist/lazy-load-component-plugin-FKp6nQa-.js ---- a/dist/lazy-load-component-plugin-FKp6nQa-.js -+++ b/dist/lazy-load-component-plugin-FKp6nQa-.js -@@ -236,6 +236,41 @@ function getTargetModuleInfo(id, instance) { - remoteEntry - }; - } -+function normalizeHref(href) { -+ if (typeof document === "undefined") { -+ return href; -+ } -+ try { -+ return new URL(href, document.baseURI).href; -+ } catch { -+ return href; -+ } -+} -+function isStylesheetLink(link) { -+ return link.relList.contains("stylesheet") || link.rel.toLowerCase().split(/\s+/u).includes("stylesheet"); -+} -+function hasStylesheetLinkInHead(href, ignoredLink) { -+ if (typeof document === "undefined" || !document.head) { -+ return false; -+ } -+ const normalizedHref = normalizeHref(href); -+ return Array.from( -+ document.head.querySelectorAll("link[href]") -+ ).some( -+ (link) => link !== ignoredLink && isStylesheetLink(link) && normalizeHref(link.href) === normalizedHref -+ ); -+} -+function StylesheetAsset({ href }) { -+ const [shouldRender, setShouldRender] = React.useState(true); -+ const linkRef = React.useRef(null); -+ React.useEffect(() => { -+ setShouldRender(!hasStylesheetLinkInHead(href, linkRef.current)); -+ }, [href]); -+ if (!shouldRender) { -+ return null; -+ } -+ return /* @__PURE__ */ React.createElement("link", { ref: linkRef, href, rel: "stylesheet", type: "text/css" }); -+} - function collectSSRAssets(options) { - const { - id, -@@ -254,15 +289,19 @@ function collectSSRAssets(options) { - } - const { module: targetModule, publicPath, remoteEntry } = moduleAndPublicPath; - if (injectLink) { -+ const stylesheetHrefs = /* @__PURE__ */ new Set(); - [...targetModule.assets.css.sync, ...targetModule.assets.css.async].sort().forEach((file, index) => { -+ const href = `${publicPath}${file}`; -+ if (stylesheetHrefs.has(href)) { -+ return; -+ } -+ stylesheetHrefs.add(href); - links.push( - /* @__PURE__ */ React.createElement( -- "link", -+ StylesheetAsset, - { - key: `${file.split(".")[0]}_${index}`, -- href: `${publicPath}${file}`, -- rel: "stylesheet", -- type: "text/css" -+ href - } - ) - ); -diff --git a/dist/lazy-load-component-plugin-DEu-DfZt.mjs b/dist/lazy-load-component-plugin-DEu-DfZt.mjs ---- a/dist/lazy-load-component-plugin-DEu-DfZt.mjs -+++ b/dist/lazy-load-component-plugin-DEu-DfZt.mjs -@@ -235,6 +235,41 @@ function getTargetModuleInfo(id, instance) { - remoteEntry - }; - } -+function normalizeHref(href) { -+ if (typeof document === "undefined") { -+ return href; -+ } -+ try { -+ return new URL(href, document.baseURI).href; -+ } catch { -+ return href; -+ } -+} -+function isStylesheetLink(link) { -+ return link.relList.contains("stylesheet") || link.rel.toLowerCase().split(/\s+/u).includes("stylesheet"); -+} -+function hasStylesheetLinkInHead(href, ignoredLink) { -+ if (typeof document === "undefined" || !document.head) { -+ return false; -+ } -+ const normalizedHref = normalizeHref(href); -+ return Array.from( -+ document.head.querySelectorAll("link[href]") -+ ).some( -+ (link) => link !== ignoredLink && isStylesheetLink(link) && normalizeHref(link.href) === normalizedHref -+ ); -+} -+function StylesheetAsset({ href }) { -+ const [shouldRender, setShouldRender] = useState(true); -+ const linkRef = useRef(null); -+ useEffect(() => { -+ setShouldRender(!hasStylesheetLinkInHead(href, linkRef.current)); -+ }, [href]); -+ if (!shouldRender) { -+ return null; -+ } -+ return /* @__PURE__ */ React__default.createElement("link", { ref: linkRef, href, rel: "stylesheet", type: "text/css" }); -+} - function collectSSRAssets(options) { - const { - id, -@@ -253,15 +288,19 @@ function collectSSRAssets(options) { - } - const { module: targetModule, publicPath, remoteEntry } = moduleAndPublicPath; - if (injectLink) { -+ const stylesheetHrefs = /* @__PURE__ */ new Set(); - [...targetModule.assets.css.sync, ...targetModule.assets.css.async].sort().forEach((file, index) => { -+ const href = `${publicPath}${file}`; -+ if (stylesheetHrefs.has(href)) { -+ return; -+ } -+ stylesheetHrefs.add(href); - links.push( - /* @__PURE__ */ React__default.createElement( -- "link", -+ StylesheetAsset, - { - key: `${file.split(".")[0]}_${index}`, -- href: `${publicPath}${file}`, -- rel: "stylesheet", -- type: "text/css" -+ href - } - ) - ); diff --git a/dist/lazy/wrapNoSSR.d.ts b/dist/lazy/wrapNoSSR.d.ts --- a/dist/lazy/wrapNoSSR.d.ts +++ b/dist/lazy/wrapNoSSR.d.ts diff --git a/app/patches/@module-federation__dts-plugin@2.9.0.patch b/app/patches/@module-federation__dts-plugin@2.9.0.patch new file mode 100644 index 000000000..64e3bf19b --- /dev/null +++ b/app/patches/@module-federation__dts-plugin@2.9.0.patch @@ -0,0 +1,26 @@ +diff --git a/dist/esm/expose-rpc-B0rqtOKP.mjs b/dist/esm/expose-rpc-B0rqtOKP.mjs +index 1692a3e..1c99630 100644 +--- a/dist/esm/expose-rpc-B0rqtOKP.mjs ++++ b/dist/esm/expose-rpc-B0rqtOKP.mjs +@@ -1415,7 +1415,7 @@ const formatCompilerError = (error) => { + const getDependentFilesWithTsc = (rootFiles, rootDir, resolvedTsConfigPath, compilerOptions, context, typeScriptContext) => { + if (!rootFiles.length) return []; + const typeScriptPackageInfo = getTypeScriptPackageInfo(typeScriptContext); +- const listFilesTsConfigPath = writeListFilesTsConfig(rootFiles, resolvedTsConfigPath, context, compilerOptions); ++ const listFilesTsConfigPath = writeListFilesTsConfig(rootFiles, resolvedTsConfigPath, context, { ...compilerOptions, rootDir }); + try { + const dependentFiles = execFileSync(process.execPath, [ + typeScriptPackageInfo.tscBinPath, +diff --git a/dist/expose-rpc-jaGpsAVL.js b/dist/expose-rpc-jaGpsAVL.js +index 55f07d8..7828615 100644 +--- a/dist/expose-rpc-jaGpsAVL.js ++++ b/dist/expose-rpc-jaGpsAVL.js +@@ -1404,7 +1404,7 @@ const formatCompilerError = (error) => { + const getDependentFilesWithTsc = (rootFiles, rootDir, resolvedTsConfigPath, compilerOptions, context, typeScriptContext) => { + if (!rootFiles.length) return []; + const typeScriptPackageInfo = getTypeScriptPackageInfo(typeScriptContext); +- const listFilesTsConfigPath = writeListFilesTsConfig(rootFiles, resolvedTsConfigPath, context, compilerOptions); ++ const listFilesTsConfigPath = writeListFilesTsConfig(rootFiles, resolvedTsConfigPath, context, { ...compilerOptions, rootDir }); + try { + const dependentFiles = (0, child_process.execFileSync)(process.execPath, [ + typeScriptPackageInfo.tscBinPath, diff --git a/app/patches/@module-federation__modern-js-v3@2.8.0.patch b/app/patches/@module-federation__modern-js-v3@2.8.0.patch deleted file mode 100644 index e06bda04d..000000000 --- a/app/patches/@module-federation__modern-js-v3@2.8.0.patch +++ /dev/null @@ -1,268 +0,0 @@ -diff --git a/dist/cjs/cli/configPlugin.js b/dist/cjs/cli/configPlugin.js ---- a/dist/cjs/cli/configPlugin.js -+++ b/dist/cjs/cli/configPlugin.js -@@ -216,9 +216,12 @@ var __webpack_exports__ = {}; - if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); - const splitChunkConfig = chain.optimization.splitChunks.entries(); - if (!isServer) (0, utils_namespaceObject.autoDeleteSplitChunkCacheGroups)(mfConfig, splitChunkConfig); -- if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups) { -+ if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups && splitChunkConfig.chunks !== undefined && splitChunkConfig.chunks !== 'async') { -+ const previousChunks = splitChunkConfig.chunks; - splitChunkConfig.chunks = 'async'; -- external_logger_js_default().warn('splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"'); -+ if (void 0 !== previousChunks) { -+ external_logger_js_default().warn(`splitChunks.chunks = ${previousChunks} is not allowed with stream SSR mode, it will auto changed to "async"`); -+ } - } - if ((0, external_utils_js_namespaceObject.isDev)() && 'auto' === chain.output.get('publicPath')) { - var _modernjsConfig_server; -@@ -294,9 +297,12 @@ var __webpack_exports__ = {}; - if ('object' != typeof devServerConfig || !('headers' in devServerConfig)) corsWarnMsgs.unshift('Detect devServer.headers is empty, mf modern plugin will add default cors header: devServer.headers["Access-Control-Allow-Headers"] = "*". It is recommended to specify an allowlist of trusted origins instead.'); - const exposes = null == (_userConfig_csrConfig = userConfig.csrConfig) ? void 0 : _userConfig_csrConfig.exposes; - const hasExposes = exposes && Array.isArray(exposes) ? exposes.length > 0 : Object.keys(null != exposes ? exposes : {}).length > 0; -- const lazyCompilationDisabledByPlugin = hasExposes && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; -+ const remotes = null == userConfig.csrConfig ? void 0 : userConfig.csrConfig.remotes; -+ const hasRemotes = remotes && Array.isArray(remotes) ? remotes.length > 0 : Object.keys(null != remotes ? remotes : {}).length > 0; -+ const hasFederationEntries = hasExposes || hasRemotes; -+ const lazyCompilationDisabledByPlugin = hasFederationEntries && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; - if (corsWarnMsgs.length > 1 && hasExposes) external_logger_js_default().warn(corsWarnMsgs.join('\n')); -- if (lazyCompilationDisabledByPlugin) external_logger_js_default().warn('Detected exposes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer apps.'); -+ if (lazyCompilationDisabledByPlugin) external_logger_js_default().warn('Detected exposes or remotes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer and consumer apps.'); - const corsHeaders = hasExposes ? { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', -@@ -318,7 +324,7 @@ var __webpack_exports__ = {}; - }, - dev: { - assetPrefix: (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev1 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev1.assetPrefix) ? modernjsConfig.dev.assetPrefix : 'auto', -- lazyCompilation: hasExposes ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation -+ lazyCompilation: hasFederationEntries ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation - } - }; - }); -diff --git a/dist/esm/cli/configPlugin.mjs b/dist/esm/cli/configPlugin.mjs ---- a/dist/esm/cli/configPlugin.mjs -+++ b/dist/esm/cli/configPlugin.mjs -@@ -185,9 +185,12 @@ function patchBundlerConfig(options) { - if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); - var splitChunkConfig = chain.optimization.splitChunks.entries(); - if (!isServer) autoDeleteSplitChunkCacheGroups(mfConfig, splitChunkConfig); -- if (!isServer && enableSSR && splitChunkConfig && (void 0 === splitChunkConfig ? "undefined" : _type_of__(splitChunkConfig)) === 'object' && splitChunkConfig.cacheGroups) { -+ if (!isServer && enableSSR && splitChunkConfig && (void 0 === splitChunkConfig ? "undefined" : _type_of__(splitChunkConfig)) === 'object' && splitChunkConfig.cacheGroups && void 0 !== splitChunkConfig.chunks && splitChunkConfig.chunks !== 'async') { -+ var previousChunks = splitChunkConfig.chunks; - splitChunkConfig.chunks = 'async'; -- logger.warn('splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"'); -+ if (void 0 !== previousChunks) { -+ logger.warn("splitChunks.chunks = ".concat(previousChunks, " is not allowed with stream SSR mode, it will auto changed to \"async\"")); -+ } - } - if (isDev() && 'auto' === chain.output.get('publicPath')) { - var _modernjsConfig_server; -@@ -272,9 +275,12 @@ var configPlugin_moduleFederationConfigPlugin = function(userConfig) { - if ((void 0 === devServerConfig ? "undefined" : _type_of__(devServerConfig)) !== 'object' || !('headers' in devServerConfig)) corsWarnMsgs.unshift('Detect devServer.headers is empty, mf modern plugin will add default cors header: devServer.headers["Access-Control-Allow-Headers"] = "*". It is recommended to specify an allowlist of trusted origins instead.'); - var exposes = null == (_userConfig_csrConfig = userConfig.csrConfig) ? void 0 : _userConfig_csrConfig.exposes; - var hasExposes = exposes && Array.isArray(exposes) ? exposes.length > 0 : Object.keys(null != exposes ? exposes : {}).length > 0; -- var lazyCompilationDisabledByPlugin = hasExposes && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; -+ var remotes = null == userConfig.csrConfig ? void 0 : userConfig.csrConfig.remotes; -+ var hasRemotes = remotes && Array.isArray(remotes) ? remotes.length > 0 : Object.keys(null != remotes ? remotes : {}).length > 0; -+ var hasFederationEntries = hasExposes || hasRemotes; -+ var lazyCompilationDisabledByPlugin = hasFederationEntries && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; - if (corsWarnMsgs.length > 1 && hasExposes) logger.warn(corsWarnMsgs.join('\n')); -- if (lazyCompilationDisabledByPlugin) logger.warn('Detected exposes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer apps.'); -+ if (lazyCompilationDisabledByPlugin) logger.warn('Detected exposes or remotes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer and consumer apps.'); - var corsHeaders = hasExposes ? { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', -@@ -296,7 +302,7 @@ var configPlugin_moduleFederationConfigPlugin = function(userConfig) { - }, - dev: { - assetPrefix: (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev1 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev1.assetPrefix) ? modernjsConfig.dev.assetPrefix : 'auto', -- lazyCompilation: hasExposes ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation -+ lazyCompilation: hasFederationEntries ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation - } - }; - }); -diff --git a/dist/esm-node/cli/configPlugin.mjs b/dist/esm-node/cli/configPlugin.mjs ---- a/dist/esm-node/cli/configPlugin.mjs -+++ b/dist/esm-node/cli/configPlugin.mjs -@@ -156,9 +156,12 @@ function patchBundlerConfig(options) { - if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); - const splitChunkConfig = chain.optimization.splitChunks.entries(); - if (!isServer) autoDeleteSplitChunkCacheGroups(mfConfig, splitChunkConfig); -- if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups) { -+ if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups && splitChunkConfig.chunks !== undefined && splitChunkConfig.chunks !== 'async') { -+ const previousChunks = splitChunkConfig.chunks; - splitChunkConfig.chunks = 'async'; -- logger.warn('splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"'); -+ if (void 0 !== previousChunks) { -+ logger.warn(`splitChunks.chunks = ${previousChunks} is not allowed with stream SSR mode, it will auto changed to "async"`); -+ } - } - if (isDev() && 'auto' === chain.output.get('publicPath')) { - var _modernjsConfig_server; -@@ -234,9 +237,12 @@ const moduleFederationConfigPlugin = (userConfig)=>({ - if ('object' != typeof devServerConfig || !('headers' in devServerConfig)) corsWarnMsgs.unshift('Detect devServer.headers is empty, mf modern plugin will add default cors header: devServer.headers["Access-Control-Allow-Headers"] = "*". It is recommended to specify an allowlist of trusted origins instead.'); - const exposes = null == (_userConfig_csrConfig = userConfig.csrConfig) ? void 0 : _userConfig_csrConfig.exposes; - const hasExposes = exposes && Array.isArray(exposes) ? exposes.length > 0 : Object.keys(null != exposes ? exposes : {}).length > 0; -- const lazyCompilationDisabledByPlugin = hasExposes && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; -+ const remotes = null == userConfig.csrConfig ? void 0 : userConfig.csrConfig.remotes; -+ const hasRemotes = remotes && Array.isArray(remotes) ? remotes.length > 0 : Object.keys(null != remotes ? remotes : {}).length > 0; -+ const hasFederationEntries = hasExposes || hasRemotes; -+ const lazyCompilationDisabledByPlugin = hasFederationEntries && (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev.lazyCompilation) !== false; - if (corsWarnMsgs.length > 1 && hasExposes) logger.warn(corsWarnMsgs.join('\n')); -- if (lazyCompilationDisabledByPlugin) logger.warn('Detected exposes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer apps.'); -+ if (lazyCompilationDisabledByPlugin) logger.warn('Detected exposes or remotes in the Module Federation config. The Modern.js v3 Module Federation plugin will set dev.lazyCompilation to false for producer and consumer apps.'); - const corsHeaders = hasExposes ? { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', -@@ -258,7 +264,7 @@ const moduleFederationConfigPlugin = (userConfig)=>({ - }, - dev: { - assetPrefix: (null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev1 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev1.assetPrefix) ? modernjsConfig.dev.assetPrefix : 'auto', -- lazyCompilation: hasExposes ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation -+ lazyCompilation: hasFederationEntries ? false : null == modernjsConfig ? void 0 : null == (_modernjsConfig_dev2 = modernjsConfig.dev) ? void 0 : _modernjsConfig_dev2.lazyCompilation - } - }; - }); -diff --git a/dist/cjs/cli/configPlugin.js b/dist/cjs/cli/configPlugin.js ---- a/dist/cjs/cli/configPlugin.js -+++ b/dist/cjs/cli/configPlugin.js -@@ -76,8 +76,8 @@ var __webpack_exports__ = {}; - const utils_namespaceObject = require("@module-federation/rsbuild-plugin/utils"); - const external_logger_js_namespaceObject = require("../logger.js"); - var external_logger_js_default = /*#__PURE__*/ __webpack_require__.n(external_logger_js_namespaceObject); - const defaultPath = external_path_default().resolve(process.cwd(), 'module-federation.config.ts'); -- const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ -- const packageEntry = require.resolve(packageName); -+ const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath, resolveFrom)=>{ -+ const packageEntry = require.resolve(packageName, resolveFrom ? { paths: [resolveFrom] } : void 0); - let packageRoot = external_path_default().dirname(packageEntry); - while(!external_fs_default().existsSync(external_path_default().join(packageRoot, 'package.json'))){ -@@ -85,6 +85,7 @@ var __webpack_exports__ = {}; - }; - const resolveSharedStrategyPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/shared-strategy.mjs', 'dist/cjs/cli/mfRuntimePlugins/shared-strategy.js'); - const resolveInjectNodeFetchPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); -+ const resolveManifestRecoveryPlugin = ()=>resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); - const resolveNodeRuntimePlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); - const resolveNodeRecordRemoteHashPlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/recordDynamicRemoteEntryHashPlugin.mjs', 'dist/src/recordDynamicRemoteEntryHashPlugin.js'); - function setEnv(enableSSR) { -@@ -147,6 +148,7 @@ var __webpack_exports__ = {}; - patchDTSConfig(mfConfig, isServer); - injectRuntimePlugins(resolveSharedStrategyPlugin(), runtimePlugins); - if (isServer) { -+ injectRuntimePlugins(resolveManifestRecoveryPlugin(), runtimePlugins); - injectRuntimePlugins(resolveNodeRuntimePlugin(), runtimePlugins); - if ((0, external_utils_js_namespaceObject.isDev)()) injectRuntimePlugins(resolveNodeRecordRemoteHashPlugin(), runtimePlugins); - injectRuntimePlugins(resolveInjectNodeFetchPlugin(), runtimePlugins); -diff --git a/dist/esm/cli/configPlugin.mjs b/dist/esm/cli/configPlugin.mjs ---- a/dist/esm/cli/configPlugin.mjs -+++ b/dist/esm/cli/configPlugin.mjs -@@ -18,7 +18,7 @@ __webpack_require__.add({ - } - }); - var defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); --var configPlugin_resolvePackageFile = function(packageName, esmRelativePath, cjsRelativePath) { -- var packageEntry = require.resolve(packageName); -+var configPlugin_resolvePackageFile = function(packageName, esmRelativePath, cjsRelativePath, resolveFrom) { -+ var packageEntry = require.resolve(packageName, void 0 !== resolveFrom ? { paths: [resolveFrom] } : void 0); - var packageRoot = path.dirname(packageEntry); - while(!fs.existsSync(path.join(packageRoot, 'package.json'))){ -@@ -35,5 +35,8 @@ var configPlugin_resolveInjectNodeFetchPlugin = function() { - return configPlugin_resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); - }; -+var configPlugin_resolveManifestRecoveryPlugin = function() { -+ return configPlugin_resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); -+}; - var configPlugin_resolveNodeRuntimePlugin = function() { - return configPlugin_resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); - }; -@@ -117,6 +120,7 @@ var configPlugin_patchMFConfig = function(mfConfig, isServer) { - configPlugin_patchDTSConfig(mfConfig, isServer); - configPlugin_injectRuntimePlugins(configPlugin_resolveSharedStrategyPlugin(), runtimePlugins); - if (isServer) { -+ configPlugin_injectRuntimePlugins(configPlugin_resolveManifestRecoveryPlugin(), runtimePlugins); - configPlugin_injectRuntimePlugins(configPlugin_resolveNodeRuntimePlugin(), runtimePlugins); - if (isDev()) configPlugin_injectRuntimePlugins(configPlugin_resolveNodeRecordRemoteHashPlugin(), runtimePlugins); - configPlugin_injectRuntimePlugins(configPlugin_resolveInjectNodeFetchPlugin(), runtimePlugins); -diff --git a/dist/esm-node/cli/configPlugin.mjs b/dist/esm-node/cli/configPlugin.mjs ---- a/dist/esm-node/cli/configPlugin.mjs -+++ b/dist/esm-node/cli/configPlugin.mjs -@@ -14,7 +14,7 @@ __webpack_require__.add({ - } - }); - const defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); --const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ -- const packageEntry = require.resolve(packageName); -+const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath, resolveFrom)=>{ -+ const packageEntry = require.resolve(packageName, resolveFrom ? { paths: [resolveFrom] } : undefined); - let packageRoot = path.dirname(packageEntry); - while(!fs.existsSync(path.join(packageRoot, 'package.json'))){ -@@ -25,6 +25,7 @@ const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ - }; - const resolveSharedStrategyPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/shared-strategy.mjs', 'dist/cjs/cli/mfRuntimePlugins/shared-strategy.js'); - const resolveInjectNodeFetchPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); -+const resolveManifestRecoveryPlugin = ()=>resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); - const resolveNodeRuntimePlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); - const resolveNodeRecordRemoteHashPlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/recordDynamicRemoteEntryHashPlugin.mjs', 'dist/src/recordDynamicRemoteEntryHashPlugin.js'); - function setEnv(enableSSR) { -@@ -86,6 +87,7 @@ const patchMFConfig = (mfConfig, isServer)=>{ - patchDTSConfig(mfConfig, isServer); - injectRuntimePlugins(resolveSharedStrategyPlugin(), runtimePlugins); - if (isServer) { -+ injectRuntimePlugins(resolveManifestRecoveryPlugin(), runtimePlugins); - injectRuntimePlugins(resolveNodeRuntimePlugin(), runtimePlugins); - if (isDev()) injectRuntimePlugins(resolveNodeRecordRemoteHashPlugin(), runtimePlugins); - injectRuntimePlugins(resolveInjectNodeFetchPlugin(), runtimePlugins); -diff --git a/dist/esm/react/data-fetch.mjs b/dist/esm/react/data-fetch.mjs ---- a/dist/esm/react/data-fetch.mjs -+++ b/dist/esm/react/data-fetch.mjs -@@ -1 +1,6 @@ --export * from "@module-federation/bridge-react/data-fetch"; -+import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react/data-fetch"; -+export * from "@module-federation/bridge-react/data-fetch"; -+export const createLazyComponent = (options)=>createBridgeLazyComponent({ -+ injectLink: false, -+ ...options, -+ }); -diff --git a/dist/cjs/server/index.js b/dist/cjs/server/index.js ---- a/dist/cjs/server/index.js -+++ b/dist/cjs/server/index.js -@@ -74,2 +74,5 @@ Object.defineProperty(exports, '__esModule', { - value: true - }); -+module.exports = Object.assign(__webpack_exports__["default"], { -+ staticServePlugin: __webpack_exports__.staticServePlugin -+}); -diff --git a/dist/esm/react/index.mjs b/dist/esm/react/index.mjs ---- a/dist/esm/react/index.mjs -+++ b/dist/esm/react/index.mjs -@@ -1 +1,6 @@ --export * from "@module-federation/bridge-react"; -+import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react"; -+export * from "@module-federation/bridge-react"; -+export const createLazyComponent = (options)=>createBridgeLazyComponent({ -+ injectLink: false, -+ ...options, -+ }); -diff --git a/dist/esm/react/v18.mjs b/dist/esm/react/v18.mjs ---- a/dist/esm/react/v18.mjs -+++ b/dist/esm/react/v18.mjs -@@ -1 +1,6 @@ --export * from "@module-federation/bridge-react/v18"; -+import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react/v18"; -+export * from "@module-federation/bridge-react/v18"; -+export const createLazyComponent = (options)=>createBridgeLazyComponent({ -+ injectLink: false, -+ ...options, -+ }); -diff --git a/dist/esm/react/v19.mjs b/dist/esm/react/v19.mjs ---- a/dist/esm/react/v19.mjs -+++ b/dist/esm/react/v19.mjs -@@ -1 +1,6 @@ --export * from "@module-federation/bridge-react/v19"; -+import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react/v19"; -+export * from "@module-federation/bridge-react/v19"; -+export const createLazyComponent = (options)=>createBridgeLazyComponent({ -+ injectLink: false, -+ ...options, -+ }); diff --git a/app/patches/@module-federation__modern-js-v3@2.9.0.patch b/app/patches/@module-federation__modern-js-v3@2.9.0.patch new file mode 100644 index 000000000..622561752 --- /dev/null +++ b/app/patches/@module-federation__modern-js-v3@2.9.0.patch @@ -0,0 +1,268 @@ +diff --git a/dist/cjs/cli/configPlugin.js b/dist/cjs/cli/configPlugin.js +index 247cf19..d3e6a21b 100644 +--- a/dist/cjs/cli/configPlugin.js ++++ b/dist/cjs/cli/configPlugin.js +@@ -74,8 +74,8 @@ var __webpack_exports__ = {}; + const external_logger_js_namespaceObject = require("../logger.js"); + var external_logger_js_default = /*#__PURE__*/ __webpack_require__.n(external_logger_js_namespaceObject); + const defaultPath = external_path_default().resolve(process.cwd(), 'module-federation.config.ts'); +- const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ +- const packageEntry = require.resolve(packageName); ++ const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath, resolveFrom)=>{ ++ const packageEntry = require.resolve(packageName, resolveFrom ? { paths: [resolveFrom] } : void 0); + let packageRoot = external_path_default().dirname(packageEntry); + while(!external_fs_default().existsSync(external_path_default().join(packageRoot, 'package.json'))){ + const parentDir = external_path_default().dirname(packageRoot); +@@ -86,6 +86,7 @@ var __webpack_exports__ = {}; + }; + const resolveSharedStrategyPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/shared-strategy.mjs', 'dist/cjs/cli/mfRuntimePlugins/shared-strategy.js'); + const resolveInjectNodeFetchPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); ++ const resolveManifestRecoveryPlugin = ()=>resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); + const resolveNodeRuntimePlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); + const resolveNodeRecordRemoteHashPlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/recordDynamicRemoteEntryHashPlugin.mjs', 'dist/src/recordDynamicRemoteEntryHashPlugin.js'); + function setEnv(enableSSR) { +@@ -137,16 +138,49 @@ var __webpack_exports__ = {}; + } + } + }; ++ const patchReactRuntimeSharing = (mfConfig)=>{ ++ let shared = mfConfig.shared; ++ if (!shared) return; ++ const entries = Array.isArray(shared) ? shared : [ ++ shared ++ ]; ++ const hasReact = entries.some((entry)=>'string' == typeof entry ? entry === 'react' : entry && Object.prototype.hasOwnProperty.call(entry, 'react')); ++ if (!hasReact) return; ++ for (const request of [ ++ 'react/jsx-runtime', ++ 'react/jsx-dev-runtime' ++ ]){ ++ const configured = entries.some((entry)=>'string' == typeof entry ? entry === request : entry && Object.prototype.hasOwnProperty.call(entry, request)); ++ if (configured) continue; ++ if (Array.isArray(shared)) shared = [ ++ ...shared, ++ { ++ [request]: { ++ singleton: true ++ } ++ } ++ ]; ++ else shared = { ++ ...shared, ++ [request]: { ++ singleton: true ++ } ++ }; ++ } ++ mfConfig.shared = shared; ++ }; + const patchMFConfig = (mfConfig, isServer)=>{ + (0, utils_namespaceObject.addDataFetchExposes)(mfConfig.exposes, isServer); + if (void 0 === mfConfig.remoteType) mfConfig.remoteType = "script"; + if (!mfConfig.name) throw new Error(`${external_constant_js_namespaceObject.PLUGIN_IDENTIFIER} mfConfig.name can not be empty!`); ++ patchReactRuntimeSharing(mfConfig); + const runtimePlugins = [ + ...mfConfig.runtimePlugins || [] + ]; + patchDTSConfig(mfConfig, isServer); + injectRuntimePlugins(resolveSharedStrategyPlugin(), runtimePlugins); + if (isServer) { ++ injectRuntimePlugins(resolveManifestRecoveryPlugin(), runtimePlugins); + injectRuntimePlugins(resolveNodeRuntimePlugin(), runtimePlugins); + if ((0, external_utils_js_namespaceObject.isDev)()) injectRuntimePlugins(resolveNodeRecordRemoteHashPlugin(), runtimePlugins); + injectRuntimePlugins(resolveInjectNodeFetchPlugin(), runtimePlugins); +@@ -224,7 +258,7 @@ var __webpack_exports__ = {}; + if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); + const splitChunkConfig = chain.optimization.splitChunks.entries(); + if (!isServer) (0, utils_namespaceObject.autoDeleteSplitChunkCacheGroups)(mfConfig, splitChunkConfig); +- if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups) { ++ if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups && splitChunkConfig.chunks !== undefined && splitChunkConfig.chunks !== 'async') { + const previousChunks = splitChunkConfig.chunks; + splitChunkConfig.chunks = 'async'; + if (previousChunks && 'async' !== previousChunks) external_logger_js_default().warn(`splitChunks.chunks = "${previousChunks}" is not allowed with stream SSR mode; forcing "async"`); +diff --git a/dist/esm/cli/configPlugin.mjs b/dist/esm/cli/configPlugin.mjs +index a390de85925ec6f5abff007aa7ba7da8890942d1..b45f6bae8b19d08c50367d2b31783cd68a997773 100644 +--- a/dist/esm/cli/configPlugin.mjs ++++ b/dist/esm/cli/configPlugin.mjs +@@ -19,8 +19,8 @@ __webpack_require__.add({ + } + }); + var defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); +-var configPlugin_resolvePackageFile = function(packageName, esmRelativePath, cjsRelativePath) { +- var packageEntry = require.resolve(packageName); ++var configPlugin_resolvePackageFile = function(packageName, esmRelativePath, cjsRelativePath, resolveFrom) { ++ var packageEntry = require.resolve(packageName, void 0 !== resolveFrom ? { paths: [resolveFrom] } : void 0); + var packageRoot = path.dirname(packageEntry); + while(!fs.existsSync(path.join(packageRoot, 'package.json'))){ + var parentDir = path.dirname(packageRoot); +@@ -35,6 +35,9 @@ var configPlugin_resolveSharedStrategyPlugin = function() { + var configPlugin_resolveInjectNodeFetchPlugin = function() { + return configPlugin_resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); + }; ++var configPlugin_resolveManifestRecoveryPlugin = function() { ++ return configPlugin_resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); ++}; + var configPlugin_resolveNodeRuntimePlugin = function() { + return configPlugin_resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); + }; +@@ -109,14 +112,47 @@ var configPlugin_patchDTSConfig = function(mfConfig, isServer) { + } + } + }; ++var configPlugin_patchReactRuntimeSharing = function(mfConfig) { ++ var shared = mfConfig.shared; ++ if (!shared) return; ++ var entries = Array.isArray(shared) ? shared : [ ++ shared ++ ]; ++ var hasReact = entries.some(function(entry) { ++ return 'string' == typeof entry ? entry === 'react' : entry && Object.prototype.hasOwnProperty.call(entry, 'react'); ++ }); ++ if (!hasReact) return; ++ var requests = [ ++ 'react/jsx-runtime', ++ 'react/jsx-dev-runtime' ++ ]; ++ for(var index = 0; index < requests.length; index++){ ++ var request = requests[index]; ++ var configured = entries.some(function(entry) { ++ return 'string' == typeof entry ? entry === request : entry && Object.prototype.hasOwnProperty.call(entry, request); ++ }); ++ if (configured) continue; ++ var runtimeEntry = {}; ++ runtimeEntry[request] = { ++ singleton: true ++ }; ++ if (Array.isArray(shared)) shared = shared.concat([ ++ runtimeEntry ++ ]); ++ else shared = Object.assign({}, shared, runtimeEntry); ++ } ++ mfConfig.shared = shared; ++}; + var configPlugin_patchMFConfig = function(mfConfig, isServer) { + addDataFetchExposes(mfConfig.exposes, isServer); + if (void 0 === mfConfig.remoteType) mfConfig.remoteType = "script"; + if (!mfConfig.name) throw new Error("".concat(PLUGIN_IDENTIFIER, " mfConfig.name can not be empty!")); ++ configPlugin_patchReactRuntimeSharing(mfConfig); + var runtimePlugins = _to_consumable_array__(mfConfig.runtimePlugins || []); + configPlugin_patchDTSConfig(mfConfig, isServer); + configPlugin_injectRuntimePlugins(configPlugin_resolveSharedStrategyPlugin(), runtimePlugins); + if (isServer) { ++ configPlugin_injectRuntimePlugins(configPlugin_resolveManifestRecoveryPlugin(), runtimePlugins); + configPlugin_injectRuntimePlugins(configPlugin_resolveNodeRuntimePlugin(), runtimePlugins); + if (isDev()) configPlugin_injectRuntimePlugins(configPlugin_resolveNodeRecordRemoteHashPlugin(), runtimePlugins); + configPlugin_injectRuntimePlugins(configPlugin_resolveInjectNodeFetchPlugin(), runtimePlugins); +@@ -193,7 +229,7 @@ function patchBundlerConfig(options) { + if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); + var splitChunkConfig = chain.optimization.splitChunks.entries(); + if (!isServer) autoDeleteSplitChunkCacheGroups(mfConfig, splitChunkConfig); +- if (!isServer && enableSSR && splitChunkConfig && (void 0 === splitChunkConfig ? "undefined" : _type_of__(splitChunkConfig)) === 'object' && splitChunkConfig.cacheGroups) { ++ if (!isServer && enableSSR && splitChunkConfig && (void 0 === splitChunkConfig ? "undefined" : _type_of__(splitChunkConfig)) === 'object' && splitChunkConfig.cacheGroups && void 0 !== splitChunkConfig.chunks && splitChunkConfig.chunks !== 'async') { + var previousChunks = splitChunkConfig.chunks; + splitChunkConfig.chunks = 'async'; + if (previousChunks && 'async' !== previousChunks) logger.warn('splitChunks.chunks = "'.concat(previousChunks, '" is not allowed with stream SSR mode; forcing "async"')); +diff --git a/dist/esm/react/data-fetch.mjs b/dist/esm/react/data-fetch.mjs +index afb5a8f6a5d1f008382e26b047c7f85ac8f9153a..7cc37e9c7dde5c3ea007ebe547cd71f6239b7447 100644 +--- a/dist/esm/react/data-fetch.mjs ++++ b/dist/esm/react/data-fetch.mjs +@@ -1 +1,6 @@ ++import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react/data-fetch"; + export * from "@module-federation/bridge-react/data-fetch"; ++export const createLazyComponent = (options)=>createBridgeLazyComponent({ ++ injectLink: false, ++ ...options, ++ }); +diff --git a/dist/esm/react/index.mjs b/dist/esm/react/index.mjs +index 7135f3a48be78722f2a46f8b2d366ef5b34d90ed..8454a2b3f7296d829df01cf3894f5912729e37fe 100644 +--- a/dist/esm/react/index.mjs ++++ b/dist/esm/react/index.mjs +@@ -1 +1,6 @@ ++import { createLazyComponent as createBridgeLazyComponent } from "@module-federation/bridge-react"; + export * from "@module-federation/bridge-react"; ++export const createLazyComponent = (options)=>createBridgeLazyComponent({ ++ injectLink: false, ++ ...options, ++ }); +diff --git a/dist/esm-node/cli/configPlugin.mjs b/dist/esm-node/cli/configPlugin.mjs +index 41356987ccc29a62903944cfb4a559dd46d1cb90..ccd7d5889d2ddf5b29d25c0edb078efe7783dcbe 100644 +--- a/dist/esm-node/cli/configPlugin.mjs ++++ b/dist/esm-node/cli/configPlugin.mjs +@@ -13,8 +13,8 @@ __webpack_require__.add({ + } + }); + const defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); +-const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ +- const packageEntry = require.resolve(packageName); ++const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath, resolveFrom)=>{ ++ const packageEntry = require.resolve(packageName, resolveFrom ? { paths: [resolveFrom] } : undefined); + let packageRoot = path.dirname(packageEntry); + while(!fs.existsSync(path.join(packageRoot, 'package.json'))){ + const parentDir = path.dirname(packageRoot); +@@ -25,6 +25,7 @@ const resolvePackageFile = (packageName, esmRelativePath, cjsRelativePath)=>{ + }; + const resolveSharedStrategyPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/shared-strategy.mjs', 'dist/cjs/cli/mfRuntimePlugins/shared-strategy.js'); + const resolveInjectNodeFetchPlugin = ()=>resolvePackageFile('@module-federation/modern-js-v3', 'dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs', 'dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js'); ++const resolveManifestRecoveryPlugin = ()=>resolvePackageFile('@modern-js/runtime', 'dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs', 'dist/cjs/module-federation/manifest-recovery-runtime-plugin.js', process.cwd()); + const resolveNodeRuntimePlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/runtimePlugin.mjs', 'dist/src/runtimePlugin.js'); + const resolveNodeRecordRemoteHashPlugin = ()=>resolvePackageFile('@module-federation/node', 'dist/src/recordDynamicRemoteEntryHashPlugin.mjs', 'dist/src/recordDynamicRemoteEntryHashPlugin.js'); + function setEnv(enableSSR) { +@@ -76,16 +77,49 @@ const patchDTSConfig = (mfConfig, isServer)=>{ + } + } + }; ++const patchReactRuntimeSharing = (mfConfig)=>{ ++ let shared = mfConfig.shared; ++ if (!shared) return; ++ const entries = Array.isArray(shared) ? shared : [ ++ shared ++ ]; ++ const hasReact = entries.some((entry)=>'string' == typeof entry ? entry === 'react' : entry && Object.prototype.hasOwnProperty.call(entry, 'react')); ++ if (!hasReact) return; ++ for (const request of [ ++ 'react/jsx-runtime', ++ 'react/jsx-dev-runtime' ++ ]){ ++ const configured = entries.some((entry)=>'string' == typeof entry ? entry === request : entry && Object.prototype.hasOwnProperty.call(entry, request)); ++ if (configured) continue; ++ if (Array.isArray(shared)) shared = [ ++ ...shared, ++ { ++ [request]: { ++ singleton: true ++ } ++ } ++ ]; ++ else shared = { ++ ...shared, ++ [request]: { ++ singleton: true ++ } ++ }; ++ } ++ mfConfig.shared = shared; ++}; + const patchMFConfig = (mfConfig, isServer)=>{ + addDataFetchExposes(mfConfig.exposes, isServer); + if (void 0 === mfConfig.remoteType) mfConfig.remoteType = "script"; + if (!mfConfig.name) throw new Error(`${PLUGIN_IDENTIFIER} mfConfig.name can not be empty!`); ++ patchReactRuntimeSharing(mfConfig); + const runtimePlugins = [ + ...mfConfig.runtimePlugins || [] + ]; + patchDTSConfig(mfConfig, isServer); + injectRuntimePlugins(resolveSharedStrategyPlugin(), runtimePlugins); + if (isServer) { ++ injectRuntimePlugins(resolveManifestRecoveryPlugin(), runtimePlugins); + injectRuntimePlugins(resolveNodeRuntimePlugin(), runtimePlugins); + if (isDev()) injectRuntimePlugins(resolveNodeRecordRemoteHashPlugin(), runtimePlugins); + injectRuntimePlugins(resolveInjectNodeFetchPlugin(), runtimePlugins); +@@ -163,7 +197,7 @@ function patchBundlerConfig(options) { + if (!chain.output.get('uniqueName')) chain.output.uniqueName(mfConfig.name); + const splitChunkConfig = chain.optimization.splitChunks.entries(); + if (!isServer) autoDeleteSplitChunkCacheGroups(mfConfig, splitChunkConfig); +- if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups) { ++ if (!isServer && enableSSR && splitChunkConfig && 'object' == typeof splitChunkConfig && splitChunkConfig.cacheGroups && splitChunkConfig.chunks !== undefined && splitChunkConfig.chunks !== 'async') { + const previousChunks = splitChunkConfig.chunks; + splitChunkConfig.chunks = 'async'; + if (previousChunks && 'async' !== previousChunks) logger.warn(`splitChunks.chunks = "${previousChunks}" is not allowed with stream SSR mode; forcing "async"`); diff --git a/app/patches/@module-federation__runtime-core@2.9.0.patch b/app/patches/@module-federation__runtime-core@2.9.0.patch new file mode 100644 index 000000000..fc15b64d0 --- /dev/null +++ b/app/patches/@module-federation__runtime-core@2.9.0.patch @@ -0,0 +1,13 @@ +diff --git a/dist/remote/index.d.ts b/dist/remote/index.d.ts +index 3bde403c13ae782e9abfbc579c5e61db40aa130f..3a1f44032c3ed8b289b87beb1dbf30d5db33184e 100644 +--- a/dist/remote/index.d.ts ++++ b/dist/remote/index.d.ts +@@ -6,7 +6,7 @@ import { AsyncWaterfallHook } from "../utils/hooks/asyncWaterfallHooks.js"; + import { PluginSystem } from "../utils/hooks/pluginSystem.js"; + import { ModuleFederation } from "../core.js"; + import { CallFrom, Options, Remote, RemoteEntryExports, RemoteInfo, UserOptions } from "../type/config.js"; +-import { PreloadAssets, PreloadOptions, PreloadRemoteArgs, PreloadRemoteResult } from "../type/preload.js"; ++import { PreloadAssets, PreloadOptions, PreloadRemoteArgs, PreloadRemoteResult, ResourceLoadContext } from "../type/preload.js"; + import { GlobalModuleInfo, ModuleInfo } from "@module-federation/sdk"; + + //#region src/remote/index.d.ts diff --git a/app/patches/@tanstack__router-core@1.171.21.patch b/app/patches/@tanstack__router-core@1.171.21.patch deleted file mode 100644 index 42b32ce7b..000000000 --- a/app/patches/@tanstack__router-core@1.171.21.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/dist/cjs/ssr/types.d.cts b/dist/cjs/ssr/types.d.cts -index 9050041c8dc7c5086f3b02126fab850d3d22c078..e1484446a3a699f5f49146a47bcf8e5d69041821 100644 ---- a/dist/cjs/ssr/types.d.cts -+++ b/dist/cjs/ssr/types.d.cts -@@ -2,7 +2,7 @@ import { Manifest } from '../manifest.cjs'; - import { MakeRouteMatch } from '../Matches.cjs'; - export interface DehydratedMatch { - i: MakeRouteMatch['id']; -- b?: MakeRouteMatch['__beforeLoadContext']; -+ b?: Record; - l?: MakeRouteMatch['loaderData']; - e?: MakeRouteMatch['error']; - u: MakeRouteMatch['updatedAt']; -diff --git a/dist/esm/ssr/types.d.ts b/dist/esm/ssr/types.d.ts -index 9f57d89eef903ea2c14adfa5ddc2e8083c721411..f71eaac2f7477f70d1b0f9dc91971432776e72cc 100644 ---- a/dist/esm/ssr/types.d.ts -+++ b/dist/esm/ssr/types.d.ts -@@ -2,7 +2,7 @@ import { Manifest } from '../manifest.js'; - import { MakeRouteMatch } from '../Matches.js'; - export interface DehydratedMatch { - i: MakeRouteMatch['id']; -- b?: MakeRouteMatch['__beforeLoadContext']; -+ b?: Record; - l?: MakeRouteMatch['loaderData']; - e?: MakeRouteMatch['error']; - u: MakeRouteMatch['updatedAt']; -diff --git a/src/ssr/types.ts b/src/ssr/types.ts -index 27bf111843058dcd08c5a6136692413f03302df5..b24c7800aec7fa9811be7fe5c744b8de7e7b40fe 100644 ---- a/src/ssr/types.ts -+++ b/src/ssr/types.ts -@@ -3,7 +3,7 @@ import type { MakeRouteMatch } from '../Matches' - - export interface DehydratedMatch { - i: MakeRouteMatch['id'] -- b?: MakeRouteMatch['__beforeLoadContext'] -+ b?: Record - l?: MakeRouteMatch['loaderData'] - e?: MakeRouteMatch['error'] - u: MakeRouteMatch['updatedAt'] diff --git a/app/patches/msgpackr@2.1.0.patch b/app/patches/msgpackr@2.1.0.patch new file mode 100644 index 000000000..462dc2f6c --- /dev/null +++ b/app/patches/msgpackr@2.1.0.patch @@ -0,0 +1,30 @@ +diff --git a/unpack.js b/unpack.js +index 2e8fc87..75305e1 100644 +--- a/unpack.js ++++ b/unpack.js +@@ -491,25 +491,8 @@ export function read() { + } + } + } +-const validName = /^[a-zA-Z_$][a-zA-Z\d_$]*$/; + function createStructureReader(structure, firstId) { + function readObject() { +- // This initial function is quick to instantiate, but runs slower. After several iterations pay the cost to build the faster function +- if (readObject.count++ > inlineObjectReadThreshold) { +- let optimizedReadObject; +- try { +- optimizedReadObject = structure.read = (new Function('r', 'return function(){return ' + (currentUnpackr.freezeData ? 'Object.freeze' : '') + +- '({' + structure.map(key => key === '__proto__' ? '__proto_:r()' : validName.test(key) ? key + ':r()' : ('[' + JSON.stringify(key) + ']:r()')).join(',') + '})}'))(read); +- } catch(error) { +- // in CF workers, the new Function call could begin to fail at any point in time +- inlineObjectReadThreshold = Infinity; // disable going forward +- return readObject(); // recursively try again +- } +- structure.read0 = optimizedReadObject; // keep the un-wrapped body reader in sync +- if (structure.highByte === 0) +- structure.read = createSecondByteReader(firstId, structure.read); +- return optimizedReadObject(); // second byte is already read, if there is one so immediately read object +- } + let object = {}; + for (let i = 0, l = structure.length; i < l; i++) { + let key = structure[i]; diff --git a/app/patches/zod@4.5.4.patch b/app/patches/zod@4.5.4.patch new file mode 100644 index 000000000..430808efd --- /dev/null +++ b/app/patches/zod@4.5.4.patch @@ -0,0 +1,56 @@ +diff --git a/v4/core/util.cjs b/v4/core/util.cjs +index 6c74bff..6999a5d 100644 +--- a/v4/core/util.cjs ++++ b/v4/core/util.cjs +@@ -213,22 +213,7 @@ function isObject(data) { + return typeof data === "object" && data !== null && !Array.isArray(data); + } + exports.allowsEval = cached(() => { +- // Skip the probe under `jitless`: strict CSPs report the caught `new Function` as a `securitypolicyviolation` even though the throw is swallowed. +- if (core_js_1.globalConfig.jitless) { +- return false; +- } +- // @ts-ignore +- if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) { +- return false; +- } +- try { +- const F = Function; +- new F(""); +- return true; +- } +- catch (_) { +- return false; +- } ++ return false; + }); + function isPlainObject(o) { + if (isObject(o) === false) +diff --git a/v4/core/util.js b/v4/core/util.js +index a7029b8..ac36de1 100644 +--- a/v4/core/util.js ++++ b/v4/core/util.js +@@ -146,22 +146,7 @@ export function isObject(data) { + return typeof data === "object" && data !== null && !Array.isArray(data); + } + export const allowsEval = /* @__PURE__*/ cached(() => { +- // Skip the probe under `jitless`: strict CSPs report the caught `new Function` as a `securitypolicyviolation` even though the throw is swallowed. +- if (globalConfig.jitless) { +- return false; +- } +- // @ts-ignore +- if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) { +- return false; +- } +- try { +- const F = Function; +- new F(""); +- return true; +- } +- catch (_) { +- return false; +- } ++ return false; + }); + export function isPlainObject(o) { + if (isObject(o) === false) diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 8942877cc..276541f65 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -6,27 +6,26 @@ settings: overrides: react-server-dom-rspack: 0.1.0 - '@tanstack/react-router': 1.170.25 - '@tanstack/router-core': 1.171.21 - '@effect/opentelemetry': 4.0.0-beta.107 - '@effect/vitest': 4.0.0-beta.107 - effect: 4.0.0-beta.107 + '@tanstack/react-router': 1.170.33 + '@tanstack/router-core': 1.171.28 + '@effect/opentelemetry': 4.0.0-rc.112 + '@effect/vitest': 4.0.0-rc.112 + effect: 4.0.0-rc.112 node-fetch: ^3.3.2 + '@tanstack/history': 1.162.2 + msgpackr: 2.1.0 + zod: 4.5.4 patchedDependencies: '@better-fetch/fetch@1.3.1': 9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747 - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': 92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c - '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12': c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c - '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': 227ad960c0ce793da1dee90eeaba8edc310b14a58334be185c7cce71ae59a110 - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': 2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12': e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0 - '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12': 254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d - '@module-federation/bridge-react@2.8.0': 54bfc79e097473222f83cbfa6d717792e3026bf16b5097c2ed91715b7da126be - '@module-federation/modern-js-v3@2.8.0': 56ff0f8c26c40b18be1de105abd019422ebaa648941607d1d30b94cd620b57f3 - '@tanstack/router-core@1.171.21': 413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d + '@module-federation/bridge-react@2.9.0': 8c084f41790295af8fd015b897c6298bbc13d927b796c624ac96cb2bdb4bc87c + '@module-federation/dts-plugin@2.9.0': b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b + '@module-federation/modern-js-v3@2.9.0': ba5049c43645a4337e1a74857b2dcef5f5330692cc5d2186bcef236dc122dae8 + '@module-federation/runtime-core@2.9.0': b241be221397f0e07dbe6c515725e12eaf3b418469bb7dd21750e6e4b215dd8d '@vercel/nft@0.29.2': c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7 drizzle-orm@1.0.0-rc.5-ab785fc: b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe - effect@4.0.0-beta.107: 88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98 + msgpackr@2.1.0: de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a + zod@4.5.4: 30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6 importers: @@ -36,48 +35,51 @@ importers: specifier: 1.6.1 version: 1.6.1 '@effect/sql-pg': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112) better-auth: specifier: 1.7.2 - version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) pg: specifier: 8.22.0 version: 8.22.0 devDependencies: + '@effect/opentelemetry': + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@effect/platform-node': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(ioredis@5.11.1(supports-color@10.2.2)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/tsgo': - specifier: 0.19.0 - version: 0.19.0 + specifier: 0.41.0 + version: 0.41.0 '@modern-js/code-tools': - specifier: npm:@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12(patch_hash=227ad960c0ce793da1dee90eeaba8edc310b14a58334be185c7cce71ae59a110)(oxlint-tsgolint@7.0.2001)' + specifier: npm:@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2(oxlint-tsgolint@7.0.2001)' '@modern-js/codesmith': specifier: 2.6.9 version: 2.6.9(supports-color@10.2.2) - '@modern-js/create': - specifier: npm:@bleedingdev/modern-js-create@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' '@modern-js/plugin-bff': - specifier: npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(3a233a8c5baa0ff66c13c170d030459d)' + specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' + '@modern-js/ultramodern-create': + specifier: npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' '@nkzw/eslint-plugin': specifier: 2.0.0 - version: 2.0.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + version: 2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@noble/hashes': specifier: 2.2.0 version: 2.2.0 '@oxlint/plugins': - specifier: 1.79.0 - version: 1.79.0 + specifier: 1.81.0 + version: 1.81.0 '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: ^26.4.1 + version: 26.4.1 '@types/pg': specifier: 8.20.0 version: 8.20.0 @@ -87,21 +89,27 @@ importers: '@typescript/native-preview': specifier: npm:typescript@7.0.2 version: typescript@7.0.2 + cross-env: + specifier: 10.1.0 + version: 10.1.0 effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 esbuild: specifier: 0.28.1 version: 0.28.1 + eslint: + specifier: 10.10.0 + version: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-plugin-github: specifier: 6.1.2 - version: 6.1.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + version: 6.1.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) eslint-plugin-perfectionist: specifier: 5.10.1 - version: 5.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) + version: 5.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) eslint-plugin-sonarjs: specifier: 4.2.0 - version: 4.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + version: 4.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) fallow: specifier: 3.22.0 version: 3.22.0 @@ -121,17 +129,17 @@ importers: specifier: ^2.1.10 version: 2.1.10 miniflare: - specifier: 4.20260708.1 - version: 4.20260708.1 + specifier: 4.20260730.0 + version: 4.20260730.0 oxc-parser: specifier: 0.147.0 version: 0.147.0 oxfmt: - specifier: 0.64.0 - version: 0.64.0 + specifier: 0.66.0 + version: 0.66.0 oxlint: - specifier: 1.79.0 - version: 1.79.0(oxlint-tsgolint@7.0.2001) + specifier: 1.81.0 + version: 1.81.0(oxlint-tsgolint@7.0.2001) oxlint-plugin-react-doctor: specifier: 0.9.12 version: 0.9.12 @@ -139,8 +147,8 @@ importers: specifier: 7.0.2001 version: 7.0.2001 ultracite: - specifier: 7.10.7 - version: 7.10.7(oxfmt@0.64.0)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)) + specifier: 7.11.0 + version: 7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) apps/shell-super-app: dependencies: @@ -161,46 +169,49 @@ importers: version: 1.6.1 '@better-auth/api-key': specifier: 1.7.2 - version: 1.7.2(831f340a6e103a07b479cd8fecebd372) + version: 1.7.2(266de75404526095522e68ad80df5da2) '@better-auth/drizzle-adapter': specifier: 1.7.2 - version: 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3)) + version: 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) + '@effect/opentelemetry': + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@effect/sql-pg': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112) '@modern-js/plugin-bff': - specifier: npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(e7b2bba40f7fca088d40067165acde85)' + specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' '@modern-js/plugin-i18n': - specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/plugin-tanstack': - specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2(3ba18dbb54a410d62a69ce25c4e9542e)' '@modern-js/runtime': - specifier: npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' '@module-federation/modern-js-v3': - specifier: 2.8.0 - version: 2.8.0(patch_hash=56ff0f8c26c40b18be1de105abd019422ebaa648941607d1d30b94cd620b57f3)(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + specifier: 2.9.0 + version: 2.9.0(patch_hash=ba5049c43645a4337e1a74857b2dcef5f5330692cc5d2186bcef236dc122dae8)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) '@tanstack/react-router': - specifier: 1.170.25 - version: 1.170.25(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 1.170.33 + version: 1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@techsio/ui-kit': specifier: 0.25.1 - version: 0.25.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) + version: 0.25.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) better-auth: specifier: 1.7.2 - version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 i18next: - specifier: 26.3.6 - version: 26.3.6(typescript@7.0.2) + specifier: 26.4.2 + version: 26.4.2(typescript@7.0.2) jose: specifier: 6.2.5 version: 6.2.5 @@ -213,52 +224,52 @@ importers: react-dom: specifier: 19.2.8 version: 19.2.8(react@19.2.8) - react-router: - specifier: 7.18.1 - version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@cloudflare/workers-types': specifier: 5.20260810.1 version: 5.20260810.1 '@modern-js/adapter-rstest': - specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)' + specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2(fdd7d76f75a0c875bbf6f9d83791f905)' '@modern-js/app-tools': - specifier: npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' + specifier: npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' '@playwright/test': specifier: 1.61.0 version: 1.61.0 '@rsbuild/plugin-tailwindcss': specifier: ^2.0.3 - version: 2.0.3(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + version: 2.0.3(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) '@rstest/core': specifier: 0.11.10 - version: 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + version: 0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 '@testing-library/react': specifier: 16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@testing-library/user-event': specifier: 14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': - specifier: ^26.2.0 + specifier: ^26.4.1 version: 26.4.1 '@types/pg': specifier: 8.20.0 version: 8.20.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.7 + version: 19.2.7(@types/react@19.2.18) bun-types: specifier: 1.4.0 version: 1.4.0 + cross-env: + specifier: 10.1.0 + version: 10.1.0 drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc @@ -266,17 +277,17 @@ importers: specifier: 20.8.3 version: 20.8.3 tailwindcss: - specifier: ^4.3.2 + specifier: ^4.3.3 version: 4.3.3 typescript: specifier: 7.0.2 version: 7.0.2 wrangler: - specifier: 4.110.0 - version: 4.110.0(@cloudflare/workers-types@5.20260810.1) + specifier: 4.116.0 + version: 4.116.0(@cloudflare/workers-types@5.20260810.1) zephyr-rspack-plugin: specifier: 1.2.4 - version: 1.2.4(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + version: 1.2.4(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) packages/core-runtime: dependencies: @@ -284,24 +295,24 @@ importers: specifier: 1.6.1 version: 1.6.1 '@effect/platform-node': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(ioredis@5.11.1(supports-color@10.2.2)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/sql-pg': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 pg: specifier: 8.22.0 version: 8.22.0 devDependencies: '@types/node': - specifier: ^20.19.43 - version: 20.19.43 + specifier: ^26.4.1 + version: 26.4.1 '@types/pg': specifier: 8.20.0 version: 8.20.0 @@ -318,31 +329,34 @@ importers: specifier: workspace:* version: link:../shared-contracts effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 jose: specifier: 6.2.5 version: 6.2.5 devDependencies: '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: ^26.4.1 + version: 26.4.1 packages/shared-contracts: dependencies: '@app/core-runtime': specifier: workspace:* version: link:../core-runtime + '@effect/opentelemetry': + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@modern-js/plugin-bff': - specifier: npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(e7b2bba40f7fca088d40067165acde85)' + specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 devDependencies: '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: ^26.4.1 + version: 26.4.1 packages/shared-design-tokens: {} @@ -361,41 +375,41 @@ importers: specifier: workspace:* version: link:../../packages/shared-design-tokens '@effect/opentelemetry': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(@opentelemetry/api-logs@0.220.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@effect/sql-pg': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112) '@modern-js/plugin-bff': - specifier: npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(e7b2bba40f7fca088d40067165acde85)' + specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(10f4d6332468433e4185bc862591bbd4)' '@modern-js/plugin-i18n': - specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/plugin-tanstack': - specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2(f7bdd6234cb87826e4d2b905b070739f)' '@modern-js/runtime': - specifier: npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + specifier: npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' '@module-federation/modern-js-v3': - specifier: 2.8.0 - version: 2.8.0(patch_hash=56ff0f8c26c40b18be1de105abd019422ebaa648941607d1d30b94cd620b57f3)(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + specifier: 2.9.0 + version: 2.9.0(patch_hash=ba5049c43645a4337e1a74857b2dcef5f5330692cc5d2186bcef236dc122dae8)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) '@module-federation/runtime': - specifier: 2.8.0 - version: 2.8.0 + specifier: 2.9.0 + version: 2.9.0 '@tanstack/react-router': - specifier: 1.170.25 - version: 1.170.25(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 1.170.33 + version: 1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 i18next: - specifier: 26.3.6 - version: 26.3.6(typescript@7.0.2) + specifier: 26.4.2 + version: 26.4.2(typescript@7.0.2) pg: specifier: 8.22.0 version: 8.22.0 @@ -405,46 +419,46 @@ importers: react-dom: specifier: 19.2.8 version: 19.2.8(react@19.2.8) - react-router: - specifier: 7.18.1 - version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@effect/tsgo': - specifier: 0.19.0 - version: 0.19.0 + specifier: 0.41.0 + version: 0.41.0 '@modern-js/adapter-rstest': - specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)' + specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2(fb58f039b6686e96e05520b02bd24b85)' '@modern-js/app-tools': - specifier: npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' + specifier: npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2 + version: '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' '@rsbuild/plugin-tailwindcss': specifier: ^2.0.3 - version: 2.0.3(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + version: 2.0.3(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) '@rstest/core': specifier: 0.11.10 - version: 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + version: 0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 '@testing-library/react': specifier: 16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/node': - specifier: ^20 - version: 20.19.43 + specifier: ^26.4.1 + version: 26.4.1 '@types/pg': specifier: 8.20.0 version: 8.20.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.7 + version: 19.2.7(@types/react@19.2.18) '@typescript/native': specifier: npm:typescript@7.0.2 version: typescript@7.0.2 + cross-env: + specifier: 10.1.0 + version: 10.1.0 drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc @@ -458,17 +472,17 @@ importers: specifier: 6.2.5 version: 6.2.5 tailwindcss: - specifier: ^4.3.2 + specifier: ^4.3.3 version: 4.3.3 typescript: specifier: 7.0.2 version: 7.0.2 wrangler: - specifier: 4.110.0 - version: 4.110.0(@cloudflare/workers-types@5.20260810.1) + specifier: 4.116.0 + version: 4.116.0(@cloudflare/workers-types@5.20260810.1) zephyr-rspack-plugin: specifier: 1.2.4 - version: 1.2.4(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + version: 1.2.4(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) packages: @@ -482,10 +496,6 @@ packages: '@authzed/authzed-node@1.6.1': resolution: {integrity: sha512-Rj3rMtWOjo3igxY/2fpPrIedCTfDq3e+weykuxNBzV/y6azBCoXp8SzpjCEJXVcWyBD8bu/EKY3cye3kOLsKpQ==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -684,33 +694,43 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} - '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12': - resolution: {integrity: sha512-mtDxN1lBqAOCrCA719fvCASjQytuJY5DJvPYldseV0DLyh6+pfvuarGXOmO5lxgJ+b7m7Le4P7tBX9ys3XNKQw==} + '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2': + resolution: {integrity: sha512-TJ3SIXJqA8BqM3JNpLkHycCHQVEC/VBlkUh18h5uCeYrkVMgA+o7qPkfvroB4N711bFK/9FLsthTtIhqeM3r0A==} - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': - resolution: {integrity: sha512-1mKQE+ufSnE/50kk69KUgLk+C3o65cJjAIKqKsAQPztgGImKF80RGR71+N7P9GB792d7pTy9SKmvOGBklzu+qQ==} - engines: {node: '>=20'} + '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2': + resolution: {integrity: sha512-Zo+VS9U/BZ1IbFb+tuiCjGWTKoNd8OYgfI2KqOsFa/l/JGjKa6vGOUlVaY8DmnvBWhBwMcLQsBDuey5ZyoB3kg==} + engines: {node: '>=26.7.0'} + peerDependencies: + '@rsbuild/core': 2.2.3 + + '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2': + resolution: {integrity: sha512-LE34bYkVv9YtaLVf8s4Mj4rrHWtW/6uVtEf9G3h+lxfvPn9LtJNbb1Tr/NKuuYFCy4yFad8Lum/yz5icYLdUhQ==} + engines: {node: '>=26.7.0'} hasBin: true + + '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2': + resolution: {integrity: sha512-UShwGHXYhMNyI9v8RYTwLOL7AcVedWSVazkm5RpcMAInq29k47zcQWr4BqFeGuWpkeJuyZj2ZoHT8f5KYuBagQ==} peerDependencies: - ts-node: ^10.9.2 tsconfig-paths: ^4.2.0 + zod: 4.5.4 peerDependenciesMeta: - ts-node: - optional: true - tsconfig-paths: + zod: optional: true - '@bleedingdev/modern-js-bff-core@3.8.2-ultramodern.12': - resolution: {integrity: sha512-z7HNnkelLycG9skIXG0GiA3OL7G8mAltfGI6/8Yi6F5k/A0rAEmPtP4BIoLo+YImMyXe8/8vbPX+EFOxrGv6MA==} + '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2': + resolution: {integrity: sha512-yngP/tQ2rEHlfYpBVxBD/MRMO0EYbhd2W7ih4Q282GhK6ENv69hioHt/mXemIJPLjTS+QH79jLqb0YgZqCe3Og==} + engines: {node: '>=26.7.0'} peerDependencies: - tsconfig-paths: ^4.2.0 - zod: ^4.4.3 + '@effect/opentelemetry': 4.0.0-rc.112 + effect: 4.0.0-rc.112 peerDependenciesMeta: - zod: + '@effect/opentelemetry': + optional: true + effect: optional: true - '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12': - resolution: {integrity: sha512-U495muXo9KMx4zJmBpV0fz0g/oeqLuMYKjWkY/1CqjghkttDcU8EC0WHzsobyH2v8J2hckL6lVTnAePFczyW9Q==} + '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2': + resolution: {integrity: sha512-z6fKlCo90nK1ut9EDxfZAh5ledxCk0wifaGGdDWSVlVxtVcZjKXlw8sE0ObSAwLcdcotd3Lob7L+OjyiLVBrJQ==} peerDependencies: react-server-dom-rspack: 0.1.0 rsbuild-plugin-rsc: 0.1.1 @@ -720,43 +740,61 @@ packages: rsbuild-plugin-rsc: optional: true - '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': - resolution: {integrity: sha512-uE+B6XobpR1REg3Oxv0eIq8VP7/MyRQ/tU78vVO74EOaxxlPOC5G97ykbHTr316CEhusLWxf4qM1v50ly/vPQA==} + '@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2': + resolution: {integrity: sha512-O2xzlfDM/udwzJePQVd8IBJV0yUcROMpURw4+CzvmbeQgHW9L/YJgJv6DI9sibARhxnDbL/SB3WD1jvvN3ar4A==} engines: {node: '>=20'} - '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12': - resolution: {integrity: sha512-n80cg2w4LoUnCJ9XN/bviK0y4MQDuHFfPkbzwIEcCV3QIOyfGvsHQZYBlquDCecIIimR0B7tYmtaxN46GFGGwA==} + '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2': + resolution: {integrity: sha512-X6YQPr5SqLkvpZIB6REGC5Beefp+6PIHsybUZ1SXr8iYpv2ZJ9D/ntB6vm9/eI1QADgtK6jqSjqQFJqpPr3xVw==} - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': - resolution: {integrity: sha512-xqx65FiG8iIIKvtJ2FYF4y7r53g3BhTG92UJFrdYlAoehtNfWB4RDj9NaFuAwI1sYKIBuzBQKpq5kUYa4759Uw==} - engines: {node: '>=20'} - hasBin: true + '@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.2': + resolution: {integrity: sha512-R7AqyBkdBfeC+ctxdiYfkmHbRuXzuN5l9wG8kXGvTuGL+tVlYRqiCcjBzWuqeSlrv6SaU0FC1NhR5F9tptqsxA==} + engines: {node: '>=26.7.0'} + peerDependencies: + '@modern-js/runtime': 3.9.0-ultramodern.2 + react: ^19.2.8 + + '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2': + resolution: {integrity: sha512-H2yGjWRfG5pbu8iQ7HMDCuRSAG67Mz9tSty0xf2XFgphYUK7nhvGZRJGaYWDWC4E3Vm6ssyQe3tWo5cN01p0OQ==} - '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12': - resolution: {integrity: sha512-gUN9ApQTxJdtcHrfkuph3ICUxzTt1eFB2Mz/tZpgeXgz4J+NxtApF2YsfF9bUr7jXN41vMWbA+UgpaEDu5fTTA==} + '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2': + resolution: {integrity: sha512-zSay8Old75sRmHaoAx8WDCzzKfO2R916acrj/sMwD5NmAgDmFK7IaTya5LePUEJD7jYtOTsM6epS7Ij7AEX+ug==} + engines: {node: '>=26.7.0'} + peerDependencies: + '@effect/opentelemetry': 4.0.0-rc.112 + effect: 4.0.0-rc.112 + peerDependenciesMeta: + '@effect/opentelemetry': + optional: true + effect: + optional: true - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12': - resolution: {integrity: sha512-DrBWF0bjLexLgxVi9YC/YhaXpn2mB7hUMzTKFYE/3xIen8BtGCiM+yZhh5BUOiFjWg5XE7tE0Y/pO/P7UEgCIQ==} + '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2': + resolution: {integrity: sha512-EdzZbsBXSYBK6IOnw6MULG2Y2O4lJEc2VG+FAGg2tGt9LLqClyh/ec5vlZP0GIPBtYc1XhseYVz/MVIwracxng==} + engines: {node: '>=26.7.0'} peerDependencies: - '@effect/opentelemetry': 4.0.0-beta.107 - effect: 4.0.0-beta.107 + '@effect/opentelemetry': 4.0.0-rc.112 + '@modern-js/app-tools': 3.9.0-ultramodern.2 + effect: 4.0.0-rc.112 peerDependenciesMeta: '@effect/opentelemetry': optional: true + '@modern-js/app-tools': + optional: true effect: optional: true - '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12': - resolution: {integrity: sha512-d3zeFy20bxnCnL7gjdW3Zzh48MY0WcFAbGVHsku8QFrmj4Vj1/2wGpWreMTplFzz8UPL7WmT5KA2QhFJI0ZiDA==} + '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2': + resolution: {integrity: sha512-CmJiwJZh/b79SMHTDwbFVy/luGyPn6furfoLA+NrwO70vmqenquTGLZO6kGFONOb2xyarCx/jw/5xIN+HK/hqQ==} engines: {node: '>=20'} peerDependencies: react: ^19.2.8 - '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12': - resolution: {integrity: sha512-ml+iuoWF9JfN+EJ1ERvXGEU1L5msGXq6rPPHvS56CkGztq49nDzzGRBdM4R0l5k1AgsvUPf9tXdfZbdddPHnVg==} + '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2': + resolution: {integrity: sha512-CMoxXdwBMdk/VCQWREzzONoLxe4f0jDjZmjTYOKryM2vln/vYKMWURTtB8mipeWemdTbiwgfE5YNu0W08yl+DA==} engines: {node: '>=20'} peerDependencies: - '@modern-js/runtime': 3.8.2-ultramodern.12 + '@modern-js/runtime': 3.9.0-ultramodern.2 i18next: '>=25.7.4' react: ^19.2.8 react-dom: ^19.2.8 @@ -767,12 +805,12 @@ packages: react-i18next: optional: true - '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12': - resolution: {integrity: sha512-Tr0We2IK7MqkpDcvE6WVtdjp6E+mXPrk1m9euJdUcyNq48kVll34Ksq1Kqz3pFIOxB3n+b7MD3mO0ZrE6oJS0A==} + '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2': + resolution: {integrity: sha512-gHB2FDofiwo7uwLlVN5y8/Eym0SpjhjJoWCVlfnnHEyQdPAPVvFr1g0p52Ucgo3GNKxSq3WNCJZnUUhXZWLotQ==} engines: {node: '>=20'} peerDependencies: - '@modern-js/app-tools': 3.8.2-ultramodern.12 - '@modern-js/runtime': 3.8.2-ultramodern.12 + '@modern-js/app-tools': 3.9.0-ultramodern.2 + '@modern-js/runtime': 3.9.0-ultramodern.2 react: ^19.2.8 react-dom: ^19.2.8 react-server-dom-rspack: 0.1.0 @@ -780,15 +818,15 @@ packages: react-server-dom-rspack: optional: true - '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12': - resolution: {integrity: sha512-vAl0plFGRTxP4kQHk3GXmOFiWW6stR/MMmTy/Ws4zLri6jDDlIXD0IhsYt1lWcrIN7SGBInHlsGYlLu+sbMZMg==} + '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2': + resolution: {integrity: sha512-t+1vPyOO7uOq9md9dUozEAtEWSoWzmJCGuaKFu/lcpk4FAUJCqGyjmWh6fKkq25vUQFpsQvhga/RHzz+dICcbQ==} - '@bleedingdev/modern-js-prod-server@3.8.2-ultramodern.12': - resolution: {integrity: sha512-oyo3dSR9boiHWj1BdE5CJHV3BYdbZK/VIWVghUkydCWa44s1ek/ImDOX4B9X48PmQyuy0Dbfywp6nuI1YCgV3A==} + '@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.2': + resolution: {integrity: sha512-l374YvcrVUt0mqzypBTmXSAW8G8uuNGPvLWQq1sv0cqKJUWCXQuwJ3e6Q9OuhoDP7QtcTC/F89YgMfjlwFO/6w==} engines: {node: '>=20'} - '@bleedingdev/modern-js-render@3.8.2-ultramodern.12': - resolution: {integrity: sha512-XTpDr00/naWTCrPYey7LzRqT7mCiePjbZlb8nH24auyPDt5ZzGsKTGVdBE/ranWJcukRCPHL+DpOebQOSnSGlA==} + '@bleedingdev/modern-js-render@3.9.0-ultramodern.2': + resolution: {integrity: sha512-aF6mb43ERAsVtGwjToA0mc9CjwaUaspeWTVZzN9jY4A2oghCSywBvHw2xpVRdPE4ywlLksT2zVStaZg3ejiUqg==} peerDependencies: react: ^19.2.8 react-dom: ^19.2.8 @@ -797,8 +835,12 @@ packages: react-server-dom-rspack: optional: true - '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12': - resolution: {integrity: sha512-cZ2cPpjnxm3/82OrV+kagA5hLo6EtQJLKK+opKztn4myo/MIqUTYRmQhGGYCeQ+yhXjs7vW4af736j72pZZ9Ig==} + '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2': + resolution: {integrity: sha512-upBf5yBAT2k5bPQ16gxcpzmF1IJ15TQKbdLGuwtsmqOMVRz2h+PyXFuzrViZ6/FaXD/TAFY1xmEaKdpYcv1MrA==} + engines: {node: '>=26.7.0'} + + '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2': + resolution: {integrity: sha512-cTpmbiu20zPNZyKeeHkMXB2nWtQBn2PME5adQmfn6/shshK7rBmwzQX4mwDlPxrvj3kcMMyF23B2vMazwFRdYw==} peerDependencies: react: ^19.2.8 react-dom: ^19.2.8 @@ -808,34 +850,34 @@ packages: react-dom: optional: true - '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12': - resolution: {integrity: sha512-BL0/Llj2GTLLEHXAz6NMg90udvkfsdihc5c3i6rQoy8e6VNt3vQSURGIPVT8RulP3UatGrzazra4nTvVd2Ww1Q==} + '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2': + resolution: {integrity: sha512-8M6QkvnWK9xZbQrGCUEUgKBpZ80orNP/5tsZROVJZRPB/7BsKC0q43kItRCEESDfeS7DqoIMDB46pK5613BGqA==} engines: {node: '>=20'} peerDependencies: react: ^19.2.8 react-dom: ^19.2.8 - '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12': - resolution: {integrity: sha512-FVjAMn9ZzR+vkBVQ+Ucwjxg39SYnVuHw5lcLRqig8l+2DCFsOcfgSmJjhLcbYh92jgDqH9k/hITTIYfIsvd3Cw==} + '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2': + resolution: {integrity: sha512-qmLSmOO0Fuah7ZAPg0tO6kJdFHKS2wLPxNKWfLh2kPjolKB0YomX9YAxxhpsiDy/uSsxobiswhP3oFuNwvn1GA==} engines: {node: '>=20'} - '@bleedingdev/modern-js-server-runtime-extensions@3.8.2-ultramodern.12': - resolution: {integrity: sha512-O540pqKNT4nNTNpQvdXMeRy15PJsOtscUVEQO7kJfqjpm8iO0TqOsmidNQR8m1js8DFA/BDTrnsaZ4zgdphrnw==} - engines: {node: '>=20'} + '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2': + resolution: {integrity: sha512-VtFytBqYuv4YYGUS+/lgTPKA3Qa6f+F7wJbb+/FxERxzRC+FDJZEuYuSQjanMoyvmYko7a/wahbzK99GqRpYWg==} + engines: {node: '>=26.7.0'} - '@bleedingdev/modern-js-server-runtime@3.8.2-ultramodern.12': - resolution: {integrity: sha512-KxfsF/bcFD+opZlguWJhbquzEyO0pN/QMDnLzxPGHHfMVALIiIN1Brnbg19Mp56elq4UcXcWRayjSl5ZvXe3Hw==} + '@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.2': + resolution: {integrity: sha512-47ft2Oe3m4fMJRv96trix915xX+frynIbqfo1YjeeAyD4M7g6OUiXQPNJR1Vthto6qPJbWZUMae1DO6mQlw90g==} - '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12': - resolution: {integrity: sha512-hAJDnNSuE/jLyrnmZla+W0eriLblgF7B0IbiIjvVSfCmVHf4sa4VaRzsZkF0sX+MxN/UWUqpCCuQFtOsTS58Mg==} + '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2': + resolution: {integrity: sha512-T/yrtMqOuMplGOPU/IROs1HWj0e5q91UmI0A7OsbXF1OU95nxP2EJbCnSvING46gPMIi4on95x6DvJnHD5DnYw==} peerDependencies: '@typescript/native-preview': '>=7.0.0-dev.20260628.1' peerDependenciesMeta: '@typescript/native-preview': optional: true - '@bleedingdev/modern-js-server@3.8.2-ultramodern.12': - resolution: {integrity: sha512-BhUNvWC4znDSqoIfX/7Jwc53XYETvcvk3a7pKpRo40FZwTKFPBs/AxSuR3N6PBoaocYhZxac6vhzUO4MfgxoEQ==} + '@bleedingdev/modern-js-server@3.9.0-ultramodern.2': + resolution: {integrity: sha512-i8L+1SY4LpHtM6jJlG4qVYM1oUGdiSPVsrCE6uUK0dcmUrZlS46jIM+DVrTOuNIEJDX3PhseYMqAddZBS6n0uw==} peerDependencies: devcert: ^1.2.3 tsconfig-paths: '>=4.2.0' @@ -845,11 +887,16 @@ packages: tsconfig-paths: optional: true - '@bleedingdev/modern-js-types@3.8.2-ultramodern.12': - resolution: {integrity: sha512-8Yb8ahUGrkznfHzH24s3QnhLeOfjpyy2eN6a89YhR3XfK4nJHgOH6rnw3K3XPEXNSpcaO8Xn2TrZ5eUs6T+5rA==} + '@bleedingdev/modern-js-types@3.9.0-ultramodern.2': + resolution: {integrity: sha512-bT/6Y+lCbntz9ZuwKoEkXBvAvPsu6/fHPmBb4kAJkB+lqyhD5m2vX8BKse5zxZTSdsrS3+pCwkETvMOirNcItA==} - '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12': - resolution: {integrity: sha512-JkEYRbNyfO+ppUtHEd78OcpmVf9MFJipCWzbKEgzl1vWr7VniNOxOWDAymmKokdjK7TNXpRK9iiMhJhoUHZsWQ==} + '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2': + resolution: {integrity: sha512-bK0z/kPwU0nKRoNFhnown7aAng/vyz7qAsybS3S7YDmBRNI6LcxiZy1QWhg1eRokr3JExYmygtKURipaFOry2Q==} + engines: {node: '>=26.7.0'} + hasBin: true + + '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2': + resolution: {integrity: sha512-TduQZaavlLiy9Ej8Dvntu2uamiC/p0H4zL22KohEg/3ANFRTY6GIN6HnxcZWgSdAu44YKzKYkhCHP/l3o0PLrA==} peerDependencies: react: ^19.2.8 react-dom: ^19.2.8 @@ -862,6 +909,12 @@ packages: '@bufbuild/protobuf@2.13.0': resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@clack/core@1.4.3': resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} @@ -883,32 +936,32 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260708.1': - resolution: {integrity: sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==} + '@cloudflare/workerd-darwin-64@1.20260730.1': + resolution: {integrity: sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260708.1': - resolution: {integrity: sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==} + '@cloudflare/workerd-darwin-arm64@1.20260730.1': + resolution: {integrity: sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260708.1': - resolution: {integrity: sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==} + '@cloudflare/workerd-linux-64@1.20260730.1': + resolution: {integrity: sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260708.1': - resolution: {integrity: sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==} + '@cloudflare/workerd-linux-arm64@1.20260730.1': + resolution: {integrity: sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260708.1': - resolution: {integrity: sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==} + '@cloudflare/workerd-windows-64@1.20260730.1': + resolution: {integrity: sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -919,6 +972,9 @@ packages: '@colordx/core@5.5.0': resolution: {integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==} + '@colordx/core@6.3.0': + resolution: {integrity: sha512-skoCSPWAZTfawy3t6Qk1lqXsx1OPb/mCNYBpbG667+PmXIc7kt+rhjoS2SmYywaW9fnKaufPG59NztX6jpRNaw==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -940,8 +996,8 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@csstools/selector-resolve-nested@4.0.0': - resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==} + '@csstools/selector-resolve-nested@4.0.1': + resolution: {integrity: sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==} engines: {node: '>=20.19.0'} peerDependencies: postcss-selector-parser: ^7.1.1 @@ -964,8 +1020,8 @@ packages: '@drizzle-team/brocli@0.12.0': resolution: {integrity: sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ==} - '@effect/opentelemetry@4.0.0-beta.107': - resolution: {integrity: sha512-WxR3OEcwVtckNYGxvERA4kiS8cb2B46lSWxQw8P6dCCzW0j0VC7hkWyzryJ16MVXfI/5xQHS3r5j9mud+JVvsg==} + '@effect/opentelemetry@4.0.0-rc.112': + resolution: {integrity: sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ==} engines: {node: '>=18.0.0'} peerDependencies: '@opentelemetry/api': '>=1.9.0 <2.0.0' @@ -977,7 +1033,7 @@ packages: '@opentelemetry/sdk-trace-node': '>=2.0.0 <3.0.0' '@opentelemetry/sdk-trace-web': '>=2.0.0 <3.0.0' '@opentelemetry/semantic-conventions': '>=1.33.0 <2.0.0' - effect: 4.0.0-beta.107 + effect: 4.0.0-rc.112 peerDependenciesMeta: '@opentelemetry/api': optional: true @@ -1000,57 +1056,57 @@ packages: resolution: {integrity: sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.0-beta.107 + effect: 4.0.0-rc.112 - '@effect/platform-node@4.0.0-beta.107': - resolution: {integrity: sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA==} + '@effect/platform-node@4.0.0-rc.112': + resolution: {integrity: sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.0-beta.107 - ioredis: '>=5.7.0 <6.0.0' + effect: 4.0.0-rc.112 + redis: '>=5.0.0 <7.0.0' - '@effect/sql-pg@4.0.0-beta.107': - resolution: {integrity: sha512-y5RWMhdLhFqn0picXqZIR4Bevo4Fo+aT2xHNiyZoAR86d8CnbXp6Agq9HmXIGWS/nd5f7Bs4d7Aif7QUTsUofg==} + '@effect/sql-pg@4.0.0-rc.112': + resolution: {integrity: sha512-UYUA3LAGH1Pg88Yau7eTlTLHRrIb/uTdxdPGxVcRdIjjrTzxtA7iKHR4hcmUeq5lQEcGLCopN7E4ffsAKF8vEQ==} peerDependencies: - effect: 4.0.0-beta.107 + effect: 4.0.0-rc.112 - '@effect/tsgo-darwin-arm64@0.19.0': - resolution: {integrity: sha512-tieZTHXZlqOtm3YbGxclXNsyxILFtZqMC1fZEy2PAoEGKBHfwNc+r0gR3k3SU9dsSNj3KV8wLsXxfQr/VH1pAA==} + '@effect/tsgo-darwin-arm64@0.41.0': + resolution: {integrity: sha512-3iEPtcHF72yDjv7T5YpnX+Io1LDLywJb1oGG3Fp/Fth4xmqiT1Vacyz5wnPCfEWGQ6CyrlyCZSSjUxPyJ70+DQ==} cpu: [arm64] os: [darwin] - '@effect/tsgo-darwin-x64@0.19.0': - resolution: {integrity: sha512-gTxeWR5xkL+UIdrTDcnGDj0dFPnk2AxvHzrA8j3U9OQZ5yHaFK4DA8W2hVxbu2lvGoHeRlyrArBf8YomKnNRdQ==} + '@effect/tsgo-darwin-x64@0.41.0': + resolution: {integrity: sha512-rrVVNacJ/Qh8DfdfgNJDuzcVmR3xPnqes3z+F1kio99umJwPIRB8iPWRAY0SrO+Y84A/y2SSb0AjMGqA5V3Aiw==} cpu: [x64] os: [darwin] - '@effect/tsgo-linux-arm64@0.19.0': - resolution: {integrity: sha512-XEMv3PA8hNkSXAwAXYA5/FPYqV7eVrZS032P/3AuZTHr1jJTcesTK2ANfBHwwEKekmb7y3yBC7f73yhpXah4sg==} + '@effect/tsgo-linux-arm64@0.41.0': + resolution: {integrity: sha512-RVI+7pG3tP56LfH06a2aq4KLp2RQ0HohW8mmoGmrBPyNEL9DnzFEGB98ZoMJ+ERYUJoIp74Jy3nKRVbnzVhxFw==} cpu: [arm64] os: [linux] - '@effect/tsgo-linux-arm@0.19.0': - resolution: {integrity: sha512-SZlUqv74YZkBDd2czXmeOTPtWPOppWNFmwCN4k11s1K4Y/BfIpHmmv0TUHU5DcPe3IoHIJV1kxXrxhfxO1nQsQ==} + '@effect/tsgo-linux-arm@0.41.0': + resolution: {integrity: sha512-JddS91IaupLa22oc4YQiYppe7iAZAEyx/2iR9sAAAhpVanWACsIAcyUy4RJrs8sPLLoLsZQaKQa+9rbQO1FLsg==} cpu: [arm] os: [linux] - '@effect/tsgo-linux-x64@0.19.0': - resolution: {integrity: sha512-T3KDEWgWuS3XAuJ5eUkEgeOVgC7pYK/8/hxASEtHJR1NDGPPk6gjg+jfcc0Y5O9P6mG4GL9LQmBIw/G1+GFRHA==} + '@effect/tsgo-linux-x64@0.41.0': + resolution: {integrity: sha512-U3+kMDVe2Opa5mxbN4B6PgOizWx5B4LXLN98CZDUl2vYtRGfwrRRhKQiB+de3ELaZ0MuDjPlCvU7S2mXQ8UiuQ==} cpu: [x64] os: [linux] - '@effect/tsgo-win32-arm64@0.19.0': - resolution: {integrity: sha512-raoGQr2lNm0qO8sH3v4TlyUlTePm6c6y2FBIZQrUw8B4N/srHS5aVOUgWqnJ/WjuQKl1xCM5SoehuhRkGnoFzw==} + '@effect/tsgo-win32-arm64@0.41.0': + resolution: {integrity: sha512-H6/xkn+B4B63OPKc0UAzQQFvLs0MXKigiD9dtvQeCU7czWGztOQILuLkyHnM1Dlc+uy4cJ5eXHiMXd+lwl+E8g==} cpu: [arm64] os: [win32] - '@effect/tsgo-win32-x64@0.19.0': - resolution: {integrity: sha512-KRWaWLXWIEm5mR/qPi9UJIRQdNs7uPfstiEv3bNR3qMVhgwndL2e5XYUoQOOYEK4MTjLM0GO+uknlHHRfI5K8A==} + '@effect/tsgo-win32-x64@0.41.0': + resolution: {integrity: sha512-vRN/Mhi381xEGrvV68IN/VO4Lc0pnlVQzHSVmNCJXLwGLmJUvuvPoyZ7Lv4wwHRX9iZe8Rch21xUGu5jSQtUSw==} cpu: [x64] os: [win32] - '@effect/tsgo@0.19.0': - resolution: {integrity: sha512-NDyG/RufHigI0bdvyggbr53J6c1GWcnNkm7rUwQbBDuRI1KRThFh2WtugMLL+i6wrRiMXJtgLHzh9lIMqy9gGA==} + '@effect/tsgo@0.41.0': + resolution: {integrity: sha512-C/U7lFM0AXsfpqRilDOgwu+llBhAo8bcdIlzgbRWf1LWlU4KZKB2UsUvBPL3qPnm+wmbCvQLeTQc4BjV9oJrcg==} hasBin: true '@emnapi/core@1.11.2': @@ -1071,6 +1127,9 @@ packages: '@emnapi/wasi-threads@1.2.3': resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1083,6 +1142,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1095,6 +1160,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1107,6 +1178,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1119,6 +1196,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1131,6 +1214,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1143,6 +1232,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1155,6 +1250,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1167,6 +1268,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -1179,6 +1286,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -1191,6 +1304,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -1203,6 +1322,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -1215,6 +1340,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -1227,6 +1358,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -1239,6 +1376,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -1251,6 +1394,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -1263,6 +1412,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -1275,6 +1430,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -1287,6 +1448,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -1299,6 +1466,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -1311,6 +1484,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -1323,6 +1502,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -1335,6 +1520,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -1347,6 +1538,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -1359,6 +1556,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -1371,6 +1574,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -1383,6 +1592,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1402,17 +1617,13 @@ packages: eslint: optional: true - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} @@ -1426,13 +1637,13 @@ packages: resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@fallow-cli/darwin-arm64@3.22.0': resolution: {integrity: sha512-X74T3osJZqFjNAM4w9OK4PWMPiubNyotDQ5OGYyvU9nNejJ8Ov9MH37H/9HE9hjdw4PxtPWlvl3LbatmuV5WMg==} @@ -1542,161 +1753,167 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] '@internationalized/number@3.6.7': resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} - '@ioredis/commands@1.10.0': - resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1709,14 +1926,26 @@ packages: resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/pattern@30.5.0': + resolution: {integrity: sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/schemas@30.4.1': resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/schemas@30.5.0': + resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.4.1': resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.5.1': + resolution: {integrity: sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1866,6 +2095,15 @@ packages: peerDependencies: tslib: '2' + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@loadable/component@5.16.7': resolution: {integrity: sha512-XvkFixLUOTEaj8lI7uwc4nf8Wmq3IulYG7SZHCWcPm/Li5gjJDFfIkgWOLPnD7jqPJVtAG9bEz4SCek+SpHYYg==} engines: {node: '>=8'} @@ -1895,11 +2133,11 @@ packages: peerDependencies: webpack: ^5.0.0-beta.16 - '@module-federation/bridge-react-webpack-plugin@2.8.0': - resolution: {integrity: sha512-7AaaiE4YOXFb+st6xlVDK65aNKZYR9S9ykH0q2mkHAHTgquXciAjypS8JuhmKn7Lob/2rkplMOc4YsGxG3Ng9Q==} + '@module-federation/bridge-react-webpack-plugin@2.9.0': + resolution: {integrity: sha512-hv3fuQkGERQ/COBKTVbFV1GWovReSg/bzJzDuxWR1F84h6NYtbYMWQqX8Egvb7HKMtn9Rflzw0GeaLr8F08vDg==} - '@module-federation/bridge-react@2.8.0': - resolution: {integrity: sha512-+MIaWVaXyrFfE2qq7ErZeflSTscJtDV6g4lZ+TQxjup1nC83Zw+Nw0fvfJnScDR5lP5SM9epSvZrrZHxpFeEgw==} + '@module-federation/bridge-react@2.9.0': + resolution: {integrity: sha512-3wEVz9IMsDnbdTZjG+0XA7BHUU2yIvfTqWRrMPR9RciSOVojJUXocZ6Anksu7WhuYnasc7am8szpms7pZGotnA==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -1911,13 +2149,13 @@ packages: react-router-dom: optional: true - '@module-federation/cli@2.8.0': - resolution: {integrity: sha512-yTxdWkCJPPo+IGASz+NqdW13cw3DhjSBEn9r85aYn1ahckDwB1WcZe0OjDWMqCcR6yi0BMLedk0SWrUeUj0fWw==} + '@module-federation/cli@2.9.0': + resolution: {integrity: sha512-r9RdlLRy3zWxuWQRonif48xdDu1reBGx18QpkZwLQ4JfSrOARjycNIGY2Y7QaUw7dedzxyee4FtKFeX/kOVLYQ==} engines: {node: '>=16.0.0'} hasBin: true - '@module-federation/dts-plugin@2.8.0': - resolution: {integrity: sha512-defjq4jOWMEfeejezPWLP5sc8kw0O6FqTT7/E5rbZPEVyjB1A0U3ynhW6GDE5/6hk9/TzdbWS+fBNi4MqUOY6Q==} + '@module-federation/dts-plugin@2.9.0': + resolution: {integrity: sha512-uMGqEG/p9odG2BVr7WRbBe2OgrvzBd3LPzcp5WP+bm3djBdUJ4PZ3ozqKp6qpy+NFnlh6vnM815Xn6AXkKy1Vw==} peerDependencies: typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 vue-tsc: '>=1.0.24' @@ -1925,8 +2163,8 @@ packages: vue-tsc: optional: true - '@module-federation/enhanced@2.8.0': - resolution: {integrity: sha512-h8vkLdhK7tlcSPmyYNGfGyt0pSzfDB0tYVYdyUt2tXwQRfaJAi3bsIpujMXElw3MXtOfSHESa6M/hPKrtWTHBw==} + '@module-federation/enhanced@2.9.0': + resolution: {integrity: sha512-Jd/JHoFL9fNKL4Nnzo/9hf6FD/oQo3gKpE8xt6EGuhxmrvg6wM0radPtqgHiSSWLMW7CiXr2agNjKvquKd83rw==} hasBin: true peerDependencies: typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1940,25 +2178,22 @@ packages: webpack: optional: true - '@module-federation/error-codes@2.8.0': - resolution: {integrity: sha512-Gaog9904EmxYOQV0hli3XQ7jXeFaADfh5bnBtTCtbZ37Qd/Sz9kQfd+gYQRyIj7RGmkv9DPiN/SsmrTMrTymKw==} + '@module-federation/error-codes@2.9.0': + resolution: {integrity: sha512-IGpd+VRlji3NyGOGGTsk7YEmPRKcDoBK4YHqIuP+OQwyb2YhHCUHO2vW9RXeQxCuKwi+c6xkTweRYG+Umy+0Zw==} - '@module-federation/error-codes@2.8.2': - resolution: {integrity: sha512-8inlDv48QOjA//CLQ3epjoHEiMQGsz1Pmtu2N+s7gQVggn6AYHpjnMe8AsyGxtpaPg3wbX0HmBZtRFggpXUB9A==} - - '@module-federation/inject-external-runtime-core-plugin@2.8.0': - resolution: {integrity: sha512-fW3jD1ZVds6r/Ul8TtUA42RsB0LfT1yjo5KjqgirH9QrmEMH21x44e3e6BC6IN820XKawRDSmz2kFA5YHIQp7Q==} + '@module-federation/inject-external-runtime-core-plugin@2.9.0': + resolution: {integrity: sha512-k3wCSZsY21HjYQ61wmIJUQleOgcHgqx/sX4qXYw0XnJnzHuFo64rzMvgr+TuBIgZ1peAQtfILLlTnWyTRjtZHw==} peerDependencies: - '@module-federation/runtime-tools': 2.8.0 + '@module-federation/runtime-tools': 2.9.0 - '@module-federation/managers@2.8.0': - resolution: {integrity: sha512-SnVBCwmi962WGg6hLFElxZUCnrRJdR6glE2ZKPBY/iK07AHUN2ZxuaBCBsVzyws+xLZGHZxBHmVstijTh8dSUA==} + '@module-federation/managers@2.9.0': + resolution: {integrity: sha512-8KhB2PF4g+M0hGaethU1H4GVWabLn5sF5TvnM6VxgAcDL4TfLY8aC/YobAXHG/SV0jpU5lsGe6s6xy6ccV0MQw==} - '@module-federation/manifest@2.8.0': - resolution: {integrity: sha512-wfVeBXc4/C2F70nRFSPqJhkcwbDgo+wQyEn3jbjJTDoUqxxhYBfHFs1ACBYOk3Qm97L7hHclHGtUG0/nvDEfAA==} + '@module-federation/manifest@2.9.0': + resolution: {integrity: sha512-Zhm9luVOw9XPou50eTwsvdgr208CszoQ6cGORQDCPAMhjpGb5oYFtilJ+LKW7hQ1LktD15bEZmGhC1anvgXEiQ==} - '@module-federation/modern-js-v3@2.8.0': - resolution: {integrity: sha512-zmFs0I/E3dLa8Vsj35ep7Ms29SePXFOShjedfFs5VnJPooif6l7fgdIJdxThyI1Rdw5JnRXmJsp4qmGm+naziA==} + '@module-federation/modern-js-v3@2.9.0': + resolution: {integrity: sha512-IzFEJnO53vEVLrm5qlDrCPO1bEmN11TLaPHrZtPqHfy7yjwhBDi/63uLAVwcqKVGZnK5Erbqg9Y3hJkZKxd4AQ==} engines: {node: '>=18.0.0'} peerDependencies: react: '>=17' @@ -1977,16 +2212,16 @@ packages: vue-tsc: optional: true - '@module-federation/node@2.7.47': - resolution: {integrity: sha512-mifMvCjWmLl53GS+badQws0j2bsu1ICpdGzCbez4I6kSpaYA8v86L6dwcHtVHIZtkUC6cjAZBDcgpxs4fK3nFQ==} + '@module-federation/node@2.7.50': + resolution: {integrity: sha512-mbpQRdafyeWgsmYoJfdhOQf76zS6onOGpC2X1ELpWXB1Y4BcZGloL0CLjNMNon9m3ucfpc99tOGAQqFzQVkSBQ==} peerDependencies: webpack: ^5.40.0 peerDependenciesMeta: webpack: optional: true - '@module-federation/rsbuild-plugin@2.8.0': - resolution: {integrity: sha512-rul5OPvLx599rWoAhCtKJ3UYqyM3Dxg0RWEfad3JdnJFqkcSLBojZXgHJE1vbF7DRdGQNmNscKw34iEyn89NwQ==} + '@module-federation/rsbuild-plugin@2.9.0': + resolution: {integrity: sha512-CaWAxZg+zOMw/BfgRXQdNBQZ/e5U7DBZvE13LzG13GEclt7yxaMQgKH0si3uOsq9f/X2NC8UnuhNHLIkloqlqA==} engines: {node: '>=16.0.0'} peerDependencies: '@rsbuild/core': ^1.3.21 || ^2.0.0-0 @@ -1994,8 +2229,8 @@ packages: '@rsbuild/core': optional: true - '@module-federation/rspack@2.8.0': - resolution: {integrity: sha512-TPcrkHpaZgL25Vx3c8oSNwyv7/KktC7uo6HTQdVWlFzbq5RSoMMGkWoir5pY5124isae2/p6v5xAuqICi4r0Zg==} + '@module-federation/rspack@2.9.0': + resolution: {integrity: sha512-9zSlmQYKRHKVWqhlZSMyjstp2A3VVrQV2Of8mrF5NXhngFemNx7Hw35+MTo+gsRfF5glpN8bAzLyIokRUo1Cog==} peerDependencies: '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -2006,32 +2241,23 @@ packages: vue-tsc: optional: true - '@module-federation/runtime-core@2.8.0': - resolution: {integrity: sha512-Tf98+epGGiPSHqmQHuXa2uXZMMvjGf1IqJDR1/FpXfmobv5ECN0mGZCjUHGNSyxvoDyXKIkKwJu7IwEoh0ouQA==} - - '@module-federation/runtime-core@2.8.2': - resolution: {integrity: sha512-PEkkK9MUp+nUCeQMS4ox3QGZfwwxgfjGA7P4umEnr5c3y8DLNDR+26tyHf/Gkjen2VsjlcyL+mEAQo1Zi4IE3g==} + '@module-federation/runtime-core@2.9.0': + resolution: {integrity: sha512-dLykRYfpbEJBTdk2NlbNoVVTB196O3qujawTBguLABJkPCWETJNk60NR1yZO34eI+DTH+eGwHU3m7FddAWnbnw==} - '@module-federation/runtime-tools@2.8.0': - resolution: {integrity: sha512-3yOqjdSHXxX4HA3GhlXg3hghGAXW2RJUsnwXCcik2/lTxOHizKI8f3RM+GGCKPxDVqtw43IShe3tA12jNL5A/A==} + '@module-federation/runtime-tools@2.9.0': + resolution: {integrity: sha512-u2puqsaHiw1bVvLNk1uh98iPo6UzaBuci/aTvOFwKvbT/RF18C3vGGnm+ShHPKQEnoN6ctPtnygyOZDPR+8cgg==} - '@module-federation/runtime@2.8.0': - resolution: {integrity: sha512-cGtUBQ1/TVy7KrXy6xPgy3FEmOGyIYkBA2T4iGH3ZH5PNPPTmqN9jF2AfneTSOj0RtBr7Pxq3CUt81E/UCvK1A==} + '@module-federation/runtime@2.9.0': + resolution: {integrity: sha512-3cyAav0hWP+dNvB7qrcK9CKxQn+JvkbRvLtZz3zS2g0EyxtDFDjgbHY0Afcf7oHf8IHhKeEdzb/3Kjr1Pjmr7A==} - '@module-federation/runtime@2.8.2': - resolution: {integrity: sha512-SUoP+PD5EjSPSi6FxEPGIZoRkFifxdeYcVQbJE9mO0VEjF51gAk3/TgX8k0vzUryOBPmXekLr9SfQXU6DqUtvA==} + '@module-federation/sdk@2.9.0': + resolution: {integrity: sha512-IMjObgBGQTXd33jTTCYcxvz8iOQGLsZ2QIPOj90rDxuBtJsN4WJwYyXqligrhY/e5p/BipUPdbIgKmWL7zFhTw==} - '@module-federation/sdk@2.8.0': - resolution: {integrity: sha512-yBP+9+0Z8nlvKEXAZS3AsQVy7bFbZf8eMivGk4q4ZdwG3TsLMlsPjb1dQb2i7gcAG6ux9y2LWLkj/0LVk74cnQ==} + '@module-federation/third-party-dts-extractor@2.9.0': + resolution: {integrity: sha512-R1Xuqnqzw6wQxWV3yU9fX1FydXAeZF2TNVFAVo1N1oLBA2j5y7aUO/Fc3VSNgd9/gxMzJgTVBdA+nMiD2SFCgg==} - '@module-federation/sdk@2.8.2': - resolution: {integrity: sha512-OPS/lbQjraLXoWniQpCwQ/vqgURHTrhsackSNcOPmcJHM3LyR+DabxUc0pl8jAqExsW2l+uepQq7+/Gkei871w==} - - '@module-federation/third-party-dts-extractor@2.8.0': - resolution: {integrity: sha512-nAMlr74OKIylkfRwlunOhytQbmsgb3gCqdXWnPQhG+ZtqWXGELLfMT4a1Q1ht3cS+sRpWj2SZRqK2M7GadI6tA==} - - '@module-federation/webpack-bundler-runtime@2.8.0': - resolution: {integrity: sha512-82fDy9v+7qV5fiN8TKVhOdrxhmAZnUIX/IKivYX5ulCt8aoOzVFTiwm/P1GQUDD8z6dqR48xgJdZdf0548Mc9w==} + '@module-federation/webpack-bundler-runtime@2.9.0': + resolution: {integrity: sha512-MdU6NQibT57MaJG3KPBjC30IVBrz5eU7IcjHoVDYnMh7OmASw3stagBu7hYjdMTP31luNdtzUn85s9axvdwTlw==} '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} @@ -2094,64 +2320,64 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@opentelemetry/api-logs@0.220.0': - resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + '@opentelemetry/api-logs@0.222.0': + resolution: {integrity: sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.9.0': - resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} + '@opentelemetry/context-async-hooks@2.11.0': + resolution: {integrity: sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.9.0': - resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@2.9.0': - resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.220.0': - resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + '@opentelemetry/sdk-logs@0.222.0': + resolution: {integrity: sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.9.0': - resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + '@opentelemetry/sdk-metrics@2.11.0': + resolution: {integrity: sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.9.0': - resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.9.0': - resolution: {integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==} + '@opentelemetry/sdk-trace-node@2.11.0': + resolution: {integrity: sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.9.0': - resolution: {integrity: sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==} + '@opentelemetry/sdk-trace-web@2.11.0': + resolution: {integrity: sha512-8hgLI437x8VRKzgUr+WRcHFp9AeaHmveKRsPIMNele9tethgCKprakRRqZCWyt41ilALHATfspUMl1An0OTMUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace@2.9.0': - resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -2513,248 +2739,126 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.63.0': - resolution: {integrity: sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxfmt/binding-android-arm-eabi@0.64.0': - resolution: {integrity: sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==} + '@oxfmt/binding-android-arm-eabi@0.66.0': + resolution: {integrity: sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.63.0': - resolution: {integrity: sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxfmt/binding-android-arm64@0.64.0': - resolution: {integrity: sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==} + '@oxfmt/binding-android-arm64@0.66.0': + resolution: {integrity: sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.63.0': - resolution: {integrity: sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxfmt/binding-darwin-arm64@0.64.0': - resolution: {integrity: sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==} + '@oxfmt/binding-darwin-arm64@0.66.0': + resolution: {integrity: sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.63.0': - resolution: {integrity: sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxfmt/binding-darwin-x64@0.64.0': - resolution: {integrity: sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==} + '@oxfmt/binding-darwin-x64@0.66.0': + resolution: {integrity: sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.63.0': - resolution: {integrity: sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxfmt/binding-freebsd-x64@0.64.0': - resolution: {integrity: sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==} + '@oxfmt/binding-freebsd-x64@0.66.0': + resolution: {integrity: sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': - resolution: {integrity: sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': - resolution: {integrity: sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm-musleabihf@0.63.0': - resolution: {integrity: sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==} + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + resolution: {integrity: sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': - resolution: {integrity: sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==} + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + resolution: {integrity: sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.63.0': - resolution: {integrity: sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-arm64-gnu@0.64.0': - resolution: {integrity: sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==} + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + resolution: {integrity: sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.63.0': - resolution: {integrity: sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-linux-arm64-musl@0.64.0': - resolution: {integrity: sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==} + '@oxfmt/binding-linux-arm64-musl@0.66.0': + resolution: {integrity: sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.63.0': - resolution: {integrity: sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==} + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + resolution: {integrity: sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': - resolution: {integrity: sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-riscv64-gnu@0.63.0': - resolution: {integrity: sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': - resolution: {integrity: sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==} + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + resolution: {integrity: sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.63.0': - resolution: {integrity: sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-linux-riscv64-musl@0.64.0': - resolution: {integrity: sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==} + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + resolution: {integrity: sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.63.0': - resolution: {integrity: sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-s390x-gnu@0.64.0': - resolution: {integrity: sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==} + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + resolution: {integrity: sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.63.0': - resolution: {integrity: sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-x64-gnu@0.64.0': - resolution: {integrity: sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==} + '@oxfmt/binding-linux-x64-gnu@0.66.0': + resolution: {integrity: sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.63.0': - resolution: {integrity: sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-linux-x64-musl@0.64.0': - resolution: {integrity: sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==} + '@oxfmt/binding-linux-x64-musl@0.66.0': + resolution: {integrity: sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.63.0': - resolution: {integrity: sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxfmt/binding-openharmony-arm64@0.64.0': - resolution: {integrity: sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==} + '@oxfmt/binding-openharmony-arm64@0.66.0': + resolution: {integrity: sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.63.0': - resolution: {integrity: sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxfmt/binding-win32-arm64-msvc@0.64.0': - resolution: {integrity: sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==} + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + resolution: {integrity: sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.63.0': - resolution: {integrity: sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==} + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + resolution: {integrity: sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.64.0': - resolution: {integrity: sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==} + '@oxfmt/binding-win32-x64-msvc@0.66.0': + resolution: {integrity: sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxfmt/binding-win32-x64-msvc@0.63.0': - resolution: {integrity: sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxfmt/binding-win32-x64-msvc@0.64.0': - resolution: {integrity: sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [x64] os: [win32] '@oxlint-tsgolint/darwin-arm64@7.0.2001': @@ -2787,252 +2891,130 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.78.0': - resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} + '@oxlint/binding-android-arm-eabi@1.81.0': + resolution: {integrity: sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm-eabi@1.79.0': - resolution: {integrity: sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxlint/binding-android-arm64@1.78.0': - resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxlint/binding-android-arm64@1.79.0': - resolution: {integrity: sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==} + '@oxlint/binding-android-arm64@1.81.0': + resolution: {integrity: sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.78.0': - resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} + '@oxlint/binding-darwin-arm64@1.81.0': + resolution: {integrity: sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-arm64@1.79.0': - resolution: {integrity: sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxlint/binding-darwin-x64@1.78.0': - resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxlint/binding-darwin-x64@1.79.0': - resolution: {integrity: sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==} + '@oxlint/binding-darwin-x64@1.81.0': + resolution: {integrity: sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.78.0': - resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} + '@oxlint/binding-freebsd-x64@1.81.0': + resolution: {integrity: sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-freebsd-x64@1.79.0': - resolution: {integrity: sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': - resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm-gnueabihf@1.79.0': - resolution: {integrity: sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==} + '@oxlint/binding-linux-arm-gnueabihf@1.81.0': + resolution: {integrity: sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.78.0': - resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} + '@oxlint/binding-linux-arm-musleabihf@1.81.0': + resolution: {integrity: sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.79.0': - resolution: {integrity: sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm64-gnu@1.78.0': - resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-arm64-gnu@1.79.0': - resolution: {integrity: sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==} + '@oxlint/binding-linux-arm64-gnu@1.81.0': + resolution: {integrity: sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.78.0': - resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-arm64-musl@1.79.0': - resolution: {integrity: sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==} + '@oxlint/binding-linux-arm64-musl@1.81.0': + resolution: {integrity: sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.78.0': - resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-ppc64-gnu@1.79.0': - resolution: {integrity: sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==} + '@oxlint/binding-linux-ppc64-gnu@1.81.0': + resolution: {integrity: sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.78.0': - resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-riscv64-gnu@1.79.0': - resolution: {integrity: sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==} + '@oxlint/binding-linux-riscv64-gnu@1.81.0': + resolution: {integrity: sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.78.0': - resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} + '@oxlint/binding-linux-riscv64-musl@1.81.0': + resolution: {integrity: sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-riscv64-musl@1.79.0': - resolution: {integrity: sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-s390x-gnu@1.78.0': - resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-s390x-gnu@1.79.0': - resolution: {integrity: sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==} + '@oxlint/binding-linux-s390x-gnu@1.81.0': + resolution: {integrity: sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.78.0': - resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-x64-gnu@1.79.0': - resolution: {integrity: sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==} + '@oxlint/binding-linux-x64-gnu@1.81.0': + resolution: {integrity: sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.78.0': - resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-x64-musl@1.79.0': - resolution: {integrity: sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==} + '@oxlint/binding-linux-x64-musl@1.81.0': + resolution: {integrity: sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.78.0': - resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxlint/binding-openharmony-arm64@1.79.0': - resolution: {integrity: sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==} + '@oxlint/binding-openharmony-arm64@1.81.0': + resolution: {integrity: sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.78.0': - resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} + '@oxlint/binding-win32-arm64-msvc@1.81.0': + resolution: {integrity: sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-arm64-msvc@1.79.0': - resolution: {integrity: sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxlint/binding-win32-ia32-msvc@1.78.0': - resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxlint/binding-win32-ia32-msvc@1.79.0': - resolution: {integrity: sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==} + '@oxlint/binding-win32-ia32-msvc@1.81.0': + resolution: {integrity: sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.78.0': - resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} + '@oxlint/binding-win32-x64-msvc@1.81.0': + resolution: {integrity: sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.79.0': - resolution: {integrity: sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxlint/plugins@1.79.0': - resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + '@oxlint/plugins@1.81.0': + resolution: {integrity: sha512-HhD8kd3r6XpelZkhxhRty/po8V6yK1VV63Eq4kSsOS2IoHUywG3DV2EX+z/ZHw46aLI74Y6aA604NRiAqLKW9w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@parcel/watcher-android-arm64@2.6.0': @@ -3172,6 +3154,42 @@ packages: '@protobufjs/utf8@1.1.2': resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@redis/bloom@6.2.1': + resolution: {integrity: sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/client@6.2.1': + resolution: {integrity: sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true + + '@redis/json@6.2.1': + resolution: {integrity: sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/search@6.2.1': + resolution: {integrity: sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/time-series@6.2.1': + resolution: {integrity: sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + '@rollup/pluginutils@5.4.0': resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} @@ -3191,8 +3209,8 @@ packages: core-js: optional: true - '@rsbuild/core@2.2.0-rc.0': - resolution: {integrity: sha512-f6orjv+wOR1u7KchE/vANGP0Eg7AP0UaiM0qn4n+5I0HOUdkVVbNCA1eZSpyX+TJ9bIdZtkbR6T47vBdoFr1MA==} + '@rsbuild/core@2.2.3': + resolution: {integrity: sha512-oJrYtYDhb7AMwY8HIda2hgMuuxHkYPYar4svdS5uTq0fXY0M1wLfllkTQz0d729pNJ4S//6GUM1WX5LsW2mFLw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3209,14 +3227,6 @@ packages: '@rsbuild/core': optional: true - '@rsbuild/plugin-check-syntax@1.6.1': - resolution: {integrity: sha512-26xtEYN0QjZYoyt0lWnvIztBWjEZJvcfw7MN4f5B4SpNggmnF7F7aNPrgkY3EccXVFx1VGQBhnCkBV//OoS07Q==} - peerDependencies: - '@rsbuild/core': ^1.0.0 || ^2.0.0-0 - peerDependenciesMeta: - '@rsbuild/core': - optional: true - '@rsbuild/plugin-check-syntax@2.0.1': resolution: {integrity: sha512-z+NMAUXEbM4fhoQlKJNgTjsf+O1tknBihsXj4JnSFlwpfoY5uDjKC6D+StATATvUilSKXFrk4WDhcIyoxkj1gg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3309,47 +3319,13 @@ packages: '@rsbuild/core': optional: true - '@rsdoctor/client@1.6.1': - resolution: {integrity: sha512-UWc8uyXPtcGZuQtVX+jBZwPxMXrMrrp0GrbVvHoHjzTxWufdn4RcG1g4PFgZLPz5smcCGYOEaNvlNn5WOtVyMw==} - - '@rsdoctor/core@1.6.1': - resolution: {integrity: sha512-PkNe5Z45gR3dEhFdOcN/ZTwUxTltJR66cJA4DHXj1Zuo9s8d6SKWHFBLi5zMqPV3ZyBAQl4tsGv2LoJDQa69wA==} - - '@rsdoctor/graph@1.6.1': - resolution: {integrity: sha512-LHbJKwO31BujQNUsD28kDoQKa5EkRxtSOABuHWRTv62drzz4NF9ZGSIHmrd6/jkbDBdCZEngZIwBkTSi3gtFoA==} - - '@rsdoctor/rspack-plugin@1.6.1': - resolution: {integrity: sha512-P9/8wAF4rBEujzB42Afz9zLUR+DoaIDlsyO9Hk2Zzg4e6A+6PlZjma06zhEDbPgcAIJ3PyeX/xgH+5pcc9HjUQ==} - peerDependencies: - '@rspack/core': '*' - peerDependenciesMeta: - '@rspack/core': - optional: true - - '@rsdoctor/sdk@1.6.1': - resolution: {integrity: sha512-9dBk9SDVcY2Z8mHepUKDpHc3eaAsYNSioLlbNPNVLH4XinzcRe/4kJiX37VNjUVPv93IzZ6GTJhkiax/6O+fGw==} - - '@rsdoctor/types@1.6.1': - resolution: {integrity: sha512-E01VGaDGvXGig3rqQC+YRRPbGQ6tBLi9XkYCl579Hq01iYHeuVYLl1bfnODAt8SkdDYGJDMFuUWj/01A0vVy7g==} - peerDependencies: - '@rspack/core': '*' - webpack: 5.x - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - - '@rsdoctor/utils@1.6.1': - resolution: {integrity: sha512-Ll+X1nAE3eBq9BpBNZvAT+4hZZMQt/rxrBRzL/I8o6S3CBpo5Kh2A2JaYgCJLiYKh0kFQfuIWuOVpR3/yoFWJg==} - '@rspack/binding-darwin-arm64@2.2.0': resolution: {integrity: sha512-KAVVT7hp3NBjtc/RY2UtOjzzc8i+s4pIhW1p52UV+Aev6ywQCu3dXwkHTonpPvJO3hqLXc4zIMH5l4HbMqBm4g==} cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-arm64@2.2.0-rc.0': - resolution: {integrity: sha512-Uvy3YnN1pCLe+2/8hF06KiCPegf8ztOuM3VE/p0MJKRitkL5DPoZVHyc40zl6e+O5WB/9tJ5tnEgRG9pDWxFng==} + '@rspack/binding-darwin-arm64@2.2.2': + resolution: {integrity: sha512-/le/AvV4HSinXTPs2Lqpujxt189Z5T10Ggt96QzckLRPvIchzqoavVDDjltzC7XcaZ87XCH/X06jNKgzkcBBNA==} cpu: [arm64] os: [darwin] @@ -3358,8 +3334,8 @@ packages: cpu: [x64] os: [darwin] - '@rspack/binding-darwin-x64@2.2.0-rc.0': - resolution: {integrity: sha512-UQO0PgLarsA0lv96uqCTAH1eAnAIPHb+qhMfds5ubWKZgKlr0taGQl253C3DN5pfzbHWfNQgAfVUaVMydm9czw==} + '@rspack/binding-darwin-x64@2.2.2': + resolution: {integrity: sha512-uFIcUPUXiPxM6ljenLafp5TemT8eLZm1riRn8fJYmpqNCK+aCcTaud18XHZpI5SjzzcY+xUHfVShgvNzKXuf2g==} cpu: [x64] os: [darwin] @@ -3369,8 +3345,8 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-arm64-gnu@2.2.0-rc.0': - resolution: {integrity: sha512-ZggL6W9ckpvhqIPi7K3zDRiEaQd5Co2qRnN5J8OMmBkQlvYQek0CE/SZHFcZsDM0//6+0mkI+VjaTeWdvqJsaQ==} + '@rspack/binding-linux-arm64-gnu@2.2.2': + resolution: {integrity: sha512-Pjby4pDSMNJQK2VBzpgCj6lb+DGuenS1fEDb6xi5/apbJb9v5WE+e43Mz7i+XqgXS8e846pjaONV3KM5WKB1LQ==} cpu: [arm64] os: [linux] libc: [glibc] @@ -3381,8 +3357,8 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-arm64-musl@2.2.0-rc.0': - resolution: {integrity: sha512-MfDTg9fn/17lRtGMsHLK8BQJs+AEtTsVMWOg1CMou/ls8IrucXoFZ9Ux0i2Jq1ph1ZiKgr4l7pMzdk3SXpNiQg==} + '@rspack/binding-linux-arm64-musl@2.2.2': + resolution: {integrity: sha512-0u9O7tTVT2z+F6o/eEY6f6+My8Jn9U56QiBwkfy90ZaoAfZyQSSTxqaK/zA7W43oFxQefeCpiPas27VUzrSakw==} cpu: [arm64] os: [linux] libc: [musl] @@ -3393,8 +3369,8 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-ppc64-gnu@2.2.0-rc.0': - resolution: {integrity: sha512-n84s99eIMr/4xQ1XCctxtu4AnchukynuyJ4DaJ4vlcRKVdFAnPSpUH669rAxztsby5xa3ftAASmxwXhMUFzMsg==} + '@rspack/binding-linux-ppc64-gnu@2.2.2': + resolution: {integrity: sha512-N3lVnhq5qOvpmP5n386JzR1GcE8HzAqq4/r440Z6yle9m7PhVFJ6oXVWxgaDTYV0mtv9YLJmaG+YrjYfUa1vuA==} cpu: [ppc64] os: [linux] libc: [glibc] @@ -3405,8 +3381,8 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-riscv64-gnu@2.2.0-rc.0': - resolution: {integrity: sha512-s5bg/LlwnMSVXZfR16KGO3jVWUpobbPp90qE2DXwfaFpcLmMweUnANERM2mCpIiW3MuVnTAXTEjvEcxfB4mXEw==} + '@rspack/binding-linux-riscv64-gnu@2.2.2': + resolution: {integrity: sha512-ReClZyp32/rJkUDV/oGDU0X6BCFyEkTR6r4stFwm/qOOaG6mUMRzZe4gur8Yh979p4Hiz9EdkmfEHY02GBXcaQ==} cpu: [riscv64] os: [linux] libc: [glibc] @@ -3417,8 +3393,8 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-riscv64-musl@2.2.0-rc.0': - resolution: {integrity: sha512-XNjEnkrNv67CZ4/IhkPQVfNkz9JHevelgZspzuuJOOyn+Z9b1m8JN+shv5Kq06sSudN9aQpLLa6slbHlKGv9lA==} + '@rspack/binding-linux-riscv64-musl@2.2.2': + resolution: {integrity: sha512-mqsorvTNerr3r8zI35NFTPYATPDlhepdiUhZjKT6ylqpHlP7vLU9fp4aEMK/CuTv2f+IVsa0+iZqtMxHs2tSjw==} cpu: [riscv64] os: [linux] libc: [musl] @@ -3429,8 +3405,8 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-s390x-gnu@2.2.0-rc.0': - resolution: {integrity: sha512-OQpj1j6Jfy+YBYDSGwJisZZEXS3g0Y/M5xlb26yqlZBKA/7kamCMDccUPTKNpuJaRQXK1DUerBsA+AK8TELG3g==} + '@rspack/binding-linux-s390x-gnu@2.2.2': + resolution: {integrity: sha512-ql2Jub8QYWSugBppmj2u0SjcIj6fOQuAaLCYQOqU7zbvz/uuWTfoqmdWKExwrxeCU9Tzt7emVM0ETuVEpoca+A==} cpu: [s390x] os: [linux] libc: [glibc] @@ -3441,8 +3417,8 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-x64-gnu@2.2.0-rc.0': - resolution: {integrity: sha512-1QoW+7zjApCgL4v5P14AmnxfvOMCk131abSBCl/+u6h/aIainSwpf+O3ar0FqvkQaSPPOQJrg03ys57WyEwlAQ==} + '@rspack/binding-linux-x64-gnu@2.2.2': + resolution: {integrity: sha512-MNYKYEHrtIVEno2q5rgpou/JVffRwn109xPK3kxct95EojHsniNa8jwy6eEHeOazP4EN60Si7NElE3aQ6JssHw==} cpu: [x64] os: [linux] libc: [glibc] @@ -3453,8 +3429,8 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-x64-musl@2.2.0-rc.0': - resolution: {integrity: sha512-KLN4bIC3M1dSEuPBX1SPOEkBRyZnnIx8vBjfIFFpKa1bw/CsOHDF7Y6IAAsf9hir7dH3cUd4vaPSCVRM4KiTDw==} + '@rspack/binding-linux-x64-musl@2.2.2': + resolution: {integrity: sha512-y9/9CmE8lrECaF17GAhacibTL+SvlEPEotQnUuBFC/WFtXh8hU2E7a7bAkpYGSLH6kM0nrJZHO3hTvM1wdWOJw==} cpu: [x64] os: [linux] libc: [musl] @@ -3463,8 +3439,8 @@ packages: resolution: {integrity: sha512-rerLPTN/HD4EvLNWs3O2N+Eb37eGvLRIP3dXXc3n+UzTebOepAsahNn44vXeRBsE4m/pHkpDJjwgWTytgQ2gBw==} cpu: [wasm32] - '@rspack/binding-wasm32-wasi@2.2.0-rc.0': - resolution: {integrity: sha512-2Vvtckz5DNghQKQKwghfM4j30MOu7t9J7mbUCCi7mTUccpy7Cnr0HPmSV9Jx4sUj6vgQEV+8lIAIxNDNtr0Vhg==} + '@rspack/binding-wasm32-wasi@2.2.2': + resolution: {integrity: sha512-VbDIjjeFwZvMSKAOGY5IbU6lLzt6AHHHncTdMMkZ94Xk7O2BOHe9BXDV32Ln29TIW2C8m1fdxfPZWDiecVghUQ==} cpu: [wasm32] '@rspack/binding-win32-arm64-msvc@2.2.0': @@ -3472,8 +3448,8 @@ packages: cpu: [arm64] os: [win32] - '@rspack/binding-win32-arm64-msvc@2.2.0-rc.0': - resolution: {integrity: sha512-enPqTT2sdt6HgItyk6SA6ubvZ2UXkFIpwhsCuoL83z9vHoDw/Y8obC8L92nIuK6FDP17tokm/r1SFPhDZJIYcg==} + '@rspack/binding-win32-arm64-msvc@2.2.2': + resolution: {integrity: sha512-rfcNg0W3ZPZvXma1gTyEt9/Z8FxASIaQr+sMWTSaTPPaeU3xY1+0hYcrD0kUFNs3/5L4u63myI4R8qRXiuW3pA==} cpu: [arm64] os: [win32] @@ -3482,8 +3458,8 @@ packages: cpu: [ia32] os: [win32] - '@rspack/binding-win32-ia32-msvc@2.2.0-rc.0': - resolution: {integrity: sha512-qYeZLJDfboqkWmNcqazjUcld2QegyErDJVaCfAPm2mnneMmKhQWsOs/DmlqRfC4ePaQN9uOtu26iLwKy2o1sgw==} + '@rspack/binding-win32-ia32-msvc@2.2.2': + resolution: {integrity: sha512-TFPvr9RZw9oHIhooDhXHzWjKcHpGPTxkznSeM2poIWU0CdEuua2rVUfsrriTF1Dmx+9kMly61DQmyOHCbb+b2g==} cpu: [ia32] os: [win32] @@ -3492,16 +3468,16 @@ packages: cpu: [x64] os: [win32] - '@rspack/binding-win32-x64-msvc@2.2.0-rc.0': - resolution: {integrity: sha512-bvzp27ZvpZW1vcCG/srk/K/6cffcR/kqDUcW8dWEMFlntPSBTml9lOC4ouSBpU7n/qIwG6hKE56FaTWWapc3Lw==} + '@rspack/binding-win32-x64-msvc@2.2.2': + resolution: {integrity: sha512-GvEGyL594dtWN9SoVnKWh0exrM8WLInaUjwcuA2JKbCi1ak/9iHxip/U1dY3DuVqBYBhmvYq96JPbl91xnzFjg==} cpu: [x64] os: [win32] '@rspack/binding@2.2.0': resolution: {integrity: sha512-nxZzJqqB0EmEKp6qjzFNkBb/SgGt0k0DSENrLvAJgvVvrm3waVsubD0cfxtPlZY/rd5SzadzxWGEHRyFcds5nA==} - '@rspack/binding@2.2.0-rc.0': - resolution: {integrity: sha512-/Q9ysTd5ajEbIS+Sv61trElR7pWAa5LvcWNrYT+AKI9APsVwy4Y3CIPjEkd7jYeNHl1PJWSLCOUy+BzRUICDUg==} + '@rspack/binding@2.2.2': + resolution: {integrity: sha512-gWjKDQfVQJSBh/I+y9WTlyERsiShSJ7eI6Yl0SJs/6gjx8t4ixuwWCsaEFFjwSL4nSn6ML5hNQ7UFY3gj72BWA==} '@rspack/core@2.2.0': resolution: {integrity: sha512-3W7oX0BAHbK4VlknH3lfyfRvupzxdZtyEa+DfKmdjzmIAcqYtHnFd0nLqp5dzitDPyDI1TIKkDhpB0AZJn0pVg==} @@ -3515,8 +3491,8 @@ packages: '@swc/helpers': optional: true - '@rspack/core@2.2.0-rc.0': - resolution: {integrity: sha512-2LAdAFF/ZdnBRltI+IievGhysby9LESxvELxS1ZVkPc1F6ZAJ1skIcCNOLmfZeoYwQ5nXZjdUCbCf9MFDMWJjg==} + '@rspack/core@2.2.2': + resolution: {integrity: sha512-/yztfDZR5syIPBrUpzBpL+6fhhl0IHBPcXlNr4tOMBULbocFIz7Z4/cqvf1ix0DBbKpIGx99v6N1IDbk2gi8hw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 @@ -3539,65 +3515,8 @@ packages: '@rspack/core': optional: true - '@rspack/resolver-binding-darwin-arm64@0.2.8': - resolution: {integrity: sha512-nTnK17kmxXEvR+WpOIZPSIzUFYeWCHoffgU9tvOLOwuTBH41kWnSQXXWu+AiMVwvJ6wdRO6Vo30hPhlXEG7Pyw==} - cpu: [arm64] - os: [darwin] - - '@rspack/resolver-binding-darwin-x64@0.2.8': - resolution: {integrity: sha512-Aqr4TK2rA6XVYUOmM5YCtYyCMZhOIR53P4cOGgGARg99A7OuMBMzUL4r1n0M0Fx35v6/sSx1OBe+odHmPxksEg==} - cpu: [x64] - os: [darwin] - - '@rspack/resolver-binding-linux-arm64-gnu@0.2.8': - resolution: {integrity: sha512-wGvkxm2G4mNTztslaOzLzx5JuySQSy5DcOWEZxHcjJJzp5L3ODbYLK18HtUc6cvmaVOmjaGrrYPrqJJ0hHTVFg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rspack/resolver-binding-linux-arm64-musl@0.2.8': - resolution: {integrity: sha512-EqRJ9zLQsLAvyDKJKVZ45BSqRIMS12f5HtJdy3KkAHU14ZmsGv8e5IKkwUZN5CNBRad8xVlOMMx3dOfF4whJzg==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rspack/resolver-binding-linux-x64-gnu@0.2.8': - resolution: {integrity: sha512-eXbeotNCTntL4/+mxJRVCxK63YeWzTfp0F3POeHJFSs6Nt0f2J/mZNFlasJmd6xm7zvE80h/HWOwbwjRBLcElA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rspack/resolver-binding-linux-x64-musl@0.2.8': - resolution: {integrity: sha512-KWFHlOWGkT+eMngoUgPGXrDi+rU04VCh9jyk0U6Ot2RTWvhGxwKykjmLS+CWZI/EBrzr9A6g2U3jzKTMNz9oCw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rspack/resolver-binding-wasm32-wasi@0.2.8': - resolution: {integrity: sha512-I6GIhgICFViE88jejIV74oiiWHnpLpQ5ogaZM1ozM9KDnfqcHoX0IVEyrIh5KqA8iLDyhuoFSW+Hf0qN7VTBBQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rspack/resolver-binding-win32-arm64-msvc@0.2.8': - resolution: {integrity: sha512-ZXCt3qUfDAEbtc2sHpvxM7lNFZM+DxfblgXUIl3Jy6BuEZbHe1i6z+t9c34ayHoGTVbVSNCtaYuG/MaWdSnPHw==} - cpu: [arm64] - os: [win32] - - '@rspack/resolver-binding-win32-ia32-msvc@0.2.8': - resolution: {integrity: sha512-2LRymjDK8MpUERD8CL0PPae5y2crU5TAg4T4EzpeL5jLARVq6izsEruiWzB6Y+D15vUYlvmgs2370GXVSB861w==} - cpu: [ia32] - os: [win32] - - '@rspack/resolver-binding-win32-x64-msvc@0.2.8': - resolution: {integrity: sha512-hzRpfbtvv4M4EVrKKIAaHDs5wT8lVcbSUjtwPs5u4IeLEix45nQPQ6ZQjmE4lIH0GP/3L3XQhZroYmTcH/xdsQ==} - cpu: [x64] - os: [win32] - - '@rspack/resolver@0.2.8': - resolution: {integrity: sha512-FBWqdHhzS8mcf/WN4Ktzr7EaeaN+hsxbN98EweegX3924beZuY6H70CSFWCv1fIHAieCUv/9XCjKggHvhCsLwA==} - - '@rstest/adapter-rsbuild@0.11.9': - resolution: {integrity: sha512-i/PSgFGyKkIn5Pk+UUQEH7KWd6hEVsHV5WW3+4Awe684VG+yBabKNWkqIg7EqSZ0jkTqtrcpdQUTX99U61DSrg==} + '@rstest/adapter-rsbuild@0.11.12': + resolution: {integrity: sha512-0XXUMCSxAYK7zuBltAABSIIAcsCrvCywyRLIkicvJwIlvRGAwhRO5DSIZLo77y5lEYQAZl6uAHwczyZC9lZ1DA==} peerDependencies: '@rsbuild/core': ^1.0.0 || ^2.0.0 '@rstest/core': ^0.11.0 @@ -3636,9 +3555,6 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@socket.io/component-emitter@3.1.2': - resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@speed-highlight/core@1.2.17': resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} @@ -3719,86 +3635,86 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/core-darwin-arm64@1.15.43': - resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + '@swc/core-darwin-arm64@1.16.2': + resolution: {integrity: sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.43': - resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + '@swc/core-darwin-x64@1.16.2': + resolution: {integrity: sha512-HrwqHyEyHVXO3qTk8EkNK7/b6sOZSEoNh+pot6RdE5x0LbNqfo8LtJUvi3UTXr+5ja/o5HbJdW80eCXo+NjbiA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.43': - resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + '@swc/core-linux-arm-gnueabihf@1.16.2': + resolution: {integrity: sha512-MdXi83Z/gGp1LIrg+h7HKxiul/z/Bty/ZJSvYAFqDl9zteC1XLSAZdScquKtXPp50rdyXqritTDCqQBhwVfZKA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.43': - resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + '@swc/core-linux-arm64-gnu@1.16.2': + resolution: {integrity: sha512-/jcTmK6Ktz3owM3YtiKvjofV6p3VpHnYzTIrOGwDIOsDigRAAVuZ8east33wYO/7UTdKYFlyHNnJNT0WJqOA3Q==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.43': - resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + '@swc/core-linux-arm64-musl@1.16.2': + resolution: {integrity: sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.15.43': - resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + '@swc/core-linux-ppc64-gnu@1.16.2': + resolution: {integrity: sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.15.43': - resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + '@swc/core-linux-s390x-gnu@1.16.2': + resolution: {integrity: sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.15.43': - resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + '@swc/core-linux-x64-gnu@1.16.2': + resolution: {integrity: sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.43': - resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + '@swc/core-linux-x64-musl@1.16.2': + resolution: {integrity: sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.43': - resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + '@swc/core-win32-arm64-msvc@1.16.2': + resolution: {integrity: sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.43': - resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + '@swc/core-win32-ia32-msvc@1.16.2': + resolution: {integrity: sha512-fycER209DYIzsibpTMC+chND05OfOjgztWL9U8OE6/uUlsOUZH3eh98isBLEnOymYUhlJLEt5++W1+KL/FOh5Q==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.43': - resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + '@swc/core-win32-x64-msvc@1.16.2': + resolution: {integrity: sha512-cSd1z6ivSrJPVr+moVwOHWjeKy6TpO4/Shwcv5KCrKYXCccxwh4pRy1C3fDioNx2PF1jPZWHKZjtXt+Be9VbaQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.43': - resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + '@swc/core@1.16.2': + resolution: {integrity: sha512-95I4kiSMeveI/Mhi+tE4fiWcWLUMfzfKrk0jtr8LRMqHgOgq+xHS+zExkDqoO4b5OeeuXHMWVdD5MeP3X6sULw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -3818,11 +3734,11 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@swc/plugin-loadable-components@12.0.0': - resolution: {integrity: sha512-3vdbr0k5NQ0W4KgIx6LrFvjKwDyMPWbeHBncfFT7Fo2IKyo7513CXMCkx6KEUEsPzTJZRhHx0pFyt2hPWnfpEA==} + '@swc/plugin-loadable-components@13.0.0': + resolution: {integrity: sha512-xgixniTjdblzaWIDm+CJnhGhjkvgiCwc1n/f8V0gNVmh5n7Wn5+RbqBvLG1+y/tEzcKqeQ55MXNSH85XJklEaQ==} - '@swc/types@0.1.27': - resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -3924,12 +3840,12 @@ packages: webpack: optional: true - '@tanstack/history@1.162.1': - resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} + '@tanstack/history@1.162.2': + resolution: {integrity: sha512-Lemp3DJbzNqcin/nZpWxycDaEqySDbnIshDbyHJMMCapD4ZQMe57szRpBXOfzfP6fyWAtHNrLrcBUyANJ6Vlow==} engines: {node: '>=20.19'} - '@tanstack/react-router@1.170.25': - resolution: {integrity: sha512-XiWYvkLAGhcZHhV2xUvicpF/VfTVSnewP6CqviDONAjmD50tBob5x/93u2W8QZAc/JgkPd+jijCmu6t4NCzmew==} + '@tanstack/react-router@1.170.33': + resolution: {integrity: sha512-iNnI98vH3kO/V4dy6YM0CInhqwWBddU0G5wZK5jiMvr3HsK2avDQSRx3RY/y6v+6zQAqb2kD6hUPHIenrJBTSw==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -3941,8 +3857,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.171.21': - resolution: {integrity: sha512-t6xUHBO94nQDrPHPh6ag5UuKHWIkVjq9s63q2ctbxgzq3vtSoGKmAIdLD5o+NU6JUS1ppXRHgL0YGzEHvH32Ng==} + '@tanstack/router-core@1.171.28': + resolution: {integrity: sha512-PvPWSklhw6i9b0rzScVh0btQsK5u/gBYN3mBHyDzhC/U4LrB3WzPXPkUunQUKvQOGXCp16UEb7Htc/ITGm5DkQ==} engines: {node: '>=20.19'} '@tanstack/store@0.9.3': @@ -3993,21 +3909,12 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cors@2.8.19': - resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -4032,9 +3939,6 @@ packages: '@types/loadable__component@5.13.10': resolution: {integrity: sha512-2/LjmgG1JcGPj7T3NViq7BB5cvOA0s63gL3Gv+FPULj2L+3ExWfsNQcsFPUIOoGsVUJeZxgNPf320JZDyxjtCQ==} - '@types/node@20.19.43': - resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} - '@types/node@26.4.1': resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} @@ -4045,8 +3949,8 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.7': + resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==} peerDependencies: '@types/react': ^19.2.0 @@ -4056,8 +3960,8 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/tapable@2.3.0': - resolution: {integrity: sha512-oMnbAXeVo+KUnje3hzdORXUbfnzTfqD0H92mLl19NE5hFqH9Q4ktq+xehNSxcNeeLm1COopYwa0zeP6Iz+oIXg==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -4130,53 +4034,6 @@ packages: resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - - '@typescript/native-preview@7.0.0-dev.20260707.2': - resolution: {integrity: sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg==} - engines: {node: '>=16.20.0'} - hasBin: true - '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -4506,10 +4363,6 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -4526,18 +4379,14 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} - acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true - adm-zip@0.5.10: - resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==} - engines: {node: '>=6.0'} + adm-zip@0.6.0: + resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==} + engines: {node: '>=14.0'} agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} @@ -4645,8 +4494,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.5.2: - resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} + autoprefixer@10.5.5: + resolution: {integrity: sha512-uiRYvQYe/nNSzBJ7OUnd2/TZVsAdob3blml44teEpee9Cc1f4rGZFewO+JT3Wo8mgFOSzNqes4FHZn/Qz8WOuw==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -4682,15 +4531,16 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - base64id@2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.11.19: resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} engines: {node: '>=6.0.0'} hasBin: true + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} + engines: {node: '>=6.0.0'} + hasBin: true + better-auth@1.7.2: resolution: {integrity: sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A==} peerDependencies: @@ -4756,7 +4606,7 @@ packages: better-call@1.4.0: resolution: {integrity: sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==} peerDependencies: - zod: ^4.0.0 + zod: 4.5.4 peerDependenciesMeta: zod: optional: true @@ -4796,13 +4646,14 @@ packages: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist-load-config@1.0.3: - resolution: {integrity: sha512-boNaPS4KlW6AITZQ60G+1oDJLuxauljDd7QNQFOYpRtldzcTDknMZ8awbwI0BT/8h1/Y/CG4k/tDOLip9lAGcg==} - browserslist-to-es-version@1.4.2: resolution: {integrity: sha512-3NV13pCv0wmPxxZZcekHAG6vt8rQ94w2c4/UBe3ZU3NDUm5TP+QFK3rjS6XeKWSHWpnPYNfQzlhnljka0BrEOA==} hasBin: true @@ -4812,6 +4663,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -4836,6 +4692,9 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -4926,8 +4785,8 @@ packages: cloneable-readable@3.0.0: resolution: {integrity: sha512-Lkfd9IRx1nfiBr7UHNxJSl/x7DOeUfYmxzCkxYJC2tyc/9vKgV75msgLGurGQsak/NvJDHMWcshzEXRlxfvhqg==} - cluster-key-slot@1.1.1: - resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} color-convert@2.0.1: @@ -4975,8 +4834,8 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} @@ -4996,10 +4855,6 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -5012,12 +4867,8 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} - core-js@3.49.0: - resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} @@ -5028,6 +4879,11 @@ packages: typescript: optional: true + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5066,6 +4922,9 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -5082,6 +4941,10 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -5093,11 +4956,11 @@ packages: peerDependencies: postcss: ^8.5.13 - cssnano-preset-default@8.0.2: - resolution: {integrity: sha512-+jQAqIKCqMmBjZs7741XkilU93ITZ/EW8gjAkMmujdCzfDkfjrDBv2VqkSu29Fzeig/0rZ3S9IAwfPLlmXEUfQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano-preset-default@9.0.3: + resolution: {integrity: sha512-UxvaUk/pNqHzjctjlaq4SSARlqPOdy7kA9Dho6fb1/xt/bYcar+57BFlSWpF5AvxjIfNK3Qkop5U9VDSUnuTvg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 cssnano-utils@5.0.3: resolution: {integrity: sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==} @@ -5105,11 +4968,11 @@ packages: peerDependencies: postcss: ^8.5.13 - cssnano-utils@6.0.1: - resolution: {integrity: sha512-zk65GIxA8tCjqVk7nTm1mE+ZKxtnxAvU5JSUaBLXbAr3ZF7IOvz3fbPOnEDvZKhnS7GOIitXTS5BgehLzNoc8Q==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano-utils@7.0.2: + resolution: {integrity: sha512-X5B8Butd9TISH65XOoSYsq/wokHHjDK7eNujX3EcTf67T6uy2BySmT1SoaSXzlFdOsRf431FTENMsPrsc9JYHg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 cssnano@7.1.9: resolution: {integrity: sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==} @@ -5117,11 +4980,11 @@ packages: peerDependencies: postcss: ^8.5.13 - cssnano@8.0.2: - resolution: {integrity: sha512-K+a76gA1v0/CsYgcsE95HGGyIuPKxpQSetwSwz4nHEM8fFXqSkzq2JzEXFL8v5+CCjxzVVVhPcTK3Oo8SaF/xA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano@9.0.3: + resolution: {integrity: sha512-v4oRMYCXcJtI1MlGcwVgSP/TG/RrznC3iKQeNcFTQZZYWomp2j8CmgT9iiSO2v64K4LJSi6pWdyznmUUutLCKw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -5187,10 +5050,6 @@ packages: supports-color: optional: true - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -5228,10 +5087,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -5322,7 +5177,7 @@ packages: arktype: '>=2.0.0' better-sqlite3: '>=9.3.0' bun-types: '*' - effect: 4.0.0-beta.107 + effect: 4.0.0-rc.112 expo-sqlite: '>=14.0.0' minipg: '>=0.2.0' mssql: ^11.0.1 @@ -5333,7 +5188,7 @@ packages: sqlite3: '>=5' typebox: '>=1.2.0' valibot: '>=1.0.0-beta.7' - zod: ^3.25.0 || ^4.0.0 + zod: 4.5.4 peerDependenciesMeta: '@aws-sdk/client-rds-data': optional: true @@ -5439,12 +5294,15 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - effect@4.0.0-beta.107: - resolution: {integrity: sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==} + effect@4.0.0-rc.112: + resolution: {integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==} electron-to-chromium@1.5.414: resolution: {integrity: sha512-aYlviXiaXBbzvKgyALpcMmqa3Np3sDr0XnZbEG62n2UpZFbEcjQ4EEMOLGzVPhwVnwTz0lvKY+GcARbunuHekw==} + electron-to-chromium@1.5.422: + resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5462,14 +5320,6 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - engine.io-parser@5.2.3: - resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} - engines: {node: '>=10.0.0'} - - engine.io@6.6.9: - resolution: {integrity: sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==} - engines: {node: '>=10.2.0'} - enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -5478,10 +5328,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -5490,11 +5336,6 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} - envinfo@7.21.0: - resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} - engines: {node: '>=4'} - hasBin: true - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -5528,6 +5369,9 @@ packages: es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -5544,9 +5388,6 @@ packages: resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} - es-toolkit@1.49.0: - resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -5557,6 +5398,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5679,10 +5525,6 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -5699,10 +5541,9 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.5: - resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -5714,6 +5555,10 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -5761,8 +5606,8 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exsolve@1.1.0: - resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} fallow-type-aware@3.22.0: resolution: {integrity: sha512-xw18u/0XJGz1DYK2atVjw8f8+TmMM0QgKEsa4Not/y2QioBcPYBScHZs4nr4wZ8V2kMCxWpeREDus6JbS4Ax2A==} @@ -5832,17 +5677,12 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - filesize@11.0.22: - resolution: {integrity: sha512-RlCVs9CY+oSsRnNZn95J9vDXjNjOwddKyTFjOYtA4yxYVIxBnwiVVGJX+TFhsmu3uUf81JDGyijtYL9xgawlTw==} - engines: {node: '>= 10.8.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5861,9 +5701,8 @@ packages: find-workspaces@0.3.1: resolution: {integrity: sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w==} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -5955,10 +5794,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-port@5.1.1: - resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} - engines: {node: '>=8'} - get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -6003,10 +5838,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -6060,6 +5891,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -6067,18 +5902,21 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.12.31: - resolution: {integrity: sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==} + hono@4.13.7: + resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + html-minifier-terser@7.2.0: resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} engines: {node: ^14.13.1 || >=16.0.0} hasBin: true - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - htmlparser2@12.0.0: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} engines: {node: '>=20.19.0'} @@ -6110,21 +5948,21 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next-chained-backend@5.0.5: - resolution: {integrity: sha512-ThdtKUcNdk/zrSuzQyz2t4uH1McomKXP/t7GOf9Cm1oabDlJ6ZbQQJkLB9VlUErL9+45baFHv/ipzgi9MOotUg==} + i18next-chained-backend@5.0.6: + resolution: {integrity: sha512-oYo86EnjkKs1+osKHCFyIi3Yw7UebEHxMgVOJ1OpgmpL4y9s46Zr9pJHpqWwlxK8Vt//cER9Sx2Tfmx3awrJQA==} i18next-fs-backend@2.6.7: resolution: {integrity: sha512-nN2TIycyR/d+OVuLm1ugPyOlMprqPRnQ7QktBgn44F3H8l7TeTH4ffHJ7nUsd4QVJfetWW7/VZ3s+4g7FoAZUA==} - i18next-http-backend@4.0.0: - resolution: {integrity: sha512-EgSjO3Q1G6f2Q5oy7u9mmxuesE0oSfzAD97NFBjC8EmkK4guBSYLljM0Fng3DarMWIIkU70jfo4+mUzmyVISTA==} + i18next-http-backend@4.0.2: + resolution: {integrity: sha512-oay62dIB2kL7+WHzoUXBjWfL3+mwijD3pQkQkIEaRLhy6kjXxUhrRenV0gInwlCASAaJaDy94+XvYizK+cAGdA==} engines: {node: '>=18'} i18next-http-middleware@3.9.8: resolution: {integrity: sha512-HMKPc/P/v3qI+JX1k8RLsc4IKT3nKwKFt4cPDZB+iSLlEkUblpjiJ2z3YZYgHgY2Xs6cnnqFkJIg4AU3JSTNaA==} - i18next@26.3.6: - resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==} + i18next@26.4.2: + resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==} peerDependencies: typescript: ^5 || ^6 || ^7 peerDependenciesMeta: @@ -6178,10 +6016,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.11.1: - resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} - engines: {node: '>=12.22.0'} - is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -6361,14 +6195,14 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isbot@5.2.0: - resolution: {integrity: sha512-gbZiGCb4B5xaoxg9mS7koAyRdvJnArk10VLSHOgz6rtBG93/pi1xOFaVvXMKZ7JXgyZ8zAbNRK5uIBdIUTFSqw==} - engines: {node: '>=18'} - isbot@5.2.1: resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} engines: {node: '>=18'} + isbot@5.2.2: + resolution: {integrity: sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -6384,6 +6218,10 @@ packages: resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-regex-util@30.5.0: + resolution: {integrity: sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@30.4.1: resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6477,9 +6315,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -6492,9 +6327,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stream-stringify@3.0.1: - resolution: {integrity: sha512-vuxs3G1ocFDiAQ/SX0okcZbtqXwgj1g71qE9+vrjJ2EkjKQlEFDAcUNRxRU8O+GekV4v5cM2qXP0Wyt/EMDBiQ==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -6518,8 +6350,8 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} @@ -6533,9 +6365,6 @@ packages: koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - kysely@0.29.4: resolution: {integrity: sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA==} engines: {node: '>=22.0.0'} @@ -6547,9 +6376,6 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} - launch-editor@2.14.1: - resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} - lefthook-darwin-arm64@2.1.10: resolution: {integrity: sha512-nw+X8wRNDoUUV6WSteyKBbcLySq+fsmZt5WV/s50ZJpysmsDKJOUMln6SllNfP+60dzUahAO7REco/2633BsLg==} cpu: [arm64] @@ -6784,10 +6610,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} @@ -6960,8 +6782,8 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - miniflare@4.20260708.1: - resolution: {integrity: sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==} + miniflare@4.20260730.0: + resolution: {integrity: sha512-1Z9SB9r/o//80UA02Re3QhtcecSHAyAjf5EcKBfQVlQrCg7Miy79hl2PvtkwFLIaJ5rcrOPdDcRr577okwZPsg==} engines: {node: '>=22.0.0'} hasBin: true @@ -6969,6 +6791,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -7054,8 +6880,8 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.4: - resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} + msgpackr@2.1.0: + resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} @@ -7086,10 +6912,6 @@ packages: engines: {node: '>= 4.4.x'} hasBin: true - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -7128,6 +6950,10 @@ packages: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} engines: {node: ^18.17.0 || >=20.5.0} @@ -7148,11 +6974,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nypm@0.6.8: - resolution: {integrity: sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==} - engines: {node: '>=18'} - hasBin: true - nypm@0.6.9: resolution: {integrity: sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==} engines: {node: '>=18'} @@ -7235,21 +7056,8 @@ packages: oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} - oxfmt@0.63.0: - resolution: {integrity: sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - svelte: ^5.0.0 - vite-plus: '*' - peerDependenciesMeta: - svelte: - optional: true - vite-plus: - optional: true - - oxfmt@0.64.0: - resolution: {integrity: sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==} + oxfmt@0.66.0: + resolution: {integrity: sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7269,21 +7077,8 @@ packages: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true - oxlint@1.78.0: - resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - oxlint-tsgolint: '>=7.0.2001' - vite-plus: '*' - peerDependenciesMeta: - oxlint-tsgolint: - optional: true - vite-plus: - optional: true - - oxlint@1.79.0: - resolution: {integrity: sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==} + oxlint@1.81.0: + resolution: {integrity: sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7350,9 +7145,6 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -7380,10 +7172,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -7427,6 +7215,9 @@ packages: pg-protocol@1.15.0: resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -7444,6 +7235,15 @@ packages: pg-native: optional: true + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -7465,8 +7265,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} @@ -7492,17 +7292,23 @@ packages: peerDependencies: postcss: ^8.4.38 + postcss-calc@11.1.0: + resolution: {integrity: sha512-QCAiLSdyJU1F9jkeIH3iqEfkm2GPornaWiVJXduTaZI0AdzTMHqvguH/1lU/ueR2Kgs17IcQ3zD+wd+K7VE9gw==} + engines: {node: ^22.22.3 || ^24.15 || >=26.0} + peerDependencies: + postcss: ^8.5.28 + postcss-colormin@7.0.10: resolution: {integrity: sha512-yFr6JezOolHLta/buLE71VKPh2mXursp4saVe98/ol8ZnEWhL+racShqPKlvd/DKWLre/39B6HhcMXf7RZ3hxg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.5.13 - postcss-colormin@8.0.1: - resolution: {integrity: sha512-qBY4ABQ6d8/mk5RRZHwMllrZMxeMey3azVY2dZUEk+RgiUC4ARdPR3/AITzNqqKTbvW/3y/MJKinDrzwqn8RDQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-colormin@9.0.2: + resolution: {integrity: sha512-h6uf6/HVT98tSLcQNQ8V0wuEQ8doBbxFwvgRqptdIeyCe8+aa29zNMPSVOkSLXXbCv25qDZaaGo/h/O4csTK3g==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-convert-values@7.0.12: resolution: {integrity: sha512-xurKu5qqk4viR3Cp3p4xBR4KfnZm4w4ys6+UBwBmeuBSNkH7+DtLnYOYnOffgtE4yx8sH9S1VZ6RAAvROXzP2Q==} @@ -7510,11 +7316,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-convert-values@8.0.1: - resolution: {integrity: sha512-IdOSIX3BzfMvCc1TAHIha2gfy17xnb5vfML8e2BIKARnFOghksESfaSAB/3CXgyLfMozZAbTRPVQF5dbuKOidw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-convert-values@9.0.2: + resolution: {integrity: sha512-nsFL7tpxgaoF0G/w+fe8kkWvikxvwIb4ySW7PYzcmD1N60f0SA51sVnEuCrx7mHmwOvS1U1JIFvWHWHoK/bh8g==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-custom-properties@15.0.1: resolution: {integrity: sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg==} @@ -7528,11 +7334,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-discard-comments@8.0.1: - resolution: {integrity: sha512-FDvzm3tXlEsQBO2XQgnta5ugsAqwBrgWH+j5QgXpegEIDYA0VPnZg2aP7LtmWtC49POskeIhXesFiU/k3NyFHA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-comments@9.0.2: + resolution: {integrity: sha512-QxOYI1haY3f9rPgY7i0W0+lQihsbvC/edPAAJAaSZmteI+UtGraWNnkWOZ/nCNZ6D+Ab8a9hksOCjJYBg/gF0A==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-discard-duplicates@7.0.4: resolution: {integrity: sha512-VBNn1+EuMZkeGVVtz0gRfbNGtx9IFgAsAV+E2pHtXPrp4qfGBkhTIiAuE/wrb+Y6Pakg9NewAlfTpYIFAWODtw==} @@ -7540,11 +7346,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-discard-duplicates@8.0.1: - resolution: {integrity: sha512-stTDXkI8YkCUfADurQhp03oq5ynsgSx6Qrw5B1swds6oTHtAeOZ9I0SHGK8cY/VpWUsIYFDWMs3IWf9jIEfFvA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-duplicates@9.0.2: + resolution: {integrity: sha512-FCTKRK9bH2ayM2AkhNicRw7z1ROmgk8p+QFsiLeG8MStgHkDH/NOjhJfQp1scvzk6u1szn1xvY52T1XVAaL68w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-discard-empty@7.0.3: resolution: {integrity: sha512-M2pyjQCU+/7cMHVtL6bKTHjv0lZnPLMpicgr67Dlth7AbuV9gjVTtUqaRwn6Pp6BwSDspUzhz8SaUrRykJU5Dw==} @@ -7552,11 +7358,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-discard-empty@8.0.1: - resolution: {integrity: sha512-Zv4fM1Yfhk71tbt6gfiptbL6jDHi+7apSnaMeaO9n1uET+1embrXQw5m93Zp5x28UyQSuv+AVkFY193jdwZ33w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-empty@9.0.2: + resolution: {integrity: sha512-skKth+zcP//uuDos1GD/M0DKwEMDCbUnOZxz/M+BvjflCAS94czq7wwXNt2eug87rxPCik/okyXKJ2kBitHGEQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-discard-overridden@7.0.3: resolution: {integrity: sha512-aNovXo9UsZuRNLzHJtp13lHIvinDPfiXBPePpXkSjCbgp++iU2FqE+YxvjIsg6EdyPZsASFbfu+JcBFVsErXIQ==} @@ -7564,11 +7370,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-discard-overridden@8.0.1: - resolution: {integrity: sha512-ykt4fvrC7yYGzbxKyqBVjDCbsjF/11JgWK8enrdkobRyqqEtb/uDUCbKOGdvrK8X7BrShW8Lv5cCRNbdkNHGkQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-overridden@9.0.2: + resolution: {integrity: sha512-cUGXcnnhOBZwE+dLFRYLoXYrLxCQtD0mp8nUEHlQI8KdDXhaIHL7DCWUkXXEcNFon11xROYOP9E+plzJ5MSjXw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-flexbugs-fixes@5.0.2: resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} @@ -7597,11 +7403,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-merge-longhand@8.0.1: - resolution: {integrity: sha512-huTfSYgQ13O81SFvAuOi7GWnO48vvybjj3xF+X3qUoPjzvvaLpJH5DcUqqXcwOEulZUcvaV4s0V9WtWs+IAQPA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-merge-longhand@9.0.3: + resolution: {integrity: sha512-JJLx47+h7TIIThb+VE3pFHZUY/HKXgXa+yAZlN0FBX2IsIURJuOavS8gQwUvj5oluzxxRA9IjR95rLllbeybBw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-merge-rules@7.0.11: resolution: {integrity: sha512-SJUPM18g2BmPhf8BVlbwqWz4aK3pLu6u6xjfwEzra7xL6IBR10sUaiB++EzqcVfadPHrKBSMlNdP+XieykhI+Q==} @@ -7609,11 +7415,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-merge-rules@8.0.1: - resolution: {integrity: sha512-o3rk4UpnPNg469tklYwbR/NtvKc/f/wJiVDTnNQ/EFPw/LeiPOHUCvV1GIBQIZHGrBAYdPjToK6a+ojYprsrxQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-merge-rules@9.0.3: + resolution: {integrity: sha512-Wd/r16vrGdC49ZeQesxgNrNdQkAGoKL51UGHd3MwyH3CHhvqHkRtXZKKEnblecuuWmvjWqptC4VA3BeAHAVrzQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-minify-font-values@7.0.3: resolution: {integrity: sha512-yilG/VOaNI74IylQvAQQxm3/wZVBkXyYUqNUAdxqwtbWUXPsbK1q8Ms0mL83v+f8YicgcyfYCRZtWACUdYajpA==} @@ -7621,11 +7427,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-minify-font-values@8.0.1: - resolution: {integrity: sha512-L8Nzs/PRlBSPrLdY/7rAiU5ZN5800+2J/4LRbfyG8SJnPljmgMaXVmQiCklvRS+yObfVRNtvmk/Ean/eoYcSeg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-font-values@9.0.2: + resolution: {integrity: sha512-+QEc9ILK7sz/6LDnFITuvipMnX71A+v7b7VX3AyiN3WCIoJtc4xQovogeiOsX7CxXtwPKmk2z1BuI8h3TSnQEA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-minify-gradients@7.0.5: resolution: {integrity: sha512-YraROyQRg3BI1+Hg8E05B/JPdnTm8EDSVu4P2BxdM+CRiOyfmou809+chGIqo6fQqwjPGQ947nbGncSjmTU1WQ==} @@ -7633,11 +7439,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-minify-gradients@8.0.1: - resolution: {integrity: sha512-qf+4s/hZMqTwpWN2teqf6+1yvR/SZK5HgHqXYuACeJXV7ABe7AXtBEomgxagUzcN4bSnmqBh5vnIml0dYqykYg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-gradients@9.0.2: + resolution: {integrity: sha512-8EefxPsmS/RqMgJUBhx5N6hoWelHK8tW7DFUN8NLNrH1WSA4jvWRZc1OgzY0IheODiFsr/xiiOAHrPUQJFeMIg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-minify-params@7.0.9: resolution: {integrity: sha512-R8itbB8BhlpoYyBm1ou0dD+vJnQ3F6adQipR4UnkCHUwlo+S9WXJaDRg1RHjC8YVAtIdrQzSWvJl40HnGDTKjA==} @@ -7645,11 +7451,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-minify-params@8.0.1: - resolution: {integrity: sha512-L0h3H59deFfFg0wQN1NVaS/8E/LfGvaMuZKGO7siwlG995zo3OshtQyRkqKdVqcBwAORBvZ1nDZrKPLRapYkQw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-params@9.0.2: + resolution: {integrity: sha512-T8h6+8/JZQzTUlsw8oF3xdWIgrjw6dCpqdpUrIatrL852HNqE7w47C4mFrZADuu7IgT+4a3zRAX1nd112sAvwQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-minify-selectors@7.1.2: resolution: {integrity: sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q==} @@ -7657,14 +7463,14 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-minify-selectors@8.0.2: - resolution: {integrity: sha512-3icdxc/zght5UAizdwqZBDE2KOWHf1jMQCxET6iLACeNlRxfTPyXS0/COpGk8CQ2cECyaEKTRUd/i/k8Gxmz4g==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-selectors@9.0.3: + resolution: {integrity: sha512-mElz5y5+pMisgaN1VgschjOQBh8I9fga/OhJUX+VKyQ14ebukeB3xZRlbu+ebIKI/+qT8tj7dfx0AEQ6gWCPOg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 - postcss-nesting@14.0.0: - resolution: {integrity: sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==} + postcss-nesting@14.0.1: + resolution: {integrity: sha512-80MH7KcmMtb7ffSBX82uZwnogltubaXF0sagu+OGD8M9jViR3ngRgCdoTzKCvKEtllB0MW2U7Rgfcub5fR1Bug==} engines: {node: '>=20.19.0'} peerDependencies: postcss: ^8.4 @@ -7675,11 +7481,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-charset@8.0.1: - resolution: {integrity: sha512-xzqr36F8UeIZOvOHsf3aul+RVJCADvSwuwpMLgizqKjisHZpBfztgW0XFLBfJvz9pJgaStaOXAtGb0zLqT6B0w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-charset@9.0.2: + resolution: {integrity: sha512-2mFe06u9nwCdG+swpN3YmcRBNAZTR1IocNvN6Lzi7kKXjB0LhEOSGeu4B6rbV84NtgdkJwdulzeh1AdABM9N5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-display-values@7.0.3: resolution: {integrity: sha512-ldsCX0QIt05pKIOobZtVQ48wXJecr+czw4+e1/YjVhLMqslShgpVxgPtI2CefURR8oyVoYaU/l829MMwExDMLw==} @@ -7687,11 +7493,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-display-values@8.0.1: - resolution: {integrity: sha512-ZDWOijOK1FFMlpgiQCUO9fCNKd7HJ9L7z9HWEq4iyubnUFWzdTSwm/LcrMbNW6iZ1oAtqeLYA0WA3xHszOI08g==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-display-values@9.0.2: + resolution: {integrity: sha512-SHON0J7MuPrwpDAz4fHQgVNBfivLNKW8hgtO0WM3d5P0vFargQ4tCUNYdVBHfXr1xRFmmifRvJKmahksaHHn+w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-positions@7.0.4: resolution: {integrity: sha512-VEvlpeGd3Ju1Hqa/oN4jaP3+ms4laYwkEL9N9u+B6k54PZjXbW1n6wI+aVprf1BQXlCYpS5+1pl/7/vHiKgARg==} @@ -7699,11 +7505,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-positions@8.0.1: - resolution: {integrity: sha512-uuivan2poSqbE48ST4do20dGaFUeXey9/H8rhHzoyVHB2I6BmkoVLZ/C9+BRjUlpaAFYVOoDY7epkiidzaYbvA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-positions@9.0.2: + resolution: {integrity: sha512-2KQaPVbXUm1oUViZXvD6nsuRiAr81J3X2dRSeK1mCdPyMSUBQ76TpAjrVvbceicdbs3Bo2j9qkfO9iZSMJBsGQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-repeat-style@7.0.4: resolution: {integrity: sha512-6mPKlY/8cSaDHxX502wERADarJsccwlky6yIrOapHH2ZgfoKAV94SbiTKfKEs4EEpdazuc3J72WsqeYk7hp9+Q==} @@ -7711,11 +7517,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-repeat-style@8.0.1: - resolution: {integrity: sha512-q2hq5fmKxk29K6DjKA3nZ17Q2dtjhLYFNmFweKALmooUqx6UWAHF1bBoWTu/EqlJ88josb82A/J0Atj9LJUmpQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-repeat-style@9.0.2: + resolution: {integrity: sha512-+EropmN1W6gVnmwbhP8Tb7BmmqXgRCFJqtWFTcA0nZBz7aGj4zXL9EWyQ20DkA0Vd+ladQOmNuaUsc0jCeXT5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-string@7.0.3: resolution: {integrity: sha512-HnEQPUchi1eznmDKEYrKUTqrprEq97SrpUYClgUkv7V2zRODD9DFoUsYU+m9ZOetmD5ku7fEMZB/lwy8IT6xVQ==} @@ -7723,11 +7529,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-string@8.0.1: - resolution: {integrity: sha512-+Wf+kQJhm1WgSGEAuUaswE9rdpR9QbrKRVemcVHs6rhOoOTVIdAbgaicftfYA6vLM346P8onRzkEVbFN29ktKQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-string@9.0.2: + resolution: {integrity: sha512-vzJqaYpeG/rYWVDMpfxA3I2XNRlkK7XKYFINAfYLKiXn0SDndmvQYZhYEQdpvapDy7QAiK1/4iPVcfxsjWfU1w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-timing-functions@7.0.3: resolution: {integrity: sha512-zmEzHdvpZBZu0OKlbJSfgASQvaayyAoVuWtvyr34IJ/LyS+DaOKvvR3EvFJ9RWWtNIx+CMvO125OVophaxNYew==} @@ -7735,11 +7541,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-timing-functions@8.0.1: - resolution: {integrity: sha512-W8/tvwRlm3T+yjGkg0IRTF4bvHj0vILYr/LOogCrJKHz2ey2HFRwfsAA8Bk9N4BGR7z7WmmDu/KzzwhJ6FoGPQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-timing-functions@9.0.2: + resolution: {integrity: sha512-CZ5T2XvUra6kJDBHZ+KQYuY0QJhpGv3atiLbkCLVk3BnBwh49zhNid/+IQrxQFwYkJ54oVJQuqrj6J4eaOFHTw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-unicode@7.0.9: resolution: {integrity: sha512-DRAdWfeh/TjmhLJsw91vdiWCnUod9iwvM7xyS02/nF/sLsCR3A8l3pztrSUrWG8DSBqfX7yEk9FM0USaVJ2mSg==} @@ -7747,11 +7553,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-unicode@8.0.1: - resolution: {integrity: sha512-Ad0YHNRBp4WHEOYUM/4wL/8MoL2fimEF8se/0q+Rt/owMzYpbxsypC1P8fN/oluwoRmRKdNVX7X2oycEobPWcQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-unicode@9.0.2: + resolution: {integrity: sha512-AOT0whCCcKASm1Ee8pchg03xgFxJopQhP1/G7CEt5X9LBD6EV4zL3a8duDJEGXYaCwuxjtwywRm8K5L6o0OJLA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-url@7.0.3: resolution: {integrity: sha512-CL93wmloq5qsffmFv+bw24MIRbmhHrp53qoh1LDAb/5TtjWEXI/np4xcP/Gw9oWCb2XyWnqHYLDUwiKRoJBA1Q==} @@ -7759,11 +7565,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-url@8.0.1: - resolution: {integrity: sha512-tkYcip6pCDY806xuxpJYqMW2M3/623jzGFJmz3m5Us47q8P28+gbRZxaea3Rr/CmwwLUiVlh+BTGYwQ6gvaP8A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-url@9.0.2: + resolution: {integrity: sha512-oGfMuPEcmjon+08+gKpuqFUPn6/B4uTAJSHRlJe6h0xeB0baq5Zm/Swk0Jyz8mzVTNOIh8PuMUvWP+tVeSpS4A==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-normalize-whitespace@7.0.3: resolution: {integrity: sha512-FdHjjn+Ht5Z2ZRjNOmeCbNq6lq09sUYKpmlF/Aq0XjVNSLTL6fmHlA/3swN2wP2caY9GV/tjSDcIIyS7aN7W0A==} @@ -7771,11 +7577,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-normalize-whitespace@8.0.1: - resolution: {integrity: sha512-XzORadNfSrKWDZZpgAEHPKINKx8r9r9RIfE9c70g/HThdpbmPHhDYCodHSVESDxmKeySAYw1p4liuBCf7j6LyA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-whitespace@9.0.2: + resolution: {integrity: sha512-gLkQNyMsT5xx1kbeT7vUmxwoIt6o5MA+1pX4LBc89ipX1GPYUpf5sv5IA0iikKDn05Zo78IsSFWc/C5FcYxRCQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-ordered-values@7.0.4: resolution: {integrity: sha512-nubSi49hDHQk4E8KIj+IbLY8Bg+8OcSUEhgyolgM+atnOvXjV7EjaR6bac4YGZoFyPa9mWoAF3EaYbWdFkKqVg==} @@ -7783,11 +7589,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-ordered-values@8.0.1: - resolution: {integrity: sha512-OLXq5lR1yk3KWQ1FPK6aWjFFdktHE9f9kb8cnt4LmIw7w30DnzgD9+sOVYJc5HenkWCX8i1MJhhFwmqc/GYqLg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-ordered-values@9.0.2: + resolution: {integrity: sha512-9DDzlg3E8ZOU/5bgCF4RCVXaEeoOuh3v23BT+ka8Mh3RpEFfpCg1kfpt1v38voIqV4gRZ1S1QHlnHWrzSzWZoA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-page-break@3.0.4: resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} @@ -7800,11 +7606,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-reduce-initial@8.0.1: - resolution: {integrity: sha512-+aQsR6+61KRoIfcFNLP3v9RM7+0iYOTtPnjl1wr6JqMW1zx6S+t2ktHRefXwacFdHIDj5+ETG0KY7K3+SGQ4Nw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-reduce-initial@9.0.2: + resolution: {integrity: sha512-rzSZ5ns9W/OGfwKPSEmlGMr0DsZy2exNT8uEKNU49GK+lgNnF+pKbPq0ZcHvFYn9oO6U+C6RfVuWWNgzGwxCJw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-reduce-transforms@7.0.3: resolution: {integrity: sha512-FXsnN9ZwcZTT8Yf8cAHA8qIGUXcX6WfLd9JoYhrdDfmvsVhhfqkkv7m4AC3rwFOfz+GzkUa87OCKF9dUcicd+g==} @@ -7812,27 +7618,31 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-reduce-transforms@8.0.1: - resolution: {integrity: sha512-x71slHVykiFi5RuKEXM0wgYpY2PngC78x6R8TnZhHF3lhqt+u/w3MGwYLX+2t5O87ssRiMfEAhQH+3J4QwVzCw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-reduce-transforms@9.0.2: + resolution: {integrity: sha512-UXR9hvucM/VKwLDdcPFxszYAMyxLmpYACdxlPnNJ+6MKi+sEJHTMiUlVY3KRRuO00kuOjZ+t8M/I7N/gFMvwfg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-selector-parser@7.1.4: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} + engines: {node: '>=4'} + postcss-svgo@7.1.3: resolution: {integrity: sha512-2QfoFOYMcj8lwcVEf9WeTlkVIAm7u2QvOEhMzkQU3KUhhGX/l8hVV9EtjMv4iq3E9iI3OeeMN0YoMLbGusuigw==} engines: {node: ^18.12.0 || ^20.9.0 || >= 18} peerDependencies: postcss: ^8.5.13 - postcss-svgo@8.0.1: - resolution: {integrity: sha512-HpnvWii7W0/FPrsejJa6ZTi0kNtTJP/Iba7CUMPX0xPV6QpnndOp+SDP74tFtgjA2cYKYNWJPOlmLXMsvi/9yA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-svgo@9.0.2: + resolution: {integrity: sha512-FY1/AvWkMyXIlc3dFtwGdZDLdK12VAQqdqLOBKIQPsh574UZ5GfzMigeccvnuzEWPOySh/4LkTfGELDtnuBPXg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-unique-selectors@7.0.7: resolution: {integrity: sha512-d+sCkaRnSefghOUdH8CMJZV9yUQhj2ojpe8Nw/lA+LV1UOfeleGkLTl6XdCFFSai9UJ+DJPb69FFuqthXYsY8w==} @@ -7840,11 +7650,11 @@ packages: peerDependencies: postcss: ^8.5.13 - postcss-unique-selectors@8.0.1: - resolution: {integrity: sha512-+xvKI5+/Cl8yYQwxDV39Uhuc4WV951xngFvPPjiPj2NIbIfm6vbbRTXblyw0FioLkIoGlw+7qUcY1h2YhaZYgw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-unique-selectors@9.0.2: + resolution: {integrity: sha512-Jyl/5yYy8VuWRCkSiI/5n75bEWty8wYho3eB1w4/ZK2XSW6AnNVJ6tGuWaCmJt0M5+ouHb/m+qXzFOsG3tOWww==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -7853,6 +7663,10 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -7946,8 +7760,12 @@ packages: pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -7984,16 +7802,6 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react-router@7.18.1: - resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true - react-router@7.18.2: resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} engines: {node: '>=20.0.0'} @@ -8037,13 +7845,9 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} + redis@6.2.1: + resolution: {integrity: sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg==} + engines: {node: '>= 20.0.0'} reduce-configs@1.1.2: resolution: {integrity: sha512-AgBP55V8FC7NaqoOP2RCbTpu6LE+YuX3LUZkNAoitcfyS3/PIC8Obg/TJrBzTkJ+lDvZv0TTAeDpLkzjTtYlbw==} @@ -8293,6 +8097,10 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -8352,9 +8160,9 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -8364,10 +8172,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.10.0: - resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} - engines: {node: '>= 0.4'} - side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -8405,17 +8209,6 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - socket.io-adapter@2.5.8: - resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} - - socket.io-parser@4.2.7: - resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==} - engines: {node: '>=10.0.0'} - - socket.io@4.8.1: - resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} - engines: {node: '>=10.2.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -8435,9 +8228,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -8513,11 +8303,11 @@ packages: peerDependencies: postcss: ^8.5.13 - stylehacks@8.0.1: - resolution: {integrity: sha512-Gv095oTD0N+BdJALNFDsxZpETHZLTxbOl5RyIO7y6VAE6sR3z0MnV3Nix7N0IATNldNTrkvSASp2KR1Yt526HA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + stylehacks@9.0.3: + resolution: {integrity: sha512-ffR7soMPCLSZoye/H4gEcOt7fpRmm+1/Mq7j4L9Hex2284Ci0oUkPSiRSpDW0X3tptLtZqGUsOxZ64fy6izthA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.15 + postcss: ^8.5.28 supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} @@ -8551,6 +8341,11 @@ packages: engines: {node: '>=16'} hasBin: true + svgo@4.1.0: + resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} + engines: {node: '>=16'} + hasBin: true + sync-child-process@1.0.2: resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} engines: {node: '>=16.0.0'} @@ -8669,12 +8464,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - - type-fest@5.8.0: - resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + type-fest@5.9.0: + resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==} engines: {node: '>=20'} type-is@2.1.0: @@ -8717,29 +8508,29 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - ultracite@7.10.2: - resolution: {integrity: sha512-WP1Hu/BKy/BDgntaiU33uq2Y8hKL2gEO5li2wfg7XFvmlyIkGDEy8qbWg7GHsxTEjEpWOAKpyFSF8aqc3JNEGQ==} + ultracite@7.11.0: + resolution: {integrity: sha512-HJByeIoO4Lhcing1k2sabI7BCHRqeMztXGYZm4Zcir7NXvidXLwDgLNZgSR4w3oPKPgx7CRTqzJitt/oPR34NA==} hasBin: true peerDependencies: - oxfmt: '>=0.1.0' - oxlint: ^1.0.0 + '@biomejs/biome': ^2.5.0 + eslint: ^10.0.0 + oxfmt: '>=0.40.0' + oxlint: ^1.79.0 + prettier: ^3.0.0 + stylelint: ^17.0.0 peerDependenciesMeta: - oxfmt: + '@biomejs/biome': optional: true - oxlint: + eslint: optional: true - - ultracite@7.10.7: - resolution: {integrity: sha512-rc+TG/zLCV+Uk1PL4XPmsq7CW8GtlaV4g7Q53Bt5o7UNGSKKNRoPY4syz1BDWwSoNBcvWKpSnfSVflsecok4EA==} - hasBin: true - peerDependencies: - oxfmt: '>=0.1.0' - oxlint: ^1.79.0 - peerDependenciesMeta: oxfmt: optional: true oxlint: optional: true + prettier: + optional: true + stylelint: + optional: true unbash@4.0.11: resolution: {integrity: sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ==} @@ -8749,9 +8540,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -8759,6 +8547,10 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + undici@8.10.1: resolution: {integrity: sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q==} engines: {node: '>=22.19.0'} @@ -8780,6 +8572,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -8794,17 +8592,9 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - uuid@14.0.1: - resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} - hasBin: true - varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -8871,17 +8661,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260708.1: - resolution: {integrity: sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==} + workerd@1.20260730.1: + resolution: {integrity: sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==} engines: {node: '>=16'} hasBin: true - wrangler@4.110.0: - resolution: {integrity: sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==} + wrangler@4.116.0: + resolution: {integrity: sha512-wP1PXxH5KJajfGEjty0NNqyxAirTFvMZpGyB5CRqEIiY0fL/SiCKtIYAJB9HXq0MP0sMXB/i/YSls+WObahAhw==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^5.20260708.1 + '@cloudflare/workers-types': ^5.20260730.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -8997,8 +8787,8 @@ packages: zephyr-xpack-internal@1.2.4: resolution: {integrity: sha512-Czju+fCo1+GL4fIZHrCwa41Qt2Z0MWZ4XHC9jKjXeje+5uS4WiVNTJR0p0+xlrPjhfVFyC7WIhuKyl9rHh9kvg==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} snapshots: @@ -9016,12 +8806,6 @@ snapshots: '@protobuf-ts/runtime-rpc': 2.11.1 google-protobuf: 4.0.2 - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -9169,61 +8953,61 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.4 - '@better-auth/api-key@1.7.2(831f340a6e103a07b479cd8fecebd372)': + '@better-auth/api-key@1.7.2(266de75404526095522e68ad80df5da2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 - better-auth: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - better-call: 1.4.0(zod@4.4.3) - zod: 4.4.3 + better-auth: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + better-call: 1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) - '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2)': + '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2)': dependencies: '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747) '@opentelemetry/semantic-conventions': 1.43.0 '@standard-schema/spec': 1.1.0 - better-call: 1.4.0(zod@4.4.3) + better-call: 1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) jose: 6.2.5 kysely: 0.29.4 nanostores: 1.4.2 - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) optionalDependencies: '@cloudflare/workers-types': 5.20260810.1 '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3))': + '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 optionalDependencies: - drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) - '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4)': + '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.4 - '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))': + '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747) @@ -9237,13 +9021,11 @@ snapshots: '@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747)': {} - '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)': + '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2(fb58f039b6686e96e05520b02bd24b85)': dependencies: - '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' - '@rstest/adapter-rsbuild': 0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)) + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@rstest/adapter-rsbuild': 0.11.12(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3)) transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' - '@rsbuild/core' @@ -9255,7 +9037,6 @@ snapshots: - clean-css - core-js - csso - - debug - devcert - lightningcss - react @@ -9264,56 +9045,28 @@ snapshots: - rollup - rsbuild-plugin-rsc - supports-color - - ts-node - tsconfig-paths - tslib - typescript - utf-8-validate - webpack - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)': + '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2(fdd7d76f75a0c875bbf6f9d83791f905)': dependencies: - '@babel/parser': 8.0.4 - '@babel/traverse': 8.0.4 - '@babel/types': 8.0.4 - '@loadable/component': 5.16.7(react@19.2.8) - '@modern-js/builder': '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' - '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server': '@bleedingdev/modern-js-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(debug@4.3.7(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)(tsconfig-paths@4.2.0)(typescript@7.0.2)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@swc/core': 1.15.43(@swc/helpers@0.5.23) - '@swc/helpers': 0.5.23 - compression-webpack-plugin: 12.0.0(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - es-module-lexer: 2.3.1 - esbuild: 0.28.1 - flatted: 3.4.4 - import-meta-resolve: 4.2.0 - mlly: 1.8.2 - ndepe: 0.1.13(supports-color@10.2.2) - pkg-types: 2.3.1 - std-env: 4.2.0 - optionalDependencies: - tsconfig-paths: 4.2.0 + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@rstest/adapter-rsbuild': 0.11.12(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3)) transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' + - '@rsbuild/core' - '@rspack/core' + - '@rstest/core' - '@swc/css' - '@typescript/native-preview' - bufferutil - clean-css - core-js - csso - - debug - devcert - lightningcss - react @@ -9322,213 +9075,107 @@ snapshots: - rollup - rsbuild-plugin-rsc - supports-color + - tsconfig-paths - tslib - typescript - utf-8-validate - webpack - '@bleedingdev/modern-js-bff-core@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.4.3)': + '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2(60a2e177378c78332c9d91520ad7a4b3)': dependencies: - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@swc/helpers': 0.5.23 - koa-compose: 4.1.0 - reflect-metadata: 0.2.2 - tsconfig-paths: 4.2.0 - type-fest: 5.8.0 - optionalDependencies: - zod: 4.4.3 - transitivePeerDependencies: - - react - - react-dom - - ? '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' - : dependencies: - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@rsbuild/plugin-assets-retry': 2.0.2(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-check-syntax': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-css-minimizer': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsbuild/plugin-less': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)) - '@rsbuild/plugin-rem': 1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-sass': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-source-build': 1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-svgr': 2.0.5(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2) - '@rsbuild/plugin-type-check': 1.6.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(@typescript/native-preview@7.0.0-dev.20260707.2)(tslib@2.8.1)(typescript@7.0.2) - '@rsbuild/plugin-typed-css-modules': 1.2.4(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsdoctor/rspack-plugin': 1.6.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@babel/parser': 8.0.4 + '@babel/traverse': 8.0.4 + '@loadable/component': 5.16.7(react@19.2.8) + '@modern-js/builder': '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) '@swc/helpers': 0.5.23 - '@typescript/native-preview': 7.0.0-dev.20260707.2 - autoprefixer: 10.5.2(postcss@8.5.26) - browserslist: 4.28.8 - core-js: 3.49.0 - cssnano: 8.0.2(postcss@8.5.26) - html-minifier-terser: 7.2.0 - lodash: 4.18.1 - postcss: 8.5.26 - postcss-custom-properties: 15.0.1(postcss@8.5.26) - postcss-flexbugs-fixes: 5.0.2(postcss@8.5.26) - postcss-font-variant: 5.0.0(postcss@8.5.26) - postcss-initial: 4.0.1(postcss@8.5.26) - postcss-media-minmax: 5.0.0(postcss@8.5.26) - postcss-nesting: 14.0.0(postcss@8.5.26) - postcss-page-break: 3.0.4(postcss@8.5.26) - rspack-manifest-plugin: 5.2.2(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)) - ts-deepmerge: 8.0.0 - optionalDependencies: - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - rsbuild-plugin-rsc: 0.1.1(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + std-env: 4.2.0 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' - '@rspack/core' - '@swc/css' - - bufferutil + - '@typescript/native-preview' - clean-css + - core-js - csso - esbuild - lightningcss - react - react-dom + - react-server-dom-rspack + - rsbuild-plugin-rsc - supports-color - tslib - typescript - - utf-8-validate - webpack - ? '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' - : dependencies: - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@rsbuild/plugin-assets-retry': 2.0.2(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-check-syntax': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-css-minimizer': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsbuild/plugin-less': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)) - '@rsbuild/plugin-rem': 1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-sass': 2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-source-build': 1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsbuild/plugin-svgr': 2.0.5(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2) - '@rsbuild/plugin-type-check': 1.6.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(@typescript/native-preview@7.0.0-dev.20260707.2)(tslib@2.8.1)(typescript@7.0.2) - '@rsbuild/plugin-typed-css-modules': 1.2.4(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsdoctor/rspack-plugin': 1.6.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2(7b73a40aca8e82f7d801abeb9b4c424b)': + dependencies: + '@babel/parser': 8.0.4 + '@babel/traverse': 8.0.4 + '@loadable/component': 5.16.7(react@19.2.8) + '@modern-js/builder': '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) '@swc/helpers': 0.5.23 - '@typescript/native-preview': 7.0.0-dev.20260707.2 - autoprefixer: 10.5.2(postcss@8.5.26) - browserslist: 4.28.8 - core-js: 3.49.0 - cssnano: 8.0.2(postcss@8.5.26) - html-minifier-terser: 7.2.0 - lodash: 4.18.1 - postcss: 8.5.26 - postcss-custom-properties: 15.0.1(postcss@8.5.26) - postcss-flexbugs-fixes: 5.0.2(postcss@8.5.26) - postcss-font-variant: 5.0.0(postcss@8.5.26) - postcss-initial: 4.0.1(postcss@8.5.26) - postcss-media-minmax: 5.0.0(postcss@8.5.26) - postcss-nesting: 14.0.0(postcss@8.5.26) - postcss-page-break: 3.0.4(postcss@8.5.26) - rspack-manifest-plugin: 5.2.2(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)) - ts-deepmerge: 8.0.0 - optionalDependencies: - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - rsbuild-plugin-rsc: 0.1.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + std-env: 4.2.0 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' - '@rspack/core' - '@swc/css' - - bufferutil + - '@typescript/native-preview' - clean-css + - core-js - csso - esbuild - lightningcss - react - react-dom + - react-server-dom-rspack + - rsbuild-plugin-rsc - supports-color - tslib - typescript - - utf-8-validate - webpack - '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12(patch_hash=227ad960c0ce793da1dee90eeaba8edc310b14a58334be185c7cce71ae59a110)(oxlint-tsgolint@7.0.2001)': - dependencies: - oxlint: 1.78.0(oxlint-tsgolint@7.0.2001) - transitivePeerDependencies: - - oxlint-tsgolint - - vite-plus - - '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@swc/helpers': 0.5.23 - encoding: 0.1.13 - path-to-regexp: 8.4.2 - qs: 6.15.3 - transitivePeerDependencies: - - react - - react-dom - - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': - dependencies: - '@modern-js/codesmith': 2.6.9(supports-color@10.2.2) - '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - esbuild: 0.28.1 - oxfmt: 0.63.0 - ultracite: 7.10.2(oxfmt@0.63.0)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)) - transitivePeerDependencies: - - oxlint - - react - - react-dom - - supports-color - - svelte - - vite-plus - - '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@swc/helpers': 0.5.23 - transitivePeerDependencies: - - react - - react-dom - - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(3a233a8c5baa0ff66c13c170d030459d)': + '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.4.3)' - '@modern-js/builder': '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' - '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@module-federation/runtime': 2.8.2 - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-web': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@babel/parser': 8.0.4 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@loadable/component': 5.16.7(react@19.2.8) + '@modern-js/app-tools-extensions': '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2(7b73a40aca8e82f7d801abeb9b4c424b)' + '@modern-js/builder': '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server': '@bleedingdev/modern-js-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(typescript@7.0.2)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 - esbuild: 0.28.1 - qs: 6.15.3 - type-is: 2.1.0 - optionalDependencies: - '@effect/opentelemetry': 4.0.0-beta.107(@opentelemetry/api-logs@0.220.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + compression-webpack-plugin: 12.0.0(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + es-module-lexer: 2.3.2 + esbuild: 0.28.2 + flatted: 3.4.4 + import-meta-resolve: 4.2.0 + mlly: 1.8.2 + ndepe: 0.1.13(supports-color@10.2.2) + pkg-types: 2.3.3 + std-env: 4.2.0 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' - '@rspack/core' @@ -9538,10 +9185,12 @@ snapshots: - clean-css - core-js - csso + - devcert - lightningcss - react - react-dom - react-server-dom-rspack + - rollup - rsbuild-plugin-rsc - supports-color - tsconfig-paths @@ -9549,37 +9198,38 @@ snapshots: - typescript - utf-8-validate - webpack - - zod - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(e7b2bba40f7fca088d40067165acde85)': + '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.4.3)' - '@modern-js/builder': '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' - '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@module-federation/runtime': 2.8.2 - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-web': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@babel/parser': 8.0.4 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@loadable/component': 5.16.7(react@19.2.8) + '@modern-js/app-tools-extensions': '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2(60a2e177378c78332c9d91520ad7a4b3)' + '@modern-js/builder': '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server': '@bleedingdev/modern-js-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(typescript@7.0.2)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 - esbuild: 0.28.1 - qs: 6.15.3 - type-is: 2.1.0 - optionalDependencies: - '@effect/opentelemetry': 4.0.0-beta.107(@opentelemetry/api-logs@0.220.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + compression-webpack-plugin: 12.0.0(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + es-module-lexer: 2.3.2 + esbuild: 0.28.2 + flatted: 3.4.4 + import-meta-resolve: 4.2.0 + mlly: 1.8.2 + ndepe: 0.1.13(supports-color@10.2.2) + pkg-types: 2.3.3 + std-env: 4.2.0 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@module-federation/runtime-tools' - '@parcel/css' - '@rspack/core' @@ -9589,10 +9239,12 @@ snapshots: - clean-css - core-js - csso + - devcert - lightningcss - react - react-dom - react-server-dom-rspack + - rollup - rsbuild-plugin-rsc - supports-color - tsconfig-paths @@ -9600,66 +9252,335 @@ snapshots: - typescript - utf-8-validate - webpack + + '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': + dependencies: + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/helpers': 0.5.23 + koa-compose: 4.1.0 + reflect-metadata: 0.2.2 + tsconfig-paths: 4.2.0 + type-fest: 5.9.0 + optionalDependencies: + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) + transitivePeerDependencies: + - react + - react-dom + + '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': + dependencies: + '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-web': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + optionalDependencies: + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + effect: 4.0.0-rc.112 + transitivePeerDependencies: + - react + - react-dom + - tsconfig-paths + - zod + + '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rsbuild/plugin-assets-retry': 2.0.2(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-check-syntax': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-css-minimizer': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsbuild/plugin-less': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)) + '@rsbuild/plugin-rem': 1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-sass': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-source-build': 1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-svgr': 2.0.5(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2) + '@rsbuild/plugin-type-check': 1.6.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2) + '@rsbuild/plugin-typed-css-modules': 1.2.4(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + autoprefixer: 10.5.5(postcss@8.5.28) + browserslist: 4.28.9 + core-js: 3.50.0 + cssnano: 9.0.3(postcss@8.5.28) + html-minifier-terser: 7.2.0 + lodash: 4.18.1 + postcss: 8.5.28 + postcss-custom-properties: 15.0.1(postcss@8.5.28) + postcss-flexbugs-fixes: 5.0.2(postcss@8.5.28) + postcss-font-variant: 5.0.0(postcss@8.5.28) + postcss-initial: 4.0.1(postcss@8.5.28) + postcss-media-minmax: 5.0.0(postcss@8.5.28) + postcss-nesting: 14.0.1(postcss@8.5.28) + postcss-page-break: 3.0.4(postcss@8.5.28) + rspack-manifest-plugin: 5.2.2(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)) + ts-deepmerge: 8.0.0 + optionalDependencies: + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + rsbuild-plugin-rsc: 0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@parcel/css' + - '@rspack/core' + - '@swc/css' + - '@typescript/native-preview' + - clean-css + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - tslib + - typescript + - webpack + + '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rsbuild/plugin-assets-retry': 2.0.2(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-check-syntax': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-css-minimizer': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsbuild/plugin-less': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)) + '@rsbuild/plugin-rem': 1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-sass': 2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-source-build': 1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsbuild/plugin-svgr': 2.0.5(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2) + '@rsbuild/plugin-type-check': 1.6.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2) + '@rsbuild/plugin-typed-css-modules': 1.2.4(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + autoprefixer: 10.5.5(postcss@8.5.28) + browserslist: 4.28.9 + core-js: 3.50.0 + cssnano: 9.0.3(postcss@8.5.28) + html-minifier-terser: 7.2.0 + lodash: 4.18.1 + postcss: 8.5.28 + postcss-custom-properties: 15.0.1(postcss@8.5.28) + postcss-flexbugs-fixes: 5.0.2(postcss@8.5.28) + postcss-font-variant: 5.0.0(postcss@8.5.28) + postcss-initial: 4.0.1(postcss@8.5.28) + postcss-media-minmax: 5.0.0(postcss@8.5.28) + postcss-nesting: 14.0.1(postcss@8.5.28) + postcss-page-break: 3.0.4(postcss@8.5.28) + rspack-manifest-plugin: 5.2.2(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)) + ts-deepmerge: 8.0.0 + optionalDependencies: + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + rsbuild-plugin-rsc: 0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@parcel/css' + - '@rspack/core' + - '@swc/css' + - '@typescript/native-preview' + - clean-css + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - tslib + - typescript + - webpack + + '@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2(oxlint-tsgolint@7.0.2001)': + dependencies: + oxlint: 1.81.0(oxlint-tsgolint@7.0.2001) + transitivePeerDependencies: + - oxlint-tsgolint + - vite-plus + + '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/helpers': 0.5.23 + encoding: 0.1.13 + path-to-regexp: 8.4.2 + qs: 6.16.0 + transitivePeerDependencies: + - react + - react-dom + + '@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react@19.2.8)': + dependencies: + '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + i18next-browser-languagedetector: 8.2.1 + react: 19.2.8 + + '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - react + - react-dom + + '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': + dependencies: + '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@module-federation/runtime': 2.9.0 + esbuild: 0.28.2 + optionalDependencies: + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + effect: 4.0.0-rc.112 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + - react + - react-dom + - tsconfig-paths + - zod + + '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(10f4d6332468433e4185bc862591bbd4)': + dependencies: + '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/core': 1.16.2(@swc/helpers@0.5.23) + qs: 6.16.0 + type-is: 2.1.0 + optionalDependencies: + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + effect: 4.0.0-rc.112 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@swc/helpers' + - '@typescript/native-preview' + - core-js + - react + - react-dom + - tsconfig-paths - zod - '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)': + dependencies: + '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/core': 1.16.2(@swc/helpers@0.5.23) + qs: 6.16.0 + type-is: 2.1.0 + optionalDependencies: + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + effect: 4.0.0-rc.112 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@swc/helpers' + - '@typescript/native-preview' + - core-js + - react + - react-dom + - tsconfig-paths + - zod + + '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 path-to-regexp: 8.4.2 react: 19.2.8 transitivePeerDependencies: - react-dom - '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-runtime': '@bleedingdev/modern-js-server-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/i18n-runtime-extensions': '@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime': '@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 i18next-browser-languagedetector: 8.2.1 - i18next-chained-backend: 5.0.5 + i18next-chained-backend: 5.0.6 i18next-fs-backend: 2.6.7 - i18next-http-backend: 4.0.0 + i18next-http-backend: 4.0.2 i18next-http-middleware: 3.9.8 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - i18next: 26.3.6(typescript@7.0.2) + i18next: 26.4.2(typescript@7.0.2) transitivePeerDependencies: - '@module-federation/runtime-tools' - core-js - '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2(3ba18dbb54a410d62a69ce25c4e9542e)': dependencies: - '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' - '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 - '@tanstack/react-router': 1.170.25(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@tanstack/router-core': 1.171.21(patch_hash=413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d) + '@tanstack/react-router': 1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/router-core': 1.171.28 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@module-federation/runtime-tools' - core-js - '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2(f7bdd6234cb87826e4d2b905b070739f)': dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/helpers': 0.5.23 + '@tanstack/react-router': 1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/router-core': 1.171.28 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + + '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) '@swc/helpers': 0.5.23 jiti: 2.7.0 transitivePeerDependencies: @@ -9668,12 +9589,12 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-prod-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -9681,49 +9602,52 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-render@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-render@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2': {} - '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 lru-cache: 11.5.2 react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - serialize-javascript: 7.0.7 optionalDependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': dependencies: '@loadable/component': 5.16.7(react@19.2.8) '@loadable/server': 5.16.7(@loadable/component@5.16.7(react@19.2.8))(react@19.2.8) - '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/render': '@bleedingdev/modern-js-render@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/render': '@bleedingdev/modern-js-render@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/core': 1.16.2(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 - '@swc/plugin-loadable-components': 12.0.0 + '@swc/plugin-loadable-components': 13.0.0 '@types/loadable__component': 5.13.10 '@types/react-helmet': 6.1.11 cookie: 2.0.1 entities: 8.0.0 - es-module-lexer: 2.3.1 - esbuild: 0.28.1 + es-module-lexer: 2.3.2 + esbuild: 0.28.2 invariant: 2.2.4 - isbot: 5.2.0 + isbot: 5.2.2 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-helmet: 6.1.0(react@19.2.8) @@ -9734,18 +9658,19 @@ snapshots: - core-js - react-server-dom-rspack - '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 '@web-std/fetch': 4.2.1 '@web-std/file': 3.0.3 '@web-std/stream': 1.0.3 cloneable-readable: 3.0.0 flatted: 3.4.4 - hono: 4.12.31 + hono: 4.13.7 ts-deepmerge: 8.0.0 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -9753,12 +9678,12 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-server-runtime-extensions@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -9766,11 +9691,11 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-server-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' '@swc/helpers': 0.5.23 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -9778,9 +9703,10 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)': + '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)': dependencies: - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@swc/core': 1.16.2(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 optionalDependencies: '@typescript/native-preview': typescript@7.0.2 @@ -9788,18 +9714,18 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(debug@4.3.7(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)(tsconfig-paths@4.2.0)(typescript@7.0.2)': + '@bleedingdev/modern-js-server@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(typescript@7.0.2)': dependencies: - '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' - '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@swc/helpers': 0.5.23 - axios: 1.18.1(debug@4.3.7(supports-color@10.2.2))(supports-color@10.2.2) connect-history-api-fallback: 2.0.0 http-compression: 1.1.3 - minimatch: 10.2.5 + minimatch: 10.2.6 path-to-regexp: 8.4.2 ws: 8.21.3 optionalDependencies: @@ -9809,17 +9735,35 @@ snapshots: - '@typescript/native-preview' - bufferutil - core-js - - debug - react - react-dom - - supports-color - utf-8-validate - '@bleedingdev/modern-js-types@3.8.2-ultramodern.12': + '@bleedingdev/modern-js-types@3.9.0-ultramodern.2': dependencies: - '@jest/types': 30.4.1 + '@jest/types': 30.5.1 + + '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': + dependencies: + '@modern-js/codesmith': 2.6.9(supports-color@10.2.2) + '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + esbuild: 0.28.2 + oxfmt: 0.66.0 + ultracite: 7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) + transitivePeerDependencies: + - '@biomejs/biome' + - eslint + - oxlint + - prettier + - react + - react-dom + - stylelint + - supports-color + - svelte + - vite-plus - '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@swc/helpers': 0.5.23 caniuse-lite: 1.0.30001810 @@ -9833,6 +9777,18 @@ snapshots: '@bufbuild/protobuf@2.13.0': {} + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 @@ -9847,31 +9803,33 @@ snapshots: '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260708.1 + workerd: 1.20260730.1 - '@cloudflare/workerd-darwin-64@1.20260708.1': + '@cloudflare/workerd-darwin-64@1.20260730.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260708.1': + '@cloudflare/workerd-darwin-arm64@1.20260730.1': optional: true - '@cloudflare/workerd-linux-64@1.20260708.1': + '@cloudflare/workerd-linux-64@1.20260730.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260708.1': + '@cloudflare/workerd-linux-arm64@1.20260730.1': optional: true - '@cloudflare/workerd-windows-64@1.20260708.1': + '@cloudflare/workerd-windows-64@1.20260730.1': optional: true '@cloudflare/workers-types@5.20260810.1': {} '@colordx/core@5.5.0': {} + '@colordx/core@6.3.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -9887,7 +9845,7 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.4)': + '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.4)': dependencies: postcss-selector-parser: 7.1.4 @@ -9895,9 +9853,9 @@ snapshots: dependencies: postcss-selector-parser: 7.1.4 - '@csstools/utilities@3.0.0(postcss@8.5.26)': + '@csstools/utilities@3.0.0(postcss@8.5.28)': dependencies: - postcss: 8.5.26 + postcss: 8.5.28 '@cyberalien/svg-utils@1.2.19': dependencies: @@ -9905,81 +9863,81 @@ snapshots: '@drizzle-team/brocli@0.12.0': {} - '@effect/opentelemetry@4.0.0-beta.107(@opentelemetry/api-logs@0.220.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))': + '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + effect: 4.0.0-rc.112 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-web': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-web': 2.11.0(@opentelemetry/api@1.9.1) - '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))': + '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + effect: 4.0.0-rc.112 ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(ioredis@5.11.1(supports-color@10.2.2))': + '@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1))': dependencies: - '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) - ioredis: 5.11.1(supports-color@10.2.2) + '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) + effect: 4.0.0-rc.112 mime: 4.1.0 + redis: 6.2.1(@opentelemetry/api@1.9.1) undici: 8.10.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))': + '@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112)': dependencies: - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) - pg: 8.22.0 + effect: 4.0.0-rc.112 + pg: 8.23.0 pg-connection-string: 2.14.0 - pg-cursor: 2.22.0(pg@8.22.0) - pg-pool: 3.14.0(pg@8.22.0) + pg-cursor: 2.22.0(pg@8.23.0) + pg-pool: 3.14.0(pg@8.23.0) pg-types: 4.1.0 transitivePeerDependencies: - pg-native - '@effect/tsgo-darwin-arm64@0.19.0': + '@effect/tsgo-darwin-arm64@0.41.0': optional: true - '@effect/tsgo-darwin-x64@0.19.0': + '@effect/tsgo-darwin-x64@0.41.0': optional: true - '@effect/tsgo-linux-arm64@0.19.0': + '@effect/tsgo-linux-arm64@0.41.0': optional: true - '@effect/tsgo-linux-arm@0.19.0': + '@effect/tsgo-linux-arm@0.41.0': optional: true - '@effect/tsgo-linux-x64@0.19.0': + '@effect/tsgo-linux-x64@0.41.0': optional: true - '@effect/tsgo-win32-arm64@0.19.0': + '@effect/tsgo-win32-arm64@0.41.0': optional: true - '@effect/tsgo-win32-x64@0.19.0': + '@effect/tsgo-win32-x64@0.41.0': optional: true - '@effect/tsgo@0.19.0': + '@effect/tsgo@0.41.0': optionalDependencies: - '@effect/tsgo-darwin-arm64': 0.19.0 - '@effect/tsgo-darwin-x64': 0.19.0 - '@effect/tsgo-linux-arm': 0.19.0 - '@effect/tsgo-linux-arm64': 0.19.0 - '@effect/tsgo-linux-x64': 0.19.0 - '@effect/tsgo-win32-arm64': 0.19.0 - '@effect/tsgo-win32-x64': 0.19.0 + '@effect/tsgo-darwin-arm64': 0.41.0 + '@effect/tsgo-darwin-x64': 0.41.0 + '@effect/tsgo-linux-arm': 0.41.0 + '@effect/tsgo-linux-arm64': 0.41.0 + '@effect/tsgo-linux-x64': 0.41.0 + '@effect/tsgo-win32-arm64': 0.41.0 + '@effect/tsgo-win32-x64': 0.41.0 '@emnapi/core@1.11.2': dependencies: @@ -10013,190 +9971,266 @@ snapshots: tslib: 2.8.1 optional: true + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/aix-ppc64@0.28.1': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.28.1': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.28.1': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.28.1': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.28.1': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.28.1': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.28.1': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.28.1': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.28.1': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.28.1': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.28.1': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.28.1': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.28.1': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.28.1': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.28.1': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.28.1': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.28.1': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.28.1': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.28.1': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.28.1': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.28.1': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.28.1': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.28.1': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.28.1': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.28.1': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))': + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.1.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint/compat@2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - '@eslint/config-array@0.21.2(supports-color@10.2.2)': + '@eslint/config-array@0.23.5(supports-color@10.2.2)': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@10.2.2) - minimatch: 3.1.5 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.7.0': dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 + '@eslint/core': 1.2.1 '@eslint/core@1.2.1': dependencies: @@ -10218,11 +10252,11 @@ snapshots: '@eslint/js@9.39.5': {} - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.7.3': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.2.1 levn: 0.4.1 '@fallow-cli/darwin-arm64@3.22.0': @@ -10329,106 +10363,114 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.1': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.1': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.1': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.1': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.1': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.1': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.1 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.1 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.1 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.1 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.1 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.1 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.2': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-arm64@0.35.2': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': optional: true '@internationalized/number@3.6.7': dependencies: '@swc/helpers': 0.5.23 - '@ioredis/commands@1.10.0': {} - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -10444,20 +10486,39 @@ snapshots: '@jest/pattern@30.4.0': dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 jest-regex-util: 30.4.0 + '@jest/pattern@30.5.0': + dependencies: + '@types/node': 26.4.1 + jest-regex-util: 30.5.0 + '@jest/schemas@30.4.1': dependencies: '@sinclair/typebox': 0.34.52 + '@jest/schemas@30.5.0': + dependencies: + '@sinclair/typebox': 0.34.52 + '@jest/types@30.4.1': dependencies: '@jest/pattern': 30.4.0 '@jest/schemas': 30.4.1 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.43 + '@types/node': 26.4.1 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jest/types@30.5.1': + dependencies: + '@jest/pattern': 30.5.0 + '@jest/schemas': 30.5.0 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 26.4.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -10624,6 +10685,14 @@ snapshots: '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) tslib: 2.8.1 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@loadable/component@5.16.7(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 @@ -10675,27 +10744,32 @@ snapshots: transitivePeerDependencies: - supports-color - '@module-federation/automatic-vendor-federation@1.2.1(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@module-federation/automatic-vendor-federation@1.2.1(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + find-package-json: 1.2.0 + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + + '@module-federation/automatic-vendor-federation@1.2.1(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: find-package-json: 1.2.0 - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) - '@module-federation/bridge-react-webpack-plugin@2.8.0': + '@module-federation/bridge-react-webpack-plugin@2.9.0': dependencies: - '@module-federation/sdk': 2.8.0 + '@module-federation/sdk': 2.9.0 - '@module-federation/bridge-react@2.8.0(patch_hash=54bfc79e097473222f83cbfa6d717792e3026bf16b5097c2ed91715b7da126be)(react-dom@19.2.8(react@19.2.8))(react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@module-federation/bridge-react@2.9.0(patch_hash=8c084f41790295af8fd015b897c6298bbc13d927b796c624ac96cb2bdb4bc87c)(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': dependencies: - '@module-federation/sdk': 2.8.0 + '@module-federation/sdk': 2.9.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - react-router: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@module-federation/cli@2.8.0(typescript@7.0.2)': + '@module-federation/cli@2.9.0(typescript@7.0.2)': dependencies: - '@module-federation/dts-plugin': 2.8.0(typescript@7.0.2) - '@module-federation/sdk': 2.8.0 + '@module-federation/dts-plugin': 2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2) + '@module-federation/sdk': 2.9.0 commander: 11.1.0 jiti: 2.4.2 transitivePeerDependencies: @@ -10704,82 +10778,126 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/dts-plugin@2.8.0(typescript@7.0.2)': + '@module-federation/dts-plugin@2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2)': dependencies: - '@module-federation/error-codes': 2.8.0 - '@module-federation/managers': 2.8.0 - '@module-federation/sdk': 2.8.0 - '@module-federation/third-party-dts-extractor': 2.8.0 - adm-zip: 0.5.10 + '@module-federation/error-codes': 2.9.0 + '@module-federation/managers': 2.9.0 + '@module-federation/sdk': 2.9.0 + '@module-federation/third-party-dts-extractor': 2.9.0 + adm-zip: 0.6.0 isomorphic-ws: 5.0.0(ws@8.21.0) typescript: 7.0.2 - undici: 7.28.0 + undici: 7.29.0 ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@module-federation/enhanced@2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.8.0 - '@module-federation/cli': 2.8.0(typescript@7.0.2) - '@module-federation/dts-plugin': 2.8.0(typescript@7.0.2) - '@module-federation/error-codes': 2.8.0 - '@module-federation/inject-external-runtime-core-plugin': 2.8.0(@module-federation/runtime-tools@2.8.0) - '@module-federation/managers': 2.8.0 - '@module-federation/manifest': 2.8.0(typescript@7.0.2) - '@module-federation/rspack': 2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2) - '@module-federation/runtime-tools': 2.8.0 - '@module-federation/sdk': 2.8.0 - '@module-federation/webpack-bundler-runtime': 2.8.0 + '@module-federation/enhanced@2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.9.0 + '@module-federation/cli': 2.9.0(typescript@7.0.2) + '@module-federation/dts-plugin': 2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2) + '@module-federation/error-codes': 2.9.0 + '@module-federation/inject-external-runtime-core-plugin': 2.9.0(@module-federation/runtime-tools@2.9.0) + '@module-federation/managers': 2.9.0 + '@module-federation/manifest': 2.9.0(typescript@7.0.2) + '@module-federation/rspack': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/runtime-tools': 2.9.0 + '@module-federation/sdk': 2.9.0 + '@module-federation/webpack-bundler-runtime': 2.9.0 schema-utils: 4.3.0 tapable: 2.3.0 optionalDependencies: typescript: 7.0.2 - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) transitivePeerDependencies: - '@rspack/core' - bufferutil - utf-8-validate - '@module-federation/error-codes@2.8.0': {} + '@module-federation/enhanced@2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.9.0 + '@module-federation/cli': 2.9.0(typescript@7.0.2) + '@module-federation/dts-plugin': 2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2) + '@module-federation/error-codes': 2.9.0 + '@module-federation/inject-external-runtime-core-plugin': 2.9.0(@module-federation/runtime-tools@2.9.0) + '@module-federation/managers': 2.9.0 + '@module-federation/manifest': 2.9.0(typescript@7.0.2) + '@module-federation/rspack': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2) + '@module-federation/runtime-tools': 2.9.0 + '@module-federation/sdk': 2.9.0 + '@module-federation/webpack-bundler-runtime': 2.9.0 + schema-utils: 4.3.0 + tapable: 2.3.0 + optionalDependencies: + typescript: 7.0.2 + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - utf-8-validate - '@module-federation/error-codes@2.8.2': {} + '@module-federation/error-codes@2.9.0': {} - '@module-federation/inject-external-runtime-core-plugin@2.8.0(@module-federation/runtime-tools@2.8.0)': + '@module-federation/inject-external-runtime-core-plugin@2.9.0(@module-federation/runtime-tools@2.9.0)': dependencies: - '@module-federation/runtime-tools': 2.8.0 + '@module-federation/runtime-tools': 2.9.0 - '@module-federation/managers@2.8.0': + '@module-federation/managers@2.9.0': dependencies: - '@module-federation/sdk': 2.8.0 + '@module-federation/sdk': 2.9.0 - '@module-federation/manifest@2.8.0(typescript@7.0.2)': + '@module-federation/manifest@2.9.0(typescript@7.0.2)': dependencies: - '@module-federation/dts-plugin': 2.8.0(typescript@7.0.2) - '@module-federation/managers': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/dts-plugin': 2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2) + '@module-federation/managers': 2.9.0 + '@module-federation/sdk': 2.9.0 transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - vue-tsc - '@module-federation/modern-js-v3@2.8.0(patch_hash=56ff0f8c26c40b18be1de105abd019422ebaa648941607d1d30b94cd620b57f3)(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@module-federation/modern-js-v3@2.9.0(patch_hash=ba5049c43645a4337e1a74857b2dcef5f5330692cc5d2186bcef236dc122dae8)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@module-federation/bridge-react': 2.8.0(patch_hash=54bfc79e097473222f83cbfa6d717792e3026bf16b5097c2ed91715b7da126be)(react-dom@19.2.8(react@19.2.8))(react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) - '@module-federation/cli': 2.8.0(typescript@7.0.2) - '@module-federation/enhanced': 2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/node': 2.7.47(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/rsbuild-plugin': 2.8.0(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/runtime': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/bridge-react': 2.9.0(patch_hash=8c084f41790295af8fd015b897c6298bbc13d927b796c624ac96cb2bdb4bc87c)(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@module-federation/cli': 2.9.0(typescript@7.0.2) + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/node': 2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/rsbuild-plugin': 2.9.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/runtime': 2.9.0 + '@module-federation/sdk': 2.9.0 '@swc/helpers': 0.5.17 jiti: 2.4.2 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - react-router: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + typescript: 7.0.2 + transitivePeerDependencies: + - '@rsbuild/core' + - '@rspack/core' + - bufferutil + - utf-8-validate + - webpack + + '@module-federation/modern-js-v3@2.9.0(patch_hash=ba5049c43645a4337e1a74857b2dcef5f5330692cc5d2186bcef236dc122dae8)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@module-federation/bridge-react': 2.9.0(patch_hash=8c084f41790295af8fd015b897c6298bbc13d927b796c624ac96cb2bdb4bc87c)(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@module-federation/cli': 2.9.0(typescript@7.0.2) + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/node': 2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/rsbuild-plugin': 2.9.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/runtime': 2.9.0 + '@module-federation/sdk': 2.9.0 + '@swc/helpers': 0.5.17 + jiti: 2.4.2 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) typescript: 7.0.2 transitivePeerDependencies: - '@rsbuild/core' @@ -10788,16 +10906,16 @@ snapshots: - utf-8-validate - webpack - '@module-federation/node@2.7.47(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@module-federation/node@2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@module-federation/enhanced': 2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/runtime': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/runtime': 2.9.0 + '@module-federation/sdk': 2.9.0 encoding: 0.1.13 node-fetch: 3.3.2 tapable: 2.3.0 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) transitivePeerDependencies: - '@rspack/core' - bufferutil @@ -10805,75 +10923,94 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/rsbuild-plugin@2.8.0(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@module-federation/node@2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@module-federation/enhanced': 2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/node': 2.7.47(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@module-federation/sdk': 2.8.0 + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/runtime': 2.9.0 + '@module-federation/sdk': 2.9.0 + encoding: 0.1.13 + node-fetch: 3.3.2 + tapable: 2.3.0 optionalDependencies: - '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) transitivePeerDependencies: - '@rspack/core' - bufferutil - typescript - utf-8-validate - vue-tsc - - webpack - '@module-federation/rspack@2.8.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(typescript@7.0.2)': + '@module-federation/rsbuild-plugin@2.9.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.8.0 - '@module-federation/dts-plugin': 2.8.0(typescript@7.0.2) - '@module-federation/inject-external-runtime-core-plugin': 2.8.0(@module-federation/runtime-tools@2.8.0) - '@module-federation/managers': 2.8.0 - '@module-federation/manifest': 2.8.0(typescript@7.0.2) - '@module-federation/runtime-tools': 2.8.0 - '@module-federation/sdk': 2.8.0 - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/node': 2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/sdk': 2.9.0 optionalDependencies: - typescript: 7.0.2 + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: + - '@rspack/core' - bufferutil + - typescript - utf-8-validate + - vue-tsc + - webpack - '@module-federation/runtime-core@2.8.0': + '@module-federation/rsbuild-plugin@2.9.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@module-federation/error-codes': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/enhanced': 2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/node': 2.7.50(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@module-federation/sdk': 2.9.0 + optionalDependencies: + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + - webpack - '@module-federation/runtime-core@2.8.2': + '@module-federation/rspack@2.9.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(typescript@7.0.2)': dependencies: - '@module-federation/error-codes': 2.8.2 - '@module-federation/sdk': 2.8.2 + '@module-federation/bridge-react-webpack-plugin': 2.9.0 + '@module-federation/dts-plugin': 2.9.0(patch_hash=b4c8e1b74e7eea711fb133800780765eaae2b10e6ab34daeeda18b882558fd9b)(typescript@7.0.2) + '@module-federation/inject-external-runtime-core-plugin': 2.9.0(@module-federation/runtime-tools@2.9.0) + '@module-federation/managers': 2.9.0 + '@module-federation/manifest': 2.9.0(typescript@7.0.2) + '@module-federation/runtime-tools': 2.9.0 + '@module-federation/sdk': 2.9.0 + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate - '@module-federation/runtime-tools@2.8.0': + '@module-federation/runtime-core@2.9.0(patch_hash=b241be221397f0e07dbe6c515725e12eaf3b418469bb7dd21750e6e4b215dd8d)': dependencies: - '@module-federation/runtime': 2.8.0 - '@module-federation/webpack-bundler-runtime': 2.8.0 + '@module-federation/error-codes': 2.9.0 + '@module-federation/sdk': 2.9.0 - '@module-federation/runtime@2.8.0': + '@module-federation/runtime-tools@2.9.0': dependencies: - '@module-federation/error-codes': 2.8.0 - '@module-federation/runtime-core': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/runtime': 2.9.0 + '@module-federation/webpack-bundler-runtime': 2.9.0 - '@module-federation/runtime@2.8.2': + '@module-federation/runtime@2.9.0': dependencies: - '@module-federation/error-codes': 2.8.2 - '@module-federation/runtime-core': 2.8.2 - '@module-federation/sdk': 2.8.2 - - '@module-federation/sdk@2.8.0': {} + '@module-federation/error-codes': 2.9.0 + '@module-federation/runtime-core': 2.9.0(patch_hash=b241be221397f0e07dbe6c515725e12eaf3b418469bb7dd21750e6e4b215dd8d) + '@module-federation/sdk': 2.9.0 - '@module-federation/sdk@2.8.2': {} + '@module-federation/sdk@2.9.0': {} - '@module-federation/third-party-dts-extractor@2.8.0': {} + '@module-federation/third-party-dts-extractor@2.9.0': {} - '@module-federation/webpack-bundler-runtime@2.8.0': + '@module-federation/webpack-bundler-runtime@2.9.0': dependencies: - '@module-federation/error-codes': 2.8.0 - '@module-federation/runtime': 2.8.0 - '@module-federation/sdk': 2.8.0 + '@module-federation/error-codes': 2.9.0 + '@module-federation/runtime': 2.9.0 + '@module-federation/sdk': 2.9.0 '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true @@ -10907,9 +11044,9 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@nkzw/eslint-plugin@2.0.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))': + '@nkzw/eslint-plugin@2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) '@noble/ciphers@2.2.0': {} @@ -10927,67 +11064,67 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@opentelemetry/api-logs@0.220.0': + '@opentelemetry/api-logs@0.222.0': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} - '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/context-async-hooks@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-web@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/semantic-conventions@1.43.0': {} @@ -11171,118 +11308,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true - '@oxfmt/binding-android-arm-eabi@0.63.0': - optional: true - - '@oxfmt/binding-android-arm-eabi@0.64.0': - optional: true - - '@oxfmt/binding-android-arm64@0.63.0': - optional: true - - '@oxfmt/binding-android-arm64@0.64.0': - optional: true - - '@oxfmt/binding-darwin-arm64@0.63.0': - optional: true - - '@oxfmt/binding-darwin-arm64@0.64.0': - optional: true - - '@oxfmt/binding-darwin-x64@0.63.0': - optional: true - - '@oxfmt/binding-darwin-x64@0.64.0': - optional: true - - '@oxfmt/binding-freebsd-x64@0.63.0': - optional: true - - '@oxfmt/binding-freebsd-x64@0.64.0': - optional: true - - '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': - optional: true - - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': - optional: true - - '@oxfmt/binding-linux-arm-musleabihf@0.63.0': - optional: true - - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': + '@oxfmt/binding-android-arm-eabi@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.63.0': + '@oxfmt/binding-android-arm64@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.64.0': + '@oxfmt/binding-darwin-arm64@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.63.0': + '@oxfmt/binding-darwin-x64@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.64.0': + '@oxfmt/binding-freebsd-x64@0.66.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.63.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.63.0': + '@oxfmt/binding-linux-arm64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': + '@oxfmt/binding-linux-arm64-musl@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.63.0': + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.64.0': + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.63.0': + '@oxfmt/binding-linux-riscv64-musl@0.66.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.64.0': + '@oxfmt/binding-linux-s390x-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.63.0': + '@oxfmt/binding-linux-x64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.64.0': + '@oxfmt/binding-linux-x64-musl@0.66.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.63.0': + '@oxfmt/binding-openharmony-arm64@0.66.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.64.0': + '@oxfmt/binding-win32-arm64-msvc@0.66.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.63.0': + '@oxfmt/binding-win32-ia32-msvc@0.66.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.64.0': - optional: true - - '@oxfmt/binding-win32-arm64-msvc@0.63.0': - optional: true - - '@oxfmt/binding-win32-arm64-msvc@0.64.0': - optional: true - - '@oxfmt/binding-win32-ia32-msvc@0.63.0': - optional: true - - '@oxfmt/binding-win32-ia32-msvc@0.64.0': - optional: true - - '@oxfmt/binding-win32-x64-msvc@0.63.0': - optional: true - - '@oxfmt/binding-win32-x64-msvc@0.64.0': + '@oxfmt/binding-win32-x64-msvc@0.66.0': optional: true '@oxlint-tsgolint/darwin-arm64@7.0.2001': @@ -11303,121 +11383,64 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-android-arm-eabi@1.78.0': - optional: true - - '@oxlint/binding-android-arm-eabi@1.79.0': - optional: true - - '@oxlint/binding-android-arm64@1.78.0': - optional: true - - '@oxlint/binding-android-arm64@1.79.0': - optional: true - - '@oxlint/binding-darwin-arm64@1.78.0': - optional: true - - '@oxlint/binding-darwin-arm64@1.79.0': - optional: true - - '@oxlint/binding-darwin-x64@1.78.0': + '@oxlint/binding-android-arm-eabi@1.81.0': optional: true - '@oxlint/binding-darwin-x64@1.79.0': + '@oxlint/binding-android-arm64@1.81.0': optional: true - '@oxlint/binding-freebsd-x64@1.78.0': + '@oxlint/binding-darwin-arm64@1.81.0': optional: true - '@oxlint/binding-freebsd-x64@1.79.0': + '@oxlint/binding-darwin-x64@1.81.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + '@oxlint/binding-freebsd-x64@1.81.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.79.0': + '@oxlint/binding-linux-arm-gnueabihf@1.81.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.78.0': + '@oxlint/binding-linux-arm-musleabihf@1.81.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.79.0': + '@oxlint/binding-linux-arm64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.78.0': + '@oxlint/binding-linux-arm64-musl@1.81.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.79.0': + '@oxlint/binding-linux-ppc64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.78.0': + '@oxlint/binding-linux-riscv64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.79.0': + '@oxlint/binding-linux-riscv64-musl@1.81.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.78.0': + '@oxlint/binding-linux-s390x-gnu@1.81.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.79.0': + '@oxlint/binding-linux-x64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.78.0': + '@oxlint/binding-linux-x64-musl@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.79.0': + '@oxlint/binding-openharmony-arm64@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.78.0': + '@oxlint/binding-win32-arm64-msvc@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.79.0': + '@oxlint/binding-win32-ia32-msvc@1.81.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.78.0': + '@oxlint/binding-win32-x64-msvc@1.81.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.79.0': - optional: true - - '@oxlint/binding-linux-x64-gnu@1.78.0': - optional: true - - '@oxlint/binding-linux-x64-gnu@1.79.0': - optional: true - - '@oxlint/binding-linux-x64-musl@1.78.0': - optional: true - - '@oxlint/binding-linux-x64-musl@1.79.0': - optional: true - - '@oxlint/binding-openharmony-arm64@1.78.0': - optional: true - - '@oxlint/binding-openharmony-arm64@1.79.0': - optional: true - - '@oxlint/binding-win32-arm64-msvc@1.78.0': - optional: true - - '@oxlint/binding-win32-arm64-msvc@1.79.0': - optional: true - - '@oxlint/binding-win32-ia32-msvc@1.78.0': - optional: true - - '@oxlint/binding-win32-ia32-msvc@1.79.0': - optional: true - - '@oxlint/binding-win32-x64-msvc@1.78.0': - optional: true - - '@oxlint/binding-win32-x64-msvc@1.79.0': - optional: true - - '@oxlint/plugins@1.79.0': {} + '@oxlint/plugins@1.81.0': {} '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -11523,59 +11546,86 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/client@6.2.1(@opentelemetry/api@1.9.1)': + dependencies: + cluster-key-slot: 1.1.2 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + + '@redis/json@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/search@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/time-series@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@rollup/pluginutils@5.4.0': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 + picomatch: 4.0.7 - '@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)': + '@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)': dependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 optionalDependencies: - core-js: 3.49.0 + core-js: 3.50.0 transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)': + '@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)': dependencies: - '@rspack/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 optionalDependencies: - core-js: 3.49.0 + core-js: 3.50.0 transitivePeerDependencies: - - '@module-federation/runtime-tools' - - '@rsbuild/plugin-assets-retry@2.0.2(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': - optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - - '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': - dependencies: - acorn: 8.17.0 - browserslist-to-es-version: 1.4.2 - htmlparser2: 10.0.0 - picocolors: 1.1.1 - source-map: 0.7.6 + - '@module-federation/runtime-tools' + + '@rsbuild/plugin-assets-retry@2.0.2(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) - '@rsbuild/plugin-check-syntax@2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': + '@rsbuild/plugin-check-syntax@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': dependencies: acorn: 8.17.0 browserslist-to-es-version: 1.4.2 htmlparser2: 12.0.0 source-map: 0.7.6 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + + '@rsbuild/plugin-css-minimizer@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + css-minimizer-webpack-plugin: 8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + reduce-configs: 1.1.2 + optionalDependencies: + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + transitivePeerDependencies: + - '@parcel/css' + - '@swc/css' + - clean-css + - csso + - esbuild + - lightningcss + - webpack - '@rsbuild/plugin-css-minimizer@2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@rsbuild/plugin-css-minimizer@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - css-minimizer-webpack-plugin: 8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + css-minimizer-webpack-plugin: 8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) reduce-configs: 1.1.2 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@parcel/css' - '@swc/css' @@ -11585,36 +11635,49 @@ snapshots: - lightningcss - webpack - '@rsbuild/plugin-less@2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@rsbuild/plugin-less@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + deepmerge: 4.3.1 + less: 4.7.0(supports-color@10.2.2) + less-loader: 12.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + reduce-configs: 2.0.1 + optionalDependencies: + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + transitivePeerDependencies: + - '@rspack/core' + - supports-color + - webpack + + '@rsbuild/plugin-less@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: deepmerge: 4.3.1 less: 4.7.0(supports-color@10.2.2) - less-loader: 12.3.3(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + less-loader: 12.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) reduce-configs: 2.0.1 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@rspack/core' - supports-color - webpack - '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))': + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))': dependencies: - '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@rspack/core' - '@rsbuild/plugin-rem@1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': + '@rsbuild/plugin-rem@1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': dependencies: deepmerge: 4.3.1 terser: 5.49.0 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) - '@rsbuild/plugin-sass@2.0.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': + '@rsbuild/plugin-sass@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': dependencies: deepmerge: 4.3.1 loader-utils: 2.0.4 @@ -11622,219 +11685,125 @@ snapshots: reduce-configs: 2.0.1 sass-embedded: 1.100.0 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) - '@rsbuild/plugin-source-build@1.0.6(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': + '@rsbuild/plugin-source-build@1.0.6(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': dependencies: fast-glob: 3.3.3 json5: 2.2.3 yaml: 2.9.0 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) - '@rsbuild/plugin-svgr@2.0.5(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2)': + '@rsbuild/plugin-svgr@2.0.5(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(typescript@7.0.2)': dependencies: - '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)) + '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)) '@svgr/core': 8.1.0(supports-color@10.2.2)(typescript@7.0.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@7.0.2))(supports-color@10.2.2) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@7.0.2))(typescript@7.0.2) deepmerge: 4.3.1 loader-utils: 3.3.1 optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@rspack/core' - supports-color - typescript - '@rsbuild/plugin-tailwindcss@2.0.3(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@rsbuild/plugin-tailwindcss@2.0.3(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - '@tailwindcss/webpack': 4.3.3(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + '@tailwindcss/webpack': 4.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) optionalDependencies: - '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@rspack/core' - webpack - '@rsbuild/plugin-type-check@1.6.0(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(@typescript/native-preview@7.0.0-dev.20260707.2)(tslib@2.8.1)(typescript@7.0.2)': + '@rsbuild/plugin-tailwindcss@2.0.3(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: - deepmerge: 4.3.1 - json5: 2.2.3 - reduce-configs: 1.1.2 - ts-checker-rspack-plugin: 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(@typescript/native-preview@7.0.0-dev.20260707.2)(tslib@2.8.1)(typescript@7.0.2) - optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@typescript/native-preview': 7.0.0-dev.20260707.2 - transitivePeerDependencies: - - '@rspack/core' - - tslib - - typescript - - '@rsbuild/plugin-typed-css-modules@1.2.4(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))': + '@tailwindcss/webpack': 4.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) optionalDependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - - '@rsdoctor/client@1.6.1': {} - - '@rsdoctor/core@1.6.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)) - '@rsdoctor/graph': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/sdk': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/types': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/utils': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rspack/resolver': 0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - browserslist-load-config: 1.0.3 - es-toolkit: 1.49.0 - filesize: 11.0.22 - fs-extra: 11.3.6 - semver: 7.8.5 - source-map: 0.7.6 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@rsbuild/core' - - '@rspack/core' - - bufferutil - - supports-color - - utf-8-validate - - webpack - - '@rsdoctor/graph@1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@rsdoctor/types': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/utils': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - es-toolkit: 1.49.0 - path-browserify: 1.0.1 - source-map: 0.7.6 + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) transitivePeerDependencies: - '@rspack/core' - webpack - '@rsdoctor/rspack-plugin@1.6.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@rsbuild/plugin-type-check@1.6.0(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)': dependencies: - '@rsdoctor/core': 1.6.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/graph': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/sdk': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/types': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/utils': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + deepmerge: 4.3.1 + json5: 2.2.3 + reduce-configs: 1.1.2 + ts-checker-rspack-plugin: 1.6.1(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2) optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@rsbuild/core' - - bufferutil - - supports-color - - utf-8-validate - - webpack - - '@rsdoctor/sdk@1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@rsdoctor/client': 1.6.1 - '@rsdoctor/graph': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/types': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@rsdoctor/utils': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - launch-editor: 2.14.1 - safer-buffer: 2.1.2 - socket.io: 4.8.1(supports-color@10.2.2) - tapable: 2.3.3 + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@typescript/native-preview': typescript@7.0.2 transitivePeerDependencies: - '@rspack/core' - - bufferutil - - supports-color - - utf-8-validate - - webpack + - tslib + - typescript - '@rsdoctor/types@1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@types/connect': 3.4.38 - '@types/estree': 1.0.5 - '@types/tapable': 2.3.0 - source-map: 0.7.6 + '@rsbuild/plugin-typed-css-modules@1.2.4(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) - - '@rsdoctor/utils@1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': - dependencies: - '@babel/code-frame': 7.26.2 - '@rsdoctor/types': 1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) - '@types/estree': 1.0.5 - acorn: 8.17.0 - acorn-import-attributes: 1.9.5(acorn@8.17.0) - acorn-walk: 8.3.5 - deep-eql: 4.1.4 - envinfo: 7.21.0 - fs-extra: 11.3.6 - get-port: 5.1.1 - json-stream-stringify: 3.0.1 - lines-and-columns: 2.0.4 - picocolors: 1.1.1 - rslog: 2.3.0 - strip-ansi: 7.2.0 - transitivePeerDependencies: - - '@rspack/core' - - webpack + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) '@rspack/binding-darwin-arm64@2.2.0': optional: true - '@rspack/binding-darwin-arm64@2.2.0-rc.0': + '@rspack/binding-darwin-arm64@2.2.2': optional: true '@rspack/binding-darwin-x64@2.2.0': optional: true - '@rspack/binding-darwin-x64@2.2.0-rc.0': + '@rspack/binding-darwin-x64@2.2.2': optional: true '@rspack/binding-linux-arm64-gnu@2.2.0': optional: true - '@rspack/binding-linux-arm64-gnu@2.2.0-rc.0': + '@rspack/binding-linux-arm64-gnu@2.2.2': optional: true '@rspack/binding-linux-arm64-musl@2.2.0': optional: true - '@rspack/binding-linux-arm64-musl@2.2.0-rc.0': + '@rspack/binding-linux-arm64-musl@2.2.2': optional: true '@rspack/binding-linux-ppc64-gnu@2.2.0': optional: true - '@rspack/binding-linux-ppc64-gnu@2.2.0-rc.0': + '@rspack/binding-linux-ppc64-gnu@2.2.2': optional: true '@rspack/binding-linux-riscv64-gnu@2.2.0': optional: true - '@rspack/binding-linux-riscv64-gnu@2.2.0-rc.0': + '@rspack/binding-linux-riscv64-gnu@2.2.2': optional: true '@rspack/binding-linux-riscv64-musl@2.2.0': optional: true - '@rspack/binding-linux-riscv64-musl@2.2.0-rc.0': + '@rspack/binding-linux-riscv64-musl@2.2.2': optional: true '@rspack/binding-linux-s390x-gnu@2.2.0': optional: true - '@rspack/binding-linux-s390x-gnu@2.2.0-rc.0': + '@rspack/binding-linux-s390x-gnu@2.2.2': optional: true '@rspack/binding-linux-x64-gnu@2.2.0': optional: true - '@rspack/binding-linux-x64-gnu@2.2.0-rc.0': + '@rspack/binding-linux-x64-gnu@2.2.2': optional: true '@rspack/binding-linux-x64-musl@2.2.0': optional: true - '@rspack/binding-linux-x64-musl@2.2.0-rc.0': + '@rspack/binding-linux-x64-musl@2.2.2': optional: true '@rspack/binding-wasm32-wasi@2.2.0': @@ -11844,7 +11813,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true - '@rspack/binding-wasm32-wasi@2.2.0-rc.0': + '@rspack/binding-wasm32-wasi@2.2.2': dependencies: '@emnapi/core': 1.11.3 '@emnapi/runtime': 1.11.3 @@ -11854,19 +11823,19 @@ snapshots: '@rspack/binding-win32-arm64-msvc@2.2.0': optional: true - '@rspack/binding-win32-arm64-msvc@2.2.0-rc.0': + '@rspack/binding-win32-arm64-msvc@2.2.2': optional: true '@rspack/binding-win32-ia32-msvc@2.2.0': optional: true - '@rspack/binding-win32-ia32-msvc@2.2.0-rc.0': + '@rspack/binding-win32-ia32-msvc@2.2.2': optional: true '@rspack/binding-win32-x64-msvc@2.2.0': optional: true - '@rspack/binding-win32-x64-msvc@2.2.0-rc.0': + '@rspack/binding-win32-x64-msvc@2.2.2': optional: true '@rspack/binding@2.2.0': @@ -11886,104 +11855,53 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.2.0 '@rspack/binding-win32-x64-msvc': 2.2.0 - '@rspack/binding@2.2.0-rc.0': + '@rspack/binding@2.2.2': optionalDependencies: - '@rspack/binding-darwin-arm64': 2.2.0-rc.0 - '@rspack/binding-darwin-x64': 2.2.0-rc.0 - '@rspack/binding-linux-arm64-gnu': 2.2.0-rc.0 - '@rspack/binding-linux-arm64-musl': 2.2.0-rc.0 - '@rspack/binding-linux-ppc64-gnu': 2.2.0-rc.0 - '@rspack/binding-linux-riscv64-gnu': 2.2.0-rc.0 - '@rspack/binding-linux-riscv64-musl': 2.2.0-rc.0 - '@rspack/binding-linux-s390x-gnu': 2.2.0-rc.0 - '@rspack/binding-linux-x64-gnu': 2.2.0-rc.0 - '@rspack/binding-linux-x64-musl': 2.2.0-rc.0 - '@rspack/binding-wasm32-wasi': 2.2.0-rc.0 - '@rspack/binding-win32-arm64-msvc': 2.2.0-rc.0 - '@rspack/binding-win32-ia32-msvc': 2.2.0-rc.0 - '@rspack/binding-win32-x64-msvc': 2.2.0-rc.0 - - '@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)': + '@rspack/binding-darwin-arm64': 2.2.2 + '@rspack/binding-darwin-x64': 2.2.2 + '@rspack/binding-linux-arm64-gnu': 2.2.2 + '@rspack/binding-linux-arm64-musl': 2.2.2 + '@rspack/binding-linux-ppc64-gnu': 2.2.2 + '@rspack/binding-linux-riscv64-gnu': 2.2.2 + '@rspack/binding-linux-riscv64-musl': 2.2.2 + '@rspack/binding-linux-s390x-gnu': 2.2.2 + '@rspack/binding-linux-x64-gnu': 2.2.2 + '@rspack/binding-linux-x64-musl': 2.2.2 + '@rspack/binding-wasm32-wasi': 2.2.2 + '@rspack/binding-win32-arm64-msvc': 2.2.2 + '@rspack/binding-win32-ia32-msvc': 2.2.2 + '@rspack/binding-win32-x64-msvc': 2.2.2 + + '@rspack/core@2.2.0(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.2.0 optionalDependencies: - '@module-federation/runtime-tools': 2.8.0 + '@module-federation/runtime-tools': 2.9.0 '@swc/helpers': 0.5.23 - '@rspack/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)': + '@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)': dependencies: - '@rspack/binding': 2.2.0-rc.0 + '@rspack/binding': 2.2.2 optionalDependencies: - '@module-federation/runtime-tools': 2.8.0 + '@module-federation/runtime-tools': 2.9.0 '@swc/helpers': 0.5.23 '@rspack/lite-tapable@1.1.2': {} - '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-refresh@0.18.0)': + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - - '@rspack/resolver-binding-darwin-arm64@0.2.8': - optional: true - - '@rspack/resolver-binding-darwin-x64@0.2.8': - optional: true - - '@rspack/resolver-binding-linux-arm64-gnu@0.2.8': - optional: true - - '@rspack/resolver-binding-linux-arm64-musl@0.2.8': - optional: true - - '@rspack/resolver-binding-linux-x64-gnu@0.2.8': - optional: true - - '@rspack/resolver-binding-linux-x64-musl@0.2.8': - optional: true - - '@rspack/resolver-binding-wasm32-wasi@0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@rspack/resolver-binding-win32-arm64-msvc@0.2.8': - optional: true - - '@rspack/resolver-binding-win32-ia32-msvc@0.2.8': - optional: true - - '@rspack/resolver-binding-win32-x64-msvc@0.2.8': - optional: true - - '@rspack/resolver@0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': - optionalDependencies: - '@rspack/resolver-binding-darwin-arm64': 0.2.8 - '@rspack/resolver-binding-darwin-x64': 0.2.8 - '@rspack/resolver-binding-linux-arm64-gnu': 0.2.8 - '@rspack/resolver-binding-linux-arm64-musl': 0.2.8 - '@rspack/resolver-binding-linux-x64-gnu': 0.2.8 - '@rspack/resolver-binding-linux-x64-musl': 0.2.8 - '@rspack/resolver-binding-wasm32-wasi': 0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - '@rspack/resolver-binding-win32-arm64-msvc': 0.2.8 - '@rspack/resolver-binding-win32-ia32-msvc': 0.2.8 - '@rspack/resolver-binding-win32-x64-msvc': 0.2.8 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) - '@rstest/adapter-rsbuild@0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))': + '@rstest/adapter-rsbuild@0.11.12(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3))': dependencies: - '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@rstest/core': 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rstest/core': 0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3) - '@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)': + '@rstest/core@0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3)': dependencies: - '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) '@types/chai': 5.2.3 optionalDependencies: happy-dom: 20.8.3 @@ -12003,8 +11921,6 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@socket.io/component-emitter@3.1.2': {} - '@speed-highlight/core@1.2.17': {} '@standard-schema/spec@1.1.0': {} @@ -12088,59 +12004,59 @@ snapshots: transitivePeerDependencies: - typescript - '@swc/core-darwin-arm64@1.15.43': + '@swc/core-darwin-arm64@1.16.2': optional: true - '@swc/core-darwin-x64@1.15.43': + '@swc/core-darwin-x64@1.16.2': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.43': + '@swc/core-linux-arm-gnueabihf@1.16.2': optional: true - '@swc/core-linux-arm64-gnu@1.15.43': + '@swc/core-linux-arm64-gnu@1.16.2': optional: true - '@swc/core-linux-arm64-musl@1.15.43': + '@swc/core-linux-arm64-musl@1.16.2': optional: true - '@swc/core-linux-ppc64-gnu@1.15.43': + '@swc/core-linux-ppc64-gnu@1.16.2': optional: true - '@swc/core-linux-s390x-gnu@1.15.43': + '@swc/core-linux-s390x-gnu@1.16.2': optional: true - '@swc/core-linux-x64-gnu@1.15.43': + '@swc/core-linux-x64-gnu@1.16.2': optional: true - '@swc/core-linux-x64-musl@1.15.43': + '@swc/core-linux-x64-musl@1.16.2': optional: true - '@swc/core-win32-arm64-msvc@1.15.43': + '@swc/core-win32-arm64-msvc@1.16.2': optional: true - '@swc/core-win32-ia32-msvc@1.15.43': + '@swc/core-win32-ia32-msvc@1.16.2': optional: true - '@swc/core-win32-x64-msvc@1.15.43': + '@swc/core-win32-x64-msvc@1.16.2': optional: true - '@swc/core@1.15.43(@swc/helpers@0.5.23)': + '@swc/core@1.16.2(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.27 + '@swc/types': 0.1.28 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.43 - '@swc/core-darwin-x64': 1.15.43 - '@swc/core-linux-arm-gnueabihf': 1.15.43 - '@swc/core-linux-arm64-gnu': 1.15.43 - '@swc/core-linux-arm64-musl': 1.15.43 - '@swc/core-linux-ppc64-gnu': 1.15.43 - '@swc/core-linux-s390x-gnu': 1.15.43 - '@swc/core-linux-x64-gnu': 1.15.43 - '@swc/core-linux-x64-musl': 1.15.43 - '@swc/core-win32-arm64-msvc': 1.15.43 - '@swc/core-win32-ia32-msvc': 1.15.43 - '@swc/core-win32-x64-msvc': 1.15.43 + '@swc/core-darwin-arm64': 1.16.2 + '@swc/core-darwin-x64': 1.16.2 + '@swc/core-linux-arm-gnueabihf': 1.16.2 + '@swc/core-linux-arm64-gnu': 1.16.2 + '@swc/core-linux-arm64-musl': 1.16.2 + '@swc/core-linux-ppc64-gnu': 1.16.2 + '@swc/core-linux-s390x-gnu': 1.16.2 + '@swc/core-linux-x64-gnu': 1.16.2 + '@swc/core-linux-x64-musl': 1.16.2 + '@swc/core-win32-arm64-msvc': 1.16.2 + '@swc/core-win32-ia32-msvc': 1.16.2 + '@swc/core-win32-x64-msvc': 1.16.2 '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -12157,11 +12073,11 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/plugin-loadable-components@12.0.0': + '@swc/plugin-loadable-components@13.0.0': dependencies: '@swc/counter': 0.1.3 - '@swc/types@0.1.27': + '@swc/types@0.1.28': dependencies: '@swc/counter': 0.1.3 @@ -12226,23 +12142,33 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/webpack@4.3.3(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))': + '@tailwindcss/webpack@4.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + optionalDependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + + '@tailwindcss/webpack@4.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': dependencies: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) - '@tanstack/history@1.162.1': {} + '@tanstack/history@1.162.2': {} - '@tanstack/react-router@1.170.25(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@tanstack/react-router@1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@tanstack/history': 1.162.1 + '@tanstack/history': 1.162.2 '@tanstack/react-store': 0.9.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@tanstack/router-core': 1.171.21(patch_hash=413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d) + '@tanstack/router-core': 1.171.28 isbot: 5.2.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -12254,23 +12180,23 @@ snapshots: react-dom: 19.2.8(react@19.2.8) use-sync-external-store: 1.6.0(react@19.2.8) - '@tanstack/router-core@1.171.21(patch_hash=413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d)': + '@tanstack/router-core@1.171.28': dependencies: - '@tanstack/history': 1.162.1 + '@tanstack/history': 1.162.2 cookie-es: 3.1.1 seroval: 1.6.4 seroval-plugins: 1.6.4(seroval@1.6.4) '@tanstack/store@0.9.3': {} - '@techsio/ui-kit@0.25.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3)': + '@techsio/ui-kit@0.25.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3)': dependencies: '@iconify-json/mdi': 1.2.3 '@iconify-json/mdi-light': 1.2.2 '@iconify-json/svg-spinners': 1.2.4 '@iconify/tailwind4': 1.2.3(tailwindcss@4.3.3) - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) '@zag-js/accordion': 1.42.0 '@zag-js/carousel': 1.42.0 '@zag-js/checkbox': 1.42.0 @@ -12313,15 +12239,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -12341,20 +12267,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 20.19.43 - - '@types/cors@2.8.19': - dependencies: - '@types/node': 20.19.43 - '@types/deep-eql@4.0.2': {} '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.5': {} - '@types/estree@1.0.9': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -12377,10 +12293,6 @@ snapshots: dependencies: '@types/react': 19.2.17 - '@types/node@20.19.43': - dependencies: - undici-types: 6.21.0 - '@types/node@26.4.1': dependencies: undici-types: 8.3.0 @@ -12391,13 +12303,13 @@ snapshots: '@types/pg@8.20.0': dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 pg-protocol: 1.15.0 pg-types: 2.2.0 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.7(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-helmet@6.1.11': dependencies: @@ -12407,15 +12319,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/tapable@2.3.0': + '@types/react@19.2.18': dependencies: - tapable: 2.3.3 + csstype: 3.2.3 '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 '@types/yargs-parser@21.0.3': {} @@ -12423,15 +12335,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/type-utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.69.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 7.0.8 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -12439,14 +12351,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -12482,13 +12394,13 @@ snapshots: dependencies: typescript: 7.0.2 - '@typescript-eslint/type-utils@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -12526,24 +12438,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2)': + '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@7.0.2) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 7.0.2 transitivePeerDependencies: - supports-color @@ -12553,37 +12465,6 @@ snapshots: '@typescript-eslint/types': 8.69.0 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2': - optional: true - - '@typescript/native-preview@7.0.0-dev.20260707.2': - optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260707.2 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260707.2 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260707.2 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260707.2 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260707.2 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260707.2 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260707.2 - '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -12658,7 +12539,7 @@ snapshots: glob: 10.5.0 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 - picomatch: 4.0.5 + picomatch: 4.0.7 resolve-from: 5.0.0 transitivePeerDependencies: - rollup @@ -13049,11 +12930,6 @@ snapshots: dependencies: event-target-shim: 5.0.1 - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -13066,13 +12942,9 @@ snapshots: dependencies: acorn: 8.17.0 - acorn-walk@8.3.5: - dependencies: - acorn: 8.17.0 - acorn@8.17.0: {} - adm-zip@0.5.10: {} + adm-zip@0.6.0: {} agent-base@6.0.2(supports-color@10.2.2): dependencies: @@ -13194,13 +13066,13 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.5.2(postcss@8.5.26): + autoprefixer@10.5.5(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-lite: 1.0.30001810 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -13242,32 +13114,32 @@ snapshots: base64-js@1.5.1: {} - base64id@2.0.0: {} - baseline-browser-mapping@2.11.19: {} - better-auth@1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + baseline-browser-mapping@2.11.21: {} + + better-auth@1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) - '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3)) - '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4) - '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747)) + '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) + '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) + '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4) + '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747)) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747) '@noble/ciphers': 2.2.0 '@noble/hashes': 2.2.0 - better-call: 1.4.0(zod@4.4.3) + better-call: 1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) defu: 6.1.7 jose: 6.2.5 kysely: 0.29.4 nanostores: 1.4.2 - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) optionalDependencies: drizzle-kit: 1.0.0-rc.5-ab785fc - drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3) + drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) pg: 8.22.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -13275,14 +13147,14 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.4.0(zod@4.4.3): + better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)): dependencies: '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747) rou3: 0.9.2 set-cookie-parser: 3.1.2 optionalDependencies: - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) better-themes@1.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: @@ -13320,12 +13192,14 @@ snapshots: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist-load-config@1.0.3: {} - browserslist-to-es-version@1.4.2: dependencies: browserslist: 4.28.8 @@ -13338,6 +13212,14 @@ snapshots: node-releases: 2.0.53 update-browserslist-db: 1.3.1(browserslist@4.28.8) + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.422 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.9) + buffer-from@1.1.2: {} buffer@5.7.1: @@ -13354,7 +13236,7 @@ snapshots: bun-types@1.4.0: dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 bundle-name@4.1.0: dependencies: @@ -13362,6 +13244,14 @@ snapshots: bytes@3.1.2: {} + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -13463,7 +13353,7 @@ snapshots: dependencies: readable-stream: 4.7.0 - cluster-key-slot@1.1.1: {} + cluster-key-slot@1.1.2: {} color-convert@2.0.1: dependencies: @@ -13487,17 +13377,23 @@ snapshots: commander@7.2.0: {} - compression-webpack-plugin@12.0.0(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + compression-webpack-plugin@12.0.0(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: schema-utils: 4.3.3 serialize-javascript: 7.0.7 - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + + compression-webpack-plugin@12.0.0(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + schema-utils: 4.3.3 + serialize-javascript: 7.0.7 + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) concat-map@0.0.1: {} confbox@0.1.8: {} - confbox@0.2.4: {} + confbox@0.3.1: {} connect-history-api-fallback@2.0.0: {} @@ -13509,8 +13405,6 @@ snapshots: cookie-es@3.1.1: {} - cookie@0.7.2: {} - cookie@1.1.1: {} cookie@2.0.1: {} @@ -13519,12 +13413,7 @@ snapshots: dependencies: is-what: 4.1.16 - core-js@3.49.0: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + core-js@3.50.0: {} cosmiconfig@8.3.6(typescript@7.0.2): dependencies: @@ -13535,6 +13424,11 @@ snapshots: optionalDependencies: typescript: 7.0.2 + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -13545,7 +13439,7 @@ snapshots: dependencies: postcss: 8.5.26 - css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 7.1.9(postcss@8.5.26) @@ -13553,11 +13447,26 @@ snapshots: postcss: 8.5.26 schema-utils: 4.3.3 serialize-javascript: 7.0.7 - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 + lightningcss: 1.33.0 + + css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + cssnano: 7.1.9(postcss@8.5.26) + jest-worker: 30.4.1 + postcss: 8.5.26 + schema-utils: 4.3.3 + serialize-javascript: 7.0.7 + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + optionalDependencies: + clean-css: 5.3.3 + csso: 5.0.5 + esbuild: 0.28.2 lightningcss: 1.33.0 css-select@5.2.2: @@ -13568,6 +13477,14 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -13585,6 +13502,8 @@ snapshots: css-what@6.2.2: {} + css-what@7.0.0: {} + cssesc@3.0.0: {} cssnano-preset-default@7.0.17(postcss@8.5.26): @@ -13621,46 +13540,46 @@ snapshots: postcss-svgo: 7.1.3(postcss@8.5.26) postcss-unique-selectors: 7.0.7(postcss@8.5.26) - cssnano-preset-default@8.0.2(postcss@8.5.26): - dependencies: - browserslist: 4.28.8 - cssnano-utils: 6.0.1(postcss@8.5.26) - postcss: 8.5.26 - postcss-calc: 10.1.1(postcss@8.5.26) - postcss-colormin: 8.0.1(postcss@8.5.26) - postcss-convert-values: 8.0.1(postcss@8.5.26) - postcss-discard-comments: 8.0.1(postcss@8.5.26) - postcss-discard-duplicates: 8.0.1(postcss@8.5.26) - postcss-discard-empty: 8.0.1(postcss@8.5.26) - postcss-discard-overridden: 8.0.1(postcss@8.5.26) - postcss-merge-longhand: 8.0.1(postcss@8.5.26) - postcss-merge-rules: 8.0.1(postcss@8.5.26) - postcss-minify-font-values: 8.0.1(postcss@8.5.26) - postcss-minify-gradients: 8.0.1(postcss@8.5.26) - postcss-minify-params: 8.0.1(postcss@8.5.26) - postcss-minify-selectors: 8.0.2(postcss@8.5.26) - postcss-normalize-charset: 8.0.1(postcss@8.5.26) - postcss-normalize-display-values: 8.0.1(postcss@8.5.26) - postcss-normalize-positions: 8.0.1(postcss@8.5.26) - postcss-normalize-repeat-style: 8.0.1(postcss@8.5.26) - postcss-normalize-string: 8.0.1(postcss@8.5.26) - postcss-normalize-timing-functions: 8.0.1(postcss@8.5.26) - postcss-normalize-unicode: 8.0.1(postcss@8.5.26) - postcss-normalize-url: 8.0.1(postcss@8.5.26) - postcss-normalize-whitespace: 8.0.1(postcss@8.5.26) - postcss-ordered-values: 8.0.1(postcss@8.5.26) - postcss-reduce-initial: 8.0.1(postcss@8.5.26) - postcss-reduce-transforms: 8.0.1(postcss@8.5.26) - postcss-svgo: 8.0.1(postcss@8.5.26) - postcss-unique-selectors: 8.0.1(postcss@8.5.26) + cssnano-preset-default@9.0.3(postcss@8.5.28): + dependencies: + browserslist: 4.28.9 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 + postcss-calc: 11.1.0(postcss@8.5.28) + postcss-colormin: 9.0.2(postcss@8.5.28) + postcss-convert-values: 9.0.2(postcss@8.5.28) + postcss-discard-comments: 9.0.2(postcss@8.5.28) + postcss-discard-duplicates: 9.0.2(postcss@8.5.28) + postcss-discard-empty: 9.0.2(postcss@8.5.28) + postcss-discard-overridden: 9.0.2(postcss@8.5.28) + postcss-merge-longhand: 9.0.3(postcss@8.5.28) + postcss-merge-rules: 9.0.3(postcss@8.5.28) + postcss-minify-font-values: 9.0.2(postcss@8.5.28) + postcss-minify-gradients: 9.0.2(postcss@8.5.28) + postcss-minify-params: 9.0.2(postcss@8.5.28) + postcss-minify-selectors: 9.0.3(postcss@8.5.28) + postcss-normalize-charset: 9.0.2(postcss@8.5.28) + postcss-normalize-display-values: 9.0.2(postcss@8.5.28) + postcss-normalize-positions: 9.0.2(postcss@8.5.28) + postcss-normalize-repeat-style: 9.0.2(postcss@8.5.28) + postcss-normalize-string: 9.0.2(postcss@8.5.28) + postcss-normalize-timing-functions: 9.0.2(postcss@8.5.28) + postcss-normalize-unicode: 9.0.2(postcss@8.5.28) + postcss-normalize-url: 9.0.2(postcss@8.5.28) + postcss-normalize-whitespace: 9.0.2(postcss@8.5.28) + postcss-ordered-values: 9.0.2(postcss@8.5.28) + postcss-reduce-initial: 9.0.2(postcss@8.5.28) + postcss-reduce-transforms: 9.0.2(postcss@8.5.28) + postcss-svgo: 9.0.2(postcss@8.5.28) + postcss-unique-selectors: 9.0.2(postcss@8.5.28) cssnano-utils@5.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 - cssnano-utils@6.0.1(postcss@8.5.26): + cssnano-utils@7.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 cssnano@7.1.9(postcss@8.5.26): dependencies: @@ -13668,11 +13587,10 @@ snapshots: lilconfig: 3.1.3 postcss: 8.5.26 - cssnano@8.0.2(postcss@8.5.26): + cssnano@9.0.3(postcss@8.5.28): dependencies: - cssnano-preset-default: 8.0.2(postcss@8.5.26) - lilconfig: 3.1.3 - postcss: 8.5.26 + cssnano-preset-default: 9.0.3(postcss@8.5.28) + postcss: 8.5.28 csso@5.0.5: dependencies: @@ -13729,10 +13647,6 @@ snapshots: optionalDependencies: supports-color: 10.2.2 - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -13766,8 +13680,6 @@ snapshots: delayed-stream@1.0.0: {} - denque@2.1.0: {} - dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -13827,17 +13739,17 @@ snapshots: get-tsconfig: 4.14.3 jiti: 2.7.0 - drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98))(pg@8.22.0)(zod@4.4.3): + drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)): optionalDependencies: '@cloudflare/workers-types': 5.20260810.1 - '@effect/sql-pg': 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) + '@effect/sql-pg': 4.0.0-rc.112(effect@4.0.0-rc.112) '@opentelemetry/api': 1.9.1 '@sinclair/typebox': 0.34.52 '@types/pg': 8.20.0 bun-types: 1.4.0 - effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + effect: 4.0.0-rc.112 pg: 8.22.0 - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) dunder-proto@1.0.1: dependencies: @@ -13847,16 +13759,15 @@ snapshots: eastasianwidth@0.2.0: {} - effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98): + effect@4.0.0-rc.112: dependencies: - '@standard-schema/spec': 1.1.0 fast-check: 4.9.0 - kubernetes-types: 1.30.0 - msgpackr: 2.0.4 - uuid: 14.0.1 + msgpackr: 2.1.0(patch_hash=de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a) electron-to-chromium@1.5.414: {} + electron-to-chromium@1.5.422: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -13869,25 +13780,6 @@ snapshots: dependencies: iconv-lite: 0.6.3 - engine.io-parser@5.2.3: {} - - engine.io@6.6.9(supports-color@10.2.2): - dependencies: - '@types/cors': 2.8.19 - '@types/node': 20.19.43 - '@types/ws': 8.18.1 - accepts: 1.3.8 - base64id: 2.0.0 - cookie: 0.7.2 - cors: 2.8.6 - debug: 4.4.3(supports-color@10.2.2) - engine.io-parser: 5.2.3 - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -13895,14 +13787,10 @@ snapshots: entities@4.5.0: {} - entities@6.0.1: {} - entities@7.0.1: {} entities@8.0.0: {} - envinfo@7.21.0: {} - environment@1.1.0: {} errno@0.1.8: @@ -13986,6 +13874,8 @@ snapshots: es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -14010,8 +13900,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.49.0: {} - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -14070,15 +13958,44 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node@0.3.10(supports-color@10.2.2): dependencies: @@ -14088,72 +14005,72 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: debug: 3.2.7(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) transitivePeerDependencies: - supports-color - eslint-plugin-escompat@3.12.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-escompat@3.12.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: browserslist: 4.28.8 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-eslint-comments@3.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: escape-string-regexp: 1.0.5 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-filenames@1.3.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.1.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-github@6.1.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: - '@eslint/compat': 2.1.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint/compat': 2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint/eslintrc': 3.3.7(supports-color@10.2.2) '@eslint/js': 9.39.5 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) aria-query: 5.3.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) - eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-escompat: 3.12.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-eslint-comments: 3.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-filenames: 1.3.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-i18n-text: 1.0.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-escompat: 3.12.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-filenames: 1.3.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6) eslint-rule-documentation: 1.0.23 globals: 17.12.0 jsx-ast-utils: 3.3.5 prettier: 3.9.6 svg-element-attributes: 1.3.1 typescript: 6.0.3 - typescript-eslint: 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + typescript-eslint: 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-i18n-text@1.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14162,9 +14079,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@10.2.2) doctrine: 2.1.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -14176,13 +14093,13 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -14192,7 +14109,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -14203,30 +14120,30 @@ snapshots: eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-perfectionist@5.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2): + eslint-plugin-perfectionist@5.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2): dependencies: - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6): dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) prettier: 3.9.6 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-sonarjs@4.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-sonarjs@4.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) functional-red-black-tree: 1.0.1 globals: 17.12.0 jsx-ast-utils-x: 0.1.0 @@ -14245,11 +14162,6 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -14263,40 +14175,36 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2): + eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2(supports-color@10.2.2) - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.7(supports-color@10.2.2) - '@eslint/js': 9.39.5 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.5(supports-color@10.2.2) + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.9 ajv: 6.15.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -14310,6 +14218,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 4.2.1 + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -14363,7 +14277,7 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exsolve@1.1.0: {} + exsolve@1.1.1: {} fallow-type-aware@3.22.0: dependencies: @@ -14443,14 +14357,12 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: + file-entry-cache@11.1.5: dependencies: - flat-cache: 4.0.1 + flat-cache: 6.1.23 file-uri-to-path@1.0.0: {} - filesize@11.0.22: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -14472,10 +14384,11 @@ snapshots: pkg-types: 1.3.1 yaml: 2.9.0 - flat-cache@4.0.1: + flat-cache@6.1.23: dependencies: + cacheable: 2.5.0 flatted: 3.4.4 - keyv: 4.5.4 + hookified: 1.15.1 flatted@3.4.4: {} @@ -14574,8 +14487,6 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 - get-port@5.1.1: {} - get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -14628,12 +14539,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.6: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.3 - path-scurry: 2.0.2 - glob@7.2.0: dependencies: fs.realpath: 1.0.0 @@ -14688,6 +14593,10 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -14696,7 +14605,11 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.12.31: {} + hono@4.13.7: {} + + hookified@1.15.1: {} + + hookified@2.2.0: {} html-minifier-terser@7.2.0: dependencies: @@ -14708,13 +14621,6 @@ snapshots: relateurl: 0.2.7 terser: 5.49.0 - htmlparser2@10.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 6.0.1 - htmlparser2@12.0.0: dependencies: domelementtype: 3.0.0 @@ -14748,17 +14654,17 @@ snapshots: dependencies: '@babel/runtime': 7.29.7 - i18next-chained-backend@5.0.5: + i18next-chained-backend@5.0.6: dependencies: '@babel/runtime': 7.29.7 i18next-fs-backend@2.6.7: {} - i18next-http-backend@4.0.0: {} + i18next-http-backend@4.0.2: {} i18next-http-middleware@3.9.8: {} - i18next@26.3.6(typescript@7.0.2): + i18next@26.4.2(typescript@7.0.2): optionalDependencies: typescript: 7.0.2 @@ -14805,18 +14711,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@5.11.1(supports-color@10.2.2): - dependencies: - '@ioredis/commands': 1.10.0 - cluster-key-slot: 1.1.1 - debug: 4.4.3(supports-color@10.2.2) - denque: 2.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -14982,10 +14876,10 @@ snapshots: isarray@2.0.5: {} - isbot@5.2.0: {} - isbot@5.2.1: {} + isbot@5.2.2: {} + isexe@2.0.0: {} isomorphic-ws@5.0.0(ws@8.21.0): @@ -15000,10 +14894,12 @@ snapshots: jest-regex-util@30.4.0: {} + jest-regex-util@30.5.0: {} + jest-util@30.4.1: dependencies: '@jest/types': 30.4.1 - '@types/node': 20.19.43 + '@types/node': 26.4.1 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -15011,13 +14907,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.4.1: dependencies: - '@types/node': 20.19.43 + '@types/node': 26.4.1 '@ungap/structured-clone': 1.3.3 jest-util: 30.4.1 merge-stream: 2.0.0 @@ -15078,8 +14974,6 @@ snapshots: jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} json-schema-traverse@0.4.1: {} @@ -15088,8 +14982,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stream-stringify@3.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -15113,9 +15005,9 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - keyv@4.5.4: + keyv@5.6.0: dependencies: - json-buffer: 3.0.1 + '@keyv/serialize': 1.1.1 kleur@4.1.5: {} @@ -15133,12 +15025,10 @@ snapshots: tinyglobby: 0.2.17 unbash: 4.0.11 yaml: 2.9.0 - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) koa-compose@4.1.0: {} - kubernetes-types@1.30.0: {} - kysely@0.29.4: {} language-subtag-registry@0.3.23: {} @@ -15147,11 +15037,6 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 - launch-editor@2.14.1: - dependencies: - picocolors: 1.1.1 - shell-quote: 1.10.0 - lefthook-darwin-arm64@2.1.10: optional: true @@ -15195,12 +15080,19 @@ snapshots: lefthook-windows-arm64: 2.1.10 lefthook-windows-x64: 2.1.10 - less-loader@12.3.3(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + less-loader@12.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + less: 4.7.0(supports-color@10.2.2) + optionalDependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + + less-loader@12.3.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(less@4.7.0(supports-color@10.2.2))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: less: 4.7.0(supports-color@10.2.2) optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) less@4.7.0(supports-color@10.2.2): dependencies: @@ -15326,8 +15218,6 @@ snapshots: lines-and-columns@1.2.4: {} - lines-and-columns@2.0.4: {} - loader-runner@4.3.2: {} loader-utils@2.0.4: @@ -15479,12 +15369,12 @@ snapshots: mimic-function@5.0.1: {} - miniflare@4.20260708.1: + miniflare@4.20260730.0: dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 + sharp: 0.35.2 undici: 7.28.0 - workerd: 1.20260708.1 + workerd: 1.20260730.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -15495,6 +15385,10 @@ snapshots: dependencies: brace-expansion: 5.0.7 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.16 @@ -15505,22 +15399,39 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + minimizer-webpack-plugin@5.6.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) optionalDependencies: - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 8.0.2(postcss@8.5.26) + cssnano: 9.0.3(postcss@8.5.28) csso: 5.0.5 esbuild: 0.28.1 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.26 + postcss: 8.5.28 + + minimizer-webpack-plugin@5.6.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.0 + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + optionalDependencies: + '@swc/core': 1.16.2(@swc/helpers@0.5.23) + clean-css: 5.3.3 + cssnano: 9.0.3(postcss@8.5.28) + csso: 5.0.5 + esbuild: 0.28.2 + html-minifier-terser: 7.2.0 + lightningcss: 1.33.0 + postcss: 8.5.28 minipass@7.1.3: {} @@ -15563,7 +15474,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.4: + msgpackr@2.1.0(patch_hash=de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a): optionalDependencies: msgpackr-extract: 3.0.4 @@ -15603,8 +15514,6 @@ snapshots: sax: 1.6.0 optional: true - negotiator@0.6.3: {} - neo-async@2.6.2: {} no-case@3.0.4: @@ -15643,6 +15552,8 @@ snapshots: node-releases@2.0.53: {} + node-releases@2.0.54: {} + nopt@8.1.0: dependencies: abbrev: 3.0.1 @@ -15662,12 +15573,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nypm@0.6.8: - dependencies: - citty: 0.2.2 - pathe: 2.0.3 - tinyexec: 1.2.4 - nypm@0.6.9: dependencies: citty: 0.2.2 @@ -15837,53 +15742,29 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 - oxfmt@0.63.0: - dependencies: - tinypool: 2.1.0 - optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.63.0 - '@oxfmt/binding-android-arm64': 0.63.0 - '@oxfmt/binding-darwin-arm64': 0.63.0 - '@oxfmt/binding-darwin-x64': 0.63.0 - '@oxfmt/binding-freebsd-x64': 0.63.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.63.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.63.0 - '@oxfmt/binding-linux-arm64-gnu': 0.63.0 - '@oxfmt/binding-linux-arm64-musl': 0.63.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.63.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.63.0 - '@oxfmt/binding-linux-riscv64-musl': 0.63.0 - '@oxfmt/binding-linux-s390x-gnu': 0.63.0 - '@oxfmt/binding-linux-x64-gnu': 0.63.0 - '@oxfmt/binding-linux-x64-musl': 0.63.0 - '@oxfmt/binding-openharmony-arm64': 0.63.0 - '@oxfmt/binding-win32-arm64-msvc': 0.63.0 - '@oxfmt/binding-win32-ia32-msvc': 0.63.0 - '@oxfmt/binding-win32-x64-msvc': 0.63.0 - - oxfmt@0.64.0: + oxfmt@0.66.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.64.0 - '@oxfmt/binding-android-arm64': 0.64.0 - '@oxfmt/binding-darwin-arm64': 0.64.0 - '@oxfmt/binding-darwin-x64': 0.64.0 - '@oxfmt/binding-freebsd-x64': 0.64.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.64.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.64.0 - '@oxfmt/binding-linux-arm64-gnu': 0.64.0 - '@oxfmt/binding-linux-arm64-musl': 0.64.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-musl': 0.64.0 - '@oxfmt/binding-linux-s390x-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-musl': 0.64.0 - '@oxfmt/binding-openharmony-arm64': 0.64.0 - '@oxfmt/binding-win32-arm64-msvc': 0.64.0 - '@oxfmt/binding-win32-ia32-msvc': 0.64.0 - '@oxfmt/binding-win32-x64-msvc': 0.64.0 + '@oxfmt/binding-android-arm-eabi': 0.66.0 + '@oxfmt/binding-android-arm64': 0.66.0 + '@oxfmt/binding-darwin-arm64': 0.66.0 + '@oxfmt/binding-darwin-x64': 0.66.0 + '@oxfmt/binding-freebsd-x64': 0.66.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.66.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.66.0 + '@oxfmt/binding-linux-arm64-gnu': 0.66.0 + '@oxfmt/binding-linux-arm64-musl': 0.66.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-musl': 0.66.0 + '@oxfmt/binding-linux-s390x-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-musl': 0.66.0 + '@oxfmt/binding-openharmony-arm64': 0.66.0 + '@oxfmt/binding-win32-arm64-msvc': 0.66.0 + '@oxfmt/binding-win32-ia32-msvc': 0.66.0 + '@oxfmt/binding-win32-x64-msvc': 0.66.0 oxlint-plugin-react-doctor@0.9.12: dependencies: @@ -15903,50 +15784,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.78.0(oxlint-tsgolint@7.0.2001): + oxlint@1.81.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.78.0 - '@oxlint/binding-android-arm64': 1.78.0 - '@oxlint/binding-darwin-arm64': 1.78.0 - '@oxlint/binding-darwin-x64': 1.78.0 - '@oxlint/binding-freebsd-x64': 1.78.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 - '@oxlint/binding-linux-arm-musleabihf': 1.78.0 - '@oxlint/binding-linux-arm64-gnu': 1.78.0 - '@oxlint/binding-linux-arm64-musl': 1.78.0 - '@oxlint/binding-linux-ppc64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-musl': 1.78.0 - '@oxlint/binding-linux-s390x-gnu': 1.78.0 - '@oxlint/binding-linux-x64-gnu': 1.78.0 - '@oxlint/binding-linux-x64-musl': 1.78.0 - '@oxlint/binding-openharmony-arm64': 1.78.0 - '@oxlint/binding-win32-arm64-msvc': 1.78.0 - '@oxlint/binding-win32-ia32-msvc': 1.78.0 - '@oxlint/binding-win32-x64-msvc': 1.78.0 - oxlint-tsgolint: 7.0.2001 - - oxlint@1.79.0(oxlint-tsgolint@7.0.2001): - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.79.0 - '@oxlint/binding-android-arm64': 1.79.0 - '@oxlint/binding-darwin-arm64': 1.79.0 - '@oxlint/binding-darwin-x64': 1.79.0 - '@oxlint/binding-freebsd-x64': 1.79.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.79.0 - '@oxlint/binding-linux-arm-musleabihf': 1.79.0 - '@oxlint/binding-linux-arm64-gnu': 1.79.0 - '@oxlint/binding-linux-arm64-musl': 1.79.0 - '@oxlint/binding-linux-ppc64-gnu': 1.79.0 - '@oxlint/binding-linux-riscv64-gnu': 1.79.0 - '@oxlint/binding-linux-riscv64-musl': 1.79.0 - '@oxlint/binding-linux-s390x-gnu': 1.79.0 - '@oxlint/binding-linux-x64-gnu': 1.79.0 - '@oxlint/binding-linux-x64-musl': 1.79.0 - '@oxlint/binding-openharmony-arm64': 1.79.0 - '@oxlint/binding-win32-arm64-msvc': 1.79.0 - '@oxlint/binding-win32-ia32-msvc': 1.79.0 - '@oxlint/binding-win32-x64-msvc': 1.79.0 + '@oxlint/binding-android-arm-eabi': 1.81.0 + '@oxlint/binding-android-arm64': 1.81.0 + '@oxlint/binding-darwin-arm64': 1.81.0 + '@oxlint/binding-darwin-x64': 1.81.0 + '@oxlint/binding-freebsd-x64': 1.81.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.81.0 + '@oxlint/binding-linux-arm-musleabihf': 1.81.0 + '@oxlint/binding-linux-arm64-gnu': 1.81.0 + '@oxlint/binding-linux-arm64-musl': 1.81.0 + '@oxlint/binding-linux-ppc64-gnu': 1.81.0 + '@oxlint/binding-linux-riscv64-gnu': 1.81.0 + '@oxlint/binding-linux-riscv64-musl': 1.81.0 + '@oxlint/binding-linux-s390x-gnu': 1.81.0 + '@oxlint/binding-linux-x64-gnu': 1.81.0 + '@oxlint/binding-linux-x64-musl': 1.81.0 + '@oxlint/binding-openharmony-arm64': 1.81.0 + '@oxlint/binding-win32-arm64-msvc': 1.81.0 + '@oxlint/binding-win32-ia32-msvc': 1.81.0 + '@oxlint/binding-win32-x64-msvc': 1.81.0 oxlint-tsgolint: 7.0.2001 p-limit@2.3.0: @@ -16005,8 +15863,6 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 - path-browserify@1.0.1: {} - path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -16024,11 +15880,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 - path-scurry@2.0.2: - dependencies: - lru-cache: 11.5.2 - minipass: 7.1.3 - path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -16044,9 +15895,9 @@ snapshots: pg-connection-string@2.14.0: {} - pg-cursor@2.22.0(pg@8.22.0): + pg-cursor@2.22.0(pg@8.23.0): dependencies: - pg: 8.22.0 + pg: 8.23.0 pg-int8@1.0.1: {} @@ -16056,8 +15907,14 @@ snapshots: dependencies: pg: 8.22.0 + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + pg-protocol@1.15.0: {} + pg-protocol@1.16.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -16086,6 +15943,16 @@ snapshots: optionalDependencies: pg-cloudflare: 1.4.0 + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pgpass@1.0.5: dependencies: split2: 4.2.0 @@ -16104,10 +15971,10 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - pkg-types@2.3.1: + pkg-types@2.3.3: dependencies: - confbox: 0.2.4 - exsolve: 1.1.0 + confbox: 0.3.1 + exsolve: 1.1.1 pathe: 2.0.3 pkg-up@3.1.0: @@ -16130,6 +15997,11 @@ snapshots: postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 + postcss-calc@11.1.0(postcss@8.5.28): + dependencies: + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.28 + postcss-colormin@7.0.10(postcss@8.5.26): dependencies: '@colordx/core': 5.5.0 @@ -16138,12 +16010,12 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-colormin@8.0.1(postcss@8.5.26): + postcss-colormin@9.0.2(postcss@8.5.28): dependencies: - '@colordx/core': 5.5.0 - browserslist: 4.28.8 + '@colordx/core': 6.3.0 + browserslist: 4.28.9 caniuse-api: 4.0.0 - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-convert-values@7.0.12(postcss@8.5.26): @@ -16152,19 +16024,19 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-convert-values@8.0.1(postcss@8.5.26): + postcss-convert-values@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-custom-properties@15.0.1(postcss@8.5.26): + postcss-custom-properties@15.0.1(postcss@8.5.28): dependencies: '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/utilities': 3.0.0(postcss@8.5.26) - postcss: 8.5.26 + '@csstools/utilities': 3.0.0(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-discard-comments@7.0.8(postcss@8.5.26): @@ -16172,50 +16044,50 @@ snapshots: postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-discard-comments@8.0.1(postcss@8.5.26): + postcss-discard-comments@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-discard-duplicates@7.0.4(postcss@8.5.26): dependencies: postcss: 8.5.26 - postcss-discard-duplicates@8.0.1(postcss@8.5.26): + postcss-discard-duplicates@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-discard-empty@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 - postcss-discard-empty@8.0.1(postcss@8.5.26): + postcss-discard-empty@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-discard-overridden@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 - postcss-discard-overridden@8.0.1(postcss@8.5.26): + postcss-discard-overridden@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-flexbugs-fixes@5.0.2(postcss@8.5.26): + postcss-flexbugs-fixes@5.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-font-variant@5.0.0(postcss@8.5.26): + postcss-font-variant@5.0.0(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-initial@4.0.1(postcss@8.5.26): + postcss-initial@4.0.1(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-media-minmax@5.0.0(postcss@8.5.26): + postcss-media-minmax@5.0.0(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-merge-longhand@7.0.7(postcss@8.5.26): dependencies: @@ -16223,11 +16095,11 @@ snapshots: postcss-value-parser: 4.2.0 stylehacks: 7.0.11(postcss@8.5.26) - postcss-merge-longhand@8.0.1(postcss@8.5.26): + postcss-merge-longhand@9.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - stylehacks: 8.0.1(postcss@8.5.26) + stylehacks: 9.0.3(postcss@8.5.28) postcss-merge-rules@7.0.11(postcss@8.5.26): dependencies: @@ -16237,22 +16109,22 @@ snapshots: postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-merge-rules@8.0.1(postcss@8.5.26): + postcss-merge-rules@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 - cssnano-utils: 6.0.1(postcss@8.5.26) - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-minify-font-values@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-font-values@8.0.1(postcss@8.5.26): + postcss-minify-font-values@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-gradients@7.0.5(postcss@8.5.26): @@ -16262,11 +16134,11 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-gradients@8.0.1(postcss@8.5.26): + postcss-minify-gradients@9.0.2(postcss@8.5.28): dependencies: - '@colordx/core': 5.5.0 - cssnano-utils: 6.0.1(postcss@8.5.26) - postcss: 8.5.26 + '@colordx/core': 6.3.0 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-params@7.0.9(postcss@8.5.26): @@ -16276,11 +16148,11 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-params@8.0.1(postcss@8.5.26): + postcss-minify-params@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - cssnano-utils: 6.0.1(postcss@8.5.26) - postcss: 8.5.26 + browserslist: 4.28.9 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-selectors@7.1.2(postcss@8.5.26): @@ -16291,37 +16163,37 @@ snapshots: postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-minify-selectors@8.0.2(postcss@8.5.26): + postcss-minify-selectors@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 cssesc: 3.0.0 - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 - postcss-nesting@14.0.0(postcss@8.5.26): + postcss-nesting@14.0.1(postcss@8.5.28): dependencies: - '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.4) + '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.4) '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.26 + postcss: 8.5.28 postcss-selector-parser: 7.1.4 postcss-normalize-charset@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 - postcss-normalize-charset@8.0.1(postcss@8.5.26): + postcss-normalize-charset@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-normalize-display-values@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-display-values@8.0.1(postcss@8.5.26): + postcss-normalize-display-values@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-positions@7.0.4(postcss@8.5.26): @@ -16329,9 +16201,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-positions@8.0.1(postcss@8.5.26): + postcss-normalize-positions@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-repeat-style@7.0.4(postcss@8.5.26): @@ -16339,9 +16211,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@8.0.1(postcss@8.5.26): + postcss-normalize-repeat-style@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-string@7.0.3(postcss@8.5.26): @@ -16349,9 +16221,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-string@8.0.1(postcss@8.5.26): + postcss-normalize-string@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-timing-functions@7.0.3(postcss@8.5.26): @@ -16359,9 +16231,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@8.0.1(postcss@8.5.26): + postcss-normalize-timing-functions@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-unicode@7.0.9(postcss@8.5.26): @@ -16370,10 +16242,10 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@8.0.1(postcss@8.5.26): + postcss-normalize-unicode@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-url@7.0.3(postcss@8.5.26): @@ -16381,9 +16253,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-url@8.0.1(postcss@8.5.26): + postcss-normalize-url@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-whitespace@7.0.3(postcss@8.5.26): @@ -16391,9 +16263,9 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@8.0.1(postcss@8.5.26): + postcss-normalize-whitespace@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-ordered-values@7.0.4(postcss@8.5.26): @@ -16402,15 +16274,15 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-ordered-values@8.0.1(postcss@8.5.26): + postcss-ordered-values@9.0.2(postcss@8.5.28): dependencies: - cssnano-utils: 6.0.1(postcss@8.5.26) - postcss: 8.5.26 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.26): + postcss-page-break@3.0.4(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-reduce-initial@7.0.9(postcss@8.5.26): dependencies: @@ -16418,20 +16290,20 @@ snapshots: caniuse-api: 3.0.0 postcss: 8.5.26 - postcss-reduce-initial@8.0.1(postcss@8.5.26): + postcss-reduce-initial@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 - postcss: 8.5.26 + postcss: 8.5.28 postcss-reduce-transforms@7.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-reduce-transforms@8.0.1(postcss@8.5.26): + postcss-reduce-transforms@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-selector-parser@7.1.4: @@ -16439,27 +16311,32 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.6: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-svgo@7.1.3(postcss@8.5.26): dependencies: postcss: 8.5.26 postcss-value-parser: 4.2.0 svgo: 4.0.2 - postcss-svgo@8.0.1(postcss@8.5.26): + postcss-svgo@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - svgo: 4.0.2 + svgo: 4.1.0 postcss-unique-selectors@7.0.7(postcss@8.5.26): dependencies: postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-unique-selectors@8.0.1(postcss@8.5.26): + postcss-unique-selectors@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-value-parser@4.2.0: {} @@ -16469,6 +16346,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -16543,7 +16426,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.2 - '@types/node': 20.19.43 + '@types/node': 26.4.1 long: 5.3.2 protocols@2.0.2: {} @@ -16559,7 +16442,11 @@ snapshots: pure-rand@8.4.2: {} - qs@6.15.3: + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -16596,14 +16483,6 @@ snapshots: react-refresh@0.18.0: {} - react-router@7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): - dependencies: - cookie: 1.1.1 - react: 19.2.8 - set-cookie-parser: 2.7.2 - optionalDependencies: - react-dom: 19.2.8(react@19.2.8) - react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: cookie: 1.1.1 @@ -16612,9 +16491,9 @@ snapshots: optionalDependencies: react-dom: 19.2.8(react@19.2.8) - react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optional: true @@ -16646,11 +16525,16 @@ snapshots: readdirp@5.0.0: optional: true - redis-errors@1.2.0: {} - - redis-parser@3.0.0: + redis@6.2.1(@opentelemetry/api@1.9.1): dependencies: - redis-errors: 1.2.0 + '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/json': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/search': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/time-series': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' reduce-configs@1.1.2: {} @@ -16726,25 +16610,19 @@ snapshots: rou3@0.9.2: {} - rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): - dependencies: - '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - optional: true - - rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): + rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): dependencies: - '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - react-server-dom-rspack: 0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + react-server-dom-rspack: 0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) optional: true rslog@2.3.0: {} - rspack-manifest-plugin@5.2.2(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23)): + rspack-manifest-plugin@5.2.2(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23)): dependencies: '@rspack/lite-tapable': 1.1.2 optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) run-applescript@7.1.0: {} @@ -16877,6 +16755,8 @@ snapshots: sax@1.6.0: {} + sax@1.6.1: {} + scheduler@0.27.0: {} schema-utils@4.3.0: @@ -16939,36 +16819,37 @@ snapshots: shallowequal@1.1.0: {} - sharp@0.34.5: + sharp@0.35.2: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 shebang-command@2.0.0: dependencies: @@ -16976,8 +16857,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.10.0: {} - side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -17024,36 +16903,6 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-adapter@2.5.8(supports-color@10.2.2): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - socket.io-parser@4.2.7(supports-color@10.2.2): - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - socket.io@4.8.1(supports-color@10.2.2): - dependencies: - accepts: 1.3.8 - base64id: 2.0.0 - cors: 2.8.6 - debug: 4.3.7(supports-color@10.2.2) - engine.io: 6.6.9(supports-color@10.2.2) - socket.io-adapter: 2.5.8(supports-color@10.2.2) - socket.io-parser: 4.2.7(supports-color@10.2.2) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -17067,8 +16916,6 @@ snapshots: split2@4.2.0: {} - standard-as-callback@2.1.0: {} - std-env@4.2.0: {} stop-iteration-iterator@1.1.0: @@ -17158,11 +17005,11 @@ snapshots: postcss: 8.5.26 postcss-selector-parser: 7.1.4 - stylehacks@8.0.1(postcss@8.5.26): + stylehacks@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + browserslist: 4.28.9 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 supports-color@10.2.2: {} @@ -17200,6 +17047,16 @@ snapshots: picocolors: 1.1.1 sax: 1.6.0 + svgo@4.1.0: + dependencies: + commander: 11.1.0 + css-select: 6.0.0 + css-tree: 3.2.1 + css-what: 7.0.0 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.1 + sync-child-process@1.0.2: dependencies: sync-message-port: 1.2.0 @@ -17274,7 +17131,7 @@ snapshots: dependencies: typescript: 7.0.2 - ts-checker-rspack-plugin@1.6.1(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(@typescript/native-preview@7.0.0-dev.20260707.2)(tslib@2.8.1)(typescript@7.0.2): + ts-checker-rspack-plugin@1.6.1(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2): dependencies: '@rspack/lite-tapable': 1.1.2 chokidar: 3.6.0 @@ -17282,8 +17139,8 @@ snapshots: picocolors: 1.1.1 typescript: 7.0.2 optionalDependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) - '@typescript/native-preview': 7.0.0-dev.20260707.2 + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + '@typescript/native-preview': typescript@7.0.2 transitivePeerDependencies: - tslib @@ -17308,9 +17165,7 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.1.0: {} - - type-fest@5.8.0: + type-fest@5.9.0: dependencies: tagged-tag: 1.0.0 @@ -17353,13 +17208,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): + typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -17391,22 +17246,7 @@ snapshots: ufo@1.6.4: {} - ultracite@7.10.2(oxfmt@0.63.0)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)): - dependencies: - '@clack/prompts': 1.7.0 - commander: 15.0.0 - cross-spawn: 7.0.6 - deepmerge: 4.3.1 - glob: 13.0.6 - jsonc-parser: 3.3.1 - nypm: 0.6.8 - yaml: 2.9.0 - zod: 4.4.3 - optionalDependencies: - oxfmt: 0.63.0 - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001) - - ultracite@7.10.7(oxfmt@0.64.0)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)): + ultracite@7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6): dependencies: '@clack/prompts': 1.7.0 cli-truncate: 6.1.1 @@ -17421,12 +17261,15 @@ snapshots: magicast: 0.5.4 nypm: 0.6.9 resolve.exports: 2.0.3 + semver: 7.8.5 string-width: 8.2.2 yaml: 2.9.0 - zod: 4.4.3 + zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) optionalDependencies: - oxfmt: 0.64.0 - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + oxfmt: 0.66.0 + oxlint: 1.81.0(oxlint-tsgolint@7.0.2001) + prettier: 3.9.6 unbash@4.0.11: {} @@ -17437,12 +17280,12 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} - undici-types@8.3.0: {} undici@7.28.0: {} + undici@7.29.0: {} + undici@8.10.1: {} unenv@2.0.0-rc.24: @@ -17459,6 +17302,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.3.2(browserslist@4.28.9): + dependencies: + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -17477,12 +17326,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.22 - uuid@14.0.1: {} - varint@6.0.0: {} - vary@1.1.2: {} - walk-up-path@4.0.0: {} watchpack@2.5.2: @@ -17503,7 +17348,45 @@ snapshots: webpack-sources@3.5.1: {} - webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26): + webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.3 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -17521,7 +17404,7 @@ snapshots: graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + minimizer-webpack-plugin: 5.6.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -17592,24 +17475,24 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260708.1: + workerd@1.20260730.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260708.1 - '@cloudflare/workerd-darwin-arm64': 1.20260708.1 - '@cloudflare/workerd-linux-64': 1.20260708.1 - '@cloudflare/workerd-linux-arm64': 1.20260708.1 - '@cloudflare/workerd-windows-64': 1.20260708.1 + '@cloudflare/workerd-darwin-64': 1.20260730.1 + '@cloudflare/workerd-darwin-arm64': 1.20260730.1 + '@cloudflare/workerd-linux-64': 1.20260730.1 + '@cloudflare/workerd-linux-arm64': 1.20260730.1 + '@cloudflare/workerd-windows-64': 1.20260730.1 - wrangler@4.110.0(@cloudflare/workers-types@5.20260810.1): + wrangler@4.116.0(@cloudflare/workers-types@5.20260810.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1) + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 4.20260708.1 + miniflare: 4.20260730.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260708.1 + workerd: 1.20260730.1 optionalDependencies: '@cloudflare/workers-types': 5.20260810.1 fsevents: 2.3.3 @@ -17706,22 +17589,40 @@ snapshots: zephyr-edge-contract@1.2.4: {} - zephyr-rspack-plugin@1.2.4(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + zephyr-rspack-plugin@1.2.4(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + zephyr-agent: 1.2.4(supports-color@10.2.2) + zephyr-xpack-internal: 1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + transitivePeerDependencies: + - supports-color + - webpack + + zephyr-rspack-plugin@1.2.4(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): + dependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + zephyr-agent: 1.2.4(supports-color@10.2.2) + zephyr-xpack-internal: 1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + transitivePeerDependencies: + - supports-color + - webpack + + zephyr-xpack-internal@1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: - '@rspack/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23) + '@module-federation/automatic-vendor-federation': 1.2.1(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) zephyr-agent: 1.2.4(supports-color@10.2.2) - zephyr-xpack-internal: 1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + zephyr-edge-contract: 1.2.4 transitivePeerDependencies: - supports-color - webpack - zephyr-xpack-internal@1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)): + zephyr-xpack-internal@1.2.4(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: - '@module-federation/automatic-vendor-federation': 1.2.1(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + '@module-federation/automatic-vendor-federation': 1.2.1(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) zephyr-agent: 1.2.4(supports-color@10.2.2) zephyr-edge-contract: 1.2.4 transitivePeerDependencies: - supports-color - webpack - zod@4.4.3: {} + zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6): {} diff --git a/app/pnpm-workspace.yaml b/app/pnpm-workspace.yaml index 87d92b794..334b6d8bc 100644 --- a/app/pnpm-workspace.yaml +++ b/app/pnpm-workspace.yaml @@ -7,94 +7,49 @@ minimumReleaseAge: 1440 minimumReleaseAgeStrict: true minimumReleaseAgeIgnoreMissingTime: false minimumReleaseAgeExclude: - - '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-bff-core@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-create-request@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-prod-server@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-render@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-server-runtime-extensions@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-server-runtime@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-server@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' - - '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12' - - 'oxlint@1.79.0' - - '@oxlint/plugins@1.79.0' - - '@oxlint/binding-android-arm-eabi@1.79.0' - - '@oxlint/binding-android-arm64@1.79.0' - - '@oxlint/binding-darwin-arm64@1.79.0' - - '@oxlint/binding-darwin-x64@1.79.0' - - '@oxlint/binding-freebsd-x64@1.79.0' - - '@oxlint/binding-linux-arm-gnueabihf@1.79.0' - - '@oxlint/binding-linux-arm-musleabihf@1.79.0' - - '@oxlint/binding-linux-arm64-gnu@1.79.0' - - '@oxlint/binding-linux-arm64-musl@1.79.0' - - '@oxlint/binding-linux-ppc64-gnu@1.79.0' - - '@oxlint/binding-linux-riscv64-gnu@1.79.0' - - '@oxlint/binding-linux-riscv64-musl@1.79.0' - - '@oxlint/binding-linux-s390x-gnu@1.79.0' - - '@oxlint/binding-linux-x64-gnu@1.79.0' - - '@oxlint/binding-linux-x64-musl@1.79.0' - - '@oxlint/binding-openharmony-arm64@1.79.0' - - '@oxlint/binding-win32-arm64-msvc@1.79.0' - - '@oxlint/binding-win32-ia32-msvc@1.79.0' - - '@oxlint/binding-win32-x64-msvc@1.79.0' - - 'oxfmt@0.64.0' - - '@oxfmt/binding-android-arm-eabi@0.64.0' - - '@oxfmt/binding-android-arm64@0.64.0' - - '@oxfmt/binding-darwin-arm64@0.64.0' - - '@oxfmt/binding-darwin-x64@0.64.0' - - '@oxfmt/binding-freebsd-x64@0.64.0' - - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0' - - '@oxfmt/binding-linux-arm-musleabihf@0.64.0' - - '@oxfmt/binding-linux-arm64-gnu@0.64.0' - - '@oxfmt/binding-linux-arm64-musl@0.64.0' - - '@oxfmt/binding-linux-ppc64-gnu@0.64.0' - - '@oxfmt/binding-linux-riscv64-gnu@0.64.0' - - '@oxfmt/binding-linux-riscv64-musl@0.64.0' - - '@oxfmt/binding-linux-s390x-gnu@0.64.0' - - '@oxfmt/binding-linux-x64-gnu@0.64.0' - - '@oxfmt/binding-linux-x64-musl@0.64.0' - - '@oxfmt/binding-openharmony-arm64@0.64.0' - - '@oxfmt/binding-win32-arm64-msvc@0.64.0' - - '@oxfmt/binding-win32-ia32-msvc@0.64.0' - - '@oxfmt/binding-win32-x64-msvc@0.64.0' - - '@rsbuild/core@2.2.0' - - '@rspack/binding-darwin-arm64@2.2.0' - - '@rspack/binding-darwin-x64@2.2.0' - - '@rspack/binding-linux-arm64-gnu@2.2.0' - - '@rspack/binding-linux-arm64-musl@2.2.0' - - '@rspack/binding-linux-ppc64-gnu@2.2.0' - - '@rspack/binding-linux-riscv64-gnu@2.2.0' - - '@rspack/binding-linux-riscv64-musl@2.2.0' - - '@rspack/binding-linux-s390x-gnu@2.2.0' - - '@rspack/binding-linux-x64-gnu@2.2.0' - - '@rspack/binding-linux-x64-musl@2.2.0' - - '@rspack/binding-wasm32-wasi@2.2.0' - - '@rspack/binding-win32-arm64-msvc@2.2.0' - - '@rspack/binding-win32-ia32-msvc@2.2.0' - - '@rspack/binding-win32-x64-msvc@2.2.0' - - '@rspack/binding@2.2.0' - - '@rspack/core@2.2.0' - - '@rstest/core@0.11.10' + - '@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-bff-runtime@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-builder@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-image@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-main-doc@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-polyfill@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-ssg@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-styled-components@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-plugin@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-render@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-sandpack-react@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-server@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-types@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-ultramodern-sandpack-profile@3.9.0-ultramodern.2' + - '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2' trustPolicy: no-downgrade trustPolicyIgnoreAfter: 1440 trustPolicyExclude: - - '@effect/opentelemetry@4.0.0-beta.107' - - effect@4.0.0-beta.107 + - '@effect/opentelemetry@4.0.0-rc.112' + - effect@4.0.0-rc.112 blockExoticSubdeps: true engineStrict: true pmOnFail: error @@ -103,15 +58,18 @@ strictDepBuilds: true peerDependencyRules: allowedVersions: react: '>=19.0.0' - '@effect/vitest>effect': 4.0.0-beta.107 + '@effect/vitest>effect': 4.0.0-rc.112 overrides: react-server-dom-rspack: 0.1.0 - '@tanstack/react-router': 1.170.25 - '@tanstack/router-core': 1.171.21 - '@effect/opentelemetry': 4.0.0-beta.107 - '@effect/vitest': 4.0.0-beta.107 - effect: 4.0.0-beta.107 + '@tanstack/react-router': 1.170.33 + '@tanstack/router-core': 1.171.28 + '@effect/opentelemetry': 4.0.0-rc.112 + '@effect/vitest': 4.0.0-rc.112 + effect: 4.0.0-rc.112 node-fetch: ^3.3.2 + '@tanstack/history': 1.162.2 + msgpackr: 2.1.0 + zod: 4.5.4 allowBuilds: '@parcel/watcher': true '@swc/core': true @@ -123,16 +81,12 @@ allowBuilds: sharp: true workerd: true patchedDependencies: - effect@4.0.0-beta.107: patches/effect-schema-sentinel.patch '@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch - '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-builder@3.8.2-ultramodern.12.patch - '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-server-utils@3.8.2-ultramodern.12.patch - '@module-federation/bridge-react@2.8.0': patches/@module-federation__bridge-react@2.8.0.patch - '@module-federation/modern-js-v3@2.8.0': patches/@module-federation__modern-js-v3@2.8.0.patch - '@tanstack/router-core@1.171.21': patches/@tanstack__router-core@1.171.21.patch - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch - '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-plugin-bff@3.8.2-ultramodern.12.patch - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch '@better-fetch/fetch@1.3.1': patches/@better-fetch__fetch@1.3.1.patch - '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-code-tools@3.8.2-ultramodern.12.patch + '@module-federation/dts-plugin@2.9.0': patches/@module-federation__dts-plugin@2.9.0.patch + '@module-federation/bridge-react@2.9.0': patches/@module-federation__bridge-react@2.9.0.patch + '@module-federation/modern-js-v3@2.9.0': patches/@module-federation__modern-js-v3@2.9.0.patch + '@module-federation/runtime-core@2.9.0': patches/@module-federation__runtime-core@2.9.0.patch + msgpackr@2.1.0: patches/msgpackr@2.1.0.patch + zod@4.5.4: patches/zod@4.5.4.patch drizzle-orm@1.0.0-rc.5-ab785fc: patches/drizzle-orm-rc5-declarations.patch diff --git a/app/scripts/audit-database-trust-boundaries.mts b/app/scripts/audit-database-trust-boundaries.mts index b4a7358ea..fb06889e0 100644 --- a/app/scripts/audit-database-trust-boundaries.mts +++ b/app/scripts/audit-database-trust-boundaries.mts @@ -1,8 +1,18 @@ #!/usr/bin/env node -import { NodeServices } from '@effect/platform-node'; import { pathToFileURL } from 'node:url'; + +import { NodeServices } from '@effect/platform-node'; +import { + Config, + Console, + Effect, + Exit, + FileSystem, + Path, + Schema, +} from 'effect'; import { Client } from 'pg'; -import { Config, Console, Effect, Exit, FileSystem, Path, Schema } from 'effect'; + import { loadDatabaseConnectionPair } from '../packages/core-runtime/src/db/config.ts'; import { collectSnapshot } from './database-trust-audit/collect-snapshot.mts'; import { @@ -43,12 +53,17 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< Effect.mapError( () => new DatabaseTrustBoundaryAuditError({ - reason: 'Administrative and runtime database configuration is unavailable', - }), - ), + reason: + 'Administrative and runtime database configuration is unavailable', + }) + ) ); - const admin = new Client({ connectionString: connections.admin.connectionString }); - const runtime = new Client({ connectionString: connections.runtime.connectionString }); + const admin = new Client({ + connectionString: connections.admin.connectionString, + }); + const runtime = new Client({ + connectionString: connections.runtime.connectionString, + }); let adminConnected = false; let runtimeConnected = false; return yield* Effect.gen(function* collectDatabaseTrustBoundaryReport() { @@ -73,8 +88,8 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< error instanceof DatabaseSessionIdentityError ? error.message : 'Database trust-boundary evidence could not be collected', - }), - ), + }) + ) ); return buildDatabaseTrustBoundaryReport(snapshot); }).pipe( @@ -84,66 +99,81 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< ...(runtimeConnected ? [runtime.end()] : []), ...(adminConnected ? [admin.end()] : []), ]); - }), - ), + }) + ) ); }); -const DatabaseTrustBoundaryReportJsonSchema = Schema.fromJsonString(Schema.Unknown, { space: 2 }); +const DatabaseTrustBoundaryReportJsonSchema = Schema.fromJsonString( + Schema.Unknown, + { space: 2 } +); const writeDatabaseTrustBoundaryReport = Effect.gen( function* writeDatabaseTrustBoundaryReportEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const defaultWorkspaceRoot = path.resolve(import.meta.dirname, '..'); - const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( + const workspaceRoot = yield* Config.string( + 'ULTRAMODERN_WORKSPACE_ROOT' + ).pipe( Config.withDefault(defaultWorkspaceRoot), Effect.mapError( () => new DatabaseTrustBoundaryAuditError({ reason: genericAuditFailureMessage, - }), - ), + }) + ) ); - const output = path.join(workspaceRoot, '.codex/reports/database/database-trust-boundary.json'); - const report = yield* auditDatabaseTrustBoundaries(); - const reportJson = yield* Schema.encodeEffect(DatabaseTrustBoundaryReportJsonSchema)( - report, - ).pipe( - Effect.mapError( - () => - new DatabaseTrustBoundaryAuditError({ - reason: genericAuditFailureMessage, - }), - ), + const output = path.join( + workspaceRoot, + '.codex/reports/database/database-trust-boundary.json' ); - yield* fileSystem.makeDirectory(path.dirname(output), { recursive: true }).pipe( + const report = yield* auditDatabaseTrustBoundaries(); + const reportJson = yield* Schema.encodeEffect( + DatabaseTrustBoundaryReportJsonSchema + )(report).pipe( Effect.mapError( () => new DatabaseTrustBoundaryAuditError({ reason: genericAuditFailureMessage, - }), - ), + }) + ) ); + yield* fileSystem + .makeDirectory(path.dirname(output), { recursive: true }) + .pipe( + Effect.mapError( + () => + new DatabaseTrustBoundaryAuditError({ + reason: genericAuditFailureMessage, + }) + ) + ); yield* fileSystem.writeFileString(output, `${reportJson}\n`).pipe( Effect.mapError( () => new DatabaseTrustBoundaryAuditError({ reason: genericAuditFailureMessage, - }), - ), + }) + ) ); yield* Console.log( - `Database trust-boundary evidence written with ${report.findings.length} finding(s).`, + `Database trust-boundary evidence written with ${report.findings.length} finding(s).` ); - }, -).pipe(Effect.tapCause((cause) => Console.error(getDatabaseTrustBoundaryFailureMessage(cause)))); + } +).pipe( + Effect.tapCause((cause) => + Console.error(getDatabaseTrustBoundaryFailureMessage(cause)) + ) +); const isMain = - process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href; + process.argv[1] !== undefined && + import.meta.url === pathToFileURL(process.argv[1]).href; if (isMain) { const exit = await Effect.runPromiseExit( - writeDatabaseTrustBoundaryReport.pipe(Effect.provide(NodeServices.layer)), + writeDatabaseTrustBoundaryReport.pipe(Effect.provide(NodeServices.layer)) ); process.exitCode = Exit.match(exit, { onFailure: () => 1, diff --git a/app/scripts/authorization/rollout-contract.mts b/app/scripts/authorization/rollout-contract.mts index 0d76d3c67..9ca9eabed 100644 --- a/app/scripts/authorization/rollout-contract.mts +++ b/app/scripts/authorization/rollout-contract.mts @@ -132,7 +132,10 @@ const validateDecodedContract = ( contract, context, ); - return yield* encodeContract({ ...contract, compatibilityEligibleEntrypoints }); + return yield* encodeContract({ + ...contract, + compatibilityEligibleEntrypoints, + }); }); const decodeContract = ( diff --git a/app/scripts/check-database-access-boundaries.mts b/app/scripts/check-database-access-boundaries.mts index a019935fd..ab855e231 100644 --- a/app/scripts/check-database-access-boundaries.mts +++ b/app/scripts/check-database-access-boundaries.mts @@ -447,7 +447,9 @@ const main = Effect.gen(function* databaseAccessBoundaryMain() { { concurrency: 1, discard: true }, ); return yield* Effect.fail( - new DatabaseAccessBoundaryCheckFailed({ violationCount: violations.length }), + new DatabaseAccessBoundaryCheckFailed({ + violationCount: violations.length, + }), ); } return yield* Console.log('Database access boundaries verified'); diff --git a/app/scripts/check-module-entrypoint-boundaries.mts b/app/scripts/check-module-entrypoint-boundaries.mts index b78840594..39585fcb7 100644 --- a/app/scripts/check-module-entrypoint-boundaries.mts +++ b/app/scripts/check-module-entrypoint-boundaries.mts @@ -17,6 +17,7 @@ import { } from 'effect'; import type { PlatformError } from 'effect/PlatformError'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; + import { gatewayContextAuthorizationEntrypoints, shellGatewayContextContract, @@ -31,6 +32,10 @@ import type { InventoryAuthorization, ProtectedEntrypointInventoryEntry, } from './authorization/protected-entrypoint-inventory.mts'; +import { + hasCompleteGeneratedModuleApiSeam, + hasGeneratedGovernedServerContract, +} from './generated-governed-http-boundary.mts'; import { toPascalCase, generatedApiGroup, @@ -45,10 +50,6 @@ import { hasGeneratedProviderReadContract, hasGeneratedProviderRegistration, } from './generated-module-api-boundary.mts'; -import { - hasCompleteGeneratedModuleApiSeam, - hasGeneratedGovernedServerContract, -} from './generated-governed-http-boundary.mts'; const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']); const ACTION_EXTENSION = '.action.ts'; diff --git a/app/scripts/check-ontos-module-contracts.mts b/app/scripts/check-ontos-module-contracts.mts index 6b67a1dbd..d2c3c9d6f 100644 --- a/app/scripts/check-ontos-module-contracts.mts +++ b/app/scripts/check-ontos-module-contracts.mts @@ -1,8 +1,11 @@ #!/usr/bin/env node import path from 'node:path'; import { pathToFileURL } from 'node:url'; + import { NodeFileSystem, NodeRuntime } from '@effect/platform-node'; import { Effect, Equal, FileSystem, Layer, Schema } from 'effect'; +import type { PlatformError } from 'effect/PlatformError'; + import { ONTOS_MODULE_CONTRACT_MAX_BYTES, ONTOS_MODULE_CONTRACT_PATH, @@ -16,7 +19,6 @@ import type { OntosModuleDeploymentContract, } from '../packages/core-runtime/src/index.ts'; import { deriveOntosModuleDeploymentContract } from './generate-ontos-module-contract.mts'; -import type { PlatformError } from 'effect/PlatformError'; import { MODULE_CONTRACT_GENERATOR_HEADER, MODULE_MANIFEST_ACTION_SLOT_END, diff --git a/app/scripts/check-ultramodern-i18n-boundaries.mts b/app/scripts/check-ultramodern-i18n-boundaries.mts index 5d8b92667..400fab87b 100644 --- a/app/scripts/check-ultramodern-i18n-boundaries.mts +++ b/app/scripts/check-ultramodern-i18n-boundaries.mts @@ -1,5 +1,6 @@ #!/usr/bin/env node import path from 'node:path'; + import { runWorkspaceSourceCheck } from '@modern-js/code-tools'; const root = path.resolve(import.meta.dirname, '..'); diff --git a/app/scripts/database-trust-audit/collect-snapshot.mts b/app/scripts/database-trust-audit/collect-snapshot.mts index 48ac14572..aa7a4ee52 100644 --- a/app/scripts/database-trust-audit/collect-snapshot.mts +++ b/app/scripts/database-trust-audit/collect-snapshot.mts @@ -1,5 +1,6 @@ -import type { Client, ClientBase, QueryResult, QueryResultRow } from 'pg'; import { Effect, Schema } from 'effect'; +import type { Client, ClientBase, QueryResult, QueryResultRow } from 'pg'; + import { assertDatabaseSessionIdentities, assertSameDatabaseTarget, @@ -81,7 +82,12 @@ interface TablePrivilegeRow { readonly delete: boolean; readonly insert: boolean; readonly insertable: boolean; - readonly kind: 'foreign-table' | 'materialized-view' | 'partitioned-table' | 'table' | 'view'; + readonly kind: + | 'foreign-table' + | 'materialized-view' + | 'partitioned-table' + | 'table' + | 'view'; readonly maintain: boolean; readonly owner: string; readonly owner_bypass_rls: boolean; @@ -108,7 +114,13 @@ interface ParameterPrivilegeRow { } interface TypePrivilegeRow { - readonly kind: 'base' | 'composite' | 'domain' | 'enum' | 'multirange' | 'range'; + readonly kind: + | 'base' + | 'composite' + | 'domain' + | 'enum' + | 'multirange' + | 'range'; readonly owner: string; readonly schema: string; readonly type: string; @@ -152,20 +164,24 @@ class DatabaseTrustBoundarySnapshotError extends Schema.TaggedError; -type DatabaseTargetMismatchFailure = InstanceType; +type DatabaseSessionIdentityFailure = InstanceType< + typeof DatabaseSessionIdentityError +>; +type DatabaseTargetMismatchFailure = InstanceType< + typeof DatabaseTargetMismatchError +>; const query = ( client: ClientBase, statement: string, - values: unknown[] = [], + values: unknown[] = [] ): Effect.Effect, DatabaseTrustBoundarySnapshotError> => Effect.tryPromise({ catch: () => @@ -182,21 +198,25 @@ export const hasTrustedContextValue = (value: string | null): boolean => const probeSettingEffect = Effect.fn('probeSetting')(function* probeSetting( client: ClientBase, setting: 'ontos.legal_entity_id' | 'ontos.tenant_id', - value: string, + value: string ) { yield* query(client, 'begin'); const settable = yield* Effect.gen(function* probeTrustedContextSetting() { yield* query(client, 'select set_config($1, $2, true)', [setting, value]); - const current = yield* query(client, 'select current_setting($1, true) as value', [ - setting, - ]); + const current = yield* query( + client, + 'select current_setting($1, true) as value', + [setting] + ); const [currentRow] = current.rows; return currentRow?.value === value; }).pipe(Effect.catch(() => Effect.succeed(false))); yield* query(client, 'rollback'); - const after = yield* query(client, 'select current_setting($1, true) as value', [ - setting, - ]); + const after = yield* query( + client, + 'select current_setting($1, true) as value', + [setting] + ); const [afterRow] = after.rows; return { retainedAfterRollback: hasTrustedContextValue(afterRow?.value ?? null), @@ -204,85 +224,92 @@ const probeSettingEffect = Effect.fn('probeSetting')(function* probeSetting( }; }); -export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSnapshotEffect( - admin: Client, - runtime: Client, -): Effect.fn.Return< - DatabaseTrustBoundarySnapshot, - | DatabaseSessionIdentityFailure - | DatabaseTargetMismatchFailure - | DatabaseTrustBoundarySnapshotError -> { - const targetQuery = `select +export const collectSnapshot = Effect.fn('collectSnapshot')( + function* collectSnapshotEffect( + admin: Client, + runtime: Client + ): Effect.fn.Return< + DatabaseTrustBoundarySnapshot, + | DatabaseSessionIdentityFailure + | DatabaseTargetMismatchFailure + | DatabaseTrustBoundarySnapshotError + > { + const targetQuery = `select current_user::text as current_role, current_database() as database, session_user::text as session_role, inet_server_addr()::text as server_address, inet_server_port() as server_port`; - const [administrativeTarget, runtimeTarget] = yield* Effect.all( - [query(admin, targetQuery), query(runtime, targetQuery)], - { concurrency: 'unbounded' }, - ); - const [administrativeTargetRow] = administrativeTarget.rows; - const [runtimeTargetRow] = runtimeTarget.rows; - if (administrativeTargetRow === undefined || runtimeTargetRow === undefined) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'database_target_identity_unavailable', - reason: 'database target identity is unavailable', + const [administrativeTarget, runtimeTarget] = yield* Effect.all( + [ + query(admin, targetQuery), + query(runtime, targetQuery), + ], + { concurrency: 'unbounded' } + ); + const [administrativeTargetRow] = administrativeTarget.rows; + const [runtimeTargetRow] = runtimeTarget.rows; + if ( + administrativeTargetRow === undefined || + runtimeTargetRow === undefined + ) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'database_target_identity_unavailable', + reason: 'database target identity is unavailable', + }); + } + const administrativeEndpoint = getEffectiveDatabaseEndpoint(admin); + const runtimeEndpoint = getEffectiveDatabaseEndpoint(runtime); + yield* Effect.try({ + catch: (cause) => + cause instanceof DatabaseTargetMismatchError + ? cause + : new DatabaseTrustBoundarySnapshotError({ + code: 'database_target_identity_unavailable', + reason: 'database target identity is unavailable', + }), + try: () => + assertSameDatabaseTarget( + { + ...administrativeEndpoint, + database: administrativeTargetRow.database, + serverAddress: administrativeTargetRow.server_address, + serverPort: administrativeTargetRow.server_port, + }, + { + ...runtimeEndpoint, + database: runtimeTargetRow.database, + serverAddress: runtimeTargetRow.server_address, + serverPort: runtimeTargetRow.server_port, + } + ), }); - } - const administrativeEndpoint = getEffectiveDatabaseEndpoint(admin); - const runtimeEndpoint = getEffectiveDatabaseEndpoint(runtime); - yield* Effect.try({ - catch: (cause) => - cause instanceof DatabaseTargetMismatchError - ? cause - : new DatabaseTrustBoundarySnapshotError({ - code: 'database_target_identity_unavailable', - reason: 'database target identity is unavailable', - }), - try: () => - assertSameDatabaseTarget( - { - ...administrativeEndpoint, - database: administrativeTargetRow.database, - serverAddress: administrativeTargetRow.server_address, - serverPort: administrativeTargetRow.server_port, - }, - { - ...runtimeEndpoint, - database: runtimeTargetRow.database, - serverAddress: runtimeTargetRow.server_address, - serverPort: runtimeTargetRow.server_port, - }, - ), - }); - yield* Effect.try({ - catch: (cause) => - cause instanceof DatabaseSessionIdentityError - ? cause - : new DatabaseTrustBoundarySnapshotError({ - code: 'database_session_identity_unavailable', - reason: 'database session identity is unavailable', - }), - try: () => - assertDatabaseSessionIdentities( - { - currentRole: administrativeTargetRow.current_role, - sessionRole: administrativeTargetRow.session_role, - }, - { - currentRole: runtimeTargetRow.current_role, - sessionRole: runtimeTargetRow.session_role, - }, - ), - }); - const administrativeRole = administrativeTargetRow.session_role; - const runtimeRole = runtimeTargetRow.session_role; + yield* Effect.try({ + catch: (cause) => + cause instanceof DatabaseSessionIdentityError + ? cause + : new DatabaseTrustBoundarySnapshotError({ + code: 'database_session_identity_unavailable', + reason: 'database session identity is unavailable', + }), + try: () => + assertDatabaseSessionIdentities( + { + currentRole: administrativeTargetRow.current_role, + sessionRole: administrativeTargetRow.session_role, + }, + { + currentRole: runtimeTargetRow.current_role, + sessionRole: runtimeTargetRow.session_role, + } + ), + }); + const administrativeRole = administrativeTargetRow.session_role; + const runtimeRole = runtimeTargetRow.session_role; - const role = yield* query( - admin, - `select + const role = yield* query( + admin, + `select rolbypassrls as bypass_rls, rolcreatedb as can_create_databases, rolcreaterole as can_create_roles, @@ -293,19 +320,19 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna rolsuper as superuser from pg_catalog.pg_roles where rolname = $1`, - [runtimeRole], - ); - const [roleRow] = role.rows; - if (roleRow === undefined) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'runtime_role_absent', - reason: 'runtime role is absent', - }); - } + [runtimeRole] + ); + const [roleRow] = role.rows; + if (roleRow === undefined) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'runtime_role_absent', + reason: 'runtime role is absent', + }); + } - const memberships = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + const memberships = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -460,28 +487,28 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna or candidate.oid in (select role_oid from reachable_roles) ) order by candidate.rolname`, - [runtimeRole], - ); - const database = yield* query( - admin, - `select + [runtimeRole] + ); + const database = yield* query( + admin, + `select current_database() as database, has_database_privilege($1, current_database(), 'CONNECT') as connect, has_database_privilege($1, current_database(), 'CREATE') as create, has_database_privilege($1, current_database(), 'TEMPORARY') as temporary`, - [runtimeRole], - ); - const [databaseRow] = database.rows; - if (databaseRow === undefined) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'database_privilege_unavailable', - reason: 'database privilege row is absent', - }); - } + [runtimeRole] + ); + const [databaseRow] = database.rows; + if (databaseRow === undefined) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'database_privilege_unavailable', + reason: 'database privilege row is absent', + }); + } - const schemas = yield* query( - admin, - `select + const schemas = yield* query( + admin, + `select namespace.nspname as schema, owner.rolname as owner, has_schema_privilege($1, namespace.oid, 'USAGE') as usage, @@ -491,12 +518,12 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna where namespace.nspname !~ '^pg_' and namespace.nspname <> 'information_schema' order by namespace.nspname`, - [runtimeRole], - ); - const schemaNames = schemas.rows.map(({ schema }) => schema); - const routines = yield* query( - admin, - `select + [runtimeRole] + ); + const schemaNames = schemas.rows.map(({ schema }) => schema); + const routines = yield* query( + admin, + `select namespace.nspname as schema, routine.proname as routine, pg_get_function_identity_arguments(routine.oid) as identity_arguments, @@ -517,11 +544,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna join pg_catalog.pg_roles as owner on owner.oid = routine.proowner where namespace.nspname = any($2::text[]) order by namespace.nspname, routine.proname, routine.oid`, - [runtimeRole, schemaNames], - ); - const tables = yield* query( - admin, - `with recursive view_dependencies(view_oid, referenced_oid, effective_owner_oid) as ( + [runtimeRole, schemaNames] + ); + const tables = yield* query( + admin, + `with recursive view_dependencies(view_oid, referenced_oid, effective_owner_oid) as ( select rewrite.ev_class, dependency.refobjid, @@ -680,11 +707,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna where relation.relkind in ('r', 'p', 'v', 'm', 'f') and namespace.nspname = any($2::text[]) order by namespace.nspname, relation.relname`, - [runtimeRole, schemaNames, administrativeRole], - ); - const types = yield* query( - admin, - `select + [runtimeRole, schemaNames, administrativeRole] + ); + const types = yield* query( + admin, + `select namespace.nspname as schema, audited_type.typname as type, case audited_type.typtype @@ -714,11 +741,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna ) and namespace.nspname = any($1::text[]) order by namespace.nspname, audited_type.typname`, - [schemaNames], - ); - const sequences = yield* query( - admin, - `select + [schemaNames] + ); + const sequences = yield* query( + admin, + `select namespace.nspname as schema, relation.relname as sequence, owner.rolname as owner, @@ -739,11 +766,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna join pg_catalog.pg_roles as owner on owner.oid = relation.relowner where relation.relkind = 'S' and namespace.nspname = any($2::text[]) order by namespace.nspname, relation.relname`, - [runtimeRole, schemaNames], - ); - const parameterPrivileges = yield* query( - admin, - `select + [runtimeRole, schemaNames] + ); + const parameterPrivileges = yield* query( + admin, + `select parameter.parname as parameter, has_parameter_privilege($1, parameter.parname, 'ALTER SYSTEM') as alter_system, has_parameter_privilege($1, parameter.parname, 'SET') as set @@ -751,11 +778,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna where has_parameter_privilege($1, parameter.parname, 'ALTER SYSTEM') or has_parameter_privilege($1, parameter.parname, 'SET') order by parameter.parname`, - [runtimeRole], - ); - const grantOptions = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + [runtimeRole] + ); + const grantOptions = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -940,11 +967,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna ) ) as authority order by target.role_name, authority.grant_option`, - [runtimeRole, schemaNames], - ); - const defaultPrivileges = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + [runtimeRole, schemaNames] + ); + const defaultPrivileges = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -1068,131 +1095,133 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna privilege, source, grantable`, - [runtimeRole, schemaNames, administrativeRole], - ); - const tenant = yield* probeSettingEffect( - runtime, - 'ontos.tenant_id', - '00000000-0000-4000-8000-000000000001', - ); - const legalEntity = yield* probeSettingEffect( - runtime, - 'ontos.legal_entity_id', - '00000000-0000-4000-8000-000000000002', - ); + [runtimeRole, schemaNames, administrativeRole] + ); + const tenant = yield* probeSettingEffect( + runtime, + 'ontos.tenant_id', + '00000000-0000-4000-8000-000000000001' + ); + const legalEntity = yield* probeSettingEffect( + runtime, + 'ontos.legal_entity_id', + '00000000-0000-4000-8000-000000000002' + ); - return { - administrativeRole, - database: databaseRow.database, - databasePrivileges: { - connect: databaseRow.connect, - create: databaseRow.create, - temporary: databaseRow.temporary, - }, - defaultPrivileges: defaultPrivileges.rows.map((privilege) => ({ - grantable: privilege.grantable, - grantee: privilege.grantee, - objectType: privilege.object_type, - owner: privilege.owner, - privilege: privilege.privilege, - schema: privilege.schema, - source: privilege.source, - })), - grantOptions: grantOptions.rows.map(({ grant_option }) => grant_option), - memberships: memberships.rows.map((membership) => ({ - attributes: { - bypassRls: membership.bypass_rls, - canCreateDatabases: membership.can_create_databases, - canCreateRoles: membership.can_create_roles, - canLogin: membership.can_login, - inherit: membership.inherit, - replication: membership.replication, - superuser: membership.superuser, + return { + administrativeRole, + database: databaseRow.database, + databasePrivileges: { + connect: databaseRow.connect, + create: databaseRow.create, + temporary: databaseRow.temporary, }, - canAdministerRole: membership.can_administer_role, - canInheritRole: membership.can_inherit_role, - canSetRole: membership.can_set_role, - createSchemas: membership.create_schemas, - databaseCreate: membership.database_create, - ownedRelations: membership.owned_relations, - ownedRoutines: membership.owned_routines, - ownedSchemas: membership.owned_schemas, - ownedTypes: membership.owned_types, - parameterPrivileges: membership.parameter_privileges, - predefinedRole: membership.predefined_role, - relationPrivilegeSchemas: membership.relation_privilege_schemas, - role: membership.role, - securityDefinerRoutines: membership.security_definer_routines, - })), - parameterPrivileges: parameterPrivileges.rows.map((privilege) => ({ - alterSystem: privilege.alter_system, - parameter: privilege.parameter, - set: privilege.set, - })), - role: { - bypassRls: roleRow.bypass_rls, - canCreateDatabases: roleRow.can_create_databases, - canCreateRoles: roleRow.can_create_roles, - canLogin: roleRow.can_login, - inherit: roleRow.inherit, - predefinedRole: roleRow.predefined_role, - replication: roleRow.replication, - superuser: roleRow.superuser, - }, - routines: routines.rows.map((routine) => ({ - executable: routine.executable, - identityArguments: routine.identity_arguments, - kind: routine.kind, - owner: routine.owner, - routine: routine.routine, - schema: routine.schema, - securityDefiner: routine.security_definer, - })), - runtimeRole, - schemas: schemas.rows, - sequences: sequences.rows.map((sequence) => ({ - owner: sequence.owner, - privileges: { - select: sequence.select, - update: sequence.update, - usage: sequence.usage, + defaultPrivileges: defaultPrivileges.rows.map((privilege) => ({ + grantable: privilege.grantable, + grantee: privilege.grantee, + objectType: privilege.object_type, + owner: privilege.owner, + privilege: privilege.privilege, + schema: privilege.schema, + source: privilege.source, + })), + grantOptions: grantOptions.rows.map(({ grant_option }) => grant_option), + memberships: memberships.rows.map((membership) => ({ + attributes: { + bypassRls: membership.bypass_rls, + canCreateDatabases: membership.can_create_databases, + canCreateRoles: membership.can_create_roles, + canLogin: membership.can_login, + inherit: membership.inherit, + replication: membership.replication, + superuser: membership.superuser, + }, + canAdministerRole: membership.can_administer_role, + canInheritRole: membership.can_inherit_role, + canSetRole: membership.can_set_role, + createSchemas: membership.create_schemas, + databaseCreate: membership.database_create, + ownedRelations: membership.owned_relations, + ownedRoutines: membership.owned_routines, + ownedSchemas: membership.owned_schemas, + ownedTypes: membership.owned_types, + parameterPrivileges: membership.parameter_privileges, + predefinedRole: membership.predefined_role, + relationPrivilegeSchemas: membership.relation_privilege_schemas, + role: membership.role, + securityDefinerRoutines: membership.security_definer_routines, + })), + parameterPrivileges: parameterPrivileges.rows.map((privilege) => ({ + alterSystem: privilege.alter_system, + parameter: privilege.parameter, + set: privilege.set, + })), + role: { + bypassRls: roleRow.bypass_rls, + canCreateDatabases: roleRow.can_create_databases, + canCreateRoles: roleRow.can_create_roles, + canLogin: roleRow.can_login, + inherit: roleRow.inherit, + predefinedRole: roleRow.predefined_role, + replication: roleRow.replication, + superuser: roleRow.superuser, }, - schema: sequence.schema, - sequence: sequence.sequence, - })), - tables: tables.rows.map((table) => ({ - deletable: table.deletable, - insertable: table.insertable, - kind: table.kind, - owner: table.owner, - ownerBypassRls: table.owner_bypass_rls, - ownerContextPrivileged: table.owner_context_privileged, - ownerContextRlsBypass: table.owner_context_rls_bypass, - ownerSuperuser: table.owner_superuser, - privileges: { - delete: table.delete, - insert: table.insert, - maintain: table.maintain, - references: table.references, - select: table.select, - trigger: table.trigger, - truncate: table.truncate, - update: table.update, + routines: routines.rows.map((routine) => ({ + executable: routine.executable, + identityArguments: routine.identity_arguments, + kind: routine.kind, + owner: routine.owner, + routine: routine.routine, + schema: routine.schema, + securityDefiner: routine.security_definer, + })), + runtimeRole, + schemas: schemas.rows, + sequences: sequences.rows.map((sequence) => ({ + owner: sequence.owner, + privileges: { + select: sequence.select, + update: sequence.update, + usage: sequence.usage, + }, + schema: sequence.schema, + sequence: sequence.sequence, + })), + tables: tables.rows.map((table) => ({ + deletable: table.deletable, + insertable: table.insertable, + kind: table.kind, + owner: table.owner, + ownerBypassRls: table.owner_bypass_rls, + ownerContextPrivileged: table.owner_context_privileged, + ownerContextRlsBypass: table.owner_context_rls_bypass, + ownerSuperuser: table.owner_superuser, + privileges: { + delete: table.delete, + insert: table.insert, + maintain: table.maintain, + references: table.references, + select: table.select, + trigger: table.trigger, + truncate: table.truncate, + update: table.update, + }, + rlsEnabled: table.rls_enabled, + rlsForced: table.rls_forced, + schema: table.schema, + securityInvoker: table.security_invoker, + table: table.table, + updatable: table.updatable, + })), + trustedContext: { + legalEntitySettingRetainedAfterRollback: + legalEntity.retainedAfterRollback, + legalEntitySettingSettable: legalEntity.settable, + tenantSettingRetainedAfterRollback: tenant.retainedAfterRollback, + tenantSettingSettable: tenant.settable, + transactionLocal: true, }, - rlsEnabled: table.rls_enabled, - rlsForced: table.rls_forced, - schema: table.schema, - securityInvoker: table.security_invoker, - table: table.table, - updatable: table.updatable, - })), - trustedContext: { - legalEntitySettingRetainedAfterRollback: legalEntity.retainedAfterRollback, - legalEntitySettingSettable: legalEntity.settable, - tenantSettingRetainedAfterRollback: tenant.retainedAfterRollback, - tenantSettingSettable: tenant.settable, - transactionLocal: true, - }, - types: types.rows, - }; -}); + types: types.rows, + }; + } +); diff --git a/app/scripts/database-trust-audit/report.mts b/app/scripts/database-trust-audit/report.mts index a1473b3ba..d7e0fb2ed 100644 --- a/app/scripts/database-trust-audit/report.mts +++ b/app/scripts/database-trust-audit/report.mts @@ -1,5 +1,5 @@ -import type { Client } from 'pg'; import { Cause, Option, Schema } from 'effect'; +import type { Client } from 'pg'; interface DatabasePrivileges { readonly connect: boolean; diff --git a/app/scripts/ensure-local-environment.mts b/app/scripts/ensure-local-environment.mts index 1053881d2..3aed978ed 100644 --- a/app/scripts/ensure-local-environment.mts +++ b/app/scripts/ensure-local-environment.mts @@ -14,7 +14,10 @@ import { } from 'effect'; import { APP_ENV_PATH } from '../packages/core-runtime/src/environment/workspace-environment.ts'; -import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; +import { + localPublicClientValues, + localSpiceDbValues, +} from './local-environment-values.mts'; const ShellIdSchema = Schema.String.pipe(Schema.brand('ShellId')); const TopologySchema = Schema.fromJsonString( @@ -22,7 +25,7 @@ const TopologySchema = Schema.fromJsonString( shell: Schema.Struct({ id: ShellIdSchema, }), - }), + }) ); const LocalOverlaySchema = Schema.fromJsonString( Schema.Struct({ @@ -30,7 +33,7 @@ const LocalOverlaySchema = Schema.fromJsonString( 'party-registry': Schema.String, }), ports: Schema.Record(Schema.String, Schema.Number), - }), + }) ); const PublicClientTopologySchema = Schema.Struct({ partyRegistryApiBaseUrl: Schema.String, @@ -38,52 +41,60 @@ const PublicClientTopologySchema = Schema.Struct({ shellPort: Schema.Number, }); -const optionalTrimmedString = (name: string) => Config.option(Config.schema(Schema.Trim, name)); +const optionalTrimmedString = (name: string) => + Config.option(Config.schema(Schema.Trim, name)); const LocalEnvironmentOverrides = Config.all({ grpcPort: optionalTrimmedString('LOCAL_SPICEDB_GRPC_PORT'), httpPort: optionalTrimmedString('LOCAL_SPICEDB_HTTP_PORT'), preSharedKey: Config.option( Config.schema( - Schema.RedactedFromValue(Schema.Trim, { label: 'LOCAL_SPICEDB_PRESHARED_KEY' }), - 'LOCAL_SPICEDB_PRESHARED_KEY', - ), + Schema.RedactedFromValue(Schema.Trim, { + label: 'LOCAL_SPICEDB_PRESHARED_KEY', + }), + 'LOCAL_SPICEDB_PRESHARED_KEY' + ) ), }); const nonEmptyValue = (value: Option.Option): string | undefined => value.pipe( Option.filter((candidate) => candidate.length > 0), - Option.getOrUndefined, + Option.getOrUndefined ); const nonEmptyRedactedValue = ( - value: Option.Option, + value: Option.Option ): Redacted.Redacted | undefined => value.pipe( Option.filter((candidate) => Redacted.value(candidate).length > 0), - Option.getOrUndefined, + Option.getOrUndefined ); const main = Effect.gen(function* ensureLocalEnvironment() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const topologyPath = yield* path.fromFileUrl( - new URL('../topology/reference-topology.json', import.meta.url), + new URL('../topology/reference-topology.json', import.meta.url) ); const overlayPath = yield* path.fromFileUrl( - new URL('../topology/local-overlays/development.json', import.meta.url), + new URL('../topology/local-overlays/development.json', import.meta.url) ); - const [original, topologySource, overlaySource, overrides] = yield* Effect.all([ - fileSystem.readFileString(APP_ENV_PATH, 'utf-8'), - fileSystem.readFileString(topologyPath, 'utf-8'), - fileSystem.readFileString(overlayPath, 'utf-8'), - LocalEnvironmentOverrides, - ]); - const topology = yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); - const overlay = yield* Schema.decodeUnknownEffect(LocalOverlaySchema)(overlaySource); + const [original, topologySource, overlaySource, overrides] = + yield* Effect.all([ + fileSystem.readFileString(APP_ENV_PATH, 'utf-8'), + fileSystem.readFileString(topologyPath, 'utf-8'), + fileSystem.readFileString(overlayPath, 'utf-8'), + LocalEnvironmentOverrides, + ]); + const topology = + yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); + const overlay = + yield* Schema.decodeUnknownEffect(LocalOverlaySchema)(overlaySource); const lines = original.replaceAll('\r\n', '\n').split('\n'); const shellId = topology.shell.id; - const publicClientTopology = yield* Schema.decodeUnknownEffect(PublicClientTopologySchema)({ + const publicClientTopology = yield* Schema.decodeUnknownEffect( + PublicClientTopologySchema + )({ partyRegistryApiBaseUrl: overlay.apis['party-registry'], shellId, shellPort: overlay.ports[shellId], @@ -96,7 +107,7 @@ const main = Effect.gen(function* ensureLocalEnvironment() { httpPort: nonEmptyValue(overrides.httpPort), preSharedKey: nonEmptyRedactedValue(overrides.preSharedKey), }), - }), + }) ); const updated = lines.map((line) => { const match = /^(?[A-Z][A-Z0-9_]*)=/u.exec(line); @@ -120,7 +131,9 @@ const main = Effect.gen(function* ensureLocalEnvironment() { } const temporaryPath = `${APP_ENV_PATH}.tmp-${process.pid}`; - yield* fileSystem.writeFileString(temporaryPath, `${updated.join('\n')}\n`, { mode: 0o600 }); + yield* fileSystem.writeFileString(temporaryPath, `${updated.join('\n')}\n`, { + mode: 0o600, + }); yield* fileSystem.rename(temporaryPath, APP_ENV_PATH); console.log(`Updated the canonical local environment at ${APP_ENV_PATH}`); }); @@ -129,8 +142,8 @@ const NodeServicesLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer); const exit = await Effect.runPromiseExit( main.pipe( Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause))), - Effect.provide(NodeServicesLive), - ), + Effect.provide(NodeServicesLive) + ) ); if (Exit.isFailure(exit)) { process.exitCode = 1; diff --git a/app/scripts/generate-ontos-module-contract.mts b/app/scripts/generate-ontos-module-contract.mts index 2ada412f6..cdafbb181 100644 --- a/app/scripts/generate-ontos-module-contract.mts +++ b/app/scripts/generate-ontos-module-contract.mts @@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; + import { NodeServices } from '@effect/platform-node'; import { Effect, @@ -18,6 +19,7 @@ import { import { Command, Flag } from 'effect/unstable/cli'; import { HttpApi } from 'effect/unstable/httpapi'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; + import { ONTOS_MODULE_CONTRACT_MAX_BYTES, ONTOS_MODULE_CONTRACT_PATH, @@ -123,7 +125,9 @@ const ReferenceTopologyTextSchema = Schema.fromJsonString(ReferenceTopologySchem const ContractJsonTextSchema = Schema.fromJsonString(OntosModuleDeploymentContractSchema, { space: 2, }); -const JsonDocumentTextSchema = Schema.fromJsonString(Schema.Unknown, { space: 2 }); +const JsonDocumentTextSchema = Schema.fromJsonString(Schema.Unknown, { + space: 2, +}); const JsonStringTextSchema = Schema.fromJsonString(Schema.String); const canonicalSlugPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; @@ -140,8 +144,10 @@ const failure = (message: string, cause?: unknown): OntosModuleContractGeneratio new OntosModuleContractGenerationError({ cause, message }); const repositoryEsbuildPath = (): string => { - const createEntry = require.resolve('@modern-js/create'); - return require.resolve('esbuild/bin/esbuild', { paths: [path.dirname(createEntry)] }); + const createEntry = require.resolve('@modern-js/ultramodern-create'); + return require.resolve('esbuild/bin/esbuild', { + paths: [path.dirname(createEntry)], + }); }; const assertPlainTarget = (value: string, label: string, pattern: RegExp) => @@ -206,7 +212,10 @@ const loadOwnerValues = (workspaceRoot: string, verticalDirectory: string, verti const platformPath = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const temporaryDirectory = yield* fileSystem - .makeTempDirectoryScoped({ directory: verticalDirectory, prefix: '.ontos-contract-' }) + .makeTempDirectoryScoped({ + directory: verticalDirectory, + prefix: '.ontos-contract-', + }) .pipe( Effect.mapError((cause) => failure('unable to create the module contract temporary directory', cause), @@ -511,7 +520,10 @@ const generateOntosModuleContractEffect = (input: GenerateInput) => input.target, ).pipe(Effect.mapError(() => failure('target must be dist or cloudflare-dist'))); const verticalDirectory = platformPath.join(workspaceRoot, 'verticals', vertical); - const contract = yield* deriveOntosModuleDeploymentContractEffect({ vertical, workspaceRoot }); + const contract = yield* deriveOntosModuleDeploymentContractEffect({ + vertical, + workspaceRoot, + }); const encodedContract = yield* Schema.encodeEffect(ContractJsonTextSchema)(contract).pipe( Effect.mapError((cause) => failure('unable to encode the OntOS module contract', cause)), ); @@ -564,12 +576,11 @@ const generateOntosModuleContractEffect = (input: GenerateInput) => return { bytes, etag, path: outputPath }; }); -export const generateOntosModuleContract: ( - input: GenerateInput, -) => Promise<{ readonly bytes: number; readonly etag: string; readonly path: string }> = flow( - generateOntosModuleContractEffect, - moduleContractRuntime.runPromise, -); +export const generateOntosModuleContract: (input: GenerateInput) => Promise<{ + readonly bytes: number; + readonly etag: string; + readonly path: string; +}> = flow(generateOntosModuleContractEffect, moduleContractRuntime.runPromise); const verticalFlag = Flag.string('vertical'); const targetFlag = Flag.choice('target', ['cloudflare-dist', 'dist']); diff --git a/app/scripts/generate-outbox-worker-deployment.mjs b/app/scripts/generate-outbox-worker-deployment.mjs index a8e71fd69..12fb9d605 100644 --- a/app/scripts/generate-outbox-worker-deployment.mjs +++ b/app/scripts/generate-outbox-worker-deployment.mjs @@ -1,6 +1,7 @@ import { NodeServices } from '@effect/platform-node'; import { Effect, FileSystem, ManagedRuntime, Path, Schema } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; + import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; const TopologySchema = Schema.fromJsonString( @@ -11,9 +12,9 @@ const TopologySchema = Schema.fromJsonString( moduleFederation: Schema.Struct({ manifestUrl: Schema.String }), package: Schema.String, path: Schema.String, - }), + }) ), - }), + }) ); class OutboxWorkerDeploymentError extends Error { @@ -39,18 +40,21 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const topologySource = yield* fs.readFileString( - path.join(root, 'topology/reference-topology.json'), + path.join(root, 'topology/reference-topology.json') ); - const topology = yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); + const topology = + yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); let result = source.replace( /\n {2}# [\s\S]*? {2}# <\/generated-outbox-worker-deployments>\n?/u, - '\n', + '\n' ); /** @type {string[]} */ const services = []; for (const vertical of topology.verticals) { const delivery = yield* outboxWorkerDelivery(root, vertical).pipe( - Effect.mapError(() => failure(`Invalid generated worker delivery for ${vertical.id}`)), + Effect.mapError(() => + failure(`Invalid generated worker delivery for ${vertical.id}`) + ) ); if (delivery === undefined) { continue; @@ -59,15 +63,21 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => .split(/(?=^ {2}- setup:)/mu) .find((section) => section.startsWith(` - setup: '${vertical.id}'\n`)); if (ownerSection === undefined) { - return yield* Effect.fail(failure(`Missing owner deployment for ${vertical.id}`)); + return yield* Effect.fail( + failure(`Missing owner deployment for ${vertical.id}`) + ); } - const port = /^ {8}PORT: '(?[0-9]+)'$/mu.exec(ownerSection)?.groups?.port; + const port = /^ {8}PORT: '(?[0-9]+)'$/mu.exec(ownerSection)?.groups + ?.port; const topologyPort = yield* Effect.try({ - catch: () => failure(`Invalid topology manifest URL for ${vertical.id}`), + catch: () => + failure(`Invalid topology manifest URL for ${vertical.id}`), try: () => new URL(vertical.moduleFederation.manifestUrl).port, }); if (port === undefined || port.length === 0 || port !== topologyPort) { - return yield* Effect.fail(failure(`Owner port disagrees with topology for ${vertical.id}`)); + return yield* Effect.fail( + failure(`Owner port disagrees with topology for ${vertical.id}`) + ); } const service = ownerSection .trimEnd() @@ -78,26 +88,29 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => (line) => !line.includes(' run build') && !line.includes("- cp 'app/topology/") && - !line.includes('VERTICAL_'), + !line.includes('VERTICAL_') ) .map((line) => line.includes('run zerops:materialize') ? line.replace( 'cd app && ', - 'cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" ', + 'cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" ' ) - : line, + : line ) .join('\n') - .replace(/(?run zerops:materialize[^\n]*)/u, '$ --worker') + .replace( + /(?run zerops:materialize[^\n]*)/u, + '$ --worker' + ) .replaceAll(`/${vertical.id}-api/${vertical.id}/readiness`, '/ready') .replace( `ULTRAMODERN_ZEROPS_SERVICE: ${vertical.id}`, - `ULTRAMODERN_ZEROPS_SERVICE: ${delivery.id}`, + `ULTRAMODERN_ZEROPS_SERVICE: ${delivery.id}` ) .replace( ` PORT: '${port}'`, - ` PORT: '${port}'\n OUTBOX_WORKER_HEALTH_PORT: '${port}'\n DATABASE_URL: \${${vertical.id}_DATABASE_URL}`, + ` PORT: '${port}'\n OUTBOX_WORKER_HEALTH_PORT: '${port}'\n DATABASE_URL: \${${vertical.id}_DATABASE_URL}` ); services.push(service); } @@ -130,16 +143,16 @@ const runCommand = ({ write }) => } else if (source !== generated) { yield* Effect.fail( failure( - 'Worker deployment drift: run node scripts/generate-outbox-worker-deployment.mjs --write', - ), + 'Worker deployment drift: run node scripts/generate-outbox-worker-deployment.mjs --write' + ) ); } }); const command = Command.make( 'generate-outbox-worker-deployment', - { write: Flag.boolean('write') }, - runCommand, + { write: Flag.boolean('write').pipe(Flag.withDefault(false)) }, + runCommand ); /** @type {ImportMeta & { main?: boolean }} */ diff --git a/app/scripts/generate-tanstack-routes.mts b/app/scripts/generate-tanstack-routes.mts index 3388c9fed..d49b55990 100644 --- a/app/scripts/generate-tanstack-routes.mts +++ b/app/scripts/generate-tanstack-routes.mts @@ -191,10 +191,7 @@ const createLocalisedUrls = ( if (route.canonicalPath === '/') { return []; } - return EffectArray.sort( - [...new Set([route.canonicalPath, ...Object.values(route.localisedPaths)])], - Order.String, - ).map((pathname) => [pathname, route.localisedPaths]); + return [[route.canonicalPath, route.localisedPaths]]; }), ); @@ -233,7 +230,7 @@ const generateRouteMetadataManifest = ( const encodedLocalisedUrls = yield* encodeJson(sortJsonValue(localisedUrls)).pipe( Effect.mapError(() => failure(`Unable to encode localised URLs for ${appId}`)), ); - const content = `// @generated by @modern-js/create. + const content = `// @generated by @modern-js/ultramodern-create. // Author route metadata in colocated src/routes/**/route.meta.ts files. // This compatibility manifest is regenerated from route-owned metadata. @@ -275,13 +272,6 @@ const program = Effect.gen(function* generateTanstackRoutesEffect() { moduleUrl: import.meta.url, }); const { forwardedArgs, workspaceRoot } = invocation; - const generationStatus = yield* launchUltramodern(invocation); - if (generationStatus !== 0) { - yield* Console.warn( - '[ultramodern] Framework route-artifact generation failed; continuing with the repository compatibility manifest. The application build remains the authoritative route-artifact gate.', - ); - } - const ultramodernConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json'); const ultramodernConfigText = yield* fileSystem .readFileString(ultramodernConfigPath) @@ -317,6 +307,13 @@ const program = Effect.gen(function* generateTanstackRoutesEffect() { }), { concurrency: 1, discard: true }, ); + + const generationStatus = yield* launchUltramodern(invocation); + if (generationStatus !== 0) { + return yield* Effect.fail( + failure(`Framework route-artifact generation failed: exit ${generationStatus}`), + ); + } }); const reportFailure = (error: RouteGenerationError) => Console.error(error.reason); diff --git a/app/scripts/generated-module-api-boundary.mts b/app/scripts/generated-module-api-boundary.mts index 13168e04a..4e5741133 100644 --- a/app/scripts/generated-module-api-boundary.mts +++ b/app/scripts/generated-module-api-boundary.mts @@ -611,7 +611,11 @@ const exportedConsts = (tokens: readonly GovernedClientToken[]): readonly Export index + 1, tokens.length, ); - declarations.push({ end: nextExport ?? tokens.length, name, start: index }); + declarations.push({ + end: nextExport ?? tokens.length, + name, + start: index, + }); } } } diff --git a/app/scripts/install-zerops-node.sh b/app/scripts/install-zerops-node.sh index b6b416d59..6a6e7f163 100644 --- a/app/scripts/install-zerops-node.sh +++ b/app/scripts/install-zerops-node.sh @@ -1,9 +1,9 @@ #!/bin/sh set -eu -node_version='26.5.0' +node_version='26.7.0' node_archive="node-v${node_version}-linux-x64-musl.tar.gz" -node_checksum='00f1398411a4216c5a6ecaad3b825a0da5ec00e79ee8c173ab65a094d97b9ad8' +node_checksum='84fc4e29e5f86022a40bac50a28a1b9275dd1f32eebbf4db499e2573ff822124' node_root="${ZEROPS_NODE_ROOT:-${HOME:-/var/www}}" node_directory="${node_root}/.local/node-${node_version}" temporary_directory="$(mktemp -d)" diff --git a/app/scripts/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index 867a83a71..7fd78ed10 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,6 +1,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; + +import { + localPublicClientValues, + localSpiceDbValues, +} from './local-environment-values.mts'; const spiceDbGrpcPort = '50052'; const spiceDbHttpPort = '8444'; @@ -15,7 +19,7 @@ await test('preserves canonical SpiceDB values when no local override is supplie 'SPICEDB_INSECURE=true', 'SPICEDB_PRESHARED_KEY=existing-key', ], - {}, + {} ); assert.deepEqual(values, { @@ -30,7 +34,7 @@ await test('preserves canonical SpiceDB values when no local override is supplie await test('applies explicit local port overrides as one consistent endpoint', () => { const values = localSpiceDbValues( ['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], - { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort }, + { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort } ); assert.equal(values.SPICEDB_ENDPOINT, spiceDbEndpoint); @@ -48,7 +52,7 @@ await test('derives local public-client URLs from configured Shell identity/port { ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', - }, + } ); }); @@ -63,11 +67,13 @@ await test('preserves explicitly configured public-client URLs', () => { partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', shellId: 'shell-super-app', shellPort: 3020, - }, + } ), { - ONTOS_PARTY_REGISTRY_API_BASE_URL: 'https://party.example.test/party-registry-api', - ONTOS_SHELL_GATEWAY_BASE_URL: 'https://gateway.example.test/shell-super-app-api', - }, + ONTOS_PARTY_REGISTRY_API_BASE_URL: + 'https://party.example.test/party-registry-api', + ONTOS_SHELL_GATEWAY_BASE_URL: + 'https://gateway.example.test/shell-super-app-api', + } ); }); diff --git a/app/scripts/materialize-outbox-worker.mjs b/app/scripts/materialize-outbox-worker.mjs index 83b147851..5f4500ac6 100644 --- a/app/scripts/materialize-outbox-worker.mjs +++ b/app/scripts/materialize-outbox-worker.mjs @@ -1,7 +1,9 @@ import { isBuiltin } from 'node:module'; + import { NodeServices } from '@effect/platform-node'; -import { build } from 'esbuild'; import { Config, Effect, FileSystem, ManagedRuntime, Path, Schema } from 'effect'; +import { build } from 'esbuild'; + import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; const TopologySchema = Schema.fromJsonString( @@ -232,7 +234,13 @@ const materializeOutboxWorkerEffect = ({ metafile: true, outfile: path.join(runtimeDir, WORKER_ENTRY), platform: 'node', - plugins: [makeProductionDependenciesPlugin({ packages, path, workspaceRoot })], + plugins: [ + makeProductionDependenciesPlugin({ + packages, + path, + workspaceRoot, + }), + ], target: 'node26', }) ), diff --git a/app/scripts/materialize-zerops-runtime.mjs b/app/scripts/materialize-zerops-runtime.mjs index 980c660c9..8c5fe1fa4 100644 --- a/app/scripts/materialize-zerops-runtime.mjs +++ b/app/scripts/materialize-zerops-runtime.mjs @@ -640,7 +640,7 @@ const materializeCommand = Command.make( appId: Flag.string('app'), packageDir: Flag.string('package-dir'), packageName: Flag.string('package'), - worker: Flag.boolean('worker'), + worker: Flag.boolean('worker').pipe(Flag.withDefault(false)), }, ({ appId, packageDir, packageName, worker }) => Effect.gen(function* materializeCommandEffect() { diff --git a/app/scripts/microvertical-api-baseline-boundary.mts b/app/scripts/microvertical-api-baseline-boundary.mts index 6101ce929..92038fd81 100644 --- a/app/scripts/microvertical-api-baseline-boundary.mts +++ b/app/scripts/microvertical-api-baseline-boundary.mts @@ -224,13 +224,19 @@ const directCallChain = (expression: Expression | undefined): DirectCallChain | return undefined; } let current = unwrapExpression(expression); - const methods: { readonly arguments: readonly Expression[]; readonly name: string }[] = []; + const methods: { + readonly arguments: readonly Expression[]; + readonly name: string; + }[] = []; while ( isCallExpression(current) && isPropertyAccessExpression(current.expression) && !isIdentifier(current.expression.expression) ) { - methods.unshift({ arguments: current.arguments, name: current.expression.name.text }); + methods.unshift({ + arguments: current.arguments, + name: current.expression.name.text, + }); current = unwrapExpression(current.expression.expression); } if (!isCallExpression(current)) { diff --git a/app/scripts/migrate-contacts-authorization.mts b/app/scripts/migrate-contacts-authorization.mts index 3f173e6b0..5331b8b14 100644 --- a/app/scripts/migrate-contacts-authorization.mts +++ b/app/scripts/migrate-contacts-authorization.mts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { pathToFileURL } from 'node:url'; + import { v1 } from '@authzed/authzed-node'; import { NodeServices } from '@effect/platform-node'; import { @@ -14,17 +15,18 @@ import { } from 'effect'; import { Argument, Command } from 'effect/unstable/cli'; import { Pool } from 'pg'; + import { loadDatabaseConnectionPair } from '../packages/core-runtime/src/db/config.ts'; -import { - toLegalEntityAccessObjectId, - toModuleAccessObjectId, -} from '../packages/core-runtime/src/permissions/context-access.ts'; import { fullyConsistent, spiceDbClientSecurity, } from '../packages/core-runtime/src/permissions/client.ts'; import type { SpiceDbConfigValue } from '../packages/core-runtime/src/permissions/config.ts'; import { loadSpiceDbConfig } from '../packages/core-runtime/src/permissions/config.ts'; +import { + toLegalEntityAccessObjectId, + toModuleAccessObjectId, +} from '../packages/core-runtime/src/permissions/context-access.ts'; const LEGACY_MODULE_ID = 'crm.core'; const CONTACTS_MODULE_ID = 'contacts.core'; @@ -109,7 +111,11 @@ const planContactsAuthorizationContextResult = ( contacts: readonly ContactsAuthorizationRelationship[], ): Result.Result => { if (legacy.length === 0 && contacts.length === 0) { - return Result.succeed({ deleteLegacy: false, state: 'unconfigured', touchContacts: false }); + return Result.succeed({ + deleteLegacy: false, + state: 'unconfigured', + touchContacts: false, + }); } if (legacy.length === 0) { return Result.succeed({ @@ -126,7 +132,11 @@ const planContactsAuthorizationContextResult = ( ), ); } - return Result.succeed({ deleteLegacy: false, state: 'legacy_only', touchContacts: true }); + return Result.succeed({ + deleteLegacy: false, + state: 'legacy_only', + touchContacts: true, + }); } if (!sameRelationshipSet(legacy, contacts)) { return Result.fail(migrationFailure('Legacy and Contacts authorization relationships differ')); @@ -265,10 +275,18 @@ const decodeRelationship = ( matchesRelationshipSubject(relation, subjectType, 'accessor') && activePrincipalIds.has(subjectId); if (isLegalEntity) { - return Result.succeed({ relation: 'legal_entity', subjectId, subjectType: 'legal_entity' }); + return Result.succeed({ + relation: 'legal_entity', + subjectId, + subjectType: 'legal_entity', + }); } if (isAccessor) { - return Result.succeed({ relation: 'accessor', subjectId, subjectType: 'principal' }); + return Result.succeed({ + relation: 'accessor', + subjectId, + subjectType: 'principal', + }); } return Result.fail(migrationFailure(OUTSIDE_AUTHORITATIVE_CONTEXT_MESSAGE)); }; @@ -326,7 +344,10 @@ const toRelationship = ( ): v1.Relationship => v1.Relationship.create({ relation: item.relation, - resource: v1.ObjectReference.create({ objectId: resourceId, objectType: 'module_access' }), + resource: v1.ObjectReference.create({ + objectId: resourceId, + objectType: 'module_access', + }), subject: v1.SubjectReference.create({ object: v1.ObjectReference.create({ objectId: item.subjectId, @@ -377,7 +398,10 @@ const checkContactsPermission = ( objectType: 'module_access', }), subject: v1.SubjectReference.create({ - object: v1.ObjectReference.create({ objectId: principalId, objectType: 'principal' }), + object: v1.ObjectReference.create({ + objectId: principalId, + objectType: 'principal', + }), }), }), ), diff --git a/app/scripts/module-federation-bridge-boundary.mts b/app/scripts/module-federation-bridge-boundary.mts new file mode 100644 index 000000000..1c783bb82 --- /dev/null +++ b/app/scripts/module-federation-bridge-boundary.mts @@ -0,0 +1,93 @@ +import { parseSync } from 'oxc-parser'; +import type { Expression, ObjectExpression, Program } from 'oxc-parser'; + +interface RouterDependencies { + readonly dependencies?: Readonly>; + readonly devDependencies?: Readonly>; +} + +const property = (object: ObjectExpression, name: string): Expression | undefined => { + // Computed keys, spreads and duplicates can overwrite an apparently literal capability. + if (object.properties.some((entry) => entry.type === 'SpreadElement' || entry.computed)) { + return undefined; + } + const entries = object.properties.filter( + (entry) => + entry.type === 'Property' && + ((entry.key.type === 'Identifier' && entry.key.name === name) || + (entry.key.type === 'Literal' && entry.key.value === name)), + ); + const entry = entries.length === 1 ? entries[0] : undefined; + return entry?.type === 'Property' && entry.kind === 'init' && !entry.method + ? entry.value + : undefined; +}; + +const exportedConfiguration = (program: Program) => { + const exported = program.body.find((statement) => statement.type === 'ExportDefaultDeclaration'); + let config = exported?.type === 'ExportDefaultDeclaration' ? exported.declaration : undefined; + if (config?.type === 'Identifier') { + const { name } = config; + const declarations = program.body.flatMap((statement) => + statement.type === 'VariableDeclaration' && statement.kind === 'const' + ? statement.declarations.filter( + (declaration) => declaration.id.type === 'Identifier' && declaration.id.name === name, + ) + : [], + ); + config = declarations.length === 1 ? (declarations[0]?.init ?? undefined) : undefined; + } + return config; +}; + +/** Check the exported configuration, not an unexecuted decoy or obsolete always-on bridge rule. */ +export const moduleFederationBridgeViolation = ( + source: string, + manifest: RouterDependencies, +): string | undefined => { + const parsed = parseSync('module-federation.config.ts', source); + if (parsed.errors.length !== 0) { + return 'Module Federation configuration must parse.'; + } + const bindings = parsed.program.body.flatMap((statement) => + statement.type === 'ImportDeclaration' && + statement.importKind !== 'type' && + statement.source.value === '@module-federation/modern-js-v3' + ? statement.specifiers.flatMap((specifier) => + specifier.type === 'ImportSpecifier' && + specifier.importKind !== 'type' && + specifier.imported.type === 'Identifier' && + specifier.imported.name === 'createModuleFederationConfig' + ? [specifier.local.name] + : [], + ) + : [], + ); + const config = exportedConfiguration(parsed.program); + if ( + config?.type !== 'CallExpression' || + config.callee.type !== 'Identifier' || + !bindings.includes(config.callee.name) || + config.arguments.length !== 1 || + config.arguments[0]?.type !== 'ObjectExpression' + ) { + return 'Module Federation must export a literal createModuleFederationConfig call or its top-level const binding.'; + } + const bridge = property(config.arguments[0], 'bridge'); + const enabled = + bridge?.type === 'ObjectExpression' ? property(bridge, 'enableBridgeRouter') : undefined; + if (enabled?.type !== 'Literal' || (enabled.value !== true && enabled.value !== false)) { + return 'Module Federation must declare bridge.enableBridgeRouter as a boolean literal.'; + } + if ( + enabled.value && + !['react-router', 'react-router-dom'].some( + (name) => + Object.hasOwn(manifest.dependencies ?? {}, name) || + Object.hasOwn(manifest.devDependencies ?? {}, name), + ) + ) { + return 'Module Federation may enable the React bridge router only when the app declares react-router or react-router-dom.'; + } + return undefined; +}; diff --git a/app/scripts/plan-deployment-impact.mts b/app/scripts/plan-deployment-impact.mts index c6b270f1c..15baef1f0 100644 --- a/app/scripts/plan-deployment-impact.mts +++ b/app/scripts/plan-deployment-impact.mts @@ -16,7 +16,7 @@ import { } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; + import type { ProtectedEntrypointInventory } from './authorization/protected-entrypoint-inventory.mts'; import type { AuthorizationRolloutContract } from './authorization/rollout-contract.mts'; import { validateAuthorizationRolloutContract } from './authorization/rollout-contract.mts'; @@ -25,6 +25,7 @@ import type { AuthorizationReadinessEvidence, } from './check-authorization-readiness.mts'; import { hashAuthorizationEvidence } from './check-authorization-readiness.mts'; +import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; import type { AuthorizationImpactReport } from './report-fail-closed-authorization-impact.mts'; declare global { @@ -164,7 +165,10 @@ const InventoryAuthorizationSchema = Schema.Union([ Schema.Struct({ kind: Schema.Literal('public') }), Schema.Struct({ kind: Schema.Literal('authenticated_principal') }), Schema.Struct({ kind: Schema.Literal('owner_local_background') }), - Schema.Struct({ kind: Schema.Literal('context_permission'), permission: Schema.String }), + Schema.Struct({ + kind: Schema.Literal('context_permission'), + permission: Schema.String, + }), Schema.Struct({ kind: Schema.Literal('action_execution'), provisioning: Schema.Literals(['explicit', 'tenant_membership_default']), @@ -387,12 +391,20 @@ export const validateAuthorizationPromotionGate = ( if (input.environment === 'production') { fail('production authorization promotion rejects report-only configuration'); } - return { environment: input.environment, mode: rollout.mode, status: 'observing' }; + return { + environment: input.environment, + mode: rollout.mode, + status: 'observing', + }; } if (!authorizationEvidenceMatches(input, requireAuthorizationEvidence(input))) { fail('authorization promotion evidence is missing, stale, mismatched, or unresolved'); } - return { environment: input.environment, mode: rollout.mode, status: 'ready' }; + return { + environment: input.environment, + mode: rollout.mode, + status: 'ready', + }; }; const INFRASTRUCTURE_PHASES = { @@ -837,7 +849,12 @@ const makeComparison = ( } return fallbackReason === undefined ? { baseRevision: options.baseRevision, headRevision, mode } - : { baseRevision: options.baseRevision, headRevision, mode, reason: fallbackReason }; + : { + baseRevision: options.baseRevision, + headRevision, + mode, + reason: fallbackReason, + }; }; interface DeploymentImpactState { diff --git a/app/scripts/postgres/bootstrap-runtime-role.mts b/app/scripts/postgres/bootstrap-runtime-role.mts index 2242dfc86..4230c46bd 100644 --- a/app/scripts/postgres/bootstrap-runtime-role.mts +++ b/app/scripts/postgres/bootstrap-runtime-role.mts @@ -1,22 +1,25 @@ import { Effect, Exit, Redacted, Schema } from 'effect'; import { Client } from 'pg'; import type { QueryResult, QueryResultRow } from 'pg'; + import { loadDatabaseConnectionPair } from '../../packages/core-runtime/src/db/config.ts'; class RuntimeRoleBootstrapError extends Schema.TaggedError()( 'RuntimeRoleBootstrapError', { reason: Schema.String, - }, + } ) {} -const quoteLiteral = (value: string): string => `'${value.replaceAll("'", "''")}'`; -const quoteIdentifier = (value: string): string => `"${value.replaceAll('"', '""')}"`; +const quoteLiteral = (value: string): string => + `'${value.replaceAll("'", "''")}'`; +const quoteIdentifier = (value: string): string => + `"${value.replaceAll('"', '""')}"`; const query = ( client: Client, text: string, - values?: unknown[], + values?: unknown[] ): Effect.Effect, RuntimeRoleBootstrapError> => Effect.tryPromise({ catch: (cause) => @@ -27,7 +30,7 @@ const query = ( }); const connectAdmin = ( - connectionString: Redacted.Redacted, + connectionString: Redacted.Redacted ): Effect.Effect => Effect.tryPromise({ catch: (cause) => @@ -35,13 +38,17 @@ const connectAdmin = ( reason: `Unable to connect to the administrative PostgreSQL database: ${String(cause)}`, }), try: async () => { - const client = new Client({ connectionString: Redacted.value(connectionString) }); + const client = new Client({ + connectionString: Redacted.value(connectionString), + }); await client.connect(); return client; }, }); -const closeAdmin = (client: Client): Effect.Effect => +const closeAdmin = ( + client: Client +): Effect.Effect => Effect.tryPromise({ catch: (cause) => new RuntimeRoleBootstrapError({ @@ -53,23 +60,26 @@ const closeAdmin = (client: Client): Effect.Effect => Effect.gen(function* bootstrapRuntimeRoleEffect() { yield* query(client, 'begin'); const exists = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_roles where rolname = $1) as exists', - ['ontos_runtime'], + ['ontos_runtime'] ); const passwordLiteral = quoteLiteral(Redacted.value(password)); yield* query( client, exists.rows[0]?.exists ? `alter role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` - : `create role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls`, + : `create role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` + ); + yield* query( + client, + `grant connect on database ${quoteIdentifier(database)} to ontos_runtime` ); - yield* query(client, `grant connect on database ${quoteIdentifier(database)} to ontos_runtime`); yield* Effect.forEach( ['core', 'auth', 'contacts', 'party'], (schema) => @@ -77,37 +87,44 @@ const bootstrapRuntimeRole = ( const schemaExists = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_namespace where nspname = $1) as exists', - [schema], + [schema] ); if (schemaExists.rows[0]?.exists) { - yield* query(client, `grant usage on schema ${schema} to ontos_runtime`); yield* query( client, - `grant select, insert, update, delete on all tables in schema ${schema} to ontos_runtime`, + `grant usage on schema ${schema} to ontos_runtime` ); yield* query( client, - `grant usage, select on all sequences in schema ${schema} to ontos_runtime`, + `grant select, insert, update, delete on all tables in schema ${schema} to ontos_runtime` ); yield* query( client, - `alter default privileges in schema ${schema} grant select, insert, update, delete on tables to ontos_runtime`, + `grant usage, select on all sequences in schema ${schema} to ontos_runtime` ); yield* query( client, - `alter default privileges in schema ${schema} grant usage, select on sequences to ontos_runtime`, + `alter default privileges in schema ${schema} grant select, insert, update, delete on tables to ontos_runtime` + ); + yield* query( + client, + `alter default privileges in schema ${schema} grant usage, select on sequences to ontos_runtime` ); } }), - { concurrency: 1, discard: true }, + { concurrency: 1, discard: true } ); const role = yield* query<{ rolbypassrls: boolean; rolsuper: boolean }>( client, 'select rolsuper, rolbypassrls from pg_catalog.pg_roles where rolname = $1', - ['ontos_runtime'], + ['ontos_runtime'] ); const [runtimeRole] = role.rows; - if (runtimeRole === undefined || runtimeRole.rolsuper || runtimeRole.rolbypassrls) { + if ( + runtimeRole === undefined || + runtimeRole.rolsuper || + runtimeRole.rolbypassrls + ) { yield* new RuntimeRoleBootstrapError({ reason: 'Runtime role must be non-superuser and must not bypass RLS', }); @@ -121,15 +138,17 @@ const main = Effect.gen(function* mainEffect() { (failure) => new RuntimeRoleBootstrapError({ reason: failure.reason, - }), - ), + }) + ) ); const password = yield* Effect.try({ catch: (cause) => new RuntimeRoleBootstrapError({ reason: `Unable to read the runtime PostgreSQL role credentials: ${String(cause)}`, }), - try: () => new Client({ connectionString: connections.runtime.connectionString }).password, + try: () => + new Client({ connectionString: connections.runtime.connectionString }) + .password, }); if (connections.runtime.user !== 'ontos_runtime') { yield* new RuntimeRoleBootstrapError({ @@ -144,10 +163,17 @@ const main = Effect.gen(function* mainEffect() { : Redacted.make(password); yield* Effect.acquireUseRelease( connectAdmin(Redacted.make(connections.admin.connectionString)), - (client) => bootstrapRuntimeRole(client, connections.admin.database, redactedPassword), - closeAdmin, + (client) => + bootstrapRuntimeRole( + client, + connections.admin.database, + redactedPassword + ), + closeAdmin + ); + yield* Effect.sync(() => + console.log('Verified least-privilege PostgreSQL role ontos_runtime') ); - yield* Effect.sync(() => console.log('Verified least-privilege PostgreSQL role ontos_runtime')); }).pipe(Effect.tapError((failure) => Effect.logError(failure.reason))); const exit = await Effect.runPromiseExit(main); diff --git a/app/scripts/postgres/bootstrap-spicedb-database.mts b/app/scripts/postgres/bootstrap-spicedb-database.mts index 2e161a301..b92be5a77 100644 --- a/app/scripts/postgres/bootstrap-spicedb-database.mts +++ b/app/scripts/postgres/bootstrap-spicedb-database.mts @@ -1,8 +1,18 @@ import { NodeFileSystem } from '@effect/platform-node'; -import { Config, ConfigProvider, Console, Effect, Exit, Match, Redacted, Schema } from 'effect'; +import { + Config, + ConfigProvider, + Console, + Effect, + Exit, + Match, + Redacted, + Schema, +} from 'effect'; import type { FileSystem } from 'effect'; import { Client } from 'pg'; import type { QueryResult, QueryResultRow } from 'pg'; + import { APP_ENV_PATH } from '../../packages/core-runtime/src/environment/workspace-environment.ts'; import { parseSpiceDbDatabaseBootstrapConfig } from '../../packages/core-runtime/src/install/spicedb-database-config.ts'; import type { SpiceDbDatabaseBootstrapConfig } from '../../packages/core-runtime/src/install/spicedb-database-config.ts'; @@ -12,21 +22,28 @@ class SpiceDbDatabaseBootstrapError extends Schema.TaggedError - new SpiceDbDatabaseBootstrapError(cause === undefined ? { reason } : { cause, reason }); +const bootstrapFailure = ( + reason: string, + cause?: unknown +): SpiceDbDatabaseBootstrapError => + new SpiceDbDatabaseBootstrapError( + cause === undefined ? { reason } : { cause, reason } + ); -const quoteLiteral = (value: string): string => `'${value.replaceAll("'", "''")}'`; +const quoteLiteral = (value: string): string => + `'${value.replaceAll("'", "''")}'`; const query = ( client: Client, text: string, - values?: unknown[], + values?: unknown[] ): Effect.Effect, SpiceDbDatabaseBootstrapError> => Effect.tryPromise({ - catch: (cause) => bootstrapFailure('SpiceDB PostgreSQL bootstrap query failed', cause), + catch: (cause) => + bootstrapFailure('SpiceDB PostgreSQL bootstrap query failed', cause), try: async () => await client.query(text, values), }); @@ -43,35 +60,46 @@ const loadRootConfiguration = (): Effect.Effect< Effect.catchTag('PlatformError', (failure) => Match.value(failure.reason).pipe( Match.tag('NotFound', () => - Effect.succeed(ConfigProvider.fromUnknown({}, { preserveEmptyStrings: true })), + Effect.succeed( + ConfigProvider.fromUnknown({}, { preserveEmptyStrings: true }) + ) ), - Match.orElse(() => Effect.fail(failure)), - ), + Match.orElse(() => Effect.fail(failure)) + ) ), Effect.mapError((cause) => - bootstrapFailure(`Unable to load the root environment from ${APP_ENV_PATH}`, cause), - ), + bootstrapFailure( + `Unable to load the root environment from ${APP_ENV_PATH}`, + cause + ) + ) ); const provider = ConfigProvider.orElse( ConfigProvider.fromEnv({ preserveEmptyStrings: true }), - fileProvider, + fileProvider ); const [adminUrl, spiceDbUrl] = yield* Effect.all( [ Config.redacted('DATABASE_ADMIN_URL').parse(provider), Config.redacted('SPICEDB_DATABASE_URL').parse(provider), ], - { concurrency: 1 }, + { concurrency: 1 } ).pipe( Effect.mapError((cause) => - bootstrapFailure('SpiceDB PostgreSQL bootstrap configuration is invalid', cause), - ), + bootstrapFailure( + 'SpiceDB PostgreSQL bootstrap configuration is invalid', + cause + ) + ) ); return yield* Effect.try({ catch: (cause) => - bootstrapFailure('SpiceDB PostgreSQL bootstrap configuration is invalid', cause), + bootstrapFailure( + 'SpiceDB PostgreSQL bootstrap configuration is invalid', + cause + ), try: () => parseSpiceDbDatabaseBootstrapConfig({ DATABASE_ADMIN_URL: Redacted.value(adminUrl), @@ -81,41 +109,51 @@ const loadRootConfiguration = (): Effect.Effect< }); const connectAdmin = ( - connectionString: Redacted.Redacted, + connectionString: Redacted.Redacted ): Effect.Effect => Effect.tryPromise({ catch: (cause) => - bootstrapFailure('Unable to connect to the administrative PostgreSQL database', cause), + bootstrapFailure( + 'Unable to connect to the administrative PostgreSQL database', + cause + ), try: async () => { - const client = new Client({ connectionString: Redacted.value(connectionString) }); + const client = new Client({ + connectionString: Redacted.value(connectionString), + }); await client.connect(); return client; }, }); -const closeAdmin = (client: Client): Effect.Effect => +const closeAdmin = ( + client: Client +): Effect.Effect => Effect.tryPromise({ catch: (cause) => - bootstrapFailure('Unable to close the administrative PostgreSQL connection', cause), + bootstrapFailure( + 'Unable to close the administrative PostgreSQL connection', + cause + ), try: async () => await client.end(), }); const bootstrapDatabase = ( client: Client, - configuration: SpiceDbDatabaseBootstrapConfig, + configuration: SpiceDbDatabaseBootstrapConfig ): Effect.Effect => Effect.gen(function* bootstrapDatabaseEffect() { const role = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_roles where rolname = $1) as exists', - [configuration.user], + [configuration.user] ); const password = quoteLiteral(configuration.password); yield* query( client, (role.rows[0]?.exists ?? false) ? `alter role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` - : `create role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls`, + : `create role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` ); const database = yield* query<{ owner: string }>( @@ -123,12 +161,14 @@ const bootstrapDatabase = ( `select pg_catalog.pg_get_userbyid(datdba) as owner from pg_catalog.pg_database where datname = $1`, - [configuration.database], + [configuration.database] ); if (database.rows.length === 0) { yield* query(client, 'create database spicedb owner spicedb'); } else if (database.rows[0]?.owner !== configuration.user) { - yield* bootstrapFailure('Existing spicedb database must be owned by the spicedb role'); + yield* bootstrapFailure( + 'Existing spicedb database must be owned by the spicedb role' + ); } }); @@ -137,10 +177,14 @@ const main = Effect.gen(function* mainEffect() { yield* Effect.acquireUseRelease( connectAdmin(Redacted.make(configuration.adminUrl)), (client) => bootstrapDatabase(client, configuration), - closeAdmin, + closeAdmin + ); + yield* Console.log( + 'Verified least-privilege PostgreSQL database and role for SpiceDB' ); - yield* Console.log('Verified least-privilege PostgreSQL database and role for SpiceDB'); }).pipe(Effect.tapError((failure) => Console.error(failure.reason))); -const exit = await Effect.runPromiseExit(Effect.provide(main, NodeFileSystem.layer)); +const exit = await Effect.runPromiseExit( + Effect.provide(main, NodeFileSystem.layer) +); process.exitCode = Exit.isFailure(exit) ? 1 : 0; diff --git a/app/scripts/proof-workerd-ssr.mts b/app/scripts/proof-workerd-ssr.mts index 930fad823..6c15e65bb 100644 --- a/app/scripts/proof-workerd-ssr.mts +++ b/app/scripts/proof-workerd-ssr.mts @@ -126,7 +126,19 @@ const CompactConfigSchema = Schema.Struct({ Schema.Struct({ apps: Schema.optionalKey(Schema.Array(RawAppSchema)) }), ), }); -const ApiResponseSchema = Schema.Struct({ marker: ApiReleaseMarkerSchema }); +const containsApiReleaseMarker = Schema.is(Schema.Struct({ marker: ApiReleaseMarkerSchema })); +const isJsonScalar = Schema.is( + Schema.Union([Schema.Null, Schema.Boolean, Schema.Number, Schema.String]), +); +const findReleaseMarkers = (value: Schema.Json): readonly ApiReleaseMarker[] => { + if (isJsonScalar(value)) { + return []; + } + return [ + ...(containsApiReleaseMarker(value) ? [value.marker] : []), + ...Object.values(value).flatMap(findReleaseMarkers), + ]; +}; const ServiceBindingFaultCommandSchema = Schema.Struct({ appId: AppIdSchema, failed: Schema.Boolean, @@ -648,18 +660,20 @@ const responseEvidence = ( }); const bytes = Buffer.from(arrayBuffer); const source = bytes.toString('utf-8'); - const body = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ApiResponseSchema))( - source, - ).pipe( + const body = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))(source).pipe( Effect.mapError((cause) => proofError(`${app.id} API response is not valid JSON`, cause)), ); - const { marker } = body; - yield* ensure( - marker.appId === app.id && - marker.build === app.envelope?.identity.buildMarker && - marker.version === app.envelope.identity.releaseVersion, - `${app.id} API response is not tied to its executed release identity: ${source.slice(0, 1000)}`, + const marker = findReleaseMarkers(body).find( + (candidate) => + candidate.appId === app.id && + candidate.build === app.envelope?.identity.buildMarker && + candidate.version === app.envelope.identity.releaseVersion, ); + if (marker === undefined) { + return yield* proofError( + `${app.id} API response is not tied to its executed release identity`, + ); + } yield* ensure(response.ok, `${app.id} API response returned HTTP ${response.status}`); return { bodyBase64: bytes.toString('base64'), @@ -670,22 +684,48 @@ const responseEvidence = ( }; }); -const resolveApiSmokeChecks = (app: App, shell: App): readonly SmokeCheck[] => { - const shellChecks = - app.apiPrefix?.startsWith('/') === true - ? shell.jsonSmokeChecks.filter( - (check) => check.route === app.apiPrefix || check.route.startsWith(`${app.apiPrefix}/`), - ) - : []; - const uniqueChecks = new Map(); - for (const check of [...app.jsonSmokeChecks, ...shellChecks]) { - const key = [(check.method ?? 'GET').toUpperCase(), check.route, check.id ?? ''].join('\u0000'); - if (!uniqueChecks.has(key)) { - uniqueChecks.set(key, check); +const encodeSmokeCheckIdentity = Schema.encodeEffect( + Schema.fromJsonString( + Schema.Struct({ + body: Schema.Json, + expect: Schema.Json, + id: Schema.optional(Schema.String), + method: Schema.String, + route: Schema.String, + }), + ), +); + +const resolveApiSmokeChecks = ( + app: App, + shell: App, +): Effect.Effect => + Effect.gen(function* resolveApiSmokeChecksEffect() { + const shellChecks = + app.apiPrefix?.startsWith('/') === true + ? shell.jsonSmokeChecks.filter( + (check) => check.route === app.apiPrefix || check.route.startsWith(`${app.apiPrefix}/`), + ) + : []; + const uniqueChecks = new Map(); + for (const check of [...app.jsonSmokeChecks, ...shellChecks]) { + const key = yield* encodeSmokeCheckIdentity({ + body: check.body ?? null, + expect: check.expect ?? null, + id: check.id, + method: (check.method ?? 'GET').toUpperCase(), + route: check.route, + }).pipe( + Effect.mapError((cause) => + proofError(`${app.id} smoke identity could not be encoded`, cause), + ), + ); + if (!uniqueChecks.has(key)) { + uniqueChecks.set(key, check); + } } - } - return [...uniqueChecks.values()]; -}; + return [...uniqueChecks.values()]; + }); const runApiCheck = ( app: App, @@ -754,7 +794,7 @@ const runAppApiProofs = ( executionByAppId: ReadonlyMap, ): Effect.Effect => Effect.gen(function* runAppApiProofsEffect() { - const checks = resolveApiSmokeChecks(app, shell); + const checks = yield* resolveApiSmokeChecks(app, shell); yield* ensure(checks.length > 0, `${app.id} has no real Cloudflare API smoke check`); const appWorkerName = yield* workerName(app); const shellWorkerName = yield* workerName(shell); diff --git a/app/scripts/report-fail-closed-authorization-impact.mts b/app/scripts/report-fail-closed-authorization-impact.mts index e5277f02a..bb45ac423 100644 --- a/app/scripts/report-fail-closed-authorization-impact.mts +++ b/app/scripts/report-fail-closed-authorization-impact.mts @@ -1,4 +1,6 @@ #!/usr/bin/env node +import { pathToFileURL } from 'node:url'; + import { NodeRuntime, NodeServices } from '@effect/platform-node'; import { Array as EffectArray, @@ -15,16 +17,17 @@ import { Schema, } from 'effect'; import { Argument, Command } from 'effect/unstable/cli'; -import { pathToFileURL } from 'node:url'; -const InventoryHashSchema = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u)); +const InventoryHashSchema = Schema.String.check( + Schema.isPattern(/^[a-f0-9]{64}$/u) +); const SourceRevisionSchema = Schema.String.check( Schema.isMinLength(1), Schema.isMaxLength(100), - Schema.isPattern(/^[a-zA-Z0-9._-]+$/u), + Schema.isPattern(/^[a-zA-Z0-9._-]+$/u) ); const EntrypointKeySchema = Schema.String.check( - Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u), + Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u) ).pipe(Schema.brand('EntrypointKey')); const CanonicalTimestampStringSchema = Schema.String.check( Schema.makeFilter((value) => { @@ -32,10 +35,10 @@ const CanonicalTimestampStringSchema = Schema.String.check( return Option.isSome(parsed) && DateTime.formatIso(parsed.value) === value ? undefined : 'timestamp must use canonical UTC ISO 8601 encoding'; - }), + }) ); const CanonicalTimestampSchema = CanonicalTimestampStringSchema.pipe( - Schema.decodeTo(Schema.DateTimeUtcFromString), + Schema.decodeTo(Schema.DateTimeUtcFromString) ); const WouldDenyEvidenceSchema = Schema.Struct({ @@ -61,11 +64,17 @@ const WouldDenyEvidenceSchema = Schema.Struct({ ]), schemaVersion: Schema.Literal(1), sourceRevision: SourceRevisionSchema, - surface: Schema.Literals(['action', 'capability_issuance', 'route', 'worker']), + surface: Schema.Literals([ + 'action', + 'capability_issuance', + 'route', + 'worker', + ]), timestamp: CanonicalTimestampSchema, type: Schema.Literal('authorization.would_deny'), }).annotate({ - identifier: 'authorization evidence is malformed or contains prohibited fields', + identifier: + 'authorization evidence is malformed or contains prohibited fields', }); const NonEmptyEvidenceSchema = Schema.NonEmptyArray(WouldDenyEvidenceSchema); @@ -78,7 +87,8 @@ const EmptyAuthorizationObservationSchema = Schema.Struct({ startedAt: CanonicalTimestampSchema, }); -export type EmptyAuthorizationObservation = (typeof EmptyAuthorizationObservationSchema)['Encoded']; +export type EmptyAuthorizationObservation = + (typeof EmptyAuthorizationObservationSchema)['Encoded']; const AuthorizationImpactAggregateSchema = Schema.Struct({ count: Schema.Number, @@ -100,17 +110,19 @@ const AuthorizationImpactReportSchema = Schema.Struct({ totalWouldDeny: Schema.Number, }); -export type AuthorizationImpactReport = (typeof AuthorizationImpactReportSchema)['Encoded']; +export type AuthorizationImpactReport = + (typeof AuthorizationImpactReportSchema)['Encoded']; class AuthorizationImpactValidationError extends Schema.TaggedError()( 'AuthorizationImpactValidationError', - { message: Schema.String }, + { message: Schema.String } ) {} const validationError = (message: string): AuthorizationImpactValidationError => new AuthorizationImpactValidationError({ message }); -type AuthorizationImpactAggregate = AuthorizationImpactReport['aggregates'][number]; +type AuthorizationImpactAggregate = + AuthorizationImpactReport['aggregates'][number]; const localeStringOrder = Order.make((left, right) => { const comparison = left.localeCompare(right); if (comparison < 0) { @@ -119,42 +131,55 @@ const localeStringOrder = Order.make((left, right) => { return comparison > 0 ? 1 : 0; }); const aggregateOrder = Order.combineAll([ - Order.mapInput(localeStringOrder, (aggregate: AuthorizationImpactAggregate) => aggregate.surface), Order.mapInput( localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.entrypointKey, + (aggregate: AuthorizationImpactAggregate) => aggregate.surface + ), + Order.mapInput( + localeStringOrder, + (aggregate: AuthorizationImpactAggregate) => aggregate.entrypointKey ), Order.mapInput( localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.policyClass, + (aggregate: AuthorizationImpactAggregate) => aggregate.policyClass ), Order.mapInput( localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.denialReason, + (aggregate: AuthorizationImpactAggregate) => aggregate.denialReason ), ]); -const reduceDecodedEvidence = (events: NonEmptyEvidence): AuthorizationImpactReport => { +const reduceDecodedEvidence = ( + events: NonEmptyEvidence +): AuthorizationImpactReport => { const [first] = events; if ( events.some( (event) => event.sourceRevision !== first.sourceRevision || - event.inventoryHash !== first.inventoryHash, + event.inventoryHash !== first.inventoryHash ) ) { return Result.getOrThrow( Result.fail( - validationError('authorization evidence mixes source revisions or inventory hashes'), - ), + validationError( + 'authorization evidence mixes source revisions or inventory hashes' + ) + ) ); } - const counts = new Map(); + const counts = new Map< + string, + AuthorizationImpactReport['aggregates'][number] + >(); for (const event of events) { - const key = [event.surface, event.entrypointKey, event.policyClass, event.denialReason].join( - '\0', - ); + const key = [ + event.surface, + event.entrypointKey, + event.policyClass, + event.denialReason, + ].join('\0'); const current = counts.get(key); counts.set(key, { count: (current?.count ?? 0) + 1, @@ -167,7 +192,7 @@ const reduceDecodedEvidence = (events: NonEmptyEvidence): AuthorizationImpactRep const timestamps = EffectArray.sort( events.map((event) => DateTime.formatIso(event.timestamp)), - Order.String, + Order.String ); return { aggregates: EffectArray.sort([...counts.values()], aggregateOrder), @@ -184,12 +209,17 @@ const reduceDecodedEvidence = (events: NonEmptyEvidence): AuthorizationImpactRep export const reduceAuthorizationImpact = ( rawEvents: readonly object[], - emptyObservation?: EmptyAuthorizationObservation, + emptyObservation?: EmptyAuthorizationObservation ): AuthorizationImpactReport => { if (rawEvents.length > 0) { const events = Result.getOrThrowWith( - Schema.decodeUnknownResult(NonEmptyEvidenceSchema, { onExcessProperty: 'error' })(rawEvents), - () => validationError('authorization evidence is malformed or contains prohibited fields'), + Schema.decodeUnknownResult(NonEmptyEvidenceSchema, { + onExcessProperty: 'error', + })(rawEvents), + () => + validationError( + 'authorization evidence is malformed or contains prohibited fields' + ) ); return reduceDecodedEvidence(events); } @@ -198,13 +228,21 @@ export const reduceAuthorizationImpact = ( Schema.decodeUnknownResult(EmptyAuthorizationObservationSchema, { onExcessProperty: 'preserve', })(emptyObservation), - () => validationError('empty authorization impact requires explicit observation bounds'), + () => + validationError( + 'empty authorization impact requires explicit observation bounds' + ) ); - if (DateTime.toEpochMillis(observation.startedAt) > DateTime.toEpochMillis(observation.endedAt)) { + if ( + DateTime.toEpochMillis(observation.startedAt) > + DateTime.toEpochMillis(observation.endedAt) + ) { return Result.getOrThrow( Result.fail( - validationError('empty authorization impact requires explicit observation bounds'), - ), + validationError( + 'empty authorization impact requires explicit observation bounds' + ) + ) ); } return { @@ -232,60 +270,72 @@ const EvidenceDocumentSchema = Schema.Union([ BoundedEvidenceBatchSchema, ]); -const writeAuthorizationImpactReport = Effect.fn('writeAuthorizationImpactReport')( - function* writeReport(inputPath: Option.Option) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const configuredRoot = yield* Config.option(Config.string('ULTRAMODERN_WORKSPACE_ROOT')); - const root = Option.getOrElse(configuredRoot, () => path.resolve(import.meta.dirname, '..')); - const input = Option.getOrElse(inputPath, () => - path.join(root, '.codex/reports/authorization/would-deny.json'), - ); - const output = path.join(root, '.codex/reports/authorization/fail-closed-impact.json'); - const source = yield* fileSystem.readFileString(input); - const document = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(EvidenceDocumentSchema), - { onExcessProperty: 'error' }, - )(source); +const writeAuthorizationImpactReport = Effect.fn( + 'writeAuthorizationImpactReport' +)(function* writeReport(inputPath: Option.Option) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const configuredRoot = yield* Config.option( + Config.string('ULTRAMODERN_WORKSPACE_ROOT') + ); + const root = Option.getOrElse(configuredRoot, () => + path.resolve(import.meta.dirname, '..') + ); + const input = Option.getOrElse(inputPath, () => + path.join(root, '.codex/reports/authorization/would-deny.json') + ); + const output = path.join( + root, + '.codex/reports/authorization/fail-closed-impact.json' + ); + const source = yield* fileSystem.readFileString(input); + const document = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString(EvidenceDocumentSchema), + { onExcessProperty: 'error' } + )(source); - let report: AuthorizationImpactReport; - if (Schema.is(Schema.Array(WouldDenyEvidenceSchema))(document)) { - report = Schema.is(NonEmptyEvidenceSchema)(document) - ? reduceDecodedEvidence(document) - : reduceAuthorizationImpact([]); - } else if (Schema.is(NonEmptyEvidenceSchema)(document.events)) { - report = reduceDecodedEvidence(document.events); - } else { - report = reduceAuthorizationImpact([], { - endedAt: DateTime.formatIso(document.endedAt), - inventoryHash: document.inventoryHash, - sourceRevision: document.sourceRevision, - startedAt: DateTime.formatIso(document.startedAt), - }); - } + let report: AuthorizationImpactReport; + if (Schema.is(Schema.Array(WouldDenyEvidenceSchema))(document)) { + report = Schema.is(NonEmptyEvidenceSchema)(document) + ? reduceDecodedEvidence(document) + : reduceAuthorizationImpact([]); + } else if (Schema.is(NonEmptyEvidenceSchema)(document.events)) { + report = reduceDecodedEvidence(document.events); + } else { + report = reduceAuthorizationImpact([], { + endedAt: DateTime.formatIso(document.endedAt), + inventoryHash: document.inventoryHash, + sourceRevision: document.sourceRevision, + startedAt: DateTime.formatIso(document.startedAt), + }); + } - const decodedReport = yield* Schema.decodeUnknownEffect(AuthorizationImpactReportSchema)( - report, - ); - const outputJson = yield* Schema.encodeEffect( - Schema.fromJsonString(AuthorizationImpactReportSchema, { space: 2 }), - )(decodedReport); - yield* fileSystem.makeDirectory(path.dirname(output), { recursive: true }); - yield* fileSystem.writeFileString(output, `${outputJson}\n`); - yield* Console.log(output); - }, -); + const decodedReport = yield* Schema.decodeUnknownEffect( + AuthorizationImpactReportSchema + )(report); + const outputJson = yield* Schema.encodeEffect( + Schema.fromJsonString(AuthorizationImpactReportSchema, { space: 2 }) + )(decodedReport); + yield* fileSystem.makeDirectory(path.dirname(output), { recursive: true }); + yield* fileSystem.writeFileString(output, `${outputJson}\n`); + yield* Console.log(output); +}); const command = Command.make( 'report-fail-closed-authorization-impact', { input: Argument.file('input').pipe(Argument.optional) }, - ({ input }) => writeAuthorizationImpactReport(input), + ({ input }) => writeAuthorizationImpactReport(input) ); const [, invokedPath] = process.argv; -if (invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href) { - const mainLayer = Layer.effectDiscard(Command.run(command, { version: '1.0.0' })).pipe( - Layer.provide(NodeServices.layer), +if ( + invokedPath !== undefined && + import.meta.url === pathToFileURL(invokedPath).href +) { + const mainLayer = Layer.effectDiscard( + Command.run(command, { version: '1.0.0' }) + ).pipe(Layer.provide(NodeServices.layer)); + NodeRuntime.runMain( + Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid) ); - NodeRuntime.runMain(Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid)); } diff --git a/app/scripts/reset-workspace-dependencies.mjs b/app/scripts/reset-workspace-dependencies.mjs index ad28275f4..fdba5abc3 100644 --- a/app/scripts/reset-workspace-dependencies.mjs +++ b/app/scripts/reset-workspace-dependencies.mjs @@ -18,20 +18,25 @@ const main = Effect.gen(function* resetWorkspaceDependenciesEffect() { fileSystem .stat(path.join(scopeDirectory, entry)) .pipe(Effect.map((info) => info.type === 'Directory')), - { concurrency: 'unbounded' }, + { concurrency: 'unbounded' } ); dependencyDirectories.push( - ...packageDirectories.map((entry) => path.join(scopeDirectory, entry, 'node_modules')), + ...packageDirectories.map((entry) => + path.join(scopeDirectory, entry, 'node_modules') + ) ); } yield* Effect.forEach( dependencyDirectories, - (directory) => fileSystem.remove(directory, { force: true, recursive: true }), - { concurrency: 'unbounded', discard: true }, + (directory) => + fileSystem.remove(directory, { force: true, recursive: true }), + { concurrency: 'unbounded', discard: true } ); - yield* Console.log(`Removed ${dependencyDirectories.length} workspace dependency directories`); + yield* Console.log( + `Removed ${dependencyDirectories.length} workspace dependency directories` + ); }); await Effect.runPromise(main.pipe(Effect.provide(NodeServices.layer))); diff --git a/app/scripts/run-zerops-migrator.mjs b/app/scripts/run-zerops-migrator.mjs index 8e2c3604b..f6e932e3b 100644 --- a/app/scripts/run-zerops-migrator.mjs +++ b/app/scripts/run-zerops-migrator.mjs @@ -3,6 +3,7 @@ import { createServer } from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + import { Cause, Config, Effect, Exit } from 'effect'; const appDirectory = fileURLToPath(new URL('../', import.meta.url)); @@ -38,15 +39,20 @@ const run = Effect.fn('run')( function* runEffect(command, commandArguments, cwd = appDirectory) { const child = yield* Effect.acquireRelease( Effect.try({ - catch: (cause) => new MigratorError(`${command} failed to start`, cause), + catch: (cause) => + new MigratorError(`${command} failed to start`, cause), try: () => spawn(command, commandArguments, { cwd, stdio: 'inherit' }), }), - stopChild, + stopChild ); yield* Effect.callback((resume) => { const onError = (cause) => { - resume(Effect.fail(new MigratorError(`${command} failed while running`, cause))); + resume( + Effect.fail( + new MigratorError(`${command} failed while running`, cause) + ) + ); }; /** * @param {number | null} code - Numeric process exit code. @@ -58,7 +64,9 @@ const run = Effect.fn('run')( return; } const outcome = signal ?? `code ${String(code)}`; - resume(Effect.fail(new MigratorError(`${command} exited with ${outcome}`))); + resume( + Effect.fail(new MigratorError(`${command} exited with ${outcome}`)) + ); }; child.once('error', onError); @@ -68,7 +76,7 @@ const run = Effect.fn('run')( child.off('exit', onExit); }); }); - }, + } ); /** @param {string} relativePath - Application-relative script path. */ @@ -83,7 +91,7 @@ const migrate = (relativeDirectory, config) => { return run( path.join(workingDirectory, 'node_modules', '.bin', 'drizzle-kit'), ['migrate', '--config', config], - workingDirectory, + workingDirectory ); }; @@ -107,21 +115,29 @@ const serveReadiness = Effect.fn('serveReadiness')( Effect.sync(() => createServer((request, response) => { if (request.url === '/ready') { - response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + }); response.end('ready\n'); return; } response.writeHead(404).end(); - }), + }) ), - closeServer, + closeServer ); yield* Effect.callback((resume) => { const onError = (cause) => - resume(Effect.fail(new MigratorError('The migration readiness server failed', cause))); + resume( + Effect.fail( + new MigratorError('The migration readiness server failed', cause) + ) + ); const onListening = () => { - console.log(`Migration verification complete; readiness listening on port ${String(port)}`); + console.log( + `Migration verification complete; readiness listening on port ${String(port)}` + ); }; const onSignal = () => resume(Effect.void); @@ -138,7 +154,7 @@ const serveReadiness = Effect.fn('serveReadiness')( process.off('SIGTERM', onSignal); }); }); - }, + } ); const main = Effect.scoped( @@ -147,12 +163,14 @@ const main = Effect.scoped( yield* migrate('packages/core-runtime', 'drizzle.config.ts'); yield* migrate('apps/shell-super-app', 'drizzle.auth.config.ts'); yield* runAppScript('scripts/postgres/bootstrap-runtime-role.mts'); - yield* runAppScript('verticals/party-registry/scripts/prepare-contacts-migration.mts'); + yield* runAppScript( + 'verticals/party-registry/scripts/prepare-contacts-migration.mts' + ); yield* migrate('verticals/party-registry', 'drizzle.contacts.config.ts'); yield* runAppScript('scripts/postgres/bootstrap-runtime-role.mts'); yield* runAppScript('scripts/verify-application-db-schema.mts'); yield* serveReadiness(yield* migratorPort); - }).pipe(Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause)))), + }).pipe(Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause)))) ); const exit = await Effect.runPromiseExit(main); diff --git a/app/scripts/scaffolding/action-service/scaffold.mts b/app/scripts/scaffolding/action-service/scaffold.mts index ba9118768..e848ae5dc 100644 --- a/app/scripts/scaffolding/action-service/scaffold.mts +++ b/app/scripts/scaffolding/action-service/scaffold.mts @@ -1,3 +1,6 @@ +import { Effect } from 'effect'; + +import { createCodesmithGenerator } from '../generator-adapter.mts'; import { ACTION_SERVICE_GENERATOR_HEADER, createMutationEffect, @@ -7,8 +10,6 @@ import { toCamelCase, tryScaffold, } from '../shared.mts'; -import { createCodesmithGenerator } from '../generator-adapter.mts'; -import { Effect } from 'effect'; import type { ActionServiceScaffoldConfig, ActionServiceScaffoldResult, diff --git a/app/scripts/scaffolding/action/scaffold.mts b/app/scripts/scaffolding/action/scaffold.mts index 8ad0a921f..c7fd57020 100644 --- a/app/scripts/scaffolding/action/scaffold.mts +++ b/app/scripts/scaffolding/action/scaffold.mts @@ -1,4 +1,6 @@ import { Effect, FileSystem } from 'effect'; + +import { createCodesmithGenerator } from '../generator-adapter.mts'; import { ACTION_GENERATOR_HEADER, CORE_ACTION_CATALOG_IMPORT_SLOT_END, @@ -33,7 +35,6 @@ import { updateMutation, withCoreDependency, } from '../shared.mts'; -import { createCodesmithGenerator } from '../generator-adapter.mts'; import type { ActionScaffoldConfig, OntosVerticalMetadata } from '../shared.mts'; const CORE_RUNTIME_DIRECTORY = 'core-runtime'; diff --git a/app/scripts/scaffolding/generator-adapter.mts b/app/scripts/scaffolding/generator-adapter.mts index bd015f52c..f2f80078f 100644 --- a/app/scripts/scaffolding/generator-adapter.mts +++ b/app/scripts/scaffolding/generator-adapter.mts @@ -1,6 +1,7 @@ import type { NodeServices } from '@effect/platform-node'; import type { GeneratorContext, GeneratorCore } from '@modern-js/codesmith'; import { Effect, flow, Schema } from 'effect'; + import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; import { applyMutationPlanEffect } from './shared.mts'; import type { ScaffoldPlan } from './shared.mts'; @@ -11,17 +12,17 @@ type TypedGeneratorContext = Omit & { type EffectScaffoldPlanner = ( workspaceRoot: string, - config: Config, + config: Config ) => Effect.Effect, PlannerError, Services>; type PromiseScaffoldPlanner = ( workspaceRoot: string, - config: Config, + config: Config ) => Promise>; type CodesmithGenerator = ( context: TypedGeneratorContext, - core: GeneratorCore, + core: GeneratorCore ) => Promise; class GeneratorAdapterFailure extends Schema.TaggedError()( @@ -29,7 +30,7 @@ class GeneratorAdapterFailure extends Schema.TaggedError( - planner: EffectScaffoldPlanner, + planner: EffectScaffoldPlanner ): CodesmithGenerator; export function createCodesmithGenerator( - planner: PromiseScaffoldPlanner, + planner: PromiseScaffoldPlanner ): CodesmithGenerator; export function createCodesmithGenerator< Config, @@ -51,16 +52,22 @@ export function createCodesmithGenerator< >( planner: | EffectScaffoldPlanner - | PromiseScaffoldPlanner, + | PromiseScaffoldPlanner ): CodesmithGenerator { - const codesmithGeneratorEffect = (context: TypedGeneratorContext, core: GeneratorCore) => + const codesmithGeneratorEffect = ( + context: TypedGeneratorContext, + core: GeneratorCore + ) => Effect.gen(function* planAndApplyScaffold() { const planned = planner(core.outputPath, context.config); const plan = Effect.isEffect(planned) ? yield* planned : yield* Effect.tryPromise({ catch: (cause) => - new GeneratorAdapterFailure({ cause, message: 'The scaffold planner failed' }), + new GeneratorAdapterFailure({ + cause, + message: 'The scaffold planner failed', + }), try: async () => await planned, }); return yield* applyMutationPlanEffect(core, plan); diff --git a/app/scripts/scaffolding/microvertical-page/scaffold.mts b/app/scripts/scaffolding/microvertical-page/scaffold.mts index e15e909fb..699784d14 100644 --- a/app/scripts/scaffolding/microvertical-page/scaffold.mts +++ b/app/scripts/scaffolding/microvertical-page/scaffold.mts @@ -1,6 +1,6 @@ import { Effect, Equal, FileSystem, Schema } from 'effect'; + import { createCodesmithGenerator } from '../generator-adapter.mts'; -import { tailwindPrefixForNamespace } from '../tailwind-prefix.mts'; import { MODULE_MANIFEST_COMPONENT_SLOT_END, MODULE_MANIFEST_COMPONENT_SLOT_START, @@ -40,6 +40,7 @@ import type { ScaffoldPlan, OntosVerticalMetadata, } from '../shared.mts'; +import { tailwindPrefixForNamespace } from '../tailwind-prefix.mts'; interface PageVerticalMetadata extends OntosVerticalMetadata { readonly locales: readonly string[]; diff --git a/app/scripts/scaffolding/module-api/scaffold.mts b/app/scripts/scaffolding/module-api/scaffold.mts index eec0b63b7..a18d03c8a 100644 --- a/app/scripts/scaffolding/module-api/scaffold.mts +++ b/app/scripts/scaffolding/module-api/scaffold.mts @@ -4,5 +4,5 @@ import type { GovernedContributionScaffoldConfig } from '../shared.mts'; export default createCodesmithGenerator( (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'module-api', config), + planGovernedContributionScaffold(workspaceRoot, 'module-api', config) ); diff --git a/app/scripts/scaffolding/outbox-message/scaffold.mts b/app/scripts/scaffolding/outbox-message/scaffold.mts index c3387310e..3fd88b41e 100644 --- a/app/scripts/scaffolding/outbox-message/scaffold.mts +++ b/app/scripts/scaffolding/outbox-message/scaffold.mts @@ -1,5 +1,6 @@ import { Cause, Effect, FileSystem, Predicate, Result, Schema } from 'effect'; import type { PlatformError } from 'effect'; + import { createCodesmithGenerator } from '../generator-adapter.mts'; import { ACTION_GENERATOR_HEADER, diff --git a/app/scripts/scaffolding/public-component/scaffold.mts b/app/scripts/scaffolding/public-component/scaffold.mts index 2e6b33859..06b236590 100644 --- a/app/scripts/scaffolding/public-component/scaffold.mts +++ b/app/scripts/scaffolding/public-component/scaffold.mts @@ -4,5 +4,5 @@ import type { GovernedContributionScaffoldConfig } from '../shared.mts'; export default createCodesmithGenerator( (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'public-component', config), + planGovernedContributionScaffold(workspaceRoot, 'public-component', config) ); diff --git a/app/scripts/scaffolding/report/scaffold.mts b/app/scripts/scaffolding/report/scaffold.mts index b16214c5d..81d6db2f7 100644 --- a/app/scripts/scaffolding/report/scaffold.mts +++ b/app/scripts/scaffolding/report/scaffold.mts @@ -4,5 +4,5 @@ import type { GovernedContributionScaffoldConfig } from '../shared.mts'; export default createCodesmithGenerator( (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'report', config), + planGovernedContributionScaffold(workspaceRoot, 'report', config) ); diff --git a/app/scripts/scaffolding/resource/scaffold.mts b/app/scripts/scaffolding/resource/scaffold.mts index 994050145..dbc8ee06c 100644 --- a/app/scripts/scaffolding/resource/scaffold.mts +++ b/app/scripts/scaffolding/resource/scaffold.mts @@ -1,4 +1,5 @@ import { Effect } from 'effect'; + import { createCodesmithGenerator } from '../generator-adapter.mts'; import { MODULE_MANIFEST_IMPORT_SLOT_END, diff --git a/app/scripts/scaffolding/search-provider-access/scaffold.mts b/app/scripts/scaffolding/search-provider-access/scaffold.mts index 891862c3b..6e93a3d2b 100644 --- a/app/scripts/scaffolding/search-provider-access/scaffold.mts +++ b/app/scripts/scaffolding/search-provider-access/scaffold.mts @@ -1,4 +1,5 @@ import { Effect, FileSystem, Schema } from 'effect'; + import { discoverOntosModuleEffect, ensureUniqueMutationPaths, @@ -262,6 +263,11 @@ export const planSearchProviderAccessScaffold = ( yield* trySync(() => ensureUniqueMutationPaths(mutations)); return { mutations, - result: { contractPath, manifestPath: vertical.manifestPath, providerPath, serverPath }, + result: { + contractPath, + manifestPath: vertical.manifestPath, + providerPath, + serverPath, + }, }; }); diff --git a/app/scripts/scaffolding/search-provider/scaffold.mts b/app/scripts/scaffolding/search-provider/scaffold.mts index d481ed28a..a0c701093 100644 --- a/app/scripts/scaffolding/search-provider/scaffold.mts +++ b/app/scripts/scaffolding/search-provider/scaffold.mts @@ -4,5 +4,5 @@ import type { GovernedContributionScaffoldConfig } from '../shared.mts'; export default createCodesmithGenerator( (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'search-provider', config), + planGovernedContributionScaffold(workspaceRoot, 'search-provider', config) ); diff --git a/app/scripts/scaffolding/shared.mts b/app/scripts/scaffolding/shared.mts index 68592d3b8..ec99fa372 100644 --- a/app/scripts/scaffolding/shared.mts +++ b/app/scripts/scaffolding/shared.mts @@ -3,8 +3,9 @@ import type { GeneratorCore } from '@modern-js/codesmith'; import { Effect, FileSystem, flow, Option, Path, Predicate, Result, Schema } from 'effect'; import { format } from 'oxfmt'; import ultraciteOxfmt from 'ultracite/oxfmt'; -import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; + import { ONTOS_MODULE_CONTRACT_SCHEMA_VERSION } from '../../packages/core-runtime/src/index.ts'; +import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; /* eslint-disable unicorn/prefer-number-coercion -- The schema version is parsed as a base-10 integer by contract. expires: 2026-12-31. */ export const ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION = Number.parseInt( @@ -392,7 +393,10 @@ const scaffoldFailureFromUnknown = (cause: unknown, fallback: string): ScaffoldF }; export const tryScaffold = (message: string, evaluate: () => Value) => - Effect.try({ catch: (cause) => scaffoldFailureFromUnknown(cause, message), try: evaluate }); + Effect.try({ + catch: (cause) => scaffoldFailureFromUnknown(cause, message), + try: evaluate, + }); export const raiseScaffoldFailure = (message: string, cause?: unknown): never => scaffoldingRuntime.runSync(Effect.die(scaffoldFailure(message, cause))); @@ -1283,7 +1287,10 @@ const formatGeneratedMutationContent = ( const formatted = yield* Effect.tryPromise({ catch: (cause) => scaffoldFailure(`failed to format generated source ${filePath}`, cause), try: async () => - await format(filePath, content, { extends: [ultraciteOxfmt], singleQuote: true }), + await format(filePath, content, { + extends: [ultraciteOxfmt], + singleQuote: true, + }), }); if (formatted.errors.length > 0) { return yield* scaffoldFailure( @@ -1366,7 +1373,9 @@ export const withCoreDependency = (vertical: VerticalMetadata): Mutation | undef const dependencies: MutableJsonObject = dependenciesValue === undefined ? {} - : { ...asJsonObject(dependenciesValue, `vertical ${vertical.slug} dependencies`) }; + : { + ...asJsonObject(dependenciesValue, `vertical ${vertical.slug} dependencies`), + }; const current = dependencies[CORE_RUNTIME_PACKAGE]; if (current !== undefined && current !== WORKSPACE_DEPENDENCY_VERSION) { return raiseScaffoldFailure( @@ -1395,7 +1404,9 @@ export const withExactDependencies = ( const dependencies: MutableJsonObject = dependenciesValue === undefined ? {} - : { ...asJsonObject(dependenciesValue, `vertical ${vertical.slug} dependencies`) }; + : { + ...asJsonObject(dependenciesValue, `vertical ${vertical.slug} dependencies`), + }; let changed = false; for (const [name, version] of Object.entries(required)) { const current = dependencies[name]; diff --git a/app/scripts/scaffolding/tests/module-contract-generator.test.mts b/app/scripts/scaffolding/tests/module-contract-generator.test.mts index 17934cf7c..92d75f6c6 100644 --- a/app/scripts/scaffolding/tests/module-contract-generator.test.mts +++ b/app/scripts/scaffolding/tests/module-contract-generator.test.mts @@ -1,17 +1,26 @@ import assert from 'node:assert/strict'; -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { + mkdir, + mkdtemp, + readFile, + rm, + symlink, + writeFile, +} from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; + import { NodeFileSystem } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; + import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; import { checkOntosModuleContracts } from '../../check-ontos-module-contracts.mts'; +import { generateOntosModuleContract } from '../../generate-ontos-module-contract.mts'; import { privateOwnerImportViolation, unconstrainedHttpApiContractSchemaViolation, } from '../../ultramodern-api-boundary-rules.mts'; -import { generateOntosModuleContract } from '../../generate-ontos-module-contract.mts'; import { getHelpText, runScaffold } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; @@ -21,7 +30,8 @@ const DOCUMENTS_APP_ID = 'documents-center'; const DOCUMENTS_MODULE_ID = 'documents.center'; const MODULE_CONTRACT_COMMAND = 'module-contract'; const MODULE_ID = 'property.registry'; -const PROPERTY_MANIFEST_PATH = 'verticals/property-registry/vertical.manifest.ts'; +const PROPERTY_MANIFEST_PATH = + 'verticals/property-registry/vertical.manifest.ts'; const PROPERTY_PACKAGE_PATH = 'verticals/property-registry/package.json'; const VERTICAL_FLAG = '--vertical'; @@ -43,40 +53,52 @@ const ModulePackageSchema = Schema.Struct({ }), scripts: StringRecordSchema, }); -const ModuleTsconfigSchema = Schema.Struct({ include: Schema.Array(Schema.String) }); +const ModuleTsconfigSchema = Schema.Struct({ + include: Schema.Array(Schema.String), +}); const ModuleContractDocumentSchema = Schema.Struct({ deployment: Schema.Struct({ appId: AppIdSchema }), manifest: Schema.Struct({ module: Schema.Struct({ id: ModuleIdSchema }), publicSurface: Schema.Struct({ - api: Schema.Array(Schema.Struct({ operationKeys: Schema.Array(OperationKeySchema) })), + api: Schema.Array( + Schema.Struct({ operationKeys: Schema.Array(OperationKeySchema) }) + ), }), }), schemaVersion: Schema.String, }); const decodeModulePackage = (source: string) => - Schema.decodeUnknownSync(ModulePackageSchema, { onExcessProperty: 'preserve' })( - JSON.parse(source), - ); + Schema.decodeUnknownSync(ModulePackageSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(source)); const decodeModuleContract = (source: string) => - Schema.decodeUnknownSync(ModuleContractDocumentSchema, { onExcessProperty: 'preserve' })( - JSON.parse(source), - ); + Schema.decodeUnknownSync(ModuleContractDocumentSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(source)); const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); -const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; +const json = (value: JsonValue): string => + `${JSON.stringify(value, null, 2)}\n`; -const write = async (root: string, relative: string, content: string): Promise => { +const write = async ( + root: string, + relative: string, + content: string +): Promise => { const target = path.join(root, relative); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, content, 'utf-8'); }; -const writePinnedEffectApi = async (root: string, slug: string): Promise => { +const writePinnedEffectApi = async ( + root: string, + slug: string +): Promise => { await write( root, `verticals/${slug}/shared/api.ts`, - `export const fixtureApi = HttpApi.make('FixtureApi').add(HttpApiGroup.make('fixture'));\n`, + `export const fixtureApi = HttpApi.make('FixtureApi').add(HttpApiGroup.make('fixture'));\n` ); await write( root, @@ -86,13 +108,17 @@ const layer = HttpApiBuilder.layer(fixtureApi).pipe( Layer.provide(fixtureLayer), ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: fixtureApi, layer }); -`, +` ); }; const createFixture = async (): Promise => { const root = await mkdtemp(path.join(tmpdir(), 'ontos-module-contract-')); - await write(root, 'package.json', json({ name: 'fixture', private: true, type: 'module' })); + await write( + root, + 'package.json', + json({ name: 'fixture', private: true, type: 'module' }) + ); await write( root, PROPERTY_PACKAGE_PATH, @@ -109,24 +135,29 @@ const createFixture = async (): Promise => { name: '@app/property-registry', private: true, scripts: { - build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + build: + 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', 'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', existing: 'preserve-me', }, type: 'module', version: '0.1.0', - }), + }) ); await write( root, 'verticals/property-registry/tsconfig.json', - json({ compilerOptions: { composite: true }, include: ['src', 'shared'], references: [] }), + json({ + compilerOptions: { composite: true }, + include: ['src', 'shared'], + references: [], + }) ); await write( root, 'verticals/property-registry/module-federation.config.ts', - `export default { exposes: {} };\n`, + `export default { exposes: {} };\n` ); await write( root, @@ -146,17 +177,21 @@ const createFixture = async (): Promise => { }, type: 'module', version: '0.1.0', - }), + }) ); await write( root, 'verticals/documents-center/tsconfig.json', - json({ compilerOptions: { composite: true }, include: ['src'], references: [] }), + json({ + compilerOptions: { composite: true }, + include: ['src'], + references: [], + }) ); await write( root, 'verticals/documents-center/module-federation.config.ts', - 'export default {};\n', + 'export default {};\n' ); await Promise.all([ writePinnedEffectApi(root, APP_ID), @@ -173,7 +208,10 @@ const createFixture = async (): Promise => { domain: 'property', id: APP_ID, kind: 'vertical', - moduleFederation: { name: 'verticalPropertyRegistry', role: 'remote' }, + moduleFederation: { + name: 'verticalPropertyRegistry', + role: 'remote', + }, package: '@app/property-registry', path: 'verticals/property-registry', }, @@ -187,7 +225,7 @@ const createFixture = async (): Promise => { path: 'verticals/documents-center', }, ], - }), + }) ); await write( root, @@ -195,23 +233,34 @@ const createFixture = async (): Promise => { json({ environment: 'development', ontosModuleManifests: Object.fromEntries([ - [DOCUMENTS_APP_ID, 'http://localhost:4102/.well-known/ontos-module-manifest.json'], - [APP_ID, 'http://localhost:4101/.well-known/ontos-module-manifest.json'], + [ + DOCUMENTS_APP_ID, + 'http://localhost:4102/.well-known/ontos-module-manifest.json', + ], + [ + APP_ID, + 'http://localhost:4101/.well-known/ontos-module-manifest.json', + ], ]), schemaVersion: 1, - }), + }) ); await mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }); await symlink( path.join(appRoot, 'packages/core-runtime'), path.join(root, 'node_modules/@app/core-runtime'), - 'dir', + 'dir' + ); + await symlink( + path.join(appRoot, 'node_modules/effect'), + path.join(root, 'node_modules/effect') ); - await symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')); return root; }; -const withFixture = async (run: (root: string) => Promise): Promise => { +const withFixture = async ( + run: (root: string) => Promise +): Promise => { const root = await createFixture(); try { await run(root); @@ -221,17 +270,30 @@ const withFixture = async (run: (root: string) => Promise): Promise }; const scaffold = async (root: string, vertical = APP_ID, module = MODULE_ID) => - await runScaffold(MODULE_CONTRACT_COMMAND, [VERTICAL_FLAG, vertical, '--module', module], { - workspaceRoot: root, - }); + await runScaffold( + MODULE_CONTRACT_COMMAND, + [VERTICAL_FLAG, vertical, '--module', module], + { + workspaceRoot: root, + } + ); void test('module-contract help is exact and write-free', async () => { - const missingRoot = path.join(tmpdir(), 'module-contract-help-does-not-exist'); + const missingRoot = path.join( + tmpdir(), + 'module-contract-help-does-not-exist' + ); const result = await runScaffold(MODULE_CONTRACT_COMMAND, ['--help'], { workspaceRoot: missingRoot, }); - assert.deepEqual(result, { help: getHelpText(MODULE_CONTRACT_COMMAND), kind: 'help' }); - assert.match(result.help, /--vertical --module /u); + assert.deepEqual(result, { + help: getHelpText(MODULE_CONTRACT_COMMAND), + kind: 'help', + }); + assert.match( + result.help, + /--vertical --module /u + ); }); void test('business generators fail closed before the mandatory module contract exists', async () => { @@ -268,7 +330,14 @@ void test('business generators fail closed before the mandatory module contract ], [ 'outbox-message', - [VERTICAL_FLAG, APP_ID, '--action', 'create-property', '--topic', 'property.created'], + [ + VERTICAL_FLAG, + APP_ID, + '--action', + 'create-property', + '--topic', + 'property.created', + ], ], [ 'outbox-worker', @@ -287,7 +356,14 @@ void test('business generators fail closed before the mandatory module contract ], [ 'policy', - ['--scope', 'microvertical', VERTICAL_FLAG, APP_ID, '--policy', 'property-visible'], + [ + '--scope', + 'microvertical', + VERTICAL_FLAG, + APP_ID, + '--policy', + 'property-visible', + ], ], ] as const; await Promise.all( @@ -295,26 +371,35 @@ void test('business generators fail closed before the mandatory module contract async ([command, flags]) => await assert.rejects( runScaffold(command, flags, { workspaceRoot: root }), - /requires scaffold:module-contract/u, - ), - ), + /requires scaffold:module-contract/u + ) + ) ); }); }); void test('rejects malformed, traversing, duplicate, and overwrite requests without partial writes', async () => { await withFixture(async (root) => { - await assert.rejects(scaffold(root, '../property', MODULE_ID), /lower-kebab-case/u); + await assert.rejects( + scaffold(root, '../property', MODULE_ID), + /lower-kebab-case/u + ); await assert.rejects(scaffold(root, APP_ID, APP_ID), /dotted/u); await assert.rejects(scaffold(root, APP_ID, 'core.modules'), /non-core/u); await scaffold(root); - const packageAfterFirst = await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'); + const packageAfterFirst = await readFile( + path.join(root, PROPERTY_PACKAGE_PATH), + 'utf-8' + ); await assert.rejects(scaffold(root), /refusing to overwrite/u); assert.equal( await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'), - packageAfterFirst, + packageAfterFirst + ); + await assert.rejects( + scaffold(root, DOCUMENTS_APP_ID, MODULE_ID), + /duplicate OntOS module ID/u ); - await assert.rejects(scaffold(root, DOCUMENTS_APP_ID, MODULE_ID), /duplicate OntOS module ID/u); }); }); @@ -322,15 +407,21 @@ void test('generates conservative owner files and patches only package and tscon await withFixture(async (root) => { const result = await scaffold(root); assert.equal(result.kind, 'generated'); - const manifest = await readFile(path.join(root, PROPERTY_MANIFEST_PATH), 'utf-8'); + const manifest = await readFile( + path.join(root, PROPERTY_MANIFEST_PATH), + 'utf-8' + ); const registration = await readFile( path.join(root, 'verticals/property-registry/vertical.registration.ts'), - 'utf-8', + 'utf-8' ); assert.match(manifest, /@ontos-deployment-app-id property-registry/u); assert.match(manifest, /@ontos-module-id property\.registry/u); assert.match(manifest, /defaultState: 'inactive'/u); - assert.doesNotMatch(manifest, /dependencies:|core\.identity|externalSystems/u); + assert.doesNotMatch( + manifest, + /dependencies:|core\.identity|externalSystems/u + ); const retiredLifecycleMarkers = [ ['must', 'be', 'active', 'first'].join('_'), ['enable', 'together', 'when', 'available'].join('_'), @@ -345,7 +436,7 @@ void test('generates conservative owner files and patches only package and tscon assert.match(registration, /generated-module-registration-workers/u); assert.doesNotMatch(registration, /handler|migration|route/u); const packageJson = decodeModulePackage( - await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'), + await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8') ); assert.deepEqual(packageJson.dependencies, { '@app/core-runtime': 'workspace:*', @@ -353,10 +444,13 @@ void test('generates conservative owner files and patches only package and tscon }); assert.deepEqual(packageJson.exports, { '.': './src/index.ts' }); assert.equal(packageJson.scripts['existing'], 'preserve-me'); - assert.match(packageJson.scripts['build'] ?? '', /--vertical property-registry --target dist/u); + assert.match( + packageJson.scripts['build'] ?? '', + /--vertical property-registry --target dist/u + ); assert.match( packageJson.scripts['cloudflare:build'] ?? '', - /--vertical property-registry --target cloudflare-dist/u, + /--vertical property-registry --target cloudflare-dist/u ); assert.deepEqual(packageJson.modernjs.ontosModule, { contractPath: '/.well-known/ontos-module-manifest.json', @@ -367,8 +461,11 @@ void test('generates conservative owner files and patches only package and tscon }); const tsconfig = Schema.decodeUnknownSync(ModuleTsconfigSchema)( JSON.parse( - await readFile(path.join(root, 'verticals/property-registry/tsconfig.json'), 'utf-8'), - ), + await readFile( + path.join(root, 'verticals/property-registry/tsconfig.json'), + 'utf-8' + ) + ) ); assert.deepEqual(tsconfig.include, [ 'src', @@ -390,13 +487,13 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl authoredManifest .replace( '// ', - "import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nconst PropertyApi = HttpApi.make('PropertyApi').add(\n HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')),\n);\n// ", + "import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nconst PropertyApi = HttpApi.make('PropertyApi').add(\n HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')),\n);\n// " ) .replace( ' // \n // ', - ' // \n PropertyApi,\n // ', + ' // \n PropertyApi,\n // ' ), - 'utf-8', + 'utf-8' ); const first = await generateOntosModuleContract({ target: 'dist', @@ -411,7 +508,10 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl ...decodedPackage, modernjs: { ...decodedPackage.modernjs, - ontosModule: { ...decodedPackage.modernjs.ontosModule, schemaVersion: 0 }, + ontosModule: { + ...decodedPackage.modernjs.ontosModule, + schemaVersion: 0, + }, }, }; await writeFile(packagePath, json(incompatiblePackage), 'utf-8'); @@ -421,7 +521,7 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl vertical: APP_ID, workspaceRoot: root, }), - /module marker does not match/u, + /module marker does not match/u ); assert.equal(await readFile(first.path, 'utf-8'), firstContent); await writeFile(packagePath, packageContent, 'utf-8'); @@ -437,11 +537,16 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl assert.equal(document.manifest.module.id, MODULE_ID); assert.equal(document.schemaVersion, '2'); assert.equal(Object.hasOwn(document.manifest, 'dependencies'), false); - assert.deepEqual(document.manifest.publicSurface.api[0]?.operationKeys, ['property.listUnits']); - assert.doesNotMatch(firstContent, /vertical\.registration|function|handler|sourcePath/u); + assert.deepEqual(document.manifest.publicSurface.api[0]?.operationKeys, [ + 'property.listUnits', + ]); + assert.doesNotMatch( + firstContent, + /vertical\.registration|function|handler|sourcePath/u + ); const headers = await readFile( path.join(root, 'verticals/property-registry/dist/public/_headers'), - 'utf-8', + 'utf-8' ); assert.match(headers, /Cache-Control: no-cache/u); assert.match(headers, /Content-Type: application\/json/u); @@ -451,7 +556,9 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl vertical: DOCUMENTS_APP_ID, workspaceRoot: root, }); - const secondDocument = decodeModuleContract(await readFile(secondDeployment.path, 'utf-8')); + const secondDocument = decodeModuleContract( + await readFile(secondDeployment.path, 'utf-8') + ); assert.equal(secondDocument.deployment.appId, DOCUMENTS_APP_ID); assert.equal(secondDocument.manifest.module.id, DOCUMENTS_MODULE_ID); @@ -460,7 +567,7 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl await writeFile( manifestPath, manifest.replace('// ', ''), - 'utf-8', + 'utf-8' ); await assert.rejects( generateOntosModuleContract({ @@ -468,7 +575,7 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl vertical: APP_ID, workspaceRoot: root, }), - /exactly one.*slot/u, + /exactly one.*slot/u ); }); }); @@ -484,10 +591,10 @@ void test('maps Cloudflare emission to the Modern output root and validates auth }); assert.match( emitted.path, - /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u, + /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u ); await runEffectTestPromise( - checkOntosModuleContracts(root).pipe(Effect.provide(NodeFileSystem.layer)), + checkOntosModuleContracts(root).pipe(Effect.provide(NodeFileSystem.layer)) ); }); }); @@ -498,33 +605,33 @@ void test('permits owner-local registration imports but rejects cross-deployment privateOwnerImportViolation( root, 'verticals/billing/src/worker-host/main.ts', - '../../vertical.registration.ts', + '../../vertical.registration.ts' ), - undefined, + undefined ); assert.match( privateOwnerImportViolation( root, 'verticals/billing/src/worker-host/main.ts', - '../../../inventory-stock/vertical.registration.ts', + '../../../inventory-stock/vertical.registration.ts' ) ?? '', - /only its own/u, + /only its own/u ); assert.match( privateOwnerImportViolation( root, 'verticals/billing/vertical.registration.ts', - '../inventory-stock/vertical.registration.ts', + '../inventory-stock/vertical.registration.ts' ) ?? '', - /only its own/u, + /only its own/u ); assert.match( privateOwnerImportViolation( root, 'apps/shell-super-app/api/index.ts', - '../../../verticals/billing/vertical.registration.ts', + '../../../verticals/billing/vertical.registration.ts' ) ?? '', - /may not import/u, + /may not import/u ); }); @@ -541,10 +648,13 @@ export const untouched = true; await scaffold(root); const generated = await readFile( path.join(root, 'verticals/property-registry/shared/api.ts'), - 'utf-8', + 'utf-8' ); assert.match(generated, /HttpApi\.make\('Fixture;Api'\)/u); - assert.match(generated, /return api; \}\)\s*\/\/ /u); + assert.match( + generated, + /return api; \}\)\s*\/\/ /u + ); assert.match(generated, /export const governedHttpApi = fixtureApi;/u); assert.match(generated, /export const untouched = true;/u); }); @@ -559,14 +669,17 @@ void test('module-contract injects only its own Layer binding into pinned handle identity, ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: fixtureApi, layer }); -`, +` ); await scaffold(root); const generated = await readFile( path.join(root, 'verticals/property-registry/api/index.ts'), - 'utf-8', + 'utf-8' + ); + assert.match( + generated, + /GovernedReadLayer\.provide\(governedReadApiHandlersLive\)/u ); - assert.match(generated, /GovernedReadLayer\.provide\(governedReadApiHandlersLive\)/u); assert.match(generated, /GovernedReadLayer\.orDie/u); assert.doesNotMatch(generated, /\bLayer\./u); }); @@ -579,7 +692,7 @@ void test('requires concrete HttpApi contract schemas through Problem Details he field: Schema.String, }); `), - undefined, + undefined ); assert.match( unconstrainedHttpApiContractSchemaViolation(` @@ -587,7 +700,7 @@ void test('requires concrete HttpApi contract schemas through Problem Details he field: Schema.Unknown, }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); assert.match( unconstrainedHttpApiContractSchemaViolation(` @@ -595,7 +708,7 @@ void test('requires concrete HttpApi contract schemas through Problem Details he success: Schema.Any, }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); assert.match( unconstrainedHttpApiContractSchemaViolation(` @@ -603,7 +716,7 @@ void test('requires concrete HttpApi contract schemas through Problem Details he diagnostics: Schema.Record(Schema.String, Schema.String), }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); assert.match( unconstrainedHttpApiContractSchemaViolation(` @@ -611,7 +724,7 @@ void test('requires concrete HttpApi contract schemas through Problem Details he diagnostics: Schema.Json, }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); }); @@ -639,12 +752,15 @@ void test('follows imported payload, query, parameter, success, and error schema ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', /must use concrete/u, - member, + member ); } }); @@ -656,7 +772,7 @@ void test('covers every supported endpoint constructor through direct and aliase HttpApiEndpoint.${method}('execute', '/reads/example', { success: Schema.Any }); `) ?? '', /must use concrete/u, - method, + method ); } @@ -665,7 +781,7 @@ void test('covers every supported endpoint constructor through direct and aliase const inspectHeaders = HttpApiEndpoint.head; inspectHeaders('execute', '/reads/example', { success: Schema.Unknown }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); const sources = new Map([ @@ -682,11 +798,14 @@ void test('covers every supported endpoint constructor through direct and aliase ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); @@ -704,7 +823,10 @@ void test('follows HttpApiEndpoint.make factories through local and imported hel makeEndpoint()('execute', '/reads/example', { success: Schema.Any }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(source) ?? '', /must use concrete/u); + assert.match( + unconstrainedHttpApiContractSchemaViolation(source) ?? '', + /must use concrete/u + ); } const sources = new Map([ @@ -726,17 +848,28 @@ void test('follows HttpApiEndpoint.make factories through local and imported hel ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); void test('follows the direct HttpApiEndpoint provider through verbs, make, and re-exports', () => { const provider = 'effect/unstable/httpapi/HttpApiEndpoint'; - for (const method of ['delete', 'get', 'head', 'options', 'patch', 'post', 'put']) { + for (const method of [ + 'delete', + 'get', + 'head', + 'options', + 'patch', + 'post', + 'put', + ]) { const entry = ` import * as Endpoint from '${provider}'; Endpoint.${method}('execute', '/reads/example', { success: Schema.Any }); @@ -747,7 +880,7 @@ void test('follows the direct HttpApiEndpoint provider through verbs, make, and sources: new Map([[contractApiFixturePath, entry]]), }) ?? '', /must use concrete/u, - method, + method ); } @@ -764,7 +897,7 @@ void test('follows the direct HttpApiEndpoint provider through verbs, make, and file: contractApiFixturePath, sources: makeSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); const namespaceEntry = ` @@ -780,7 +913,7 @@ void test('follows the direct HttpApiEndpoint provider through verbs, make, and file: contractApiFixturePath, sources: namespaceSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); }); @@ -792,7 +925,7 @@ void test('follows local and imported function helpers used as public schemas', } HttpApiEndpoint.get('read', '/reads/example', { success: unsafeResponse() }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); const sources = new Map([ @@ -813,11 +946,14 @@ void test('follows local and imported function helpers used as public schemas', ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); @@ -834,14 +970,20 @@ void test('follows imported arbitrary Problem Details extension records', () => unsafeContractFixturePath, `export const ExtensionFields = { diagnostics: Schema.Record(Schema.String, Schema.String) };`, ], - [contractBarrelFixturePath, `export { ExtensionFields as UnsafeExtensions } from './unsafe';`], + [ + contractBarrelFixturePath, + `export { ExtensionFields as UnsafeExtensions } from './unsafe';`, + ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); @@ -862,14 +1004,20 @@ void test('follows transitive imported schema aliases without rejecting unused u export const UnusedUnsafeResponse = Schema.Any; `, ], - ['contracts/internal-response.ts', `export const InternalResponse = Schema.Json;`], + [ + 'contracts/internal-response.ts', + `export const InternalResponse = Schema.Json;`, + ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); const safeSources = new Map([ @@ -880,11 +1028,14 @@ void test('follows transitive imported schema aliases without rejecting unused u ] as const, ]); assert.equal( - unconstrainedHttpApiContractSchemaViolation(safeSources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources: safeSources, - }), - undefined, + unconstrainedHttpApiContractSchemaViolation( + safeSources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources: safeSources, + } + ), + undefined ); }); @@ -915,11 +1066,14 @@ void test('rejects Effect Schema namespace aliases and destructured unsafe membe [unsafeContractFixturePath, unsafeSource], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); } }); @@ -941,11 +1095,14 @@ void test('follows barrel re-exports and relative namespace imports', () => { [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); } }); @@ -969,11 +1126,14 @@ void test('follows local re-exports of imported schemas', () => { [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); @@ -983,7 +1143,10 @@ void test('rejects direct Effect schema imports used through an aliased endpoint import { Any as UnsafeSchema } from 'effect'; Endpoint.get('read', '/reads/example', { success: UnsafeSchema }); `; - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + assert.match( + unconstrainedHttpApiContractSchemaViolation(content) ?? '', + /must use concrete/u + ); }); void test('follows schemas imported through @app package subpaths', () => { @@ -1000,11 +1163,14 @@ void test('follows schemas imported through @app package subpaths', () => { [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); }); @@ -1028,7 +1194,9 @@ void test('follows star barrels, default imports, and package export maps', () = import UnsafeSchema from './unsafe'; HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `, - extraSources: [[unsafeContractFixturePath, `export default Schema.Json;`]], + extraSources: [ + [unsafeContractFixturePath, `export default Schema.Json;`], + ], }, { entry: ` @@ -1036,8 +1204,14 @@ void test('follows star barrels, default imports, and package export maps', () = HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `, extraSources: [ - [packageFixturePath, `{"name":"@app/example","exports":{"./api":"./shared/unsafe.ts"}}`], - ['packages/example/shared/unsafe.ts', `export const UnsafeSchema = Schema.Unknown;`], + [ + packageFixturePath, + `{"name":"@app/example","exports":{"./api":"./shared/unsafe.ts"}}`, + ], + [ + 'packages/example/shared/unsafe.ts', + `export const UnsafeSchema = Schema.Unknown;`, + ], ], }, ]; @@ -1047,11 +1221,14 @@ void test('follows star barrels, default imports, and package export maps', () = ...fixture.extraSources, ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); } }); @@ -1071,56 +1248,66 @@ void test('covers ordinary endpoint aliases and TypeScript module forms', () => HttpApiEndpoint.get('read', '/reads/example', { success: S.Any }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + assert.match( + unconstrainedHttpApiContractSchemaViolation(content) ?? '', + /must use concrete/u + ); } - const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ + const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ - ` + [ + ` import SafeDefault, { UnsafeSchema } from './unsafe'; HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `, - new Map([ - [ - unsafeContractFixturePath, - `export default Schema.String; export const UnsafeSchema = Schema.Any;`, - ], - ]), - ], - [ - ` + new Map([ + [ + unsafeContractFixturePath, + `export default Schema.String; export const UnsafeSchema = Schema.Any;`, + ], + ]), + ], + [ + ` import { Schemas } from './barrel'; HttpApiEndpoint.get('read', '/reads/example', { success: Schemas.UnsafeSchema }); `, - new Map([ - [contractBarrelFixturePath, `export * as Schemas from './unsafe';`], - [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Unknown;`], - ]), - ], - [ - ` + new Map([ + [contractBarrelFixturePath, `export * as Schemas from './unsafe';`], + [ + unsafeContractFixturePath, + `export const UnsafeSchema = Schema.Unknown;`, + ], + ]), + ], + [ + ` import { UnsafeSchema } from '@app/example/api'; HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `, - new Map([ - [ - packageFixturePath, - `{"exports":{"./api":{"types":"./src/unsafe.ts","default":"./dist/unsafe.js"}}}`, - ], - [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], - ]), - ], - [ - ` + new Map([ + [ + packageFixturePath, + `{"exports":{"./api":{"types":"./src/unsafe.ts","default":"./dist/unsafe.js"}}}`, + ], + [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], + ]), + ], + [ + ` import { UnsafeSchema } from '@app/example/unsafe'; HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `, - new Map([ - [packageFixturePath, `{"exports":{"./*":{"types":"./src/*.ts"}}}`], - [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Unknown;`], - ]), - ], - ]; + new Map([ + [packageFixturePath, `{"exports":{"./*":{"types":"./src/*.ts"}}}`], + [ + packageUnsafeFixturePath, + `export const UnsafeSchema = Schema.Unknown;`, + ], + ]), + ], + ]; for (const [entry, extraSources] of fixtures) { const sources = new Map([[contractApiFixturePath, entry], ...extraSources]); assert.match( @@ -1128,7 +1315,7 @@ void test('covers ordinary endpoint aliases and TypeScript module forms', () => file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } }); @@ -1151,7 +1338,10 @@ void test('covers destructured, computed, and provenance-safe aliases', () => { factory('InvalidProblem', 400, { values: Schema.Record(Schema.String, Schema.String) }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + assert.match( + unconstrainedHttpApiContractSchemaViolation(content) ?? '', + /must use concrete/u + ); } assert.equal( @@ -1162,7 +1352,7 @@ void test('covers destructured, computed, and provenance-safe aliases', () => { const makeProblemDetailsSchema = () => undefined; makeProblemDetailsSchema('SafeLocalCall', 400, { value: Schema.Any }); `), - undefined, + undefined ); assert.equal( unconstrainedHttpApiContractSchemaViolation(` @@ -1170,7 +1360,7 @@ void test('covers destructured, computed, and provenance-safe aliases', () => { success: Schema.Record(Schema.String, Schema.String), }); `), - undefined, + undefined ); }); @@ -1188,7 +1378,10 @@ void test('resolves recursive namespace exports for endpoints, factories, and sc 'contracts/factories.ts', `export { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from '@app/shared-contracts/problem-details';`, ], - ['contracts/http.ts', `export { HttpApiEndpoint } from 'effect/unstable/httpapi';`], + [ + 'contracts/http.ts', + `export { HttpApiEndpoint } from 'effect/unstable/httpapi';`, + ], [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Unknown;`], ]); for (const entry of [ @@ -1216,14 +1409,17 @@ void test('resolves recursive namespace exports for endpoints, factories, and sc const sources = new Map([ ...commonSources, [contractApiFixturePath, entry] as const, - ['contracts/renamed.ts', `export { Problems as Renamed } from './barrel';`] as const, + [ + 'contracts/renamed.ts', + `export { Problems as Renamed } from './barrel';`, + ] as const, ]); assert.match( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } }); @@ -1235,7 +1431,7 @@ void test('resolves lexical shadows without inspecting unused inner bindings', ( { const ResponseSchema = Schema.String; void ResponseSchema; } HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); assert.equal( unconstrainedHttpApiContractSchemaViolation(` @@ -1243,39 +1439,55 @@ void test('resolves lexical shadows without inspecting unused inner bindings', ( { const ResponseSchema = Schema.Any; void ResponseSchema; } HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `), - undefined, + undefined ); }); void test('evaluates package export conditions, wildcard specificity, and null exclusions', () => { - const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ - [ - '@app/example/api', - new Map([ - [ - packageFixturePath, - `{"exports":{"./api":{"types":"./src/safe.d.ts","default":"./src/unsafe.ts"}}}`, - ], - ['packages/example/src/safe.d.ts', `export const UnsafeSchema: unknown;`], - [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], - ]), - ], + const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ - '@app/example/api/schema', - new Map([ - [packageFixturePath, `{"exports":{"./*":"./src/safe.ts","./api/*":"./src/unsafe/*.ts"}}`], - [packageSafeFixturePath, packageSafeSchemaFixture], - ['packages/example/src/unsafe/schema.ts', `export const UnsafeSchema = Schema.Unknown;`], - ]), - ], - [ - '@app/example/api', - new Map([ - [packageFixturePath, `{"exports":{"./blocked":null,"./api":"./unusual/unsafe.ts"}}`], - ['packages/example/unusual/unsafe.ts', `export const UnsafeSchema = Schema.Json;`], - ]), - ], - ]; + [ + '@app/example/api', + new Map([ + [ + packageFixturePath, + `{"exports":{"./api":{"types":"./src/safe.d.ts","default":"./src/unsafe.ts"}}}`, + ], + [ + 'packages/example/src/safe.d.ts', + `export const UnsafeSchema: unknown;`, + ], + [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], + ]), + ], + [ + '@app/example/api/schema', + new Map([ + [ + packageFixturePath, + `{"exports":{"./*":"./src/safe.ts","./api/*":"./src/unsafe/*.ts"}}`, + ], + [packageSafeFixturePath, packageSafeSchemaFixture], + [ + 'packages/example/src/unsafe/schema.ts', + `export const UnsafeSchema = Schema.Unknown;`, + ], + ]), + ], + [ + '@app/example/api', + new Map([ + [ + packageFixturePath, + `{"exports":{"./blocked":null,"./api":"./unusual/unsafe.ts"}}`, + ], + [ + 'packages/example/unusual/unsafe.ts', + `export const UnsafeSchema = Schema.Json;`, + ], + ]), + ], + ]; for (const [specifier, extraSources] of fixtures) { const entry = ` import { UnsafeSchema } from '${specifier}'; @@ -1287,7 +1499,7 @@ void test('evaluates package export conditions, wildcard specificity, and null e file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } @@ -1299,14 +1511,17 @@ void test('evaluates package export conditions, wildcard specificity, and null e [contractApiFixturePath, safeEntry], [packageFixturePath, `{"exports":{"./api":"./src/safe.ts"}}`], [packageSafeFixturePath, `export const ResponseSchema = Schema.String;`], - ['packages/example/src/api.ts', `export const ResponseSchema = Schema.Any;`], + [ + 'packages/example/src/api.ts', + `export const ResponseSchema = Schema.Any;`, + ], ]); assert.equal( unconstrainedHttpApiContractSchemaViolation(safeEntry, { file: contractApiFixturePath, sources: safeSources, }), - undefined, + undefined ); const specificEntry = ` @@ -1315,7 +1530,10 @@ void test('evaluates package export conditions, wildcard specificity, and null e `; const specificSources = new Map([ [contractApiFixturePath, specificEntry], - [packageFixturePath, `{"exports":{"./foo/*":"./src/safe.ts","./*/bar":"./src/unsafe.ts"}}`], + [ + packageFixturePath, + `{"exports":{"./foo/*":"./src/safe.ts","./*/bar":"./src/unsafe.ts"}}`, + ], [packageSafeFixturePath, `export const ResponseSchema = Schema.String;`], [packageUnsafeFixturePath, `export const ResponseSchema = Schema.Any;`], ]); @@ -1324,7 +1542,7 @@ void test('evaluates package export conditions, wildcard specificity, and null e file: contractApiFixturePath, sources: specificSources, }), - undefined, + undefined ); }); @@ -1346,7 +1564,7 @@ void test('terminates on safe and unsafe cyclic re-exports', () => { file: contractApiFixturePath, sources: safeSources, }), - undefined, + undefined ); const unsafeSources = new Map([ ...safeSources, @@ -1360,7 +1578,7 @@ void test('terminates on safe and unsafe cyclic re-exports', () => { file: contractApiFixturePath, sources: unsafeSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); }); @@ -1383,12 +1601,15 @@ void test('honors explicit export precedence and star-export binding identity', file: contractApiFixturePath, sources: explicitSources, }), - undefined, + undefined ); const diamondSources = new Map([ [contractApiFixturePath, entry], - [contractBarrelFixturePath, `export * from './left'; export * from './right';`], + [ + contractBarrelFixturePath, + `export * from './left'; export * from './right';`, + ], ['contracts/left.ts', `export * from './origin';`], ['contracts/right.ts', `export * from './origin';`], ['contracts/origin.ts', `export const ResponseSchema = Schema.Unknown;`], @@ -1398,20 +1619,26 @@ void test('honors explicit export precedence and star-export binding identity', file: contractApiFixturePath, sources: diamondSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); const ambiguousSources = new Map([ ...diamondSources, - ['contracts/left.ts', `export const ResponseSchema = Schema.String;`] as const, - ['contracts/right.ts', `export const ResponseSchema = Schema.Any;`] as const, + [ + 'contracts/left.ts', + `export const ResponseSchema = Schema.String;`, + ] as const, + [ + 'contracts/right.ts', + `export const ResponseSchema = Schema.Any;`, + ] as const, ]); assert.equal( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: ambiguousSources, }), - undefined, + undefined ); }); @@ -1430,7 +1657,7 @@ void test('uses TypeScript source and relative-file resolution precedence', () = file: contractApiFixturePath, sources, }), - undefined, + undefined ); const nodeNextEntry = ` @@ -1447,7 +1674,7 @@ void test('uses TypeScript source and relative-file resolution precedence', () = file: contractApiFixturePath, sources: nodeNextSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); }); @@ -1499,7 +1726,10 @@ void test('tracks object, mutable, rest, var, enum, and namespace provenance', ( HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + assert.match( + unconstrainedHttpApiContractSchemaViolation(content) ?? '', + /must use concrete/u + ); } for (const content of [ @@ -1520,20 +1750,26 @@ void test('tracks object, mutable, rest, var, enum, and namespace provenance', ( HttpApiEndpoint.get('read', '/reads/example', { success: Schema.Any }); `, ]) { - assert.equal(unconstrainedHttpApiContractSchemaViolation(content), undefined); + assert.equal( + unconstrainedHttpApiContractSchemaViolation(content), + undefined + ); } assert.equal( unconstrainedHttpApiContractSchemaViolation(` const { String, ...S } = Schema; HttpApiEndpoint.get('read', '/reads/example', { success: S.String }); `), - undefined, + undefined ); }); void test('follows external schema and endpoint provider re-exports', () => { const fixtures: readonly (readonly [string, string])[] = [ - [`export * from 'effect/Schema';`, `import { Any as UnsafeSchema } from './barrel';`], + [ + `export * from 'effect/Schema';`, + `import { Any as UnsafeSchema } from './barrel';`, + ], [ `export { Unknown as UnsafeSchema } from 'effect';`, `import { UnsafeSchema } from './barrel';`, @@ -1555,7 +1791,7 @@ void test('follows external schema and endpoint provider re-exports', () => { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } @@ -1576,7 +1812,7 @@ void test('follows external schema and endpoint provider re-exports', () => { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } }); @@ -1587,14 +1823,14 @@ void test('follows local and imported aliases of Problem Details factories', () const factory = makeProblemDetailsSchema; const InvalidProblem = factory('InvalidProblem', 400, { field: Schema.Unknown }); `) ?? '', - /must use concrete/u, + /must use concrete/u ); assert.equal( unconstrainedHttpApiContractSchemaViolation(` const factory = makeProblemDetailsSchema; const ValidProblem = factory('ValidProblem', 400, { field: Schema.String }); `), - undefined, + undefined ); const sources = new Map([ @@ -1606,18 +1842,24 @@ void test('follows local and imported aliases of Problem Details factories', () const InvalidProblem = factory('InvalidProblem', 400, UnsafeExtensions); `, ], - ['contracts/factory.ts', `export const factory = makeProblemDetailsSchema;`], + [ + 'contracts/factory.ts', + `export const factory = makeProblemDetailsSchema;`, + ], [ unsafeContractFixturePath, `export const UnsafeExtensions = { values: Schema.Record(Schema.String, Schema.String) };`, ], ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + sources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources, + } + ) ?? '', + /must use concrete/u ); for (const extensions of [ `{ field: Schema.Unknown }`, @@ -1634,14 +1876,20 @@ void test('follows local and imported aliases of Problem Details factories', () ] as const, ]); assert.match( - unconstrainedHttpApiContractSchemaViolation(inlineSources.get(contractApiFixturePath) ?? '', { - file: contractApiFixturePath, - sources: inlineSources, - }) ?? '', - /must use concrete/u, + unconstrainedHttpApiContractSchemaViolation( + inlineSources.get(contractApiFixturePath) ?? '', + { + file: contractApiFixturePath, + sources: inlineSources, + } + ) ?? '', + /must use concrete/u ); } - for (const factoryName of ['makeProblemDetailsSchema', 'makeRetryableProblemDetailsSchema']) { + for (const factoryName of [ + 'makeProblemDetailsSchema', + 'makeRetryableProblemDetailsSchema', + ]) { const factorySources = new Map([ ['contracts/factory.ts', `export const factory = ${factoryName};`], [ @@ -1669,13 +1917,16 @@ void test('follows local and imported aliases of Problem Details factories', () factory('InvalidProblem', 400, Extensions.UnsafeExtensions); `, ]) { - const aliasedSources = new Map([...factorySources, [contractApiFixturePath, entry] as const]); + const aliasedSources = new Map([ + ...factorySources, + [contractApiFixturePath, entry] as const, + ]); assert.match( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: aliasedSources, }) ?? '', - /must use concrete/u, + /must use concrete/u ); } } diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index a4a340716..063ccc0ee 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -7,7 +7,9 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { pathToFileURL } from 'node:url'; + import { Schema } from 'effect'; + import { getHelpText, runScaffold } from '../cli.mts'; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); diff --git a/app/scripts/scaffolding/tests/retire-contribution.test.mts b/app/scripts/scaffolding/tests/retire-contribution.test.mts index 5e6e8e209..d004d5c51 100644 --- a/app/scripts/scaffolding/tests/retire-contribution.test.mts +++ b/app/scripts/scaffolding/tests/retire-contribution.test.mts @@ -13,6 +13,7 @@ import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; + import { getHelpText, runScaffold } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; @@ -194,7 +195,10 @@ await test('retire-contribution help is write-free and documents the narrow kind const result = await runScaffold(RETIRE_CONTRIBUTION_COMMAND, ['--help'], { workspaceRoot: path.join(tmpdir(), 'retire-help-missing'), }); - assert.deepEqual(result, { help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), kind: 'help' }); + assert.deepEqual(result, { + help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), + kind: 'help', + }); assert.match(result.help, /--kind /u); }); diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index a44c8d9a2..c631a7de2 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,21 +1,12 @@ -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; import nodeTest from 'node:test'; -import { Clock, ConfigProvider, Predicate, Redacted } from 'effect'; -import { defineAction } from '../../../packages/core-runtime/src/actions/definition.ts'; -import { GatewayAssertionRedemptionService } from '../../../packages/core-runtime/src/auth/gateway-assertion-redemption.ts'; -import { defineSystemModuleEntrypoint } from '../../../packages/core-runtime/src/modules/module-entrypoint.ts'; -import { makeActionTestHarness } from '../../../packages/core-runtime/src/testing/actions.ts'; -import { TrustedPrincipalContextSchema } from '../../../packages/core-runtime/src/actions/principal-context.ts'; -import type { TrustedPrincipalContext } from '../../../packages/core-runtime/src/actions/principal-context.ts'; -import type { GatewayPrincipalVerifierLive } from '../../../packages/gateway-principal-verifier/src/server.ts'; -import type { bindActionHttpRunner as ActionHttpRunnerBinding } from '../../../verticals/party-registry/api/action-http-runner.ts'; +import { pathToFileURL } from 'node:url'; + import { defineEffectBff, Effect, @@ -29,19 +20,34 @@ import { Layer, Schema, } from '@modern-js/plugin-bff/effect-edge'; -import { - GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS, - GATEWAY_ASSERTION_TTL_SECONDS, -} from '../../../packages/shared-contracts/src/gateway-context.ts'; +import { Clock, ConfigProvider, Predicate, Redacted } from 'effect'; import { SignJWT, exportJWK, generateKeyPair, generateSecret, importJWK } from 'jose'; import type { JWK } from 'jose'; + +import type { GatewayIssuerConfigValue } from '../../../apps/shell-super-app/api/auth/gateway-issuer-config.ts'; import { issueGatewayContextAssertion, makeGatewayIssuerLayer, } from '../../../apps/shell-super-app/api/auth/gateway-issuer.ts'; -import type { GatewayIssuerConfigValue } from '../../../apps/shell-super-app/api/auth/gateway-issuer-config.ts'; -import { getHelpText, runScaffold } from '../cli.mts'; +import { defineAction } from '../../../packages/core-runtime/src/actions/definition.ts'; +import { TrustedPrincipalContextSchema } from '../../../packages/core-runtime/src/actions/principal-context.ts'; +import type { TrustedPrincipalContext } from '../../../packages/core-runtime/src/actions/principal-context.ts'; +import { GatewayAssertionRedemptionService } from '../../../packages/core-runtime/src/auth/gateway-assertion-redemption.ts'; +import { defineSystemModuleEntrypoint } from '../../../packages/core-runtime/src/modules/module-entrypoint.ts'; +import { makeActionTestHarness } from '../../../packages/core-runtime/src/testing/actions.ts'; +import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; +import type { GatewayPrincipalVerifierLive } from '../../../packages/gateway-principal-verifier/src/server.ts'; +import { + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS, + GATEWAY_ASSERTION_TTL_SECONDS, +} from '../../../packages/shared-contracts/src/gateway-context.ts'; +import type { bindActionHttpRunner as ActionHttpRunnerBinding } from '../../../verticals/party-registry/api/action-http-runner.ts'; import { hasValidGovernedHttpCompositionRoot } from '../../generated-governed-http-boundary.mts'; +import { + assertPublishedOutboxDependencyUsage, + publishedOutboxContractExports, +} from '../../published-outbox-contracts.mts'; +import { getHelpText, runScaffold } from '../cli.mts'; import type { ScaffoldCommand } from '../cli.mts'; import { GOVERNED_HTTP_API_ADDITION_SLOT_END, @@ -58,10 +64,6 @@ import { readGeneratedSlotEntries, } from '../shared.mts'; import type { JsonValue } from '../shared.mts'; -import { - assertPublishedOutboxDependencyUsage, - publishedOutboxContractExports, -} from '../../published-outbox-contracts.mts'; interface Fixture { readonly root: string; @@ -272,7 +274,9 @@ const problemFields = { title: Schema.String, type: Schema.String, }; -const asProblemDetails = HttpApiSchema.asJson({ contentType: 'application/problem+json' }); +const asProblemDetails = HttpApiSchema.asJson({ + contentType: 'application/problem+json', +}); const ActionAuthenticationProblemSchema = Schema.TaggedStruct( 'ActionAuthenticationProblem', problemFields, @@ -315,7 +319,9 @@ const generatedPrincipalErrorHandlers = { ActionPrincipalScopeError: failActionAuthentication, ActionPrincipalUnavailableError: failActionVerificationUnavailable, }; -const GeneratedBindingResultSchema = Schema.Struct({ accepted: Schema.Literal(true) }); +const GeneratedBindingResultSchema = Schema.Struct({ + accepted: Schema.Literal(true), +}); const generatedBindingAction = defineAction( { accessEvidencePolicy: { @@ -328,7 +334,10 @@ const generatedBindingAction = defineAction( domainEvents: {}, entrypoint: defineSystemModuleEntrypoint({ access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, entrypointKey: 'core.test.generated-action-http', moduleKey: 'core.shell', role: 'action', @@ -354,13 +363,13 @@ const InventoryLocaleSchema = Schema.Struct({ }); const decodeFixturePackage = (source: string) => - Schema.decodeUnknownSync(FixturePackageSchema, { onExcessProperty: 'preserve' })( - JSON.parse(source), - ); + Schema.decodeUnknownSync(FixturePackageSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(source)); const decodeInventoryLocale = (source: string) => - Schema.decodeUnknownSync(InventoryLocaleSchema, { onExcessProperty: 'preserve' })( - JSON.parse(source), - ); + Schema.decodeUnknownSync(InventoryLocaleSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(source)); const inventorySlug = 'inventory-stock'; const shellAppId = 'shell-super-app'; @@ -489,7 +498,7 @@ const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n` const inventoryHandlerRootFile = 'verticals/inventory-stock/api/index.ts'; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const require = createRequire(import.meta.url); -const createEntry = require.resolve('@modern-js/create'); +const createEntry = require.resolve('@modern-js/ultramodern-create'); const esbuildPath = require.resolve('esbuild/bin/esbuild', { paths: [path.dirname(createEntry)], }); @@ -502,7 +511,10 @@ const makeGatewayKey = async ( configuration: GatewayIssuerConfigValue; publicJwk: JWK; }> => { - const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); + const pair = await generateKeyPair('EdDSA', { + crv: 'Ed25519', + extractable: true, + }); const privateJwk = await exportJWK(pair.privateKey); const publicJwk = await exportJWK(pair.publicKey); return { @@ -911,7 +923,9 @@ test('documents every command and treats --help as a write-free operation', asyn test('search-provider access updates only generated access metadata and fails atomically on drift', async () => { await withFixture(async (fixture) => { - await mkdir(path.join(fixture.root, 'verticals/retired/node_modules'), { recursive: true }); + await mkdir(path.join(fixture.root, 'verticals/retired/node_modules'), { + recursive: true, + }); await addInventoryItemResourceType(fixture); await run(fixture, scaffoldCommand.searchProvider, [ scaffoldFlag.vertical, @@ -1056,8 +1070,12 @@ test('generated read clients fetch mounted owner URLs and support separately dep scaffoldFlag.resource, 'item', ]); - await mkdir(path.join(fixture.root, 'node_modules/@app'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules/@modern-js'), { recursive: true }); + await mkdir(path.join(fixture.root, 'node_modules/@app'), { + recursive: true, + }); + await mkdir(path.join(fixture.root, 'node_modules/@modern-js'), { + recursive: true, + }); await symlink( path.join(appRoot, sharedContractsPackagePath), path.join(fixture.root, sharedContractsNodeModulePath), @@ -1537,7 +1555,9 @@ test('governed contribution generators patch owner contracts and lazy adapters a ); assert.match(searchContract, /HttpApiGroup\.make\('inventoryItemsSearch'\)/u); - await mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true }); + await mkdir(path.join(fixture.root, 'node_modules', '@app'), { + recursive: true, + }); await mkdir(path.dirname(path.join(fixture.root, pluginBffNodeModulePath)), { recursive: true, }); @@ -2625,7 +2645,9 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 scaffoldFlag.vertical, 'billing', ]); - await mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true }); + await mkdir(path.join(fixture.root, 'node_modules', '@app'), { + recursive: true, + }); await symlink( path.join(appRoot, 'packages/core-runtime'), path.join(fixture.root, 'node_modules/@app/core-runtime'), @@ -2808,7 +2830,9 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 retiringAssertion.token, { ...environment, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [current.publicJwk] }), + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ + keys: [current.publicJwk], + }), }, 1_700_000_000 + GATEWAY_ASSERTION_TTL_SECONDS + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS + 1, ), @@ -3083,7 +3107,10 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 assert.equal( generatedBindingResponse.status, 200, - JSON.stringify({ body: generatedBindingBody, snapshot: harness.snapshot() }), + JSON.stringify({ + body: generatedBindingBody, + snapshot: harness.snapshot(), + }), ); assert.deepEqual(generatedBindingBody, { accepted: true }); assert.equal(harness.snapshot().invocations.length, 1); @@ -3661,7 +3688,10 @@ test('rejects Action generation when a vertical app identity is duplicated', asy billingPackagePath, json({ ...billingPackage, - modernjs: { ...billingPackage.modernjs, appId: inventoryVertical.appId }, + modernjs: { + ...billingPackage.modernjs, + appId: inventoryVertical.appId, + }, }), 'utf-8', ); @@ -4588,7 +4618,9 @@ test('renders a newly generated federated page with English and Czech owner reso '--page', 'customers', ]); - await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }); + await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { + recursive: true, + }); await Promise.all( ['react', 'react-dom'].map( async (packageName) => @@ -5488,7 +5520,10 @@ test('rejects page generation when an owning locale has no truthful starter tran packagePath, json({ ...packageJson, - exports: { ...packageJson.exports, './locales/de': './locales/de/inventory.json' }, + exports: { + ...packageJson.exports, + './locales/de': './locales/de/inventory.json', + }, }), 'utf-8', ); diff --git a/app/scripts/setup-agent-reference-repos.mts b/app/scripts/setup-agent-reference-repos.mts index 145f4599e..4628de1cf 100644 --- a/app/scripts/setup-agent-reference-repos.mts +++ b/app/scripts/setup-agent-reference-repos.mts @@ -1,526 +1,368 @@ -#!/usr/bin/env node -import { - NodeChildProcessSpawner, - NodeFileSystem, - NodePath, - NodeRuntime, - NodeStdio, - NodeTerminal, -} from '@effect/platform-node'; -import { - Config, - Context, - DateTime, - Effect, - FileSystem, - Layer, - Option, - Path, - Schema, - Stream, -} from 'effect'; -import { Command, Flag } from 'effect/unstable/cli'; -import { ChildProcess } from 'effect/unstable/process'; - -const LOG_PREFIX = '[agent-reference-repos]'; -const REPOSITORY_STRATEGY = 'git-subtree-squash' as const; -const WORKSPACE_ROOT = '.'; - -const ReferenceRepositorySchema = Schema.Struct({ - id: Schema.String, - name: Schema.String, - path: Schema.String, - readOnly: Schema.optional(Schema.Boolean), - ref: Schema.String, - url: Schema.String, -}); - -const ReferenceRepositoryConfigSchema = Schema.Struct({ - defaultEnabled: Schema.Boolean, - installDir: Schema.Literal('repos'), - repositories: Schema.Array(ReferenceRepositorySchema), - schemaVersion: Schema.Literal(1), - strategy: Schema.Literal(REPOSITORY_STRATEGY), -}); - -const InstalledRepositorySchema = Schema.Struct({ - commit: Schema.optional(Schema.String), - id: Schema.String, - installedAt: Schema.optional(Schema.DateTimeUtc), - name: Schema.String, - path: Schema.String, - readOnly: Schema.Boolean, - ref: Schema.String, - schemaVersion: Schema.optional(Schema.Literal(1)), - status: Schema.Literals(['installed', 'present']), - strategy: Schema.Literal(REPOSITORY_STRATEGY), - url: Schema.String, -}); - -const InstalledManifestSchema = Schema.Struct({ - generatedAt: Schema.DateTimeUtc, - installDir: Schema.Literal('repos'), - repositories: Schema.Array(InstalledRepositorySchema), - schemaVersion: Schema.Literal(1), - strategy: Schema.Literal(REPOSITORY_STRATEGY), -}); - -const ReferenceRepositoryConfigJsonSchema = Schema.fromJsonString(ReferenceRepositoryConfigSchema); -const InstalledManifestJsonSchema = Schema.fromJsonString(InstalledManifestSchema, { space: 2 }); - -type ReferenceRepository = typeof ReferenceRepositorySchema.Type; -type InstalledRepository = typeof InstalledRepositorySchema.Type; - -class AgentReferenceRepoSetupError extends Schema.TaggedError()( - 'AgentReferenceRepoSetupError', - { reason: Schema.String }, -) {} - -const truthy = (value: string): boolean => /^(?:1|true|yes|on)$/iu.test(value); -const falsy = (value: string): boolean => /^(?:0|false|no|off)$/iu.test(value); -const environmentValue = (name: string) => Config.string(name).pipe(Config.withDefault('')); -const identityValue = (value: string, fallback: string): string => - value.length > 0 ? value : fallback; - -const SetupEnvironment = Config.all({ - agentRepos: environmentValue('ULTRAMODERN_AGENT_REPOS'), - authorEmail: environmentValue('GIT_AUTHOR_EMAIL'), - authorName: environmentValue('GIT_AUTHOR_NAME'), - committerEmail: environmentValue('GIT_COMMITTER_EMAIL'), - committerName: environmentValue('GIT_COMMITTER_NAME'), - refresh: environmentValue('ULTRAMODERN_AGENT_REPOS_REFRESH'), - required: environmentValue('ULTRAMODERN_AGENT_REPOS_REQUIRED'), - skipAgentRepos: environmentValue('ULTRAMODERN_SKIP_AGENT_REPOS'), -}); - -interface RuntimeSettings { - readonly gitIdentity: Readonly>; - readonly refresh: boolean; - readonly required: boolean; - readonly skipRequested: boolean; -} - -const loadRuntimeSettings = Effect.fn('loadRuntimeSettings')(function* loadRuntimeSettingsEffect() { - const environment = yield* SetupEnvironment; - return { - gitIdentity: { - GIT_AUTHOR_EMAIL: identityValue(environment.authorEmail, 'ultramodern-agent-refs@local'), - GIT_AUTHOR_NAME: identityValue(environment.authorName, 'UltraModern Agent Reference Setup'), - GIT_COMMITTER_EMAIL: identityValue( - environment.committerEmail, - 'ultramodern-agent-refs@local', - ), - GIT_COMMITTER_NAME: identityValue( - environment.committerName, - 'UltraModern Agent Reference Setup', - ), - }, - refresh: truthy(environment.refresh), - required: truthy(environment.required), - skipRequested: truthy(environment.skipAgentRepos) || falsy(environment.agentRepos), - } satisfies RuntimeSettings; -}); - -const RuntimeConfiguration = Context.Service( - 'scripts/setup-agent-reference-repos/RuntimeConfiguration', -); - -const setupError = (reason: string) => new AgentReferenceRepoSetupError({ reason }); - -const commandFailure = ( - command: string, - commandArguments: readonly string[], - detail: string, -): AgentReferenceRepoSetupError => { - const invocation = [command, ...commandArguments].join(' '); - const detailSuffix = detail.length > 0 ? `: ${detail}` : ''; - return setupError(`${invocation} failed${detailSuffix}`); +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +const root = process.cwd(); +const args = new Set(process.argv.slice(2)); +const checkOnly = args.has('--check'); +const configPath = path.join(root, '.agents', 'agent-reference-repos.json'); +const manifestPath = path.join(root, '.modernjs', 'agent-reference-repos.json'); + +const truthy = (value) => /^(1|true|yes|on)$/i.test(String(value ?? '')); +const falsy = (value) => /^(0|false|no|off)$/i.test(String(value ?? '')); + +const skipRequested = + truthy(process.env.ULTRAMODERN_SKIP_AGENT_REPOS) || + falsy(process.env.ULTRAMODERN_AGENT_REPOS); +const required = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REQUIRED); +const refresh = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REFRESH); + +const gitIdentityEnv = { + GIT_AUTHOR_NAME: + process.env.GIT_AUTHOR_NAME || 'UltraModern Agent Reference Setup', + GIT_AUTHOR_EMAIL: + process.env.GIT_AUTHOR_EMAIL || 'ultramodern-agent-refs@local', + GIT_COMMITTER_NAME: + process.env.GIT_COMMITTER_NAME || 'UltraModern Agent Reference Setup', + GIT_COMMITTER_EMAIL: + process.env.GIT_COMMITTER_EMAIL || 'ultramodern-agent-refs@local', }; -interface CommandResult { - readonly status: number; - readonly stderr: string; - readonly stdout: string; +const log = (message) => console.log(`[agent-reference-repos] ${message}`); +const warn = (message) => console.warn(`[agent-reference-repos] ${message}`); + +function fail(message) { + if (required || checkOnly) { + throw new Error(message); + } + warn(message); +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } -const executeCommand = Effect.fn('executeCommand')(function* executeCommandEffect( - command: string, - commandArguments: readonly string[], - timeoutMilliseconds: number, -) { - const settings = yield* RuntimeConfiguration; - const invocation = ChildProcess.make(command, commandArguments, { - cwd: WORKSPACE_ROOT, - env: settings.gitIdentity, - extendEnv: true, - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', +function run(command, commandArgs, options = {}) { + const result = spawnSync(command, commandArgs, { + cwd: options.cwd ?? root, + encoding: 'utf-8', + env: { + ...process.env, + ...gitIdentityEnv, + ...(options.env ?? {}), + }, + stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'], + timeout: options.timeout ?? 120000, }); - return yield* Effect.scoped( - Effect.gen(function* collectCommandResultEffect() { - const handle = yield* invocation; - const [status, stdout, stderr] = yield* Effect.all( - [ - handle.exitCode.pipe(Effect.map(Number)), - handle.stdout.pipe(Stream.decodeText(), Stream.mkString), - handle.stderr.pipe(Stream.decodeText(), Stream.mkString), - ], - { concurrency: 'unbounded' }, - ); - return { status, stderr: stderr.trim(), stdout: stdout.trim() } satisfies CommandResult; - }), - ).pipe( - Effect.timeout(timeoutMilliseconds), - Effect.mapError((error) => commandFailure(command, commandArguments, String(error))), - ); -}); - -const runCommand = Effect.fn('runCommand')(function* runCommandEffect( - command: string, - commandArguments: readonly string[], - timeoutMilliseconds: number, -) { - const result = yield* executeCommand(command, commandArguments, timeoutMilliseconds); + if (result.error) { + throw result.error; + } if (result.status !== 0) { - return yield* commandFailure(command, commandArguments, result.stderr); + const stderr = result.stderr?.trim(); + throw new Error( + `${command} ${commandArgs.join(' ')} failed${stderr ? `: ${stderr}` : ''}` + ); } - return result.stdout; -}); - -const readConfig = Effect.fn('readConfig')(function* readConfigEffect(configPath: string) { - const fileSystem = yield* FileSystem.FileSystem; - const contents = yield* fileSystem - .readFileString(configPath) - .pipe(Effect.mapError(() => setupError(`Unable to read ${configPath}`))); - return yield* Schema.decodeUnknownEffect(ReferenceRepositoryConfigJsonSchema)(contents).pipe( - Effect.mapError(() => - setupError(`Invalid reference repository configuration at ${configPath}`), - ), - ); -}); + return result.stdout?.trim() ?? ''; +} -const assertSafeRepoPath = Effect.fn('assertSafeRepoPath')(function* assertSafeRepoPathEffect( - relativePath: string, -) { - const path = yield* Path.Path; +function assertSafeRepoPath(relativePath) { if ( + typeof relativePath !== 'string' || relativePath.length === 0 || path.isAbsolute(relativePath) || - relativePath.split(/[\\/]+/u).includes('..') || + relativePath.split(/[\\/]+/).includes('..') || !relativePath.startsWith('repos/') ) { - return yield* setupError(`Unsafe reference repository path: ${relativePath}`); + throw new Error(`Unsafe reference repository path: ${relativePath}`); } - return yield* Effect.void; -}); +} -const hasGit = Effect.fn('hasGit')(function* hasGitEffect() { - const result = yield* executeCommand('git', ['--version'], 30_000); +function hasGit() { + const result = spawnSync('git', ['--version'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }); return result.status === 0; -}); +} -const hasGitSubtree = Effect.fn('hasGitSubtree')(function* hasGitSubtreeEffect() { - const result = yield* executeCommand('git', ['subtree', '-h'], 30_000); +function hasGitSubtree() { + const result = spawnSync('git', ['subtree', '-h'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }); return ( - (result.status === 0 || result.status === 129) && result.stdout.includes('usage: git subtree') + (result.status === 0 || result.status === 129) && + result.stdout.includes('usage: git subtree') ); -}); +} -const isGitWorkTree = Effect.fn('isGitWorkTree')(function* isGitWorkTreeEffect() { - const result = yield* executeCommand('git', ['rev-parse', '--is-inside-work-tree'], 30_000); - return result.status === 0 && result.stdout === 'true'; -}); +function isGitWorkTree() { + const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { + cwd: root, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + return result.status === 0 && result.stdout.trim() === 'true'; +} -const hasCommits = Effect.fn('hasCommits')(function* hasCommitsEffect() { - const result = yield* executeCommand('git', ['rev-parse', '--verify', 'HEAD'], 30_000); +function hasCommits() { + const result = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], { + cwd: root, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }); return result.status === 0; -}); - -const porcelainStatus = Effect.fn('porcelainStatus')(function* porcelainStatusEffect() { - return yield* runCommand('git', ['status', '--porcelain'], 30_000); -}); - -const commitInstallerChanges = Effect.fn('commitInstallerChanges')( - function* commitInstallerChangesEffect(message: string) { - return yield* runCommand('git', ['commit', '--no-verify', '-m', message], 120_000); - }, -); - -const ensureGitRepository = Effect.fn('ensureGitRepository')(function* ensureGitRepositoryEffect( - checkOnly: boolean, -) { - if (!(yield* isGitWorkTree())) { +} + +function porcelainStatus() { + return run('git', ['status', '--porcelain'], { timeout: 30000 }); +} + +function commitInstallerChanges(message) { + run('git', ['commit', '-m', message], { + timeout: 120000, + }); +} + +function ensureGitRepository() { + if (!isGitWorkTree()) { if (checkOnly) { - return yield* setupError('workspace is not a git repository'); + fail('workspace is not a git repository'); + return false; } - yield* Effect.logInfo(`${LOG_PREFIX} initializing git repository for agent reference subtrees`); - yield* runCommand('git', ['init'], 30_000); + log('initializing git repository for agent reference subtrees'); + run('git', ['init'], { timeout: 30000 }); } - if (!(yield* hasCommits())) { + if (!hasCommits()) { if (checkOnly) { - return yield* setupError('workspace has no initial git commit'); + fail('workspace has no initial git commit'); + return false; } - yield* Effect.logInfo( - `${LOG_PREFIX} creating initial workspace commit before adding reference subtrees`, - ); - yield* runCommand('git', ['add', '-A'], 30_000); - yield* commitInstallerChanges('Initialize UltraModern workspace'); - return yield* Effect.void; + log('creating initial workspace commit before adding reference subtrees'); + run('git', ['add', '-A'], { timeout: 30000 }); + commitInstallerChanges('Initialize UltraModern workspace'); + return true; } - const status = yield* porcelainStatus(); - if (status.length > 0) { - return yield* setupError( - 'workspace has uncommitted changes; commit or stash them before installing reference subtrees', + const status = porcelainStatus(); + if (status) { + fail( + 'workspace has uncommitted changes; commit or stash them before installing reference subtrees' ); + return false; } - return yield* Effect.void; -}); -const remoteCommit = Effect.fn('remoteCommit')(function* remoteCommitEffect( - repository: ReferenceRepository, -) { - const branchOutput = yield* runCommand( - 'git', - ['ls-remote', repository.url, `refs/heads/${repository.ref}`], - 120_000, - ); - const output = - branchOutput.length > 0 - ? branchOutput - : yield* runCommand('git', ['ls-remote', repository.url, repository.ref], 120_000); - const commit = output.split(/\s+/u).at(0) ?? ''; - if (!/^[a-f\d]{40}$/iu.test(commit)) { - return yield* setupError(`Could not resolve ${repository.url}#${repository.ref}`); + return true; +} + +function remoteCommit(repo) { + let output = run('git', ['ls-remote', repo.url, `refs/heads/${repo.ref}`], { + timeout: 120000, + }); + if (!output) { + output = run('git', ['ls-remote', repo.url, repo.ref], { + timeout: 120000, + }); + } + const [commit] = output.split(/\s+/); + if (!/^[a-f0-9]{40}$/i.test(commit ?? '')) { + throw new Error(`Could not resolve ${repo.url}#${repo.ref}`); } return commit; -}); +} -const subtreeCommitExists = Effect.fn('subtreeCommitExists')(function* subtreeCommitExistsEffect( - repository: ReferenceRepository, -) { - const result = yield* executeCommand( +function subtreeCommitExists(repo) { + const result = spawnSync( 'git', - ['log', '--grep', `git-subtree-dir: ${repository.path}`, '--format=%H', '-n', '1'], - 30_000, - ); - return result.status === 0 && result.stdout.length > 0; -}); - -const installedManifestEntry = Effect.fn('installedManifestEntry')( - function* installedManifestEntryEffect(manifestPath: string, repository: ReferenceRepository) { - const fileSystem = yield* FileSystem.FileSystem; - if (!(yield* fileSystem.exists(manifestPath))) { - return Option.none(); + [ + 'log', + '--grep', + `git-subtree-dir: ${repo.path}`, + '--format=%H', + '-n', + '1', + ], + { + cwd: root, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], } - const repositories = yield* fileSystem.readFileString(manifestPath).pipe( - Effect.flatMap(Schema.decodeUnknownEffect(InstalledManifestJsonSchema)), - Effect.map((manifest) => manifest.repositories), - Effect.option, - ); - return repositories.pipe( - Option.flatMap((entries) => - Option.fromUndefinedOr(entries.find((entry) => entry.id === repository.id)), - ), - ); - }, -); - -const assertSubtreePresent = Effect.fn('assertSubtreePresent')(function* assertSubtreePresentEffect( - manifestPath: string, - repository: ReferenceRepository, -) { - yield* assertSafeRepoPath(repository.path); - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const targetPath = path.join(WORKSPACE_ROOT, repository.path); - if (!(yield* fileSystem.exists(targetPath))) { - return yield* setupError(`${repository.path} is missing`); + ); + return result.status === 0 && result.stdout.trim().length > 0; +} + +function installedManifestEntry(repo) { + if (!fs.existsSync(manifestPath)) { + return undefined; } - if (!(yield* subtreeCommitExists(repository))) { - return yield* setupError( - `${repository.path} is present but has no git-subtree commit evidence`, - ); + try { + const manifest = readJson(manifestPath); + return manifest.repositories?.find((entry) => entry.id === repo.id); + } catch { + return undefined; + } +} + +function assertSubtreePresent(repo) { + assertSafeRepoPath(repo.path); + const targetPath = path.join(root, repo.path); + if (!fs.existsSync(targetPath)) { + fail(`${repo.path} is missing`); + return undefined; + } + if (!subtreeCommitExists(repo)) { + fail(`${repo.path} is present but has no git-subtree commit evidence`); + return undefined; } - const installedEntry = yield* installedManifestEntry(manifestPath, repository); - return Option.getOrElse(installedEntry, (): InstalledRepository => ({ - id: repository.id, - name: repository.name, - path: repository.path, - readOnly: repository.readOnly !== false, - ref: repository.ref, - status: 'present', - strategy: REPOSITORY_STRATEGY, - url: repository.url, - })); -}); - -const addSubtree = Effect.fn('addSubtree')(function* addSubtreeEffect( - manifestPath: string, - repository: ReferenceRepository, -) { - yield* assertSafeRepoPath(repository.path); - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const settings = yield* RuntimeConfiguration; - const targetPath = path.join(WORKSPACE_ROOT, repository.path); - const existing = yield* fileSystem.exists(targetPath); - - if (existing && !settings.refresh) { - return yield* assertSubtreePresent(manifestPath, repository); + return ( + installedManifestEntry(repo) ?? { + id: repo.id, + name: repo.name, + url: repo.url, + ref: repo.ref, + path: repo.path, + readOnly: repo.readOnly !== false, + status: 'present', + strategy: 'git-subtree-squash', + } + ); +} + +function addSubtree(repo) { + assertSafeRepoPath(repo.path); + const targetPath = path.join(root, repo.path); + const existing = fs.existsSync(targetPath); + + if (existing && !refresh) { + return assertSubtreePresent(repo); } - if (existing) { - return yield* setupError( - `${repository.path} already exists; refresh for subtree references is intentionally manual`, + + if (existing && refresh) { + fail( + `${repo.path} already exists; refresh for subtree references is intentionally manual` ); + return undefined; } - const commit = yield* remoteCommit(repository); - yield* Effect.logInfo( - `${LOG_PREFIX} adding ${repository.name} as git subtree at ${repository.path} (${commit})`, - ); - yield* runCommand('git', ['fetch', '--depth', '1', repository.url, repository.ref], 300_000); - yield* runCommand( + if (checkOnly) { + fail(`${repo.path} is missing`); + return undefined; + } + + const commit = remoteCommit(repo); + log(`adding ${repo.name} as git subtree at ${repo.path} (${commit})`); + run('git', ['fetch', '--depth', '1', repo.url, repo.ref], { + timeout: 300000, + }); + run( 'git', [ 'subtree', 'add', '--prefix', - repository.path, + repo.path, 'FETCH_HEAD', '--squash', '-m', - `Add ${repository.name} agent reference repo`, + `Add ${repo.name} agent reference repo`, ], - 600_000, + { timeout: 600000 } ); - const installedAt = yield* DateTime.now; + return { - commit, - id: repository.id, - installedAt, - name: repository.name, - path: repository.path, - readOnly: repository.readOnly !== false, - ref: repository.ref, schemaVersion: 1, + id: repo.id, + name: repo.name, + url: repo.url, + ref: repo.ref, + commit, + path: repo.path, + readOnly: repo.readOnly !== false, + strategy: 'git-subtree-squash', status: 'installed', - strategy: REPOSITORY_STRATEGY, - url: repository.url, - } satisfies InstalledRepository; -}); - -const writeManifest = Effect.fn('writeManifest')(function* writeManifestEffect( - manifestPath: string, - entries: readonly InstalledRepository[], -) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const generatedAt = yield* DateTime.now; - const contents = yield* Schema.encodeEffect(InstalledManifestJsonSchema)({ - generatedAt, - installDir: 'repos', - repositories: entries, - schemaVersion: 1, - strategy: REPOSITORY_STRATEGY, - }).pipe(Effect.mapError(() => setupError('Unable to encode the agent reference manifest'))); - yield* fileSystem - .makeDirectory(path.dirname(manifestPath), { recursive: true }) - .pipe(Effect.mapError(() => setupError(`Unable to create the directory for ${manifestPath}`))); - yield* fileSystem - .writeFileString(manifestPath, `${contents}\n`) - .pipe(Effect.mapError(() => setupError(`Unable to write ${manifestPath}`))); -}); - -const commitManifestIfChanged = Effect.fn('commitManifestIfChanged')( - function* commitManifestIfChangedEffect(manifestPath: string) { - const status = yield* runCommand('git', ['status', '--porcelain', '--', manifestPath], 30_000); - if (status.length === 0) { - return yield* Effect.void; - } - yield* runCommand('git', ['add', manifestPath], 30_000); - yield* commitInstallerChanges('Record agent reference repo manifest'); - return yield* Effect.void; - }, -); - -const runSetup = Effect.fn('runSetup')(function* runSetupEffect(checkOnly: boolean) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const settings = yield* RuntimeConfiguration; - const configPath = path.join(WORKSPACE_ROOT, '.agents', 'agent-reference-repos.json'); - const manifestPath = path.join(WORKSPACE_ROOT, '.modernjs', 'agent-reference-repos.json'); - - if (!(yield* fileSystem.exists(configPath))) { - return yield* setupError('Missing .agents/agent-reference-repos.json'); + installedAt: new Date().toISOString(), + }; +} + +function writeManifest(entries) { + fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); + fs.writeFileSync( + manifestPath, + `${JSON.stringify( + { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + strategy: 'git-subtree-squash', + installDir: 'repos', + repositories: entries, + }, + null, + 2 + )}\n` + ); +} + +function commitManifestIfChanged() { + const status = run('git', ['status', '--porcelain', '--', manifestPath], { + timeout: 30000, + }); + if (!status) { + return; } - const config = yield* readConfig(configPath); - const enabled = config.defaultEnabled && !settings.skipRequested; + run('git', ['add', manifestPath], { timeout: 30000 }); + commitInstallerChanges('Record agent reference repo manifest'); +} + +function main() { + if (!fs.existsSync(configPath)) { + fail('Missing .agents/agent-reference-repos.json'); + return; + } + + const config = readJson(configPath); + const enabled = config.defaultEnabled !== false && !skipRequested; + if (!enabled) { - yield* Effect.logInfo( - `${LOG_PREFIX} setup skipped; set ULTRAMODERN_SKIP_AGENT_REPOS=0 to enable it again`, - ); - return yield* Effect.void; + log('setup skipped; set ULTRAMODERN_SKIP_AGENT_REPOS=0 to enable it again'); + return; + } + + if (!hasGit()) { + fail('git is required to install agent reference repositories'); + return; } - if (!(yield* hasGit())) { - return yield* setupError('git is required to install agent reference repositories'); + if (!hasGitSubtree()) { + fail('git subtree is required to install agent reference repositories'); + return; } - if (!(yield* hasGitSubtree())) { - return yield* setupError('git subtree is required to install agent reference repositories'); + if (!ensureGitRepository()) { + return; } - yield* ensureGitRepository(checkOnly); - - const entries = yield* Effect.forEach( - config.repositories, - (repository) => - checkOnly - ? assertSubtreePresent(manifestPath, repository) - : addSubtree(manifestPath, repository), - { concurrency: 1 }, - ); + + const entries = []; + for (const repo of config.repositories ?? []) { + const result = checkOnly ? assertSubtreePresent(repo) : addSubtree(repo); + if (result) { + entries.push(result); + } + } + if (!checkOnly) { - yield* writeManifest(manifestPath, entries); - yield* commitManifestIfChanged(manifestPath); + writeManifest(entries); + commitManifestIfChanged(); } - return yield* Effect.void; -}); - -const reportSetupFailure = (checkOnly: boolean) => (error: AgentReferenceRepoSetupError) => - Effect.gen(function* reportSetupFailureEffect() { - const settings = yield* RuntimeConfiguration; - if (settings.required || checkOnly) { - yield* Effect.logError(`${LOG_PREFIX} ${error.reason}`); - return yield* error; - } - yield* Effect.logWarning(`${LOG_PREFIX} ${error.reason}`); - return yield* Effect.void; - }); +} -const setupCommand = Command.make( - 'setup-agent-reference-repos', - { checkOnly: Flag.boolean('check') }, - ({ checkOnly }) => - runSetup(checkOnly).pipe( - Effect.catchTag('AgentReferenceRepoSetupError', reportSetupFailure(checkOnly)), - ), -); - -const corePlatformLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer); -const childProcessLayer = NodeChildProcessSpawner.layer.pipe(Layer.provide(corePlatformLayer)); -const runtimeConfigurationLayer = Layer.effect(RuntimeConfiguration, loadRuntimeSettings()); -const applicationLayer = Layer.mergeAll( - corePlatformLayer, - childProcessLayer, - NodeStdio.layer, - NodeTerminal.layer, - runtimeConfigurationLayer, -); - -const executableLayer = Layer.effectDiscard(Command.run(setupCommand, { version: '1.0.0' })).pipe( - Layer.provide(applicationLayer), -); - -NodeRuntime.runMain(Effect.scoped(Layer.build(executableLayer))); +try { + main(); +} catch (error) { + if (required || checkOnly) { + console.error(`[agent-reference-repos] ${error.message}`); + process.exitCode = 1; + } else { + warn(error.message); + } +} diff --git a/app/scripts/shared/ultramodern-command.mts b/app/scripts/shared/ultramodern-command.mts index 1bf128c64..331213b14 100644 --- a/app/scripts/shared/ultramodern-command.mts +++ b/app/scripts/shared/ultramodern-command.mts @@ -29,12 +29,12 @@ export const resolveUltramodernInvocation = (options: CommandOptions) => ); const forwardedArgs = yield* stdio.args; const args = ['ultramodern', options.command, ...forwardedArgs]; - const nodeExecutable = options.nodeExecutable ?? 'node'; + const nodeExecutable = options.nodeExecutable ?? process.execPath; const launch = Option.match(createBin, { onNone: () => ({ args, - executable: 'modern-js-create', - target: 'modern-js-create from PATH', + executable: 'ultramodern-create', + target: 'ultramodern-create from PATH', }), onSome: (bin) => ({ args: [bin, ...args], diff --git a/app/scripts/shared/ultramodern-launch.mts b/app/scripts/shared/ultramodern-launch.mts index fd48f5647..c2f06f2a3 100644 --- a/app/scripts/shared/ultramodern-launch.mts +++ b/app/scripts/shared/ultramodern-launch.mts @@ -10,9 +10,9 @@ export const ultramodernLaunch = ( const launch = Option.match(createBin, { onNone: () => ({ args: ultramodernArgs, - executable: 'modern-js-create', + executable: 'ultramodern-create', shell: pathSeparator === '\\', - target: 'modern-js-create from PATH', + target: 'ultramodern-create from PATH', }), onSome: (bin) => ({ args: [bin, ...ultramodernArgs], diff --git a/app/scripts/shared/ultramodern-wrapper-source.mts b/app/scripts/shared/ultramodern-wrapper-source.mts index 551a8ec01..0576d6824 100644 --- a/app/scripts/shared/ultramodern-wrapper-source.mts +++ b/app/scripts/shared/ultramodern-wrapper-source.mts @@ -18,7 +18,7 @@ const withoutComments = (source: string): string => { const hasSharedUltramodernDispatch = (source: string): boolean => source.includes("Config.string('ULTRAMODERN_CREATE_BIN')") && source.includes("['ultramodern', options.command, ...forwardedArgs]") && - source.includes("executable: 'modern-js-create'") && + source.includes("executable: 'ultramodern-create'") && source.includes('ChildProcess.make(launch.executable, launch.args,') && source.includes('resolveUltramodernInvocation(options).pipe(') && source.includes('Effect.flatMap(launchUltramodern)'); @@ -33,7 +33,7 @@ export const hasUltramodernSkillsDispatch = (source: string, implementation: str wrapper.includes("['ultramodern', ...skillArgs]") && wrapper.includes('ultramodernLaunch(createBin, ultramodernArgs, workspaceRoot, path.sep)') && wrapper.includes("Config.string('ULTRAMODERN_CREATE_BIN')") && - runner.includes("executable: 'modern-js-create'") && + runner.includes("executable: 'ultramodern-create'") && runner.includes('ChildProcess.make(launch.executable, launch.args,') ); }; diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index b0afde6e0..a09d1ddf6 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -2,8 +2,8 @@ import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import os from 'node:os'; @@ -12,22 +12,30 @@ import test from 'node:test'; import type { TestContext } from 'node:test'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; + import { Predicate, Schema } from 'effect'; import type { Effect as EffectType } from 'effect/Effect'; import { build as bundleSource, transform } from 'esbuild'; import { format } from 'oxfmt'; + import { MicroVerticalReadinessSchema } from '../../packages/shared-contracts/src/microvertical-api-baseline.ts'; import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation as microVerticalApiBaselineViolationForFile, } from '../microvertical-api-baseline-boundary.mts'; import type { MicroVerticalApiBaselineExpectation } from '../microvertical-api-baseline-boundary.mts'; - import { strictEffectRuntimeTopologyViolation } from '../ultramodern-api-boundary-rules.mts'; +import { moduleFederationBridgeViolation } from '../module-federation-bridge-boundary.mts'; +import { hasValidGovernedHttpCompositionRoot } from '../generated-governed-http-boundary.mts'; +import { + hasGeneratedOperationGatewayContract, + hasGeneratedOperationPrincipalContract, +} from '../generated-module-api-boundary.mts'; const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); const partyId = 'party-registry'; const partyDirectory = 'verticals/party-registry'; +const partySharedApiPath = `${partyDirectory}/shared/api.ts`; const generatedFixtureId = 'inventory-stock'; const generatedApiPrefix = '/inventory-stock-api'; const generatedServiceModuleName = 'api/service'; @@ -67,7 +75,6 @@ const ssrBundlePath = 'bundles/index.js'; const apiBundlePath = 'api/index.js'; const routesManifestFile = 'routes-manifest.json'; const mfManifestFile = 'mf-manifest.json'; -const generatedProofScope = 'generated-proof'; const generatedClientContractImport = '../../shared/api.ts'; const generatedSharedApiModule = 'api/shared'; const generatedSharedContractsPackage = '@generated-proof/shared-contracts'; @@ -111,7 +118,9 @@ const unexpectedTopologyImport = (specifier: string): never => { throw new Error(`Unexpected strict-topology import: ${specifier}`); }; -const generatorRoot = await realpath(path.join(workspaceRoot, 'node_modules/@modern-js/create')); +const generatorRoot = await realpath( + path.join(workspaceRoot, 'node_modules/@modern-js/ultramodern-create'), +); const require = createRequire(import.meta.url); const AppIdSchema = Schema.String.pipe(Schema.brand('AppId')); @@ -132,8 +141,14 @@ const BuildArtifactSchema = Schema.Struct({ kind: Schema.String, schemaVersion: Schema.Number, surfaces: Schema.Struct({ - api: Schema.Struct({ ...IdentitySchema.fields, surface: Schema.Literal('api') }), - ui: Schema.Struct({ ...IdentitySchema.fields, surface: Schema.Literal('ui') }), + api: Schema.Struct({ + ...IdentitySchema.fields, + surface: Schema.Literal('api'), + }), + ui: Schema.Struct({ + ...IdentitySchema.fields, + surface: Schema.Literal('ui'), + }), }), }); interface ReleaseEnvelope { @@ -212,13 +227,9 @@ interface ApiGeneratorFixture { readonly exposes?: Readonly>; readonly id: string; } -type CreateSharedApi = (scope: string, app: ApiGeneratorFixture) => string; +type CreateSharedApi = (app: ApiGeneratorFixture) => string; type CreateApiClient = (app: WorkspaceAppFixture, contractImportPath: string) => string; -type CreateApiServiceEntry = ( - scope: string, - app: ApiGeneratorFixture, - contractImportPath: string, -) => string; +type CreateApiServiceEntry = (app: ApiGeneratorFixture, contractImportPath: string) => string; interface GeneratedWorkspaceScriptArtifact { readonly content: string; readonly relativePath: string; @@ -340,7 +351,9 @@ const SharedApiGeneratorModuleSchema = Schema.Struct({ const StrictEffectApiBoundaryRuleModuleSchema = Schema.Struct({ createStrictEffectApiBoundariesRule: callable(), }); -const ComponentModuleSchema = Schema.Struct({ createLayout: callable() }); +const ComponentModuleSchema = Schema.Struct({ + createLayout: callable(), +}); const FederationConfigModuleSchema = Schema.Struct({ createAppModernConfig: callable(), createBackendModuleFederationConfig: callable(), @@ -348,8 +361,12 @@ const FederationConfigModuleSchema = Schema.Struct({ const BuildModuleGeneratorSchema = Schema.Struct({ createUltramodernBuildModule: callable(), }); -const SharedApiGeneratorSchema = Schema.Struct({ createSharedApi: callable() }); -const ApiClientGeneratorSchema = Schema.Struct({ createApiClient: callable() }); +const SharedApiGeneratorSchema = Schema.Struct({ + createSharedApi: callable(), +}); +const ApiClientGeneratorSchema = Schema.Struct({ + createApiClient: callable(), +}); const ApiServiceGeneratorSchema = Schema.Struct({ createApiServiceEntry: callable(), }); @@ -357,7 +374,9 @@ const WorkspaceScriptsGeneratorSchema = Schema.Struct({ migratedWorkspaceScriptArtifacts: callable(), }); const GeneratedApiRuntimeModuleSchema = Schema.Struct({ - default: Schema.Struct({ createHandler: callable() }), + default: Schema.Struct({ + createHandler: callable(), + }), }); const CloudflareEvidenceSchema = Schema.Struct({ assertions: Schema.Array(Schema.Struct({ status: Schema.String, type: Schema.String })), @@ -368,7 +387,9 @@ const CloudflareProofModuleSchema = Schema.Struct({ const ModuleFederationValidationModuleSchema = Schema.Struct({ validateModuleFederationTypes: callable(), }); -const ModuleFederationValidationResultSchema = Schema.Struct({ hostOnlyAppCount: Schema.Number }); +const ModuleFederationValidationResultSchema = Schema.Struct({ + hostOnlyAppCount: Schema.Number, +}); const ModuleFederationInspectionModuleSchema = Schema.Struct({ inspectModuleFederationConfigSource: callable(), }); @@ -384,7 +405,9 @@ const CompilerStatsFixtureSchema = Schema.Struct({ hasErrors: callable(), toString: callable(), }); -const CompiledReaderSchema = Schema.Struct({ allowedOrigins: Schema.Array(Schema.String) }); +const CompiledReaderSchema = Schema.Struct({ + allowedOrigins: Schema.Array(Schema.String), +}); const PackageJsonSchema = Schema.Struct({ scripts: Schema.Record(Schema.String, Schema.String), }); @@ -472,7 +495,10 @@ const writeText = async (root: string, logicalPath: string, value: string): Prom const runNode = ( argumentsList: readonly string[], - options: { readonly cwd?: string; readonly env?: Readonly> } = {}, + options: { + readonly cwd?: string; + readonly env?: Readonly>; + } = {}, ): string => execFileSync(process.execPath, argumentsList, { cwd: options.cwd, @@ -480,12 +506,22 @@ const runNode = ( env: options.env, } satisfies ExecFileSyncOptionsWithStringEncoding); -const releaseFrameworkRoot = path.join( - workspaceRoot, - 'verticals/party-registry/node_modules/@modern-js/app-tools/dist', +const appToolsRequire = createRequire( + await realpath( + path.join( + workspaceRoot, + 'verticals/party-registry/node_modules/@modern-js/app-tools/package.json', + ), + ), +); +const releaseFrameworkRoot = path.resolve( + path.dirname( + appToolsRequire.resolve('@modern-js/app-tools-extensions/release-envelope/framework-output'), + ), + '../..', ); const releaseFramework = await loadReleaseFramework( - path.join(releaseFrameworkRoot, 'esm-node/ultramodern-release-envelope/framework-output.mjs'), + path.join(releaseFrameworkRoot, 'esm-node/release-envelope/framework-output.mjs'), ); void test('MicroVertical templates use the shared strict Effect BFF assembly primitive', async () => { @@ -501,9 +537,12 @@ void test('MicroVertical templates use the shared strict Effect BFF assembly pri ); const packageGenerator = Schema.decodeUnknownSync(PackageGeneratorModuleSchema)(packageModule); const source = apiServiceGenerator.createApiServiceEntry( - 'fixture', { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + api: { + consumedBy: [], + prefix: generatedApiPrefix, + stem: generatedFixtureId, + }, id: generatedFixtureId, }, generatedSharedApiImport, @@ -1367,7 +1406,7 @@ const strictBoundaryReports = ( }; const reportsAssemblyViolation = (messages: readonly string[]): boolean => messages.some((message) => - /server-only shared Effect BFF assembly helper|explicitly composed handler Layer/u.test( + /server-only shared Effect BFF assembly helper|explicitly composed handler Layer|Generated API entries must export defineEffectBff|Generated API entries must implement handlers through HttpApiBuilder/u.test( message, ), ); @@ -1388,15 +1427,17 @@ void test('published lint validators reject comment, string, and local strict-ro apiServiceSource, ); const generatedSource = apiServiceGenerator.createApiServiceEntry( - 'app', { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + api: { + consumedBy: [], + prefix: generatedApiPrefix, + stem: generatedFixtureId, + }, id: generatedFixtureId, }, generatedSharedApiImport, ); const generatedRpcSource = apiServiceGenerator.createApiServiceEntry( - 'app', { api: { consumedBy: [], @@ -1721,7 +1762,11 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', sharedApiSource, ); const descriptor = { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + api: { + consumedBy: [], + prefix: generatedApiPrefix, + stem: generatedFixtureId, + }, id: generatedFixtureId, } as const; const fixture = await mkdtemp( @@ -1731,9 +1776,9 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', await writeText( fixture, apiIndexFile, - apiServiceGenerator.createApiServiceEntry('app', descriptor, generatedSharedApiImport), + apiServiceGenerator.createApiServiceEntry(descriptor, generatedSharedApiImport), ); - await writeText(fixture, sharedApiFile, sharedApiGenerator.createSharedApi('app', descriptor)); + await writeText(fixture, sharedApiFile, sharedApiGenerator.createSharedApi(descriptor)); await writeText( fixture, buildMarkerFile, @@ -1820,7 +1865,11 @@ const releaseFixture = async (context: TestContext) => { }); await putJson(mfManifestFile, manifest); await putJson(routesManifestFile, { - routeAssets: { index: { assets: [`https://assets.example.test/app/${compiledUiAssetPath}`] } }, + routeAssets: { + index: { + assets: [`https://assets.example.test/app/${compiledUiAssetPath}`], + }, + }, }); await putJson('route.json', { routes: [{ bundle: ssrBundlePath }] }); await putJson('package.json', { type: 'module' }); @@ -1847,7 +1896,7 @@ void test('empty MF producers retain complete build and Node staged release evid path.join( releaseFrameworkRoot, moduleFormat, - `ultramodern-release-envelope/framework-output.${extension}`, + `release-envelope/framework-output.${extension}`, ), ); const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ @@ -1864,7 +1913,9 @@ void test('empty MF producers retain complete build and Node staged release evid outputDirectory: fixture.root, }); assert.ok(staged.surfaces.uiClient.includes(compiledUiAssetPath)); - await framework.verifyNodeReleaseEnvelopeStaging({ outputDirectory: fixture.root }); + await framework.verifyNodeReleaseEnvelopeStaging({ + outputDirectory: fixture.root, + }); }), ); const fixture = await releaseFixture(context); @@ -1890,7 +1941,7 @@ void test('empty MF producers bind root-relative route assets when publicPath is path.join( releaseFrameworkRoot, moduleFormat, - `ultramodern-release-envelope/framework-output.${extension}`, + `release-envelope/framework-output.${extension}`, ), ); const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ @@ -1932,11 +1983,20 @@ void test('empty-producer fallback rejects undeclared, foreign, traversing, miss const invalidManifests = [ { ...baseline.manifest, exposes: [{ name: './Page' }] }, { ...baseline.manifest, remotes: [{ name: 'shell' }] }, - { metaData: baseline.manifest.metaData, remotes: baseline.manifest.remotes }, - { exposes: baseline.manifest.exposes, metaData: baseline.manifest.metaData }, + { + metaData: baseline.manifest.metaData, + remotes: baseline.manifest.remotes, + }, + { + exposes: baseline.manifest.exposes, + metaData: baseline.manifest.metaData, + }, { ...baseline.manifest, - metaData: { ...baseline.manifest.metaData, remoteEntry: { name: '', path: '' } }, + metaData: { + ...baseline.manifest.metaData, + remoteEntry: { name: '', path: '' }, + }, }, ]; await Promise.all( @@ -1968,13 +2028,19 @@ void test('empty MF producers cannot bypass backend, SSR, revision, or identity const fixture = await releaseFixture(context); await fixture.putJson('backend-mf-manifest.json', { backendFederation: { - deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'b'.repeat(40) }, + deliveryUnit: { + ...fixture.artifact.deliveryUnit, + sourceRevision: 'b'.repeat(40), + }, }, }); await assert.rejects(fixture.emit, /must match/u); const workspaceArtifact = { ...fixture.artifact, - deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'workspace' }, + deliveryUnit: { + ...fixture.artifact.deliveryUnit, + sourceRevision: 'workspace', + }, surfaces: { api: { ...fixture.artifact.surfaces.api, sourceRevision: 'workspace' }, ui: { ...fixture.artifact.surfaces.ui, sourceRevision: 'workspace' }, @@ -1984,7 +2050,9 @@ void test('empty MF producers cannot bypass backend, SSR, revision, or identity await assert.rejects(fixture.emit, /workspace/u); }); -const GlobalVarsSchema = Schema.Struct({ ULTRAMODERN_SHELL_ORIGIN: Schema.String }); +const GlobalVarsSchema = Schema.Struct({ + ULTRAMODERN_SHELL_ORIGIN: Schema.String, +}); const evaluatePartyBuildGlobalVars = async (shellOrigin: string) => { const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-party-config-')); @@ -1997,12 +2065,22 @@ const evaluatePartyBuildGlobalVars = async (shellOrigin: string) => { const effectModuleUrl = pathToFileURL( require.resolve('effect', { paths: [workspaceRoot] }), ).href; - const { code } = await transform(configSource, { format: 'cjs', loader: 'ts' }); + const { code } = await transform(configSource, { + define: { + 'import.meta.url': JSON.stringify( + pathToFileURL(path.join(workspaceRoot, 'verticals/party-registry/modern.config.ts')).href, + ), + }, + format: 'cjs', + loader: 'ts', + }); await writeFile( harnessPath, `import * as effect from ${JSON.stringify(effectModuleUrl)}; import * as sharedBuild from ${JSON.stringify(pathToFileURL(path.join(workspaceRoot, 'packages/shared-contracts/tooling/modern-config.ts')).href)}; import { runInNewContext } from 'node:vm'; +import * as nodeUrl from 'node:url'; +import * as nodePath from 'node:path'; const framework = { ...sharedBuild, appTools: () => ({}), @@ -2021,7 +2099,8 @@ const module = { exports: {} }; runInNewContext(${JSON.stringify(code)}, { exports: module.exports, module, - require: specifier => specifier === 'effect' ? effect : framework, + URL, + require: specifier => specifier === 'effect' ? effect : specifier === 'node:url' ? nodeUrl : specifier === 'node:path' ? nodePath : framework, }); process.stdout.write(JSON.stringify(module.exports.default.source.globalVars)); `, @@ -2081,7 +2160,11 @@ void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin with }, ], }, - output: { filename: 'reader.cjs', library: { type: 'commonjs2' }, path: temporaryRoot }, + output: { + filename: 'reader.cjs', + library: { type: 'commonjs2' }, + path: temporaryRoot, + }, plugins: [definePlugin], target: 'node', }); @@ -2099,7 +2182,10 @@ void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin with toString: statsSource.toString, }); const hasErrors = stats.hasErrors.bind(statsSource)(); - const errorText = stats.toString.bind(statsSource)({ all: false, errors: true }); + const errorText = stats.toString.bind(statsSource)({ + all: false, + errors: true, + }); assert.equal(hasErrors, false, errorText); } finally { await closeCompiler(); @@ -2117,11 +2203,79 @@ const normalizedGeneratedSource = async (fileName: string, source: string) => { assert.deepEqual(result.errors, []); return result.code.replaceAll(/^\s*\n/gmu, ''); }; +const ScaffoldSemanticKindSchema = Schema.Literals(['backend', 'layout']); +type ScaffoldSemanticKind = typeof ScaffoldSemanticKindSchema.Type; +const scaffoldSemanticKinds = new Map([ + ['backend-federation.config.ts', 'backend'], + ['src/routes/layout.tsx', 'layout'], +]); + +// Evaluate only controlled scaffolds with inert dependency adapters in a separate Node process. +// No application server, deployment, real plugin or environment file is loaded by this harness. +const evaluateScaffoldSemantics = async ( + source: string, + kind: ScaffoldSemanticKind, +): Promise => { + const scratchRoot = path.join(workspaceRoot, '.scratch'); + await mkdir(scratchRoot, { recursive: true }); + const fixture = await mkdtemp(path.join(scratchRoot, 'scaffold-semantics-')); + try { + const effectUrl = pathToFileURL(require.resolve('effect')).href; + const { code } = await transform(source, { + define: { 'import.meta.url': JSON.stringify('file:///fixture/config.ts') }, + format: 'cjs', + jsxFactory: 'element', + loader: kind === 'layout' ? 'tsx' : 'ts', + }); + const harnessPath = path.join(fixture, 'evaluate.mjs'); + await writeFile( + harnessPath, + ` +import * as effect from ${JSON.stringify(effectUrl)}; +import { runInNewContext } from 'node:vm'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +const kind = ${JSON.stringify(kind)}; +const pluginNames = ['appTools', 'bffPlugin', 'i18nPlugin', 'tanstackRouterPlugin', 'moduleFederationPlugin', 'pluginTailwindcss', 'ultramodernReleaseEnvelopePlugin']; +const framework = { + ...Object.fromEntries(pluginNames.map(name => [name, () => ({ name })])), + builtinModules: [], createRequire: () => name => ({ version: name === 'effect/package.json' ? '4.0.0-rc.112' : '3.9.0-ultramodern.2' }), + defineConfig: config => config, presetUltramodern: config => config, + createModuleFederationConfig: config => config, + getBuildConfigEnvironment: () => undefined, ultramodernLocalisedUrls: {}, +}; +const module = { exports: {} }; +runInNewContext(${JSON.stringify(code)}, { + module, exports: module.exports, URL, + element: (type, props, ...children) => ({ type, props, children }), + require: specifier => { + if (specifier === 'effect') return effect; + if (specifier === 'effect/Schema') return effect.Schema; + if (specifier === 'node:url') return { fileURLToPath }; + if (specifier === 'node:path') return path; + if (specifier === './package.json') return { dependencies: { '@module-federation/runtime': '2.9.0' } }; + if (specifier === '@modern-js/plugin-tanstack/runtime') return { Outlet: 'Outlet' }; + if (specifier === './index.css') return {}; + return framework; + }, +}); +let evidence = module.exports; +if (kind === 'layout') evidence = evidence.default(); +if (kind === 'backend') evidence = evidence.default; +process.stdout.write(JSON.stringify(evidence)); +`, + ); + return runNode([harnessPath]); + } finally { + await rm(fixture, { force: true, recursive: true }); + } +}; const evaluatedInfrastructureSource = async ( fileName: string, source: string, cloudflare: boolean, + injection: Readonly>, ): Promise => { const partyRoot = path.join(workspaceRoot, partyDirectory); const result = await bundleSource({ @@ -2179,7 +2333,7 @@ const moduleShim = { ...nodeModule, createRequire: () => Object.assign(() => ({} const module = { exports: {} }; runInNewContext(${JSON.stringify(code)}, { exports: module.exports, module, URL, - ULTRAMODERN_BUILD_MARKER: 'injected-build', ULTRAMODERN_SOURCE_REVISION: 'injected-revision', + ...${JSON.stringify(injection)}, __resolve: name => 'file:///dependencies/' + name, require: name => ({ effect, '@app/shared-contracts/ultramodern-build': buildIdentity, 'node:module': moduleShim, 'node:path': nodePath, 'node:url': nodeUrl }[name] ?? framework), }); @@ -2282,27 +2436,38 @@ void test('all published scaffold formats retain Party infrastructure behavior a path.join(workspaceRoot, partyDirectory, fileName), 'utf-8', ); - if (fileName === 'modern.config.ts' || fileName === 'shared/ultramodern-build.ts') { + if (fileName === 'modern.config.ts' || fileName === buildMarkerFile) { + const injections: Readonly>[] = [ + {}, + { + ULTRAMODERN_BUILD_MARKER: 'executed-build', + ULTRAMODERN_SOURCE_REVISION: 'a'.repeat(40), + }, + ]; await Promise.all( - [false, true].map(async (cloudflare) => { - const [expected, evaluated] = await Promise.all([ - evaluatedInfrastructureSource(fileName, source, cloudflare), - evaluatedInfrastructureSource(fileName, actual, cloudflare), - ]); - const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json)); - assert.deepEqual( - decode(expected), - decode(evaluated), - `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior`, - ); - }), + [false, true].flatMap((cloudflare) => + injections.map(async (injection) => { + const [expected, evaluated] = await Promise.all([ + evaluatedInfrastructureSource(fileName, source, cloudflare, injection), + evaluatedInfrastructureSource(fileName, actual, cloudflare, injection), + ]); + const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json)); + assert.deepEqual( + decode(expected), + decode(evaluated), + `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior`, + ); + }), + ), ); return; } + const kind = scaffoldSemanticKinds.get(fileName); + assert.ok(kind, `Unknown scaffold ${fileName}`); assert.equal( - await normalizedGeneratedSource(fileName, source), - await normalizedGeneratedSource(fileName, actual), - `${moduleFormat}: ${fileName} must match the controlled scaffold`, + await evaluateScaffoldSemantics(source, kind), + await evaluateScaffoldSemantics(actual, kind), + `${moduleFormat}: ${fileName} must preserve typed runtime, ownership and release gates`, ); }), ); @@ -2341,13 +2506,9 @@ void test('all published scaffold formats generate the shared MicroVertical API const descriptor: unknown = descriptorModule.createVerticalDescriptor(inventoryStockId, 4103); Schema.asserts(WorkspaceAppFixtureSchema, descriptor); const app = { ...descriptor, exposes: {} }; - const contract = sharedApiModule.createSharedApi(generatedProofScope, app); + const contract = sharedApiModule.createSharedApi(app); const client = clientModule.createApiClient(app, generatedClientContractImport); - const service = serviceModule.createApiServiceEntry( - generatedProofScope, - app, - generatedSharedApiImport, - ); + const service = serviceModule.createApiServiceEntry(app, generatedSharedApiImport); assert.match(contract, /MicroVerticalBuildMarkerSchema/u, moduleFormat); assert.match(contract, /MicroVerticalReadinessSchema/u, moduleFormat); @@ -2375,12 +2536,13 @@ void test('all published scaffold formats generate the shared MicroVertical API const customStemApp = { ...app, - api: { ...app.api, prefix: warehouseApiPrefix, stem: warehouseItemsApiStem }, + api: { + ...app.api, + prefix: warehouseApiPrefix, + stem: warehouseItemsApiStem, + }, }; - const customStemContract = sharedApiModule.createSharedApi( - generatedProofScope, - customStemApp, - ); + const customStemContract = sharedApiModule.createSharedApi(customStemApp); assert.equal( microVerticalApiBaselineViolation(warehouseItemsApiStem, customStemContract, { ...generatedBaselineExpectation, @@ -2396,7 +2558,7 @@ void test('all published scaffold formats generate the shared MicroVertical API 4105, ); Schema.asserts(WorkspaceAppFixtureSchema, checkoutDescriptor); - const checkoutContract = sharedApiModule.createSharedApi(generatedProofScope, { + const checkoutContract = sharedApiModule.createSharedApi({ ...checkoutDescriptor, exposes: {}, }); @@ -2512,16 +2674,12 @@ void test('all published scaffold formats emit the executable AST baseline valid await writeText( checkoutWorkspace, 'verticals/shopping/shared/api.ts', - sharedApiModule.createSharedApi(generatedProofScope, checkoutStemDescriptor), + sharedApiModule.createSharedApi(checkoutStemDescriptor), ); await writeText( checkoutWorkspace, 'verticals/shopping/api/index.ts', - serviceModule.createApiServiceEntry( - generatedProofScope, - checkoutStemDescriptor, - generatedSharedApiImport, - ), + serviceModule.createApiServiceEntry(checkoutStemDescriptor, generatedSharedApiImport), ); await writeText( checkoutWorkspace, @@ -2679,7 +2837,7 @@ void test('two generated MicroVertical root contracts execute invariant readines api: { ...descriptor.api, prefix: fixture.prefix, stem: fixture.stem }, exposes: {}, }; - const contract = sharedApiModule.createSharedApi('app', generatedDescriptor); + const contract = sharedApiModule.createSharedApi(generatedDescriptor); const basePath = `${fixture.prefix}/${fixture.stem}`; assert.equal( microVerticalApiBaselineViolation(fixture.stem, contract, { @@ -2717,11 +2875,7 @@ void test('two generated MicroVertical root contracts execute invariant readines await writeText( ownerRoot, apiIndexFile, - apiServiceModule.createApiServiceEntry( - 'app', - generatedDescriptor, - generatedSharedApiImport, - ), + apiServiceModule.createApiServiceEntry(generatedDescriptor, generatedSharedApiImport), ); const clientEntryPath = `src/api/${fixture.id}-client.ts`; await writeText( @@ -2900,7 +3054,7 @@ void test('repository checker respects custom readiness prefixes and diagnoses m }; const ownerPath = `verticals/${inventoryStockId}`; const readinessContract = shared - .createSharedApi('app', app) + .createSharedApi(app) .replace( /(?\.addHttpApi\(warehouseItemsFoundationApi\))[\s\S]*?(?=export const warehouseItemsOperationContexts)/u, '$;\n\n', @@ -2913,7 +3067,7 @@ void test('repository checker respects custom readiness prefixes and diagnoses m await writeText( fixture, `${ownerPath}/api/index.ts`, - service.createApiServiceEntry('app', app, generatedSharedApiImport), + service.createApiServiceEntry(app, generatedSharedApiImport), ); await writeText( fixture, @@ -2936,7 +3090,10 @@ void test('repository checker respects custom readiness prefixes and diagnoses m }); assert.match(check(), /UltraModern API boundary check passed/u); const cases = [ - { expected: /topology must declare this MicroVertical owner/u, verticals: [] }, + { + expected: /topology must declare this MicroVertical owner/u, + verticals: [], + }, { expected: /topology must declare api\.basePath/u, verticals: [ @@ -3375,7 +3532,10 @@ const validateModuleFederationTypes = (input: { const publicUrl = 'https://party.example.test'; const buildMarker = 'party-build'; interface ApiOnlyAppFixture { - readonly deliveryUnit: { readonly buildMarker: string; readonly unitId: string }; + readonly deliveryUnit: { + readonly buildMarker: string; + readonly unitId: string; + }; readonly deploy: { readonly cloudflare: { readonly jsonSmokeChecks: readonly object[]; @@ -3424,7 +3584,9 @@ const mockPublicResponses = (context: TestContext, failedPath?: string) => { route === mfManifestPath ? { metaData: { publicPath: `${publicUrl}/` } } : { marker: { build: buildMarker }, status: 'ready' }; - return Response.json(body, { headers: { 'access-control-allow-origin': '*' } }); + return Response.json(body, { + headers: { 'access-control-allow-origin': '*' }, + }); }); return requested; }; @@ -3535,7 +3697,10 @@ void test('MF proof accepts explicit API-only intent but keeps exposed-app archi await mkdir(path.join(fixture, appDir), { recursive: true }); const configPath = path.join(fixture, appDir, 'module-federation.config.ts'); const validate = () => - validateModuleFederationTypes({ appDirs: [appDir], workspaceRoot: fixture }); + validateModuleFederationTypes({ + appDirs: [appDir], + workspaceRoot: fixture, + }); await writeFile( configPath, '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };', @@ -3558,10 +3723,7 @@ void test('Party deployment declares no fake SSR/locale URL while retaining back assert.equal(party.cloudflare.routes.locale, undefined); assert.equal(party.cloudflare.routes.mfManifest, mfManifestPath); assert.equal(party.cloudflare.routes.apiReadiness, readinessPath); - assert.equal( - party.backendFederation.exposes['./effect-api'].contract, - 'verticals/party-registry/shared/api.ts', - ); + assert.equal(party.backendFederation.exposes['./effect-api'].contract, partySharedApiPath); assert.equal( party.backendFederation.exposes['./effect-api'].openapi, '/party-registry-api/openapi.json', @@ -3689,10 +3851,7 @@ void test('proves generated Layer bindings and API aliases without accepting unu void test('accepts only the trusted final identity terminator in a governed API slot', async () => { const identityTerminator = '.pipe(identity)'; - const source = await readFile( - path.join(workspaceRoot, 'verticals/party-registry/shared/api.ts'), - 'utf-8', - ); + const source = await readFile(path.join(workspaceRoot, partySharedApiPath), 'utf-8'); assert.ok(source.includes(identityTerminator)); assert.equal(microVerticalApiBaselineViolation(partyId, source), undefined); const mutations = [ @@ -3712,3 +3871,272 @@ void test('accepts only the trusted final identity terminator in a governed API ); } }); + +// Consumer adaptation is compared by governed semantics, not generated byte equality. +void test('consumer migration preserves native tooling and governed safety', async (context) => { + const source = async (relativePath: string) => + await readFile(path.join(workspaceRoot, relativePath), 'utf-8'); + await context.test( + 'authenticated cohort and scoped release-age policy remain pinned', + async () => { + const releaseVersion = '3.9.0-ultramodern.2'; + const cohort = Schema.decodeUnknownSync( + Schema.fromJsonString( + Schema.Struct({ + aliases: Schema.Record(Schema.String, Schema.String), + packages: Schema.Array( + Schema.Struct({ + sourceName: Schema.String, + targetName: Schema.String, + version: Schema.Literal(releaseVersion), + }), + ), + release: Schema.Struct({ version: Schema.Literal(releaseVersion) }), + source: Schema.Struct({ + commit: Schema.Literal('d2c75828230edf92775feca796c0960af754508f'), + }), + }), + ), + )(await source('.modernjs/release-cohort.json')); + assert.equal( + cohort.aliases['@modern-js/ultramodern-create'], + '@bleedingdev/modern-js-ultramodern-create', + ); + assert.equal(cohort.aliases['@modern-js/create'], undefined); + assert.equal( + new Set(cohort.packages.map((entry) => entry.sourceName)).size, + cohort.packages.length, + ); + for (const entry of cohort.packages) { + assert.equal(cohort.aliases[entry.sourceName], entry.targetName); + } + const workspace = await source('pnpm-workspace.yaml'); + for (const line of [ + 'minimumReleaseAge: 1440', + 'minimumReleaseAgeStrict: true', + 'minimumReleaseAgeIgnoreMissingTime: false', + ]) { + assert.equal(workspace.split('\n').filter((candidate) => candidate === line).length, 1); + } + const exclusions = /^minimumReleaseAgeExclude:\n(?(?:[ \t]+[^\n]*\n)*)/mu.exec( + workspace, + )?.groups?.entries; + assert.ok(exclusions !== undefined && exclusions.length > 0); + const allowed = new Set( + cohort.packages.map((entry) => `${entry.targetName}@${entry.version}`), + ); + const declared = exclusions + .trim() + .split('\n') + .map((line) => line.trim().replaceAll(/^-\s*['"]?|['"]$/gu, '')); + assert.ok(declared.length > 0); + for (const entry of declared) { + assert.ok( + allowed.has(entry), + `Release-age exception must name an exact authenticated package: ${entry}`, + ); + } + const validator = await source('scripts/validate-ultramodern-workspace.mts'); + assert.match(validator, /authenticated release cohort projection/u); + assert.ok(validator.includes(cohort.source.commit)); + assert.doesNotMatch(validator, /['"]@modern-js\/create['"]/u); + }, + ); + await context.test( + 'current generator handoff preserves arguments and nonzero failures', + async () => { + const scratchRoot = path.join(workspaceRoot, '.scratch'); + await mkdir(scratchRoot, { recursive: true }); + const fixture = await mkdtemp(path.join(scratchRoot, 'consumer-migration-')); + try { + const executable = path.join(fixture, 'generator.mjs'); + await writeFile( + executable, + `process.stdout.write(JSON.stringify({ args: process.argv.slice(2), root: process.env.ULTRAMODERN_WORKSPACE_ROOT })); process.exitCode = 37;`, + ); + const wrappers = [ + ['migrate-strict-effect.mts', 'migrate-strict-effect'], + ['ultramodern-typecheck.mts', 'typecheck'], + ] as const; + const wrapperSources = await Promise.all( + wrappers.map(async ([file]) => await source(`scripts/${file}`)), + ); + for (const [index, [file, command]] of wrappers.entries()) { + const script = wrapperSources[index] ?? ''; + assert.match(script, /runUltramodernScript/u); + assert.doesNotMatch(script, /['"]modern-js-create['"]/u); + const runner = await source('scripts/shared/ultramodern-command.mts'); + assert.match(runner, /'ultramodern-create'/u); + assert.doesNotMatch(runner, /['"]modern-js-create['"]/u); + assert.match(await source('scripts/ultramodern-command-failure.mts'), /Schema\.TaggedError/u); + assert.match(script, /Effect\.runPromiseExit/u); + const result = spawnSync( + process.execPath, + [path.join(workspaceRoot, 'scripts', file), '--fixture-argument'], + { + cwd: fixture, + encoding: 'utf-8', + env: { + ULTRAMODERN_CREATE_BIN: executable, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + }, + ); + assert.equal(result.status, 37, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { + args: ['ultramodern', command, '--fixture-argument'], + root: fixture, + }); + const missing = spawnSync(process.execPath, [path.join(workspaceRoot, 'scripts', file)], { + cwd: fixture, + encoding: 'utf-8', + env: { + PATH: fixture, + ULTRAMODERN_CREATE_BIN: '', + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + }); + assert.equal(missing.status, 1); + assert.match( + missing.stdout + missing.stderr, + /Failed to launch ultramodern-create from PATH/u, + ); + } + } finally { + await rm(fixture, { force: true, recursive: true }); + } + }, + ); + await context.test( + 'native route, isolated materialization and workerd adaptations survive', + async () => { + const files = [ + 'generate-tanstack-routes.mts', + 'materialize-zerops-runtime.mjs', + 'proof-workerd-ssr.mts', + ]; + const scripts = await Promise.all(files.map(async (file) => await source(`scripts/${file}`))); + for (const [index, file] of files.entries()) { + const script = scripts[index] ?? ''; + assert.match(script, /Effect\.gen/u, file); + assert.match(script, /FileSystem/u, file); + assert.doesNotMatch( + script, + /import\s*\{[^}]*spawnSync[^}]*\}\s*from\s*['"]node:child_process/u, + file, + ); + assert.doesNotMatch(script, /['"]modern-js-create['"]/u, file); + } + const materializer = await source('scripts/materialize-zerops-runtime.mjs'); + assert.match(materializer, /Flag\.boolean\('worker'\)/u); + assert.match(materializer, /appPackage\.name !== packageName/u); + assert.match(materializer, /makeTempDirectoryScoped/u); + assert.match(materializer, /removeIncompatiblePlatformDependencies/u); + assert.doesNotMatch(materializer, /--skip-build/u); + const proof = await source('scripts/proof-workerd-ssr.mts'); + assert.match(proof, /WorkerdProofError extends Schema\.TaggedError/u); + assert.match(proof, /findReleaseMarkers/u); + assert.match(proof, /not tied to its executed release identity/u); + assert.match(proof, /check\.body \?\? null/u); + assert.match(proof, /check\.expect \?\? null/u); + assert.match(proof, /Exit\.isFailure\(exit\)/u); + }, + ); + await context.test( + 'custom Party contracts remain accepted and forged auth remains rejected', + async () => { + const principal = await source('verticals/party-registry/api/auth/action-principal.ts'); + const gateway = await source('verticals/party-registry/src/api/action-gateway.ts'); + const sharedApi = await source(partySharedApiPath); + const handlerRoot = await source('verticals/party-registry/api/index.ts'); + assert.equal(hasGeneratedOperationPrincipalContract(principal), true); + assert.equal(hasGeneratedOperationGatewayContract(gateway, partyId), true); + assert.equal(hasValidGovernedHttpCompositionRoot(sharedApi, handlerRoot), true); + assert.equal(microVerticalApiBaselineViolation(partyId, sharedApi), undefined); + for (const [before, after] of [ + [ + 'makeMicroverticalHttpPrincipalAuthentication(verifyOperationPrincipal)', + 'makeMicroverticalHttpPrincipalAuthentication(forgedPrincipal)', + ], + ["'@app/core-runtime/http/principal-authentication'", "'./counterfeit.ts'"], + ]) { + assert.ok(principal.includes(before)); + assert.equal( + hasGeneratedOperationPrincipalContract(principal.replace(before, after)), + false, + ); + } + const audience = "ACTION_GATEWAY_AUDIENCE = 'party-registry'"; + assert.ok(gateway.includes(audience)); + assert.equal( + hasGeneratedOperationGatewayContract( + gateway.replace(audience, "ACTION_GATEWAY_AUDIENCE = 'other-owner'"), + partyId, + ), + false, + ); + // Exercise the complete Core/Party server, client, permission and transport negative matrix. + const governed = spawnSync( + process.execPath, + [ + '--test', + '--test-name-pattern=governed', + 'scripts/tests/module-entrypoint-boundaries.test.mts', + ], + { + cwd: workspaceRoot, + encoding: 'utf-8', + env: { PATH: path.dirname(process.execPath) }, + }, + ); + assert.equal(governed.status, 0, governed.stdout + governed.stderr); + assert.match(governed.stdout, /governed servers bind/u); + assert.match(governed.stdout, /rejects generated governed clients/u); + }, + ); + await context.test( + 'manifest-aware bridge accepts TanStack without permitting disguised router capability', + () => { + const imported = + "import { createModuleFederationConfig as createConfig } from '@module-federation/modern-js-v3';"; + const config = (body: string) => `${imported} export default createConfig(${body});`; + const disabled = '{ bridge: { enableBridgeRouter: false } }'; + const enabled = '{ bridge: { enableBridgeRouter: true } }'; + assert.equal(moduleFederationBridgeViolation(config(disabled), {}), undefined); + assert.equal( + moduleFederationBridgeViolation( + `${imported} const config = createConfig(${disabled}); export default config;`, + {}, + ), + undefined, + ); + assert.equal( + moduleFederationBridgeViolation(config(enabled), { + dependencies: { 'react-router': '7.18.0' }, + }), + undefined, + ); + assert.equal( + moduleFederationBridgeViolation(config(enabled), { + devDependencies: { 'react-router-dom': '7.18.0' }, + }), + undefined, + ); + for (const candidate of [ + config(enabled), + config('{}'), + config('{ bridge: {} }'), + config('{ bridge: { enableBridgeRouter: Boolean(false) } }'), + config('{ bridge: { enableBridgeRouter: false, ...override } }'), + config('{ bridge: { enableBridgeRouter: false, [key]: true } }'), + config('{ bridge: { enableBridgeRouter: false, enableBridgeRouter: true } }'), + config('{ bridge: { enableBridgeRouter: false }, ...override }'), + config(disabled).replace('import {', 'import type {'), + `${imported} function decoy(createConfig) { return createConfig(${disabled}); } export default otherConfig;`, + `function createConfig(value) { return value; } export default createConfig(${disabled});`, + ]) { + assert.notEqual(moduleFederationBridgeViolation(candidate, {}), undefined, candidate); + } + }, + ); +}); diff --git a/app/scripts/tests/audit-database-trust-boundaries.test.mts b/app/scripts/tests/audit-database-trust-boundaries.test.mts index 37fa6e226..3d9eab010 100644 --- a/app/scripts/tests/audit-database-trust-boundaries.test.mts +++ b/app/scripts/tests/audit-database-trust-boundaries.test.mts @@ -1,8 +1,10 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; + import { Cause } from 'effect'; import { Client } from 'pg'; + import { assertDatabaseSessionIdentities, assertSameDatabaseTarget, @@ -153,11 +155,13 @@ const hardenedSnapshot = { }, } as const satisfies DatabaseTrustBoundarySnapshot; -const buildHardenedReport = (overrides: Partial = {}) => - buildDatabaseTrustBoundaryReport({ ...hardenedSnapshot, ...overrides }); +const buildHardenedReport = ( + overrides: Partial = {} +) => buildDatabaseTrustBoundaryReport({ ...hardenedSnapshot, ...overrides }); -const findingCodes = (report: ReturnType) => - report.findings.map(({ code }) => code); +const findingCodes = ( + report: ReturnType +) => report.findings.map(({ code }) => code); const reversed = (values: readonly Value[]): Value[] => { const [head, ...tail] = values; @@ -173,19 +177,28 @@ void test('builds deterministic current-state evidence and identifies the materi assert.deepEqual( report.schemas.map(({ schema }) => schema), - ['auth', 'contacts', 'core'], + ['auth', 'contacts', 'core'] ); assert.deepEqual( report.tables.map(({ schema, table }) => `${schema}.${table}`), - ['auth.user', 'contacts.customers', 'core.tenants'], + ['auth.user', 'contacts.customers', 'core.tenants'] ); assert.deepEqual( - report.defaultPrivileges.map(({ grantee, schema, source }) => `${source}:${grantee}:${schema}`), - ['inherited:analytics_reader:null', 'public:PUBLIC:auth', 'direct:ontos_runtime:contacts'], + report.defaultPrivileges.map( + ({ grantee, schema, source }) => `${source}:${grantee}:${schema}` + ), + [ + 'inherited:analytics_reader:null', + 'public:PUBLIC:auth', + 'direct:ontos_runtime:contacts', + ] ); assert.deepEqual( report.findings.map(({ code, severity }) => `${severity}:${code}`), - ['high:runtime_role_can_forge_trusted_context', 'high:runtime_role_has_cross_schema_dml'], + [ + 'high:runtime_role_can_forge_trusted_context', + 'high:runtime_role_has_cross_schema_dml', + ] ); assert.deepEqual(report.summary, { auditedSchemaCount: 3, @@ -216,7 +229,7 @@ void test('orders audit evidence by code units rather than locale collation', () assert.deepEqual( report.types.map(({ schema, type }) => `${schema}.${type}`), - ['zeta.status', 'ärea.status'], + ['zeta.status', 'ärea.status'] ); }); @@ -239,7 +252,7 @@ void test('totally orders default privileges from distinct creator roles', () => assert.deepEqual( report.defaultPrivileges.map(({ owner }) => owner), - ['alpha_owner', 'zeta_owner'], + ['alpha_owner', 'zeta_owner'] ); }); @@ -248,13 +261,15 @@ void test('extracts typed audit failures from an Effect cause', () => { assert.equal( getDatabaseTrustBoundaryFailureMessage( - Cause.fail(new DatabaseTrustBoundaryAuditError({ reason })), + Cause.fail(new DatabaseTrustBoundaryAuditError({ reason })) ), - reason, + reason ); assert.equal( - getDatabaseTrustBoundaryFailureMessage(Cause.die(new Error('driver defect'))), - 'Database trust-boundary audit failed', + getDatabaseTrustBoundaryFailureMessage( + Cause.die(new Error('driver defect')) + ), + 'Database trust-boundary audit failed' ); }); @@ -298,7 +313,10 @@ void test('reports privilege escalation paths without embedding credentials or c 'runtime_role_can_forge_trusted_context', 'runtime_role_has_cross_schema_dml', ]); - assert.doesNotMatch(JSON.stringify(report), /postgresql:|password|secret|tenant-id|entity-id/iu); + assert.doesNotMatch( + JSON.stringify(report), + /postgresql:|password|secret|tenant-id|entity-id/iu + ); }); void test('flags database-level CREATE even when no existing schema is writable', () => { @@ -332,7 +350,9 @@ void test('classifies reachable predefined PostgreSQL roles as privileged', () = ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_assume_privileged_role']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_can_assume_privileged_role', + ]); }); void test('classifies a directly authenticated predefined PostgreSQL role as privileged', () => { @@ -346,10 +366,14 @@ void test('classifies a directly authenticated predefined PostgreSQL role as pri void test('flags effective configuration parameter authority', () => { const report = buildHardenedReport({ - parameterPrivileges: [{ alterSystem: false, parameter: 'session_replication_role', set: true }], + parameterPrivileges: [ + { alterSystem: false, parameter: 'session_replication_role', set: true }, + ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_parameter_authority']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_has_parameter_authority', + ]); assert.equal(report.summary.parameterPrivilegeCount, 1); }); @@ -406,7 +430,7 @@ void test('flags selectable privileged owner-context views but accepts security const ownerContextReport = buildDatabaseTrustBoundaryReport(base); assert.deepEqual( ownerContextReport.findings.map(({ code }) => code), - ['runtime_role_can_use_privileged_owner_view'], + ['runtime_role_can_use_privileged_owner_view'] ); assert.equal(ownerContextReport.summary.privilegedOwnerViewCount, 1); @@ -415,18 +439,28 @@ void test('flags selectable privileged owner-context views but accepts security tables: [ { ...ownerContextView, - privileges: { ...ownerContextView.privileges, select: false, update: true }, + privileges: { + ...ownerContextView.privileges, + select: false, + update: true, + }, }, ], }); - assert.deepEqual(findingCodes(writableReport), ['runtime_role_can_use_privileged_owner_view']); + assert.deepEqual(findingCodes(writableReport), [ + 'runtime_role_can_use_privileged_owner_view', + ]); const readOnlyReport = buildDatabaseTrustBoundaryReport({ ...base, tables: [ { ...ownerContextView, - privileges: { ...ownerContextView.privileges, select: false, update: true }, + privileges: { + ...ownerContextView.privileges, + select: false, + update: true, + }, updatable: false, }, ], @@ -460,7 +494,9 @@ void test('flags owner-context views that bypass RLS through owner-matched depen }, }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_use_privileged_owner_view']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_can_use_privileged_owner_view', + ]); assert.equal(report.summary.privilegedOwnerViewCount, 1); }); @@ -484,7 +520,9 @@ void test('flags privileged owners in nested owner-context views', () => { }, }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_use_privileged_owner_view']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_can_use_privileged_owner_view', + ]); }); void test('flags ownership of an audited relation as DDL authority', () => { @@ -591,7 +629,9 @@ void test('flags direct sequence mutation authority', () => { ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_sequence_mutation_authority']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_has_sequence_mutation_authority', + ]); }); void test('classifies every assumable role and escalates relation authority', () => { @@ -671,28 +711,35 @@ void test('treats ADMIN OPTION as an escalation path when SET OPTION is false', void test('traverses SET OPTION descendants after every ADMIN OPTION role', async () => { const source = await readFile( new URL('../database-trust-audit/collect-snapshot.mts', import.meta.url), - 'utf-8', + 'utf-8' ); assert.equal( - source.match(/where membership\.admin_option or membership\.set_option/gu)?.length, - 3, + source.match(/where membership\.admin_option or membership\.set_option/gu) + ?.length, + 3 ); assert.match( source, - /candidate\.oid in \(select role_oid from reachable_roles\) as can_set_role/u, + /candidate\.oid in \(select role_oid from reachable_roles\) as can_set_role/u ); assert.doesNotMatch( source, - /or pg_has_role\(\$1, grantee\.oid, 'SET'\)\s+or grantee\.oid in \(select role_oid from administrable_roles\)/u, + /or pg_has_role\(\$1, grantee\.oid, 'SET'\)\s+or grantee\.oid in \(select role_oid from administrable_roles\)/u + ); + assert.match( + source, + /view_dependencies\(view_oid, referenced_oid, effective_owner_oid\)/u ); - assert.match(source, /view_dependencies\(view_oid, referenced_oid, effective_owner_oid\)/u); assert.match(source, /target_roles\(role_oid, role_name\)/u); - assert.match(source, /format\('role:%I:%s', target\.role_name, authority\.grant_option\)/u); + assert.match( + source, + /format\('role:%I:%s', target\.role_name, authority\.grant_option\)/u + ); assert.match(source, /pg_has_role\(effective_owner\.oid, \$3, 'USAGE'\)/u); assert.match( source, - /pg_has_role\(\s*dependency\.effective_owner_oid,\s*referenced_relation\.relowner,\s*'USAGE'\s*\)/u, + /pg_has_role\(\s*dependency\.effective_owner_oid,\s*referenced_relation\.relowner,\s*'USAGE'\s*\)/u ); }); @@ -744,7 +791,9 @@ void test('does not inherit cluster attributes without SET ROLE or ADMIN OPTION' ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_assume_other_role']); + assert.deepEqual(findingCodes(report), [ + 'runtime_role_can_assume_other_role', + ]); }); void test('uses node-postgres effective query-parameter socket endpoints', () => { @@ -763,24 +812,24 @@ void test('requires direct, distinct live database session identities', () => { assert.doesNotThrow(() => assertDatabaseSessionIdentities( { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, - { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' }, - ), + { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' } + ) ); assert.throws( () => assertDatabaseSessionIdentities( { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, - { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, + { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' } ), - /distinct authenticated PostgreSQL roles/u, + /distinct authenticated PostgreSQL roles/u ); assert.throws( () => assertDatabaseSessionIdentities( { currentRole: 'startup_role', sessionRole: 'ontos_runtime' }, - { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' }, + { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' } ), - /current_user must equal session_user/u, + /current_user must equal session_user/u ); }); @@ -798,11 +847,15 @@ void test('rejects evidence collected from different servers or databases', () = assert.doesNotThrow(() => assertSameDatabaseTarget(target, { ...target })); assert.throws( () => assertSameDatabaseTarget(target, { ...target, database: 'other' }), - /same PostgreSQL server and database/u, + /same PostgreSQL server and database/u ); assert.throws( - () => assertSameDatabaseTarget(target, { ...target, serverAddress: alternateServerAddress }), - /same PostgreSQL server and database/u, + () => + assertSameDatabaseTarget(target, { + ...target, + serverAddress: alternateServerAddress, + }), + /same PostgreSQL server and database/u ); assert.throws( () => @@ -818,9 +871,9 @@ void test('rejects evidence collected from different servers or databases', () = configuredHost: '/var/run/postgresql-b', serverAddress: null, serverPort: null, - }, + } ), - /same PostgreSQL server and database/u, + /same PostgreSQL server and database/u ); }); @@ -839,6 +892,6 @@ void test('treats transaction-local context retention as a critical boundary fai [ 'high:runtime_role_can_forge_trusted_context', 'critical:trusted_context_survives_transaction', - ], + ] ); }); diff --git a/app/scripts/tests/authorization-rollout-contract.test.mts b/app/scripts/tests/authorization-rollout-contract.test.mts index 675c8079c..6a558c887 100644 --- a/app/scripts/tests/authorization-rollout-contract.test.mts +++ b/app/scripts/tests/authorization-rollout-contract.test.mts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + import { validateAuthorizationRolloutContract } from '../authorization/rollout-contract.mts'; const entrypointKey = 'contacts.create-contact'; @@ -23,16 +24,19 @@ const context = { }; await test('rollout contract accepts an active configuration bound to the classified inventory', () => { - assert.deepEqual(validateAuthorizationRolloutContract(contract, context), contract); + assert.deepEqual( + validateAuthorizationRolloutContract(contract, context), + contract + ); }); await test('the historical baseline revision does not have to equal the self-referential current commit', () => { assert.deepEqual( validateAuthorizationRolloutContract( { ...contract, baselineSourceRevision: 'historical-baseline-revision' }, - context, + context ).baselineSourceRevision, - 'historical-baseline-revision', + 'historical-baseline-revision' ); }); @@ -40,9 +44,9 @@ await test('enforced rollout remains active after the report-only deadline', () assert.equal( validateAuthorizationRolloutContract( { ...contract, mode: 'enforced' }, - { ...context, nowEpochMs: Date.parse('2026-11-01T00:00:00.000Z') }, + { ...context, nowEpochMs: Date.parse('2026-11-01T00:00:00.000Z') } ).mode, - 'enforced', + 'enforced' ); }); @@ -53,15 +57,23 @@ await test('rollout contract rejects expiry, stale inventory binding, extra fiel ...context, nowEpochMs: Date.parse(expiry), }), - /inactive or expired/u, + /inactive or expired/u ); assert.throws( - () => validateAuthorizationRolloutContract(contract, { ...context, inventoryHash: 'other' }), - /does not match/u, + () => + validateAuthorizationRolloutContract(contract, { + ...context, + inventoryHash: 'other', + }), + /does not match/u ); assert.throws( - () => validateAuthorizationRolloutContract({ ...contract, arbitrary: true }, context), - /malformed/u, + () => + validateAuthorizationRolloutContract( + { ...contract, arbitrary: true }, + context + ), + /malformed/u ); assert.throws( () => @@ -70,16 +82,19 @@ await test('rollout contract rejects expiry, stale inventory binding, extra fiel ...contract, compatibilityEligibleEntrypoints: [entrypointKey, entrypointKey], }, - context, + context ), - /duplicates/u, + /duplicates/u ); assert.throws( () => validateAuthorizationRolloutContract( - { ...contract, compatibilityEligibleEntrypoints: ['contacts.new-action'] }, - context, + { + ...contract, + compatibilityEligibleEntrypoints: ['contacts.new-action'], + }, + context ), - /unknown entrypoint/u, + /unknown entrypoint/u ); }); diff --git a/app/scripts/tests/check-authorization-readiness.test.mts b/app/scripts/tests/check-authorization-readiness.test.mts index 05bb222c8..c53f5b6bd 100644 --- a/app/scripts/tests/check-authorization-readiness.test.mts +++ b/app/scripts/tests/check-authorization-readiness.test.mts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + import type { ProtectedEntrypointInventory } from '../authorization/protected-entrypoint-inventory.mts'; import { checkAuthorizationReadiness, @@ -19,7 +20,10 @@ const contactsOwner = 'contacts.core'; const inventory: ProtectedEntrypointInventory = { entries: [ { - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, deployment: 'contacts', entrypointKey: contactsCreateCustomerEntrypoint, owner: contactsOwner, @@ -67,7 +71,7 @@ const negativeSmoke: AuthorizationNegativeSmokeEvidence = { credential, outcome: 'denied' as const, scenario, - })), + })) ), schemaVersion: 1, sourceRevision: inventory.sourceRevision, @@ -158,7 +162,7 @@ void test('readiness rejects unapproved contexts and unresolved or stale impact ...ready, context: { ...ready.context, approvalStatus: 'pending' }, }), - /unapproved/u, + /unapproved/u ); assert.throws( () => @@ -166,7 +170,7 @@ void test('readiness rejects unapproved contexts and unresolved or stale impact ...ready, impact: { ...ready.impact, totalWouldDeny: 1 }, }), - /stale or unresolved/u, + /stale or unresolved/u ); assert.throws( () => @@ -174,7 +178,7 @@ void test('readiness rejects unapproved contexts and unresolved or stale impact ...ready, impact: { ...ready.impact, sourceRevision: 'other' }, }), - /stale or unresolved/u, + /stale or unresolved/u ); }); @@ -191,16 +195,19 @@ void test('readiness rejects missing relationships, module state, worker ownersh ...ready, observation: { ...ready.observation, [key]: [] }, }), - /incomplete/u, + /incomplete/u ); } assert.throws( () => checkAuthorizationReadiness({ ...ready, - observation: { ...ready.observation, replayMigrationHash: 'f'.repeat(64) }, + observation: { + ...ready.observation, + replayMigrationHash: 'f'.repeat(64), + }, }), - /stale/u, + /stale/u ); }); @@ -211,15 +218,18 @@ void test('readiness rejects incorrect issuer/audience topology, short observati ...ready, observation: { ...ready.observation, gatewayAudiences: ['other'] }, }), - /issuer or audience/u, + /issuer or audience/u ); assert.throws( () => checkAuthorizationReadiness({ ...ready, - observation: { ...ready.observation, gatewayIssuer: 'http://insecure.test' }, + observation: { + ...ready.observation, + gatewayIssuer: 'http://insecure.test', + }, }), - /issuer or audience/u, + /issuer or audience/u ); assert.throws( () => @@ -233,14 +243,17 @@ void test('readiness rejects incorrect issuer/audience topology, short observati }, }, }), - /observation/u, + /observation/u ); assert.throws( () => checkAuthorizationReadiness({ ...ready, - negativeSmoke: { ...negativeSmoke, scenarios: negativeSmoke.scenarios.slice(1) }, + negativeSmoke: { + ...negativeSmoke, + scenarios: negativeSmoke.scenarios.slice(1), + }, }), - /smoke evidence is incomplete/u, + /smoke evidence is incomplete/u ); }); diff --git a/app/scripts/tests/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 27e770710..f375a1094 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -3,6 +3,7 @@ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; + import { checkDatabaseAccessBoundaries } from '../check-database-access-boundaries.mts'; void test('allows owner database factories and rejects Action, read, nested BFF, and hidden Core database bypasses deterministically', async () => { @@ -38,7 +39,8 @@ void test('allows owner database factories and rejects Action, read, nested BFF, "export { InventoryPersistence } from './infrastructure/inventory-persistence.ts';\n", 'verticals/stock/src/infrastructure/inventory-persistence.ts': "import { Pool } from 'pg';\nexport class InventoryPersistence {}\n", - 'verticals/stock/src/reads/list.read.ts': "import { stock } from '../db/schema.ts';\n", + 'verticals/stock/src/reads/list.read.ts': + "import { stock } from '../db/schema.ts';\n", 'verticals/stock/src/reads/side-effect.read.ts': "import 'pg';\n", 'verticals/stock/src/services/generated-action-service.ts': "// @generated by OntOS Codesmith Action Service v1\nimport { eq } from 'drizzle-orm';\nimport { stock } from '../db/schema.ts';\nexport const findStock = () => eq(stock.id, 'one');\n", @@ -60,7 +62,7 @@ void test('allows owner database factories and rejects Action, read, nested BFF, const file = path.join(root, relative); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, source); - }), + }) ); const violations = await checkDatabaseAccessBoundaries(root); assert.deepEqual( @@ -81,7 +83,7 @@ void test('allows owner database factories and rejects Action, read, nested BFF, 'verticals/stock/src/testing-harness-dynamic-leak.ts:1', 'verticals/stock/src/testing-harness-export-leak.ts:1', 'verticals/stock/src/testing-harness-leak.ts:1', - ], + ] ); } finally { await rm(root, { force: true, recursive: true }); diff --git a/app/scripts/tests/locki-feature.test.mts b/app/scripts/tests/locki-feature.test.mts index 8639e4b41..65efdf2cd 100644 --- a/app/scripts/tests/locki-feature.test.mts +++ b/app/scripts/tests/locki-feature.test.mts @@ -1,6 +1,14 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { chmod, cp, mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; +import { + chmod, + cp, + mkdir, + mkdtemp, + readFile, + stat, + writeFile, +} from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { execPath } from 'node:process'; @@ -11,7 +19,10 @@ const workflowScript = path.join(workspaceRoot, 'scripts/locki-feature.sh'); const featureSlug = 'customer-search'; void test('pins pnpm to the npm mise backend for cross-platform sandbox installation', async () => { - const miseConfiguration = await readFile(path.join(workspaceRoot, '.mise.toml'), 'utf-8'); + const miseConfiguration = await readFile( + path.join(workspaceRoot, '.mise.toml'), + 'utf-8' + ); assert.match(miseConfiguration, /\[tool_alias\][\s\S]*pnpm = "npm:pnpm"/u); assert.match(miseConfiguration, /\[tools\][\s\S]*pnpm = "11\.25\.0"/u); }); @@ -42,9 +53,15 @@ const makeFixture = async (withEnvironment = true): Promise => { const logPath = path.join(root, 'commands.log'); await mkdir(path.join(sourceRoot, 'app/scripts'), { recursive: true }); await mkdir(binDirectory, { recursive: true }); - await cp(workflowScript, path.join(sourceRoot, 'app/scripts/locki-feature.sh')); + await cp( + workflowScript, + path.join(sourceRoot, 'app/scripts/locki-feature.sh') + ); if (withEnvironment) { - await writeFile(path.join(sourceRoot, 'app/.env'), Buffer.from('OPAQUE-SECRET\0VALUE\n')); + await writeFile( + path.join(sourceRoot, 'app/.env'), + Buffer.from('OPAQUE-SECRET\0VALUE\n') + ); } await executable( path.join(binDirectory, 'git'), @@ -57,7 +74,7 @@ if [ "$3" = "cat-file" ]; then fi if [ "$3" = "diff" ]; then exit 0; fi exit 9 -`, +` ); await executable( path.join(binDirectory, 'mise'), @@ -66,13 +83,13 @@ printf 'mise %s\\n' "$*" >>"$TEST_LOG" if [ "\${1-}" = "install" ] && [ -n "\${LOCKI_SANDBOX_ID-}" ]; then exit 18; fi if [ "$*" = "exec -- pnpm install --frozen-lockfile" ] && [ "\${ULTRAMODERN_SKIP_CODEX_SKILLS-}" != "1" ]; then exit 19; fi if [ "\${FAIL_PREPARATION-}" = "true" ] && [ "\${1-}" = "install" ]; then exit 17; fi -`, +` ); await executable( path.join(binDirectory, 'docker'), `#!/bin/sh printf 'docker %s\\n' "$*" >>"$TEST_LOG" -`, +` ); await executable( path.join(binDirectory, 'locki'), @@ -103,7 +120,7 @@ case "$command_name" in ai) ;; *) exit 8 ;; esac -`, +` ); return { binDirectory, logPath, sourceRoot, targetRoot }; }; @@ -111,11 +128,14 @@ esac const runWorkflow = ( fixture: Fixture, commandArguments: readonly string[], - extraEnvironment: Readonly> = {}, + extraEnvironment: Readonly> = {} ): WorkflowResult => { const result = spawnSync( '/bin/sh', - [path.join(fixture.sourceRoot, 'app/scripts/locki-feature.sh'), ...commandArguments], + [ + path.join(fixture.sourceRoot, 'app/scripts/locki-feature.sh'), + ...commandArguments, + ], { encoding: 'utf-8', env: { @@ -125,7 +145,7 @@ const runWorkflow = ( TEST_SOURCE_ROOT: fixture.sourceRoot, TEST_TARGET_ROOT: fixture.targetRoot, }, - }, + } ); assert.ifError(result.error); return { code: result.status, stderr: result.stderr, stdout: result.stdout }; @@ -138,15 +158,18 @@ void test('creates one sandbox from main, copies .env opaquely, and prepares in assert.equal(result.stdout.includes('OPAQUE-SECRET'), false); assert.deepEqual( await readFile(path.join(fixture.targetRoot, 'app/.env')), - await readFile(path.join(fixture.sourceRoot, 'app/.env')), + await readFile(path.join(fixture.sourceRoot, 'app/.env')) ); const environmentStat = await stat(path.join(fixture.targetRoot, 'app/.env')); assert.equal(environmentStat.mode % 0o1000, 0o600); const log = await readFile(fixture.logPath, 'utf-8'); - assert.match(log, /locki new --from main --branch codex\/customer-search --json/u); assert.match( log, - /locki exec --match sandbox-42 -- sh app\/scripts\/locki-feature\.sh --prepare/u, + /locki new --from main --branch codex\/customer-search --json/u + ); + assert.match( + log, + /locki exec --match sandbox-42 -- sh app\/scripts\/locki-feature\.sh --prepare/u ); assert.equal(log.includes('locki ai'), false); const expectedOrder = [ @@ -161,13 +184,18 @@ void test('creates one sandbox from main, copies .env opaquely, and prepares in let previous = -1; for (const command of expectedOrder) { const index = log.indexOf(command); - assert.ok(index > previous, `${command} must follow the previous preparation step`); + assert.ok( + index > previous, + `${command} must follow the previous preparation step` + ); previous = index; } }); void test('rejects unsafe slugs and alternate options before creating a sandbox', async () => { - const assertRejected = async (commandArguments: readonly string[]): Promise => { + const assertRejected = async ( + commandArguments: readonly string[] + ): Promise => { const fixture = await makeFixture(); const result = runWorkflow(fixture, commandArguments); assert.equal(result.code, 2); @@ -201,16 +229,21 @@ void test('refuses an app path that resolves outside the returned worktree', asy const fixture = await makeFixture(); const result = runWorkflow(fixture, [featureSlug], { ESCAPE_TARGET: 'true' }); assert.equal(result.code, 1); - assert.match(result.stderr, /Refusing to copy \.env outside the Locki worktree/u); + assert.match( + result.stderr, + /Refusing to copy \.env outside the Locki worktree/u + ); assert.deepEqual( await readFile(path.join(fixture.sourceRoot, 'app/.env')), - Buffer.from('OPAQUE-SECRET\0VALUE\n'), + Buffer.from('OPAQUE-SECRET\0VALUE\n') ); }); void test('preserves a failed sandbox and never launches AI', async () => { const fixture = await makeFixture(); - const result = runWorkflow(fixture, [featureSlug], { FAIL_PREPARATION: 'true' }); + const result = runWorkflow(fixture, [featureSlug], { + FAIL_PREPARATION: 'true', + }); assert.equal(result.code, 1); assert.match(result.stdout, /locki exec --match sandbox-42/u); assert.match(result.stdout, /locki rm --match sandbox-42/u); @@ -224,6 +257,7 @@ void test('launches the configured AI only after successful preparation', async assert.equal(result.code, 0, result.stderr); const log = await readFile(fixture.logPath, 'utf-8'); assert.ok( - log.indexOf('mise exec -- pnpm db:verify') < log.indexOf('locki ai --match sandbox-42'), + log.indexOf('mise exec -- pnpm db:verify') < + log.indexOf('locki ai --match sandbox-42') ); }); diff --git a/app/scripts/tests/migrate-contacts-authorization.test.mts b/app/scripts/tests/migrate-contacts-authorization.test.mts index 204d881d0..89995b901 100644 --- a/app/scripts/tests/migrate-contacts-authorization.test.mts +++ b/app/scripts/tests/migrate-contacts-authorization.test.mts @@ -1,10 +1,15 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + import { planContactsAuthorizationContext } from '../migrate-contacts-authorization.mts'; import type { ContactsAuthorizationRelationship } from '../migrate-contacts-authorization.mts'; const legacyRelationships = [ - { relation: 'legal_entity', subjectId: 'legal-entity', subjectType: 'legal_entity' }, + { + relation: 'legal_entity', + subjectId: 'legal-entity', + subjectType: 'legal_entity', + }, { relation: 'accessor', subjectId: 'principal', subjectType: 'principal' }, ] as const satisfies readonly ContactsAuthorizationRelationship[]; const prepareMode = 'prepare'; @@ -13,39 +18,54 @@ const finalizeMode = 'finalize'; const alreadyPreparedState = 'already_prepared'; await test('prepare creates Contacts relationships from a legacy-only context', () => { - assert.deepEqual(planContactsAuthorizationContext(prepareMode, legacyRelationships, []), { - deleteLegacy: false, - state: 'legacy_only', - touchContacts: true, - }); + assert.deepEqual( + planContactsAuthorizationContext(prepareMode, legacyRelationships, []), + { + deleteLegacy: false, + state: 'legacy_only', + touchContacts: true, + } + ); }); await test('prepare and verify accept an exactly prepared context', () => { const reordered = [legacyRelationships[1], legacyRelationships[0]] as const; assert.equal( - planContactsAuthorizationContext(prepareMode, legacyRelationships, reordered).state, - alreadyPreparedState, + planContactsAuthorizationContext( + prepareMode, + legacyRelationships, + reordered + ).state, + alreadyPreparedState ); assert.equal( - planContactsAuthorizationContext(verifyMode, legacyRelationships, reordered).state, - alreadyPreparedState, + planContactsAuthorizationContext(verifyMode, legacyRelationships, reordered) + .state, + alreadyPreparedState ); }); await test('finalize removes only an exactly matched legacy context', () => { assert.deepEqual( - planContactsAuthorizationContext(finalizeMode, legacyRelationships, legacyRelationships), - { deleteLegacy: true, state: alreadyPreparedState, touchContacts: false }, + planContactsAuthorizationContext( + finalizeMode, + legacyRelationships, + legacyRelationships + ), + { deleteLegacy: true, state: alreadyPreparedState, touchContacts: false } ); }); await test('all modes are idempotent after legacy relationships are gone', () => { for (const mode of [prepareMode, verifyMode, finalizeMode] as const) { - assert.deepEqual(planContactsAuthorizationContext(mode, [], legacyRelationships), { - deleteLegacy: false, - state: 'already_finalized', - touchContacts: false, - }); + assert.deepEqual( + planContactsAuthorizationContext(mode, [], legacyRelationships), + { + deleteLegacy: false, + state: 'already_finalized', + touchContacts: false, + } + ); } }); @@ -53,7 +73,7 @@ await test('verify and finalize fail closed when Contacts relationships are miss for (const mode of [verifyMode, finalizeMode] as const) { assert.throws( () => planContactsAuthorizationContext(mode, legacyRelationships, []), - /Contacts authorization is missing/u, + /Contacts authorization is missing/u ); } }); @@ -62,8 +82,9 @@ await test('every mode rejects partial or divergent relationship sets', () => { const partial = legacyRelationships.slice(0, 1); for (const mode of [prepareMode, verifyMode, finalizeMode] as const) { assert.throws( - () => planContactsAuthorizationContext(mode, legacyRelationships, partial), - /relationships differ/u, + () => + planContactsAuthorizationContext(mode, legacyRelationships, partial), + /relationships differ/u ); } }); diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index ab89b91e4..b8c98d913 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -5,7 +5,9 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { test as registerNodeTest } from 'node:test'; + import { Option, Schema } from 'effect'; + import { generateOutboxWorkerDeployment } from '../generate-outbox-worker-deployment.mjs'; import { materializeOutboxWorker } from '../materialize-outbox-worker.mjs'; @@ -17,11 +19,15 @@ const WorkerArtifactSchema = Schema.Struct({ serviceId: ServiceIdSchema, sourceInputs: Schema.Array(Schema.String), }); -const decodeWorkerArtifact = Schema.decodeUnknownSync(Schema.fromJsonString(WorkerArtifactSchema)); +const decodeWorkerArtifact = Schema.decodeUnknownSync( + Schema.fromJsonString(WorkerArtifactSchema) +); const decodeExitEvent = Schema.decodeUnknownSync( - Schema.Tuple([Schema.OptionFromNullOr(Schema.Number), Schema.Unknown]), + Schema.Tuple([Schema.OptionFromNullOr(Schema.Number), Schema.Unknown]) +); +const decodeDataEvent = Schema.decodeUnknownSync( + Schema.Tuple([Schema.Unknown]) ); -const decodeDataEvent = Schema.decodeUnknownSync(Schema.Tuple([Schema.Unknown])); const test = (name: string, run: () => void | Promise): void => { void registerNodeTest(name, run); @@ -29,19 +35,23 @@ const test = (name: string, run: () => void | Promise): void => { const makeFixture = async () => { const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-worker-artifact-')); - await mkdir(path.join(root, LEDGER_PATH, 'src/worker-host'), { recursive: true }); + await mkdir(path.join(root, LEDGER_PATH, 'src/worker-host'), { + recursive: true, + }); await mkdir(path.join(root, 'topology'), { recursive: true }); await writeFile( path.join(root, LEDGER_PATH, 'package.json'), JSON.stringify({ name: LEDGER_PACKAGE, - scripts: { 'worker:start': `node --experimental-strip-types ./${WORKER_HOST_ENTRY}` }, + scripts: { + 'worker:start': `node --experimental-strip-types ./${WORKER_HOST_ENTRY}`, + }, type: 'module', - }), + }) ); await writeFile( path.join(root, LEDGER_PATH, WORKER_HOST_ENTRY), - '// @generated by scaffold:outbox-worker worker-host\nconsole.log("worker-started"); setInterval(() => {}, 1000);\n', + '// @generated by scaffold:outbox-worker worker-host\nconsole.log("worker-started"); setInterval(() => {}, 1000);\n' ); await writeFile( path.join(root, 'topology/reference-topology.json'), @@ -49,12 +59,14 @@ const makeFixture = async () => { verticals: [ { id: 'ledger', - moduleFederation: { manifestUrl: 'http://localhost:4110/mf-manifest.json' }, + moduleFederation: { + manifestUrl: 'http://localhost:4110/mf-manifest.json', + }, package: LEDGER_PACKAGE, path: LEDGER_PATH, }, ], - }), + }) ); return root; }; @@ -68,10 +80,19 @@ test('generates a separate supervised worker setup without changing owner config assert.match(generated, /zerops:materialize .* --worker/u); assert.match(generated, /DATABASE_URL: \$\{ledger_DATABASE_URL\}/u); assert.match(generated, /OUTBOX_WORKER_HEALTH_PORT: '4110'/u); - assert.doesNotMatch(generated.split("setup: 'ledger-worker'")[1], / run build/u); - assert.doesNotMatch(generated.split("setup: 'ledger-worker'")[1], /(?:^|\s)&(?:\s|$)/u); + assert.doesNotMatch( + generated.split("setup: 'ledger-worker'")[1], + / run build/u + ); + assert.doesNotMatch( + generated.split("setup: 'ledger-worker'")[1], + /(?:^|\s)&(?:\s|$)/u + ); assert.equal(generated.match(/ONTOS_KEEP_ME: 'true'/gu)?.length, 2); - assert.equal(await generateOutboxWorkerDeployment(root, generated), generated); + assert.equal( + await generateOutboxWorkerDeployment(root, generated), + generated + ); } finally { await rm(root, { force: true, recursive: true }); } @@ -98,7 +119,7 @@ test('materializes and starts a relocatable production worker artifact', async ( { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], - }, + } ); let diagnostics = ''; child.stdout.on('data', (data) => { @@ -114,13 +135,15 @@ test('materializes and starts a relocatable production worker artifact', async ( const artifact = decodeWorkerArtifact( await readFile( path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), - 'utf-8', - ), + 'utf-8' + ) ); assert.equal(artifact.serviceId, 'ledger-worker'); assert.equal( - artifact.sourceInputs.some((input: string) => input.endsWith(WORKER_HOST_ENTRY)), - true, + artifact.sourceInputs.some((input: string) => + input.endsWith(WORKER_HOST_ENTRY) + ), + true ); const runtime = spawn(process.execPath, ['worker.mjs'], { cwd: path.join(root, '.zerops/runtime/ledger-worker'), @@ -150,7 +173,9 @@ test('keeps the live Party Registry worker deployment generated and independentl }); test('bundles the real Party host including the production Effect HTTP health adapter', async () => { - const runtimeDir = await mkdtemp(path.join(os.tmpdir(), 'ontos-party-worker-bundle-')); + const runtimeDir = await mkdtemp( + path.join(os.tmpdir(), 'ontos-party-worker-bundle-') + ); try { const runtimePackage = await materializeOutboxWorker({ appId: 'party-registry', @@ -161,12 +186,19 @@ test('bundles the real Party host including the production Effect HTTP health ad }); const bundle = await readFile(path.join(runtimeDir, 'worker.mjs'), 'utf-8'); const artifact = decodeWorkerArtifact( - await readFile(path.join(runtimeDir, 'worker-artifact.json'), 'utf-8'), + await readFile(path.join(runtimeDir, 'worker-artifact.json'), 'utf-8') + ); + assert.ok( + artifact.sourceInputs.includes( + 'packages/core-runtime/src/outbox/health.ts' + ) ); - assert.ok(artifact.sourceInputs.includes('packages/core-runtime/src/outbox/health.ts')); assert.match(bundle, /@effect\/platform-node\/NodeHttpServer/u); assert.doesNotMatch(bundle, /from ["']@effect\/platform-node["']/u); - assert.equal(runtimePackage.dependencies['@effect/platform-node'], '4.0.0-beta.107'); + assert.equal( + runtimePackage.dependencies['@effect/platform-node'], + '4.0.0-beta.107' + ); } finally { await rm(runtimeDir, { force: true, recursive: true }); } diff --git a/app/scripts/tests/protected-entrypoint-inventory.test.mts b/app/scripts/tests/protected-entrypoint-inventory.test.mts index f8d7331d1..db166b277 100644 --- a/app/scripts/tests/protected-entrypoint-inventory.test.mts +++ b/app/scripts/tests/protected-entrypoint-inventory.test.mts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + import { makeProtectedEntrypointInventory, serializeProtectedEntrypointInventory, @@ -27,29 +28,33 @@ const entries = [ void test('inventory normalization, hashing, and serialization are deterministic', () => { const left = makeProtectedEntrypointInventory('revision', entries); - const right = makeProtectedEntrypointInventory('revision', [entries[1], entries[0]]); + const right = makeProtectedEntrypointInventory('revision', [ + entries[1], + entries[0], + ]); assert.equal( serializeProtectedEntrypointInventory(left), - serializeProtectedEntrypointInventory(right), + serializeProtectedEntrypointInventory(right) ); assert.match(left.inventoryHash, /^[a-f0-9]{64}$/u); assert.deepEqual( left.entries.map((entry) => entry.surface), - ['action', 'route'], + ['action', 'route'] ); }); void test('inventory rejects duplicate and unsafe entrypoint identities', () => { assert.throws( - () => makeProtectedEntrypointInventory('revision', [...entries, entries[0]]), - /duplicate protected entrypoint/u, + () => + makeProtectedEntrypointInventory('revision', [...entries, entries[0]]), + /duplicate protected entrypoint/u ); assert.throws( () => makeProtectedEntrypointInventory('revision', [ { ...entries[0], entrypointKey: 'tenant@example.com' }, ]), - /stable, non-sensitive identifier/u, + /stable, non-sensitive identifier/u ); }); @@ -66,16 +71,19 @@ void test('inventory rejects malformed and excess authorization classification d authorization: authorizationWithExcessData, }, ]), - /classification is invalid/u, + /classification is invalid/u ); assert.throws( () => makeProtectedEntrypointInventory('revision', [ { ...entries[0], - authorization: { kind: 'context_permission', permission: 'tenant@example.com' }, + authorization: { + kind: 'context_permission', + permission: 'tenant@example.com', + }, }, ]), - /classification is invalid/u, + /classification is invalid/u ); }); diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 54ca1bab0..0fe4839c5 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -1,4 +1,3 @@ -import { runPinnedKnip } from './quality-audit-test-support.mts'; import assert from 'node:assert/strict'; import { mkdirSync, @@ -12,11 +11,14 @@ import { import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; + import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; -import { runQualityAudit } from '../quality-audit.mts'; + import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import { buildKnipModel, KnipConfigSchema } from '../../quality-audit/knip-model.mts'; +import { runQualityAudit } from '../quality-audit.mts'; +import { runPinnedKnip } from './quality-audit-test-support.mts'; const rspackPackageName = '@rspack/core'; const fixtureModuleSource = 'module.exports = {};'; @@ -58,7 +60,11 @@ const fixture = async () => { root, packageFile, await stringify({ - dependencies: { 'drizzle-orm': '1.0.0-rc.4', effect: '4.0.0-beta.107', jose: '6.2.5' }, + dependencies: { + 'drizzle-orm': '1.0.0-rc.4', + effect: '4.0.0-beta.107', + jose: '6.2.5', + }, name: 'knip-consumer-controls', private: true, type: 'module', @@ -208,7 +214,10 @@ await test('real pinned Knip models exact consumers and preserves neighboring fi node: false, project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], }, - 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + 'verticals/*': { + entry: [indexFile, configurationFiles], + project: ['**/*.{ts,mts}'], + }, }, }), ); @@ -346,7 +355,10 @@ await test('runner calibrates only the proven resolver record and retains the di node: false, project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], }, - 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + 'verticals/*': { + entry: [indexFile, configurationFiles], + project: ['**/*.{ts,mts}'], + }, }, }), ); diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index e7091e43f..a6efcc0e3 100644 --- a/app/scripts/tests/quality-audit-runtime-model.test.mts +++ b/app/scripts/tests/quality-audit-runtime-model.test.mts @@ -1,14 +1,16 @@ -import { runPinnedKnip } from './quality-audit-test-support.mts'; import assert from 'node:assert/strict'; import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; + import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; + import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import { buildKnipModel } from '../../quality-audit/knip-model.mts'; import { buildKnipRuntimeEvidence } from '../../quality-audit/knip-runtime-model.mts'; +import { runPinnedKnip } from './quality-audit-test-support.mts'; const shellRoot = 'apps/shell'; const layoutFile = `${shellRoot}/src/routes/layout.tsx`; @@ -145,7 +147,10 @@ await test('runtime consumers require the exact CSS, shell, deployment and compi root, compilerConfig, await stringify({ - compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, + compilerOptions: { + plugins: [{ name: pluginName }], + types: [pluginName], + }, }), ); write( diff --git a/app/scripts/tests/quality-audit-test-support.mts b/app/scripts/tests/quality-audit-test-support.mts index 2ace22af6..8e471bc13 100644 --- a/app/scripts/tests/quality-audit-test-support.mts +++ b/app/scripts/tests/quality-audit-test-support.mts @@ -1,7 +1,9 @@ import { spawnSync } from 'node:child_process'; import { mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; + import { Schema } from 'effect'; + import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import { KnipConfigSchema } from '../../quality-audit/knip-model.mts'; @@ -13,14 +15,14 @@ export const runPinnedKnip = async ( model: { readonly config: typeof KnipConfigSchema.Type; readonly consumerSource: string; - }, + } ) => { const directory = path.dirname(consumerPath); mkdirSync(directory, { recursive: true }); writeFileSync(consumerPath, model.consumerSource); const configPath = path.join(directory, 'knip.json'); const configuration = await runEffectTestPromise( - Schema.encodeEffect(Schema.fromJsonString(KnipConfigSchema))(model.config), + Schema.encodeEffect(Schema.fromJsonString(KnipConfigSchema))(model.config) ); writeFileSync(configPath, configuration); return spawnSync( @@ -35,6 +37,6 @@ export const runPinnedKnip = async ( 'json', '--no-progress', ], - { encoding: 'utf-8', timeout: 60_000 }, + { encoding: 'utf-8', timeout: 60_000 } ); }; diff --git a/app/scripts/tests/report-fail-closed-authorization-impact.test.mts b/app/scripts/tests/report-fail-closed-authorization-impact.test.mts index 637909ffe..9d518b39c 100644 --- a/app/scripts/tests/report-fail-closed-authorization-impact.test.mts +++ b/app/scripts/tests/report-fail-closed-authorization-impact.test.mts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + import { reduceAuthorizationImpact } from '../report-fail-closed-authorization-impact.mts'; const inventoryHash = 'a'.repeat(64); @@ -32,7 +33,10 @@ const event = (changed: EvidenceFixtureOverride = {}) => ({ }); await test('impact reduction is deterministic and aggregates sanitized evidence', () => { - const report = reduceAuthorizationImpact([event({ timestamp: observationEndedAt }), event()]); + const report = reduceAuthorizationImpact([ + event({ timestamp: observationEndedAt }), + event(), + ]); assert.equal(report.totalWouldDeny, 2); assert.equal(report.aggregates[0]?.count, 2); assert.deepEqual(report.observation, { @@ -43,11 +47,18 @@ await test('impact reduction is deterministic and aggregates sanitized evidence' await test('impact reduction rejects mixed build evidence and sensitive extra fields', () => { assert.throws( - () => reduceAuthorizationImpact([event(), event({ sourceRevision: 'other' })]), - /mixes/u, + () => + reduceAuthorizationImpact([event(), event({ sourceRevision: 'other' })]), + /mixes/u + ); + assert.throws( + () => reduceAuthorizationImpact([event({ principalId: 'secret' })]), + /prohibited/u + ); + assert.throws( + () => reduceAuthorizationImpact([event({ tenantId: 'secret' })]), + /prohibited/u ); - assert.throws(() => reduceAuthorizationImpact([event({ principalId: 'secret' })]), /prohibited/u); - assert.throws(() => reduceAuthorizationImpact([event({ tenantId: 'secret' })]), /prohibited/u); }); await test('a bounded empty observation produces a zero-impact report', () => { @@ -63,15 +74,22 @@ await test('a bounded empty observation produces a zero-impact report', () => { await test('impact reduction rejects sensitive values smuggled into allowed evidence fields', () => { assert.throws( - () => reduceAuthorizationImpact([event({ entrypointKey: 'tenant@example.com' })]), - prohibitedValuePattern, + () => + reduceAuthorizationImpact([ + event({ entrypointKey: 'tenant@example.com' }), + ]), + prohibitedValuePattern ); assert.throws( - () => reduceAuthorizationImpact([event({ denialReason: 'principal-a2000000' })]), - prohibitedValuePattern, + () => + reduceAuthorizationImpact([ + event({ denialReason: 'principal-a2000000' }), + ]), + prohibitedValuePattern ); assert.throws( - () => reduceAuthorizationImpact([event({ policyClass: 'raw-relation-tuple' })]), - prohibitedValuePattern, + () => + reduceAuthorizationImpact([event({ policyClass: 'raw-relation-tuple' })]), + prohibitedValuePattern ); }); diff --git a/app/scripts/tests/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index a06cccede..b311e8d4d 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -1,11 +1,26 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { fileURLToPath, pathToFileURL } from 'node:url'; + import { NodeServices } from '@effect/platform-node'; -import { Config, Effect, ManagedRuntime, Predicate, Schema, Stream } from 'effect'; +import { + Config, + Effect, + ManagedRuntime, + Predicate, + Schema, + Stream, +} from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; interface TypecheckResult { @@ -18,10 +33,14 @@ interface WorkspaceScriptPlan { readonly typecheck: string; } -type WorkspaceScriptPlanFactory = (applications: readonly string[]) => WorkspaceScriptPlan; +type WorkspaceScriptPlanFactory = ( + applications: readonly string[] +) => WorkspaceScriptPlan; const callable = void>() => - Schema.Opaque()(Schema.Unknown.pipe(Schema.refine(Predicate.isFunction))); + Schema.Opaque()( + Schema.Unknown.pipe(Schema.refine(Predicate.isFunction)) + ); const PackageJsonSchema = Schema.Struct({ scripts: Schema.Struct({ typecheck: Schema.String }), @@ -34,35 +53,44 @@ const WorkspaceScriptPlanModuleSchema = Schema.Struct({ const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); const packageJsonFile = 'package.json'; const tsconfigFile = 'tsconfig.json'; -const packageJson = Schema.decodeUnknownSync(Schema.fromJsonString(PackageJsonSchema))( - readFileSync(path.join(workspaceRoot, packageJsonFile), 'utf-8'), +const packageJson = Schema.decodeUnknownSync( + Schema.fromJsonString(PackageJsonSchema) +)(readFileSync(path.join(workspaceRoot, packageJsonFile), 'utf-8')); +const typecheckWrapper = path.join( + workspaceRoot, + 'scripts/ultramodern-typecheck.mts' ); -const typecheckWrapper = path.join(workspaceRoot, 'scripts/ultramodern-typecheck.mts'); const executablePath = path.join(workspaceRoot, 'node_modules/.bin'); const typecheckRuntime = ManagedRuntime.make(NodeServices.layer); const runTypecheck = async ( fixture: string, - commandArguments: readonly string[], + commandArguments: readonly string[] ): Promise => await typecheckRuntime.runPromise( Effect.gen(function* runTypecheckEffect() { - const inheritedPath = yield* Config.string('PATH').pipe(Config.withDefault('')); + const inheritedPath = yield* Config.string('PATH').pipe( + Config.withDefault('') + ); const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; return yield* Effect.scoped( Effect.gen(function* collectTypecheckResult() { const handle = yield* processSpawner.spawn( - ChildProcess.make(process.execPath, [typecheckWrapper, ...commandArguments], { - cwd: fixture, - env: { - PATH: `${executablePath}${path.delimiter}${inheritedPath}`, - ULTRAMODERN_WORKSPACE_ROOT: fixture, - }, - extendEnv: true, - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }), + ChildProcess.make( + process.execPath, + [typecheckWrapper, ...commandArguments], + { + cwd: fixture, + env: { + PATH: `${executablePath}${path.delimiter}${inheritedPath}`, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + } + ) ); const [status, stdout, stderr] = yield* Effect.all( [ @@ -70,47 +98,56 @@ const runTypecheck = async ( handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString), ], - { concurrency: 'unbounded' }, + { concurrency: 'unbounded' } ); return { status, stderr, stdout }; - }), + }) ); - }), + }) ); test.after(async () => { await typecheckRuntime.dispose(); }); -void test('installed workspace generator keeps build mode as the root typecheck default', async () => { +void test('installed generator project default preserves the consumer reference-build gate', async () => { const generator = Schema.decodeUnknownSync(WorkspaceScriptPlanModuleSchema)( await import( pathToFileURL( path.join( workspaceRoot, - 'node_modules/@modern-js/create/dist/esm-node/ultramodern-workspace/workspace-script-plan.js', - ), + 'node_modules/@modern-js/ultramodern-create/dist/esm-node/ultramodern-workspace/workspace-script-plan.js' + ) ).href - ), + ) ); const scriptPlan = Schema.decodeUnknownSync(WorkspaceScriptPlanSchema)( - generator.createWorkspaceRootScriptPlan([]), + generator.createWorkspaceRootScriptPlan([]) ); assert.equal( scriptPlan.typecheck, - 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json', + 'node ./scripts/ultramodern-typecheck.mts --project tsconfig.json' + ); + assert.equal( + packageJson.scripts.typecheck, + 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json' ); }); void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', async () => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-')); + const fixture = mkdtempSync( + path.join(os.tmpdir(), 'ontos-drizzle-declarations-') + ); try { symlinkSync( path.join(workspaceRoot, 'node_modules'), path.join(fixture, 'node_modules'), - 'dir', + 'dir' + ); + writeFileSync( + path.join(fixture, packageJsonFile), + '{"private":true,"type":"module"}\n' ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); writeFileSync( path.join(fixture, tsconfigFile), JSON.stringify({ @@ -125,13 +162,13 @@ void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', types: ['node'], }, files: ['./consumer.mts', './consumer.cts'], - }), + }) ); for (const extension of ['mts', 'cts']) { writeFileSync( path.join(fixture, `consumer.${extension}`), 'import { pgTable, uuid } from "drizzle-orm/pg-core";\n' + - 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', + 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n' ); } const result = await runTypecheck(fixture, ['--project', tsconfigFile]); @@ -142,18 +179,23 @@ void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', }); void test('root typecheck checks referenced projects and rejects a newly introduced type error', async () => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-typecheck-references-')); + const fixture = mkdtempSync( + path.join(os.tmpdir(), 'ontos-typecheck-references-') + ); try { mkdirSync(path.join(fixture, 'referenced')); symlinkSync( path.join(workspaceRoot, 'node_modules'), path.join(fixture, 'node_modules'), - 'dir', + 'dir' + ); + writeFileSync( + path.join(fixture, packageJsonFile), + '{"private":true,"type":"module"}\n' ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); writeFileSync( path.join(fixture, tsconfigFile), - JSON.stringify({ files: [], references: [{ path: './referenced' }] }), + JSON.stringify({ files: [], references: [{ path: './referenced' }] }) ); writeFileSync( path.join(fixture, 'referenced/tsconfig.json'), @@ -167,26 +209,41 @@ void test('root typecheck checks referenced projects and rejects a newly introdu types: [], }, files: ['./index.ts'], - }), + }) ); const sourceFile = path.join(fixture, 'referenced/index.ts'); - writeFileSync(sourceFile, 'export const referenceGateFixture: number = 1;\n'); - const [runtime, wrapper, ...args] = packageJson.scripts.typecheck.split(' '); + writeFileSync( + sourceFile, + 'export const referenceGateFixture: number = 1;\n' + ); + const [runtime, wrapper, ...args] = + packageJson.scripts.typecheck.split(' '); assert.equal(runtime, 'node'); assert.equal(wrapper, './scripts/ultramodern-typecheck.mts'); assert.equal(path.join(workspaceRoot, wrapper), typecheckWrapper); const initial = await runTypecheck(fixture, args); assert.equal(initial.status, 0, initial.stdout + initial.stderr); assert.ok( - readFileSync(path.join(fixture, 'referenced/output/index.d.ts'), 'utf-8').includes( - 'referenceGateFixture', - ), - 'the referenced project must actually be built; a root files:[] project check is a no-op', + readFileSync( + path.join(fixture, 'referenced/output/index.d.ts'), + 'utf-8' + ).includes('referenceGateFixture'), + 'the referenced project must actually be built; a root files:[] project check is a no-op' + ); + writeFileSync( + sourceFile, + 'export const referenceGateFixture: number = "invalid";\n' ); - writeFileSync(sourceFile, 'export const referenceGateFixture: number = "invalid";\n'); const invalid = await runTypecheck(fixture, args); - assert.notEqual(invalid.status, 0, 'a referenced source type error must fail the root gate'); - assert.match(invalid.stdout + invalid.stderr, /referenced[/\\]index\.ts.*TS2322/u); + assert.notEqual( + invalid.status, + 0, + 'a referenced source type error must fail the root gate' + ); + assert.match( + invalid.stdout + invalid.stderr, + /referenced[/\\]index\.ts.*TS2322/u + ); } finally { rmSync(fixture, { force: true, recursive: true }); } diff --git a/app/scripts/ultramodern-performance-readiness.config.mjs b/app/scripts/ultramodern-performance-readiness.config.mjs index 486c34af7..a73c2b27e 100644 --- a/app/scripts/ultramodern-performance-readiness.config.mjs +++ b/app/scripts/ultramodern-performance-readiness.config.mjs @@ -2,5 +2,6 @@ export default { enabled: true, failOn: 'framework-invariant', - reportPath: '.codex/reports/performance-readiness/ultramodern-performance-readiness.json', + reportPath: + '.codex/reports/performance-readiness/ultramodern-performance-readiness.json', }; diff --git a/app/scripts/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index 9472cde48..20f263c38 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -18,7 +18,17 @@ import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import { NodeRuntime, NodeServices } from '@effect/platform-node'; -import { Config, Effect, Inspectable, Layer, Predicate, Result, Schema } from 'effect'; +import { + Array as EffectArray, + Config, + Effect, + Inspectable, + Layer, + Order, + Predicate, + Result, + Schema, +} from 'effect'; import type { Json } from 'effect/Schema'; import compactConfigDocument from '../.modernjs/ultramodern.json' with { type: 'json' }; import shellPackageDocument from '../apps/shell-super-app/package.json' with { type: 'json' }; @@ -34,6 +44,7 @@ import { resolvePublishedContractModuleId, } from './published-outbox-contracts.mts'; import { checkOntosModuleContracts } from './check-ontos-module-contracts.mts'; +import { moduleFederationBridgeViolation } from './module-federation-bridge-boundary.mts'; const nodeRequire = createRequire(import.meta.url); const nodeFileSystemModule = ['node', 'fs'].join(':'); @@ -91,7 +102,7 @@ const SHARED_VALIDATOR_STRING_020 = '@app/shared-design-tokens'; const SHARED_VALIDATOR_STRING_021 = '@app/shell-super-app'; const SHARED_VALIDATOR_STRING_022 = '@modern-js/app-tools'; const SHARED_VALIDATOR_STRING_023 = '@modern-js/code-tools'; -const SHARED_VALIDATOR_STRING_024 = '@modern-js/create'; +const SHARED_VALIDATOR_STRING_024 = '@modern-js/ultramodern-create'; const SHARED_VALIDATOR_STRING_025 = '@modern-js/plugin-bff'; const SHARED_VALIDATOR_STRING_026 = '@modern-js/plugin-bff/effect'; const SHARED_VALIDATOR_STRING_027 = '@modern-js/plugin-i18n'; @@ -104,9 +115,9 @@ const SHARED_VALIDATOR_STRING_033 = '/party-registry-api/openapi.json'; const SHARED_VALIDATOR_STRING_034 = '/party-registry-api/party-registry/readiness'; const SHARED_VALIDATOR_STRING_035 = '#super-app-platform'; const SHARED_VALIDATOR_STRING_036 = '2026-06-02'; -const SHARED_VALIDATOR_STRING_037 = '3.8.2-ultramodern.12'; +const SHARED_VALIDATOR_STRING_037 = '3.9.0-ultramodern.2'; const SHARED_VALIDATOR_STRING_038 = '3f023644c8a07e9a'; -const SHARED_VALIDATOR_STRING_039 = '4.0.0-beta.107'; +const SHARED_VALIDATOR_STRING_039 = '4.0.0-rc.112'; const SHARED_VALIDATOR_STRING_040 = 'additionalShellBuildMarkerIds'; const SHARED_VALIDATOR_STRING_041 = 'additionalShellDegradedStateIds'; const SHARED_VALIDATOR_STRING_042 = 'additionalShellDeliveryUnitIds'; @@ -252,7 +263,7 @@ const SHARED_VALIDATOR_STRING_175 = '@app/gateway-principal-verifier'; const SHARED_VALIDATOR_STRING_176 = 'gateway-principal-verifier'; const SHARED_VALIDATOR_STRING_177 = 'packages/gateway-principal-verifier'; -// Generated by modern-js-create with an immutable expected proof contract. +// Generated by ultramodern-create with an immutable expected proof contract. const root = process.cwd(); const isString = Schema.is(Schema.String); const isNumber = Schema.is(Schema.Number); @@ -472,7 +483,7 @@ const createVerticalBackendFederationContract = () => ({ compatibility: { contractVersion: SHARED_VALIDATOR_STRING_079, effectVersion: SHARED_VALIDATOR_STRING_039, - moduleFederationVersion: '2.8.0', + moduleFederationVersion: '2.9.0', packageName: SHARED_VALIDATOR_STRING_018, }, deliveryUnit: { @@ -606,17 +617,20 @@ const workspaceValidationContractDefinition = { aliases: { '@modern-js/adapter-rstest': '@bleedingdev/modern-js-adapter-rstest', '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools', + '@modern-js/app-tools-extensions': '@bleedingdev/modern-js-app-tools-extensions', '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core', + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect', '@modern-js/bff-runtime': '@bleedingdev/modern-js-bff-runtime', '@modern-js/builder': '@bleedingdev/modern-js-builder', '@modern-js/code-tools': '@bleedingdev/modern-js-code-tools', - '@modern-js/create': '@bleedingdev/modern-js-create', '@modern-js/create-request': '@bleedingdev/modern-js-create-request', + '@modern-js/i18n-runtime-extensions': '@bleedingdev/modern-js-i18n-runtime-extensions', '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils', '@modern-js/image': '@bleedingdev/modern-js-image', '@modern-js/main-doc': '@bleedingdev/modern-js-main-doc', '@modern-js/plugin': '@bleedingdev/modern-js-plugin', '@modern-js/plugin-bff': '@bleedingdev/modern-js-plugin-bff', + '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions', '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader', '@modern-js/plugin-i18n': '@bleedingdev/modern-js-plugin-i18n', '@modern-js/plugin-polyfill': '@bleedingdev/modern-js-plugin-polyfill', @@ -626,6 +640,7 @@ const workspaceValidationContractDefinition = { '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server', '@modern-js/render': '@bleedingdev/modern-js-render', '@modern-js/runtime': '@bleedingdev/modern-js-runtime', + '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions', '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils', '@modern-js/sandpack-react': '@bleedingdev/modern-js-sandpack-react', '@modern-js/server': '@bleedingdev/modern-js-server', @@ -635,6 +650,9 @@ const workspaceValidationContractDefinition = { '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils', '@modern-js/tsconfig': '@bleedingdev/modern-js-tsconfig', '@modern-js/types': '@bleedingdev/modern-js-types', + '@modern-js/ultramodern-create': '@bleedingdev/modern-js-ultramodern-create', + '@modern-js/ultramodern-sandpack-profile': + '@bleedingdev/modern-js-ultramodern-sandpack-profile', '@modern-js/utils': '@bleedingdev/modern-js-utils', }, packages: [ @@ -644,15 +662,25 @@ const workspaceValidationContractDefinition = { version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_022, + sourceName: '@modern-js/app-tools', targetName: '@bleedingdev/modern-js-app-tools', version: SHARED_VALIDATOR_STRING_037, }, + { + sourceName: '@modern-js/app-tools-extensions', + targetName: '@bleedingdev/modern-js-app-tools-extensions', + version: SHARED_VALIDATOR_STRING_037, + }, { sourceName: '@modern-js/bff-core', targetName: '@bleedingdev/modern-js-bff-core', version: SHARED_VALIDATOR_STRING_037, }, + { + sourceName: '@modern-js/bff-effect', + targetName: '@bleedingdev/modern-js-bff-effect', + version: SHARED_VALIDATOR_STRING_037, + }, { sourceName: '@modern-js/bff-runtime', targetName: '@bleedingdev/modern-js-bff-runtime', @@ -664,18 +692,18 @@ const workspaceValidationContractDefinition = { version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_023, + sourceName: '@modern-js/code-tools', targetName: '@bleedingdev/modern-js-code-tools', version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_024, - targetName: '@bleedingdev/modern-js-create', + sourceName: '@modern-js/create-request', + targetName: '@bleedingdev/modern-js-create-request', version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: '@modern-js/create-request', - targetName: '@bleedingdev/modern-js-create-request', + sourceName: '@modern-js/i18n-runtime-extensions', + targetName: '@bleedingdev/modern-js-i18n-runtime-extensions', version: SHARED_VALIDATOR_STRING_037, }, { @@ -699,17 +727,22 @@ const workspaceValidationContractDefinition = { version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_025, + sourceName: '@modern-js/plugin-bff', targetName: '@bleedingdev/modern-js-plugin-bff', version: SHARED_VALIDATOR_STRING_037, }, + { + sourceName: '@modern-js/plugin-bff-extensions', + targetName: '@bleedingdev/modern-js-plugin-bff-extensions', + version: SHARED_VALIDATOR_STRING_037, + }, { sourceName: '@modern-js/plugin-data-loader', targetName: '@bleedingdev/modern-js-plugin-data-loader', version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_027, + sourceName: '@modern-js/plugin-i18n', targetName: '@bleedingdev/modern-js-plugin-i18n', version: SHARED_VALIDATOR_STRING_037, }, @@ -729,7 +762,7 @@ const workspaceValidationContractDefinition = { version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_028, + sourceName: '@modern-js/plugin-tanstack', targetName: '@bleedingdev/modern-js-plugin-tanstack', version: SHARED_VALIDATOR_STRING_037, }, @@ -744,10 +777,15 @@ const workspaceValidationContractDefinition = { version: SHARED_VALIDATOR_STRING_037, }, { - sourceName: SHARED_VALIDATOR_STRING_029, + sourceName: '@modern-js/runtime', targetName: '@bleedingdev/modern-js-runtime', version: SHARED_VALIDATOR_STRING_037, }, + { + sourceName: '@modern-js/runtime-extensions', + targetName: '@bleedingdev/modern-js-runtime-extensions', + version: SHARED_VALIDATOR_STRING_037, + }, { sourceName: '@modern-js/runtime-utils', targetName: '@bleedingdev/modern-js-runtime-utils', @@ -793,6 +831,16 @@ const workspaceValidationContractDefinition = { targetName: '@bleedingdev/modern-js-types', version: SHARED_VALIDATOR_STRING_037, }, + { + sourceName: '@modern-js/ultramodern-create', + targetName: '@bleedingdev/modern-js-ultramodern-create', + version: SHARED_VALIDATOR_STRING_037, + }, + { + sourceName: '@modern-js/ultramodern-sandpack-profile', + targetName: '@bleedingdev/modern-js-ultramodern-sandpack-profile', + version: SHARED_VALIDATOR_STRING_037, + }, { sourceName: '@modern-js/utils', targetName: '@bleedingdev/modern-js-utils', @@ -806,7 +854,7 @@ const workspaceValidationContractDefinition = { schema: 'bleedingdev.ultramodern.release-cohort', schemaVersion: 1, source: { - commit: '69f2b5648e13a057261f22bb36cb2d8ca2d5962f', + commit: 'd2c75828230edf92775feca796c0960af754508f', repository: 'BleedingDev/ultramodern.js', }, }, @@ -1049,13 +1097,6 @@ const workspaceValidationContractDefinition = { }, ], patterns: [ - { - diagnostic: 'Generated Module Federation must keep bridge routing enabled.', - expression: '\\benableBridgeRouter\\s*:\\s*false\\b', - fixArea: 'remove enableBridgeRouter: false', - flags: 'u', - id: 'bridge-router-disabled', - }, { diagnostic: 'Generated Module Federation must keep dynamic remote type hints enabled.', expression: '\\bdisableDynamicRemoteTypeHints\\s*:\\s*true\\b', @@ -1204,7 +1245,7 @@ const workspaceValidationContractDefinition = { }, node: { engineRange: '>=26', - version: '26.5.0', + version: '26.7.0', }, oldRemotePaths: ['apps/remotes'], packageScope: 'app', @@ -1336,7 +1377,7 @@ const workspaceValidationContractDefinition = { profile: SHARED_VALIDATOR_STRING_057, schemaVersion: 1, tooling: { - command: 'modern-js-create ultramodern', + command: 'ultramodern-create ultramodern', wrappers: { apiBoundaries: 'scripts/check-ultramodern-api-boundaries.mts', backendFederationGenerate: SHARED_VALIDATOR_STRING_116, @@ -1356,7 +1397,7 @@ const workspaceValidationContractDefinition = { workspace: { node: { engineRange: '>=26', - version: '26.5.0', + version: '26.7.0', }, packageManager: { name: 'pnpm', @@ -1549,6 +1590,10 @@ const workspaceValidationContractDefinition = { id: SHARED_VALIDATOR_STRING_098, kind: 'vertical', moduleFederation: { + dts: { + compilerInstance: SHARED_VALIDATOR_STRING_068, + tsConfigPath: SHARED_VALIDATOR_STRING_007, + }, exposes: [SHARED_VALIDATOR_STRING_005], name: SHARED_VALIDATOR_STRING_159, role: 'remote', @@ -1884,8 +1929,8 @@ const workspaceValidationContractDefinition = { versions: { cloudflareCompatibilityDate: SHARED_VALIDATOR_STRING_036, effect: SHARED_VALIDATOR_STRING_039, - moduleFederation: '2.8.0', - node: '26.5.0', + moduleFederation: '2.9.0', + node: '26.7.0', pnpm: '11.25.0', }, }; @@ -4574,8 +4619,14 @@ const assertTsConfigReferenceGraph = () => { 'restore the generated root project-reference graph', ); assertSameJson( - shellTsConfig.references ?? [], - expectedShellReferences, + EffectArray.sort( + shellTsConfig.references ?? [], + Order.mapInput(Order.String, (reference: { readonly path: string }) => reference.path), + ), + EffectArray.sort( + expectedShellReferences, + Order.mapInput(Order.String, (reference: { readonly path: string }) => reference.path), + ), 'apps/shell-super-app/tsconfig.json references', 'restore the generated shell project-reference graph', ); @@ -4584,8 +4635,11 @@ const assertTsConfigReferenceGraph = () => { 'tsconfig.base.json must not use skipLibCheck', ); assertSameJson( - shellTsConfig.include ?? [], - ['api', 'src', SHARED_VALIDATOR_STRING_075, SHARED_VALIDATOR_STRING_091, 'shared'], + EffectArray.sort(shellTsConfig.include ?? [], Order.String), + EffectArray.sort( + ['api', 'server', 'src', SHARED_VALIDATOR_STRING_075, SHARED_VALIDATOR_STRING_091, 'shared'], + Order.String, + ), 'apps/shell-super-app/tsconfig.json include', 'restore the generated shell typecheck boundary', ); @@ -5132,7 +5186,6 @@ const requiredPaths = [ SHARED_VALIDATOR_STRING_103, `patches/@module-federation__modern-js-v3@${expectedModuleFederationVersion}.patch`, `patches/@module-federation__bridge-react@${expectedModuleFederationVersion}.patch`, - 'patches/effect-schema-sentinel.patch', 'tsconfig.json', 'tsconfig.base.json', 'oxlint.config.ts', @@ -5239,30 +5292,8 @@ assert( pnpmWorkspace.includes("'@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch"), 'pnpm-workspace.yaml must patch the deployment tracer for transient filesystem markers', ); -assert( - pnpmWorkspace.includes( - "'@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch", - ), - 'pnpm-workspace.yaml must patch generated deploy entries for nested CommonJS defaults', -); -const modernAppToolsPatch = readText( - 'patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch', -); -assert( - modernAppToolsPatch.split( - sourceFragment( - 'typeof plugin_', - templatePlaceholderOpening, - "index}_ns.default?.default === 'function'", - ), - ).length - - 1 === - 3 && - modernAppToolsPatch.includes( - sourceFragment('plugin_', templatePlaceholderOpening, 'index}_ns.default.default'), - ), - 'Modern.js deploy entries must unwrap callable direct and nested plugin defaults in CJS and ESM generators', -); +// The published 3.9 cohort owns deploy-entry interop; no obsolete app-tools +// patch is required. Release output is exercised by the runtime script proofs. const vercelNftPatch = readText('patches/@vercel__nft@0.29.2.patch'); assert( vercelNftPatch.split('isBuildHostSystemPath').length - 1 >= 4 && @@ -5351,14 +5382,19 @@ assert( ), 'pnpm-workspace.yaml must patch the generated Module Federation React bridge cohort', ); -assert( - [`'effect@${expectedEffectVersion}'`, `effect@${expectedEffectVersion}`].some((effectPatchKey) => - pnpmWorkspace.includes(`${effectPatchKey}: patches/effect-schema-sentinel.patch`), - ), - 'pnpm-workspace.yaml must patch the generated Effect declaration cohort', -); assertWorkspaceValidationContract(workspaceValidationContract); assertGeneratedSurfacePolicy(); +for (const appPath of [ + SHARED_VALIDATOR_STRING_047, + ...fullStackVerticals.filter((vertical) => vertical.emitsUi).map((vertical) => vertical.path), + ...expectedAdditionalShells.map((shell) => shell.path), +]) { + const violation = moduleFederationBridgeViolation( + readText(`${appPath}/module-federation.config.ts`), + readJson(PackageJsonSchema, `${appPath}/package.json`), + ); + assert(violation === undefined, `${appPath}: ${violation}`); +} for (const oldRemotePath of oldRemotePaths) { assertNotExists(oldRemotePath); } @@ -6400,7 +6436,7 @@ if (hasDeliveryUnits) { 'Zerops manifest must deploy package-pruned runtime directories', ); const localVirtualStoreInstall = - 'PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm'; + 'PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm'; assert( zeropsYaml.split(localVirtualStoreInstall).length - 1 === fullStackVerticals.length + 2 + workerDeliveryCount, @@ -6431,7 +6467,7 @@ if (hasDeliveryUnits) { ); assert( zeropsYaml.includes( - `start: sh -c ${quoteYamlString('cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.5.0/bin:$PATH" exec npm run serve')}`, + `start: sh -c ${quoteYamlString('cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve')}`, ), 'Zerops shell service must start from materialized runtime package', ); @@ -6472,7 +6508,7 @@ if (hasDeliveryUnits) { ); assert( zeropsYaml.includes( - `PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app ${quoteShellValue(vertical.id)} --package ${quoteShellValue(vertical.packageName)} --package-dir ${quoteShellValue(vertical.path)}`, + `PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app ${quoteShellValue(vertical.id)} --package ${quoteShellValue(vertical.packageName)} --package-dir ${quoteShellValue(vertical.path)}`, ), `${vertical.id} Zerops service must materialize its runtime package`, ); @@ -6786,7 +6822,7 @@ assert( 'Shell app env must not redeclare framework-owned css asset modules', ); assert( - shellRouteMetadata.includes('@generated by @modern-js/create'), + shellRouteMetadata.includes('@generated by @modern-js/ultramodern-create'), 'Shell route metadata compatibility manifest must be marked generated', ); assert( @@ -7228,7 +7264,7 @@ for (const vertical of fullStackVerticals) { ); if (vertical.emitsUi) { assert( - routeMetadata.includes('@generated by @modern-js/create'), + routeMetadata.includes('@generated by @modern-js/ultramodern-create'), `${vertical.id} route metadata compatibility manifest must be marked generated`, ); assert( diff --git a/app/scripts/verify-application-db-schema.mts b/app/scripts/verify-application-db-schema.mts index ba1c888ee..1ad48adb7 100644 --- a/app/scripts/verify-application-db-schema.mts +++ b/app/scripts/verify-application-db-schema.mts @@ -1,9 +1,15 @@ import { Console, Effect, Exit, Schema } from 'effect'; import { Client } from 'pg'; import type { QueryResult, QueryResultRow } from 'pg'; + import { loadDatabaseConnectionPair } from '../packages/core-runtime/src/db/config.ts'; -const EXPECTED_APPLICATION_SCHEMAS = ['auth', 'contacts', 'core', 'party'] as const; +const EXPECTED_APPLICATION_SCHEMAS = [ + 'auth', + 'contacts', + 'core', + 'party', +] as const; const EXPECTED_MIGRATION_JOURNALS = [ '__drizzle_migrations_auth', '__drizzle_migrations_contacts', @@ -16,27 +22,33 @@ class ApplicationDatabaseVerificationError extends Schema.TaggedError - new ApplicationDatabaseVerificationError(cause === undefined ? { reason } : { cause, reason }); + new ApplicationDatabaseVerificationError( + cause === undefined ? { reason } : { cause, reason } + ); const query = ( client: Client, text: string, - reason: string, + reason: string ): Effect.Effect, ApplicationDatabaseVerificationError> => Effect.tryPromise({ catch: (cause) => verificationFailure(reason, cause), try: async () => await client.query(text), }); -const orderedValuesMatch = (actual: readonly string[], expected: readonly string[]): boolean => - actual.length === expected.length && actual.every((value, index) => value === expected[index]); +const orderedValuesMatch = ( + actual: readonly string[], + expected: readonly string[] +): boolean => + actual.length === expected.length && + actual.every((value, index) => value === expected[index]); const verifyApplicationCatalog = (client: Client) => Effect.gen(function* verifyApplicationCatalogEffect() { @@ -52,7 +64,7 @@ const verifyApplicationCatalog = (client: Client) => and namespace.nspname not in ('drizzle', 'public') order by namespace.nspname `, - 'Unable to verify the application schema catalog', + 'Unable to verify the application schema catalog' ); const journals = yield* query<{ table_name: string }>( client, @@ -65,19 +77,19 @@ const verifyApplicationCatalog = (client: Client) => and relation.relkind = 'r' order by relation.relname `, - 'Unable to verify the application migration journals', + 'Unable to verify the application migration journals' ); const actualSchemas = schemas.rows.map((row) => row.schema_name); const actualJournals = journals.rows.map((row) => row.table_name); if (!orderedValuesMatch(actualSchemas, EXPECTED_APPLICATION_SCHEMAS)) { yield* verificationFailure( - `Application schema mismatch; expected=[${EXPECTED_APPLICATION_SCHEMAS.join(', ')}], actual=[${actualSchemas.join(', ')}]`, + `Application schema mismatch; expected=[${EXPECTED_APPLICATION_SCHEMAS.join(', ')}], actual=[${actualSchemas.join(', ')}]` ); } if (!orderedValuesMatch(actualJournals, EXPECTED_MIGRATION_JOURNALS)) { yield* verificationFailure( - `Migration journal mismatch; expected=[${EXPECTED_MIGRATION_JOURNALS.join(', ')}], actual=[${actualJournals.join(', ')}]`, + `Migration journal mismatch; expected=[${EXPECTED_MIGRATION_JOURNALS.join(', ')}], actual=[${actualJournals.join(', ')}]` ); } }); @@ -95,12 +107,21 @@ const main = Effect.gen(function* verifyApplicationDatabase() { Effect.gen(function* acquireAdministrativeClient() { const client = yield* Effect.try({ catch: (cause) => - verificationFailure('Unable to create the administrative PostgreSQL client', cause), - try: () => new Client({ connectionString: configuration.admin.connectionString }), + verificationFailure( + 'Unable to create the administrative PostgreSQL client', + cause + ), + try: () => + new Client({ + connectionString: configuration.admin.connectionString, + }), }); yield* Effect.tryPromise({ catch: (cause) => - verificationFailure('Unable to connect to the administrative PostgreSQL database', cause), + verificationFailure( + 'Unable to connect to the administrative PostgreSQL database', + cause + ), try: async () => await client.connect(), }); return client; @@ -109,15 +130,21 @@ const main = Effect.gen(function* verifyApplicationDatabase() { (client) => Effect.tryPromise({ catch: (cause) => - verificationFailure('Unable to close the administrative PostgreSQL connection', cause), + verificationFailure( + 'Unable to close the administrative PostgreSQL connection', + cause + ), try: async () => await client.end(), - }), + }) ); - yield* Console.log('Verified exact application schemas and migration journals'); + yield* Console.log( + 'Verified exact application schemas and migration journals' + ); for (const ownerVerifierPath of ownerVerifierPaths) { yield* Effect.tryPromise({ - catch: (cause) => verificationFailure('An owner database verifier failed', cause), + catch: (cause) => + verificationFailure('An owner database verifier failed', cause), try: async () => { await import(ownerVerifierPath); }, diff --git a/app/topology/reference-topology.json b/app/topology/reference-topology.json index 351db5771..572e6f634 100644 --- a/app/topology/reference-topology.json +++ b/app/topology/reference-topology.json @@ -300,8 +300,8 @@ "compatibility": { "contractVersion": "microvertical-server-effect-v1", "packageName": "@app/party-registry", - "effectVersion": "4.0.0-beta.107", - "moduleFederationVersion": "2.8.0" + "effectVersion": "4.0.0-rc.112", + "moduleFederationVersion": "2.9.0" }, "cache": { "cloudflareSnapshot": "immutable", diff --git a/app/verticals/party-registry/api/party-registry-production-layers.ts b/app/verticals/party-registry/api/party-registry-production-layers.ts index ce857a54d..fb5c05837 100644 --- a/app/verticals/party-registry/api/party-registry-production-layers.ts +++ b/app/verticals/party-registry/api/party-registry-production-layers.ts @@ -2,6 +2,7 @@ import { ActionRuntimeLive, ContextAccessLive, CorePersistenceLive, + CoreSearchProjectionStoreLive, CoreSearchQueryRuntimeLive, ReadRuntimeLive, TenantModuleStateServiceLive, @@ -55,6 +56,7 @@ export const partyRegistryAresSubjectServiceLive = AresSubjectServiceLive.pipe( ); const coreSearchQueryRuntimeLive = CoreSearchQueryRuntimeLive.pipe( + Layer.provide(CoreSearchProjectionStoreLive), Layer.provide(CorePersistenceLive), ); export const partyRegistrySearchProjectionGatewayLive = PartySearchProjectionGatewayLive.pipe( diff --git a/app/verticals/party-registry/modern.config.ts b/app/verticals/party-registry/modern.config.ts index 9900accde..3cc10263a 100644 --- a/app/verticals/party-registry/modern.config.ts +++ b/app/verticals/party-registry/modern.config.ts @@ -7,7 +7,7 @@ import { } from '../../packages/shared-contracts/tooling/modern-config.ts'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; -import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools'; +import { appTools, defineConfig, presetUltramodern, ultramodernReleaseEnvelopePlugin } from '@modern-js/app-tools'; import type { AppTools, AppToolsUserConfig, CliPlugin } from '@modern-js/app-tools'; import { getBuildConfigEnvironment, withBuildConfigEnvironment } from '@modern-js/app-tools/config'; import { bffPlugin } from '@modern-js/plugin-bff'; @@ -224,6 +224,7 @@ export default defineConfig( }, plugins: [ appTools(), + ultramodernReleaseEnvelopePlugin(), tanstackRouterPlugin(), i18nPlugin({ backend: { @@ -255,7 +256,9 @@ export default defineConfig( reactI18next: false, }), bffPlugin(), - moduleFederationPlugin(), + moduleFederationPlugin({ + configPath: fileURLToPath(new URL('module-federation.config.ts', import.meta.url)), + }), zephyrRspackPlugin(), ], server: { diff --git a/app/verticals/party-registry/module-federation.config.ts b/app/verticals/party-registry/module-federation.config.ts index 24013e242..a47fb8f35 100644 --- a/app/verticals/party-registry/module-federation.config.ts +++ b/app/verticals/party-registry/module-federation.config.ts @@ -15,9 +15,14 @@ const runtimeVersion = packageVersion('@modern-js/runtime/package.json'); const reactVersion = packageVersion('react/package.json'); const reactDomVersion = packageVersion('react-dom/package.json'); -const tsgoCompilerInstance = resolveEffectTsgoCompiler({ from: import.meta.url }); +const tsgoCompilerInstance = resolveEffectTsgoCompiler({ + from: import.meta.url, +}); const moduleFederationConfig: Parameters[0] = createModuleFederationConfig({ + bridge: { + enableBridgeRouter: false, + }, dts: { displayErrorInTerminal: true, generateTypes: { compilerInstance: tsgoCompilerInstance }, @@ -61,8 +66,16 @@ const moduleFederationConfig: Parameters[0] singleton: true, treeShaking: false, }, - react: { requiredVersion: reactVersion, singleton: true, treeShaking: false }, - 'react-dom': { requiredVersion: reactDomVersion, singleton: true, treeShaking: false }, + react: { + requiredVersion: reactVersion, + singleton: true, + treeShaking: false, + }, + 'react-dom': { + requiredVersion: reactDomVersion, + singleton: true, + treeShaking: false, + }, 'react-dom/client': { requiredVersion: reactDomVersion, singleton: true, diff --git a/app/verticals/party-registry/package.json b/app/verticals/party-registry/package.json index a8bdf55b3..5fc978b84 100644 --- a/app/verticals/party-registry/package.json +++ b/app/verticals/party-registry/package.json @@ -41,9 +41,9 @@ "./resources/person-engagement-profile": "./shared/resources/person-engagement-profile.ts" }, "scripts": { - "build": "modern build && node ../../scripts/generate-ontos-module-contract.mts --vertical party-registry --target dist && node ../../scripts/generate-public-surface-assets.mts --app party-registry --target dist && MODERNJS_DEPLOY=node modern deploy --skip-build", - "cloudflare:build": "MODERNJS_DEPLOY=cloudflare modern build && node ../../scripts/generate-ontos-module-contract.mts --vertical party-registry --target cloudflare-dist && node ../../scripts/generate-public-surface-assets.mts --app party-registry --target cloudflare-dist && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build && node ../../scripts/verify-cloudflare-output.mts --app party-registry", - "cloudflare:deploy": "ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json", + "build": "modern build && node ../../scripts/generate-ontos-module-contract.mts --vertical party-registry --target dist && node ../../scripts/generate-public-surface-assets.mts --app party-registry --target dist && cross-env MODERNJS_DEPLOY=node modern deploy --skip-build", + "cloudflare:build": "cross-env MODERNJS_DEPLOY=cloudflare modern build && node ../../scripts/generate-ontos-module-contract.mts --vertical party-registry --target cloudflare-dist && node ../../scripts/generate-public-surface-assets.mts --app party-registry --target cloudflare-dist && cross-env MODERNJS_DEPLOY=cloudflare modern deploy --skip-build && node ../../scripts/verify-cloudflare-output.mts --app party-registry", + "cloudflare:deploy": "cross-env ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json", "cloudflare:preview": "pnpm run cloudflare:build && wrangler dev --config .output/wrangler.json", "cloudflare:proof": "node ../../scripts/proof-cloudflare-version.mts --app party-registry", "db:generate": "drizzle-kit generate --config drizzle.config.ts && drizzle-kit generate --config drizzle.contacts.config.ts", @@ -65,44 +65,44 @@ "@app/gateway-principal-verifier": "workspace:*", "@app/shared-contracts": "workspace:*", "@app/shared-design-tokens": "workspace:*", - "@effect/opentelemetry": "4.0.0-beta.107", - "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12", - "@modern-js/plugin-i18n": "npm:@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12", - "@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12", - "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12", - "@module-federation/modern-js-v3": "2.8.0", - "@module-federation/runtime": "2.8.0", - "@tanstack/react-router": "1.170.25", + "@effect/opentelemetry": "4.0.0-rc.112", + "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2", + "@modern-js/plugin-i18n": "npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2", + "@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.2", + "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2", + "@module-federation/modern-js-v3": "2.9.0", + "@module-federation/runtime": "2.9.0", + "@tanstack/react-router": "1.170.33", "drizzle-orm": "1.0.0-rc.5-ab785fc", - "effect": "4.0.0-beta.107", - "i18next": "26.3.6", + "effect": "4.0.0-rc.112", + "i18next": "26.4.2", "pg": "8.22.0", "react": "19.2.8", "react-dom": "19.2.8", - "react-router": "7.18.1", - "@effect/sql-pg": "4.0.0-beta.107" + "@effect/sql-pg": "4.0.0-rc.112" }, "devDependencies": { - "@effect/tsgo": "0.19.0", - "@modern-js/adapter-rstest": "npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12", - "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12", + "@effect/tsgo": "0.41.0", + "@modern-js/adapter-rstest": "npm:@bleedingdev/modern-js-adapter-rstest@3.9.0-ultramodern.2", + "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2", "@rsbuild/plugin-tailwindcss": "^2.0.3", "@rstest/core": "0.11.10", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", - "@types/node": "^20", + "@types/node": "^26.4.1", "@types/pg": "8.20.0", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", "@typescript/native": "npm:typescript@7.0.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "fast-check": "4.9.0", "happy-dom": "20.8.3", "jose": "6.2.5", - "tailwindcss": "^4.3.2", + "tailwindcss": "^4.3.3", "typescript": "7.0.2", - "wrangler": "4.110.0", - "zephyr-rspack-plugin": "1.2.4" + "wrangler": "4.116.0", + "zephyr-rspack-plugin": "1.2.4", + "cross-env": "10.1.0" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/verticals/party-registry/src/routes/ultramodern-route-metadata.ts b/app/verticals/party-registry/src/routes/ultramodern-route-metadata.ts index a1c6481ab..f4e533578 100644 --- a/app/verticals/party-registry/src/routes/ultramodern-route-metadata.ts +++ b/app/verticals/party-registry/src/routes/ultramodern-route-metadata.ts @@ -1,4 +1,4 @@ -// @generated by @modern-js/create. +// @generated by @modern-js/ultramodern-create. // Author route metadata in colocated src/routes/**/route.meta.ts files. // This compatibility manifest is regenerated from route-owned metadata. diff --git a/app/verticals/party-registry/tests/integration/governed-identity.test.ts b/app/verticals/party-registry/tests/integration/governed-identity.test.ts index fc6d3e71d..71cf266aa 100644 --- a/app/verticals/party-registry/tests/integration/governed-identity.test.ts +++ b/app/verticals/party-registry/tests/integration/governed-identity.test.ts @@ -5,6 +5,7 @@ import { } from '@app/core-runtime/testing/effect-runtime'; import type { TrustedPrincipalContext } from '@app/core-runtime'; import { + CoreSearchProjectionStoreLive, CoreSearchQueryRuntimeLive, loadDatabaseConnectionPair, ReadRuntime, @@ -341,6 +342,7 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ); const searchLayer = PartySearchProjectionGatewayLive.pipe( Layer.provide(CoreSearchQueryRuntimeLive), + Layer.provide(CoreSearchProjectionStoreLive), ); const searchContext = yield* Layer.build(searchLayer).pipe( Effect.provideContext(fixtureContext), diff --git a/app/verticals/party-registry/tsconfig.json b/app/verticals/party-registry/tsconfig.json index b926449db..eef17fc5e 100644 --- a/app/verticals/party-registry/tsconfig.json +++ b/app/verticals/party-registry/tsconfig.json @@ -7,35 +7,34 @@ "emitDeclarationOnly": true, "incremental": true, "noEmit": false, - "skipLibCheck": true, "outDir": "../../node_modules/.cache/tsgo/declarations/verticals__party-registry", "tsBuildInfoFile": "../../node_modules/.cache/tsgo/verticals__party-registry.tsbuildinfo" }, "include": [ "src", - "scripts", - "tests", - "drizzle.config.ts", "locales/**/*.json", "package.json", "shared", "server", "api", + "scripts", + "tests", + "drizzle.config.ts", "vertical.manifest.ts", "vertical.registration.ts" ], "references": [ { - "path": "../../packages/core-runtime" + "path": "../../packages/shared-contracts" }, { - "path": "../../packages/gateway-principal-verifier" + "path": "../../packages/shared-design-tokens" }, { - "path": "../../packages/shared-contracts" + "path": "../../packages/core-runtime" }, { - "path": "../../packages/shared-design-tokens" + "path": "../../packages/gateway-principal-verifier" } ] } diff --git a/app/verticals/party-registry/tsconfig.mf-types.json b/app/verticals/party-registry/tsconfig.mf-types.json index d372b32c8..a4849d0e9 100644 --- a/app/verticals/party-registry/tsconfig.mf-types.json +++ b/app/verticals/party-registry/tsconfig.mf-types.json @@ -1,9 +1,13 @@ { "extends": "../../tsconfig.base.json", "include": [ + "src/components/page-contacts.tsx", + "src/modern-app-env.d.ts", "src/federation-entry.tsx", "src/federation/page-contacts.tsx", - "shared/api.ts", - "src/modern-app-env.d.ts" - ] + "shared/api.ts" + ], + "compilerOptions": { + "skipLibCheck": true + } } diff --git a/app/zerops.yaml b/app/zerops.yaml index 679a85efc..ee3f9b1de 100644 --- a/app/zerops.yaml +++ b/app/zerops.yaml @@ -9,8 +9,8 @@ zerops: - sudo apk add --no-cache curl libstdc++ - sh /build/source/app/scripts/install-zerops-node.sh --with-pnpm buildCommands: - - cd app && PATH="$HOME/.local/node-26.5.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm + - cd app && PATH="$HOME/.local/node-26.7.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm deployFiles: - 'app/.mise.toml' - 'app/apps/shell-super-app' @@ -52,7 +52,7 @@ zerops: httpGet: port: 8080 path: '/ready' - start: sh -c 'cd app && PATH="/var/www/.local/node-26.5.0/bin:$PATH" exec node scripts/run-zerops-migrator.mjs' + start: sh -c 'cd app && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec node scripts/run-zerops-migrator.mjs' - setup: 'spicedb' build: @@ -95,10 +95,10 @@ zerops: - sudo apk add --no-cache curl libstdc++ - sh /build/source/app/scripts/install-zerops-node.sh --with-pnpm buildCommands: - - cd app && PATH="$HOME/.local/node-26.5.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm - - cd app && NODE_OPTIONS=--max-old-space-size=4096 PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false --filter '@app/party-registry' run build - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'party-registry' --package '@app/party-registry' --package-dir 'verticals/party-registry' + - cd app && PATH="$HOME/.local/node-26.7.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm + - cd app && NODE_OPTIONS=--max-old-space-size=4096 PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false --filter '@app/party-registry' run build + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'party-registry' --package '@app/party-registry' --package-dir 'verticals/party-registry' - cp 'app/topology/reference-topology.json' 'app/.zerops/runtime/party-registry/topology.json' - cp 'app/topology/local-overlays/development.json' 'app/.zerops/runtime/party-registry/local-overlay.json' deployFiles: @@ -132,7 +132,7 @@ zerops: httpGet: port: 4102 path: '/party-registry-api/party-registry/readiness' - start: sh -c 'cd app/.zerops/runtime/party-registry && PATH="/var/www/.local/node-26.5.0/bin:$PATH" exec npm run serve' + start: sh -c 'cd app/.zerops/runtime/party-registry && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve' - setup: 'shellsuperapp' build: @@ -143,10 +143,10 @@ zerops: - sudo apk add --no-cache curl libstdc++ - sh /build/source/app/scripts/install-zerops-node.sh --with-pnpm buildCommands: - - cd app && PATH="$HOME/.local/node-26.5.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm - - cd app && NODE_OPTIONS=--max-old-space-size=4096 PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false --filter '@app/shell-super-app' run build - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'shell-super-app' --package '@app/shell-super-app' --package-dir 'apps/shell-super-app' + - cd app && PATH="$HOME/.local/node-26.7.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm + - cd app && NODE_OPTIONS=--max-old-space-size=4096 PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false --filter '@app/shell-super-app' run build + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'shell-super-app' --package '@app/shell-super-app' --package-dir 'apps/shell-super-app' - cp 'app/topology/reference-topology.json' 'app/.zerops/runtime/shell-super-app/topology.json' - cp 'app/topology/local-overlays/development.json' 'app/.zerops/runtime/shell-super-app/local-overlay.json' deployFiles: @@ -180,7 +180,7 @@ zerops: httpGet: port: 3020 path: '/' - start: sh -c 'cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.5.0/bin:$PATH" exec npm run serve' + start: sh -c 'cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve' # - setup: 'party-registry-worker' @@ -190,9 +190,9 @@ zerops: - sudo apk add --no-cache curl libstdc++ - sh /build/source/app/scripts/install-zerops-node.sh --with-pnpm buildCommands: - - cd app && PATH="$HOME/.local/node-26.5.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs - - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm - - cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.5.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'party-registry' --package '@app/party-registry' --package-dir 'verticals/party-registry' --worker + - cd app && PATH="$HOME/.local/node-26.7.0/bin:$PATH" node scripts/reset-workspace-dependencies.mjs + - cd app && PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm + - cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app 'party-registry' --package '@app/party-registry' --package-dir 'verticals/party-registry' --worker deployFiles: - 'app/.zerops/runtime/party-registry-worker' - 'app/scripts/install-zerops-node.sh' @@ -225,5 +225,5 @@ zerops: httpGet: port: 4102 path: '/ready' - start: sh -c 'cd app/.zerops/runtime/party-registry-worker && PATH="/var/www/.local/node-26.5.0/bin:$PATH" exec npm run serve' + start: sh -c 'cd app/.zerops/runtime/party-registry-worker && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve' # From 13bcc931596f61f56f6175df17a747f35f7f1ba1 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 15:38:18 +0200 Subject: [PATCH 02/11] fix(tooling): retain formatter preset features Remove migration-only sorting disables and formatting overrides. Keep policy decisions in the configured preset. Co-Authored-By: Claude Code --- app/oxfmt.config.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/oxfmt.config.ts b/app/oxfmt.config.ts index e4ebaf17f..fe413499c 100644 --- a/app/oxfmt.config.ts +++ b/app/oxfmt.config.ts @@ -3,12 +3,6 @@ import ultracite from 'ultracite/oxfmt'; export default defineConfig({ ...ultracite, - printWidth: 100, - proseWrap: 'preserve', - trailingComma: 'all', - sortImports: false, - sortPackageJson: false, - sortTailwindcss: false, ignorePatterns: [ '.agents', '.codex/skills', From e9f566a5c1e79257d56422350fbdb617e045cab9 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 16:33:20 +0200 Subject: [PATCH 03/11] fix(lint): retain sorting without ESLint Load rule-only plugin entrypoints and patch Perfectionist's eager utility imports. Keep all six sorting rules and explicit declaration dependencies without installing the ESLint runner. Eight real Oxlint parity controls pass. Co-Authored-By: Claude Code --- app/oxlint.config.ts | 12 +- app/package.json | 3 +- .../eslint-plugin-perfectionist@5.10.1.patch | 1158 +++++++++++++++++ app/pnpm-lock.yaml | 784 +++-------- app/pnpm-workspace.yaml | 5 + .../tests/native-sorting-plugins.test.mts | 125 ++ 6 files changed, 1498 insertions(+), 589 deletions(-) create mode 100644 app/patches/eslint-plugin-perfectionist@5.10.1.patch create mode 100644 app/tools/oxlint/effect-native/tests/native-sorting-plugins.test.mts diff --git a/app/oxlint.config.ts b/app/oxlint.config.ts index 2d3b757bf..6c41db292 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -3,7 +3,17 @@ import core from 'ultracite/oxlint/core'; import { jsPluginSettings, selectJsPlugins } from 'ultracite/oxlint/js-plugins'; import react from 'ultracite/oxlint/react'; -const jsPlugins = selectJsPlugins(['github', 'sonarjs', 'react-doctor']); +const selectedJsPlugins = selectJsPlugins(['github', 'sonarjs', 'react-doctor']); +const jsPlugins = { + ...selectedJsPlugins, + // Load GitHub's published rule-only entrypoint, not its ESLint configuration aggregator. + // The aggregator eagerly imports eslint-plugin-import and the ESLint runner; the rules do not. + jsPlugins: selectedJsPlugins.jsPlugins.map((plugin) => + plugin.name === 'github' + ? { ...plugin, specifier: 'eslint-plugin-github/lib/plugin.js' } + : plugin, + ), +}; const antiSlopRules = { 'anti-slop/no-chained-type-assertions': 'error', diff --git a/app/package.json b/app/package.json index 7c6164614..b9b9cd064 100644 --- a/app/package.json +++ b/app/package.json @@ -129,8 +129,7 @@ "fallow": "3.22.0", "@modern-js/ultramodern-create": "npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2", "cross-env": "10.1.0", - "@effect/opentelemetry": "4.0.0-rc.112", - "eslint": "10.10.0" + "@effect/opentelemetry": "4.0.0-rc.112" }, "engines": { "node": ">=26", diff --git a/app/patches/eslint-plugin-perfectionist@5.10.1.patch b/app/patches/eslint-plugin-perfectionist@5.10.1.patch new file mode 100644 index 000000000..93600dbd9 --- /dev/null +++ b/app/patches/eslint-plugin-perfectionist@5.10.1.patch @@ -0,0 +1,1158 @@ +diff --git a/dist/rules/sort-array-includes.js b/dist/rules/sort-array-includes.js +index 79f4ec229c..585fa165d1 100644 +--- a/dist/rules/sort-array-includes.js ++++ b/dist/rules/sort-array-includes.js +@@ -18,7 +18,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { additionalCustomGroupMatchOptionsJsonSchema } from './sort-arrays/types.js' + import { sortArray } from './sort-arrays/sort-array.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-arrays/compute-array-elements.js b/dist/rules/sort-arrays/compute-array-elements.js +index 9ab801256a..3dd907cf62 100644 +--- a/dist/rules/sort-arrays/compute-array-elements.js ++++ b/dist/rules/sort-arrays/compute-array-elements.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes array elements for the given expression. + * +diff --git a/dist/rules/sort-arrays/compute-node-name.js b/dist/rules/sort-arrays/compute-node-name.js +index 592c8740bc..f8aecb3362 100644 +--- a/dist/rules/sort-arrays/compute-node-name.js ++++ b/dist/rules/sort-arrays/compute-node-name.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an array member. + * +diff --git a/dist/rules/sort-arrays/sort-array.js b/dist/rules/sort-arrays/sort-array.js +index e063c8f44d..80d37a5b03 100644 +--- a/dist/rules/sort-arrays/sort-array.js ++++ b/dist/rules/sort-arrays/sort-array.js +@@ -19,7 +19,7 @@ import { allSelectors } from './types.js' + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' + import { computeArrayElements } from './compute-array-elements.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function sortArray({ + cachedGroupsByModifiersAndSelectors, + mustHaveMatchedContextOptions, +diff --git a/dist/rules/sort-arrays.js b/dist/rules/sort-arrays.js +index 3852050c16..7e83dfa31e 100644 +--- a/dist/rules/sort-arrays.js ++++ b/dist/rules/sort-arrays.js +@@ -18,7 +18,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { additionalCustomGroupMatchOptionsJsonSchema } from './sort-arrays/types.js' + import { sortArray } from './sort-arrays/sort-array.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-classes/compute-dependencies-by-sorting-node.js b/dist/rules/sort-classes/compute-dependencies-by-sorting-node.js +index 4340686505..fb4576f808 100644 +--- a/dist/rules/sort-classes/compute-dependencies-by-sorting-node.js ++++ b/dist/rules/sort-classes/compute-dependencies-by-sorting-node.js +@@ -3,7 +3,7 @@ import { matches } from '../../utils/matches.js' + import { computeDependenciesBySortingNode as computeDependenciesBySortingNode$1 } from '../../utils/compute-dependencies-by-sorting-node.js' + import { computeParentNodesWithTypes } from '../../utils/compute-parent-nodes-with-types.js' + import { computeIdentifierNameDetails } from './compute-identifier-name-details.js' +-import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/types' + function computeDependenciesBySortingNode({ + ignoreCallbackDependenciesPatterns, + sortingNodes, +diff --git a/dist/rules/sort-classes/compute-dependencies.js b/dist/rules/sort-classes/compute-dependencies.js +index 3e000595fb..ced91e7496 100644 +--- a/dist/rules/sort-classes/compute-dependencies.js ++++ b/dist/rules/sort-classes/compute-dependencies.js +@@ -1,7 +1,7 @@ + import { matches } from '../../utils/matches.js' + import { computeIdentifierNameDetails } from './compute-identifier-name-details.js' + import { computeDependencyName } from './compute-dependency-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the dependencies of a class member AST node. + * +diff --git a/dist/rules/sort-classes/compute-identifier-name-details.js b/dist/rules/sort-classes/compute-identifier-name-details.js +index 91b71a1476..40f3f4ee87 100644 +--- a/dist/rules/sort-classes/compute-identifier-name-details.js ++++ b/dist/rules/sort-classes/compute-identifier-name-details.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name details of an identifier. + * +diff --git a/dist/rules/sort-classes/compute-matched-context-options.js b/dist/rules/sort-classes/compute-matched-context-options.js +index 64e9f7ee25..3fc513ba30 100644 +--- a/dist/rules/sort-classes/compute-matched-context-options.js ++++ b/dist/rules/sort-classes/compute-matched-context-options.js +@@ -1,7 +1,7 @@ + import { passesAllNamesMatchPatternFilter } from '../../utils/context-matching/passes-all-names-match-pattern-filter.js' + import { passesAstSelectorFilter } from '../../utils/context-matching/passes-ast-selector-filter.js' + import { computeMethodOrPropertyNameDetails } from './node-info/compute-method-or-property-name-details.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for a given class node. + * +diff --git a/dist/rules/sort-classes/compute-overload-signature-groups.js b/dist/rules/sort-classes/compute-overload-signature-groups.js +index 8824b0a12f..c40da3eaba 100644 +--- a/dist/rules/sort-classes/compute-overload-signature-groups.js ++++ b/dist/rules/sort-classes/compute-overload-signature-groups.js +@@ -1,7 +1,7 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' + import { isSortable } from '../../utils/is-sortable.js' + import { OverloadSignatureGroup } from '../../utils/overload-signature/overload-signature-group.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Returns a list of groups of overload signatures. + * +diff --git a/dist/rules/sort-classes/is-known-class-element.js b/dist/rules/sort-classes/is-known-class-element.js +index 8105ef40fe..8f5bbef6b4 100644 +--- a/dist/rules/sort-classes/is-known-class-element.js ++++ b/dist/rules/sort-classes/is-known-class-element.js +@@ -1,5 +1,5 @@ + import '../../utils/assert-is-never.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks whether a class element is supported by the sort-classes rule. + * +diff --git a/dist/rules/sort-classes/node-info/common-modifiers.js b/dist/rules/sort-classes/node-info/common-modifiers.js +index c5504f4ebd..d002c0efe3 100644 +--- a/dist/rules/sort-classes/node-info/common-modifiers.js ++++ b/dist/rules/sort-classes/node-info/common-modifiers.js +@@ -1,6 +1,6 @@ + import { UnreachableCaseError } from '../../../utils/unreachable-case-error.js' + import '../../../utils/assert-is-never.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeAccessibilityModifier({ hasPrivateHash, node }) { + if (hasPrivateHash) { + return ['private'] +diff --git a/dist/rules/sort-classes/node-info/compute-method-details.js b/dist/rules/sort-classes/node-info/compute-method-details.js +index af38f82777..a06f4d7cc8 100644 +--- a/dist/rules/sort-classes/node-info/compute-method-details.js ++++ b/dist/rules/sort-classes/node-info/compute-method-details.js +@@ -9,7 +9,7 @@ import { + computeStaticModifier, + } from './common-modifiers.js' + import { computeMethodOrPropertyNameDetails } from './compute-method-or-property-name-details.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes details related to a method. + * +diff --git a/dist/rules/sort-classes/node-info/compute-method-or-property-name-details.js b/dist/rules/sort-classes/node-info/compute-method-or-property-name-details.js +index 599c80cd25..c5824426e1 100644 +--- a/dist/rules/sort-classes/node-info/compute-method-or-property-name-details.js ++++ b/dist/rules/sort-classes/node-info/compute-method-or-property-name-details.js +@@ -1,5 +1,5 @@ + import { computeIdentifierNameDetails } from '../compute-identifier-name-details.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name details of a method or property node. + * +diff --git a/dist/rules/sort-classes/node-info/is-function-expression.js b/dist/rules/sort-classes/node-info/is-function-expression.js +index c8eb4f405e..0877ab6aec 100644 +--- a/dist/rules/sort-classes/node-info/is-function-expression.js ++++ b/dist/rules/sort-classes/node-info/is-function-expression.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks whether a node is a function expression or an arrow function + * expression. +diff --git a/dist/rules/sort-classes/sort-class.js b/dist/rules/sort-classes/sort-class.js +index ab75d20907..cc218036bd 100644 +--- a/dist/rules/sort-classes/sort-class.js ++++ b/dist/rules/sort-classes/sort-class.js +@@ -41,7 +41,7 @@ import { computePropertyDetails } from './node-info/compute-property-details.js' + import { computeAccessorDetails } from './node-info/compute-accessor-details.js' + import { computeMethodDetails } from './node-info/compute-method-details.js' + import { isKnownClassElement } from './is-known-class-element.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-classes.js b/dist/rules/sort-classes.js +index 831f893a16..576709a748 100644 +--- a/dist/rules/sort-classes.js ++++ b/dist/rules/sort-classes.js +@@ -31,7 +31,7 @@ import { + additionalCustomGroupMatchOptionsJsonSchema, + } from './sort-classes/types.js' + import { defaultOptions, sortClass } from './sort-classes/sort-class.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_classes_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-decorators.js b/dist/rules/sort-decorators.js +index dfcd8e5d39..8f9a8b2ee5 100644 +--- a/dist/rules/sort-decorators.js ++++ b/dist/rules/sort-decorators.js +@@ -29,7 +29,7 @@ import { complete } from '../utils/complete.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { getNodeDecorators } from '../utils/get-node-decorators.js' + import { getDecoratorName } from '../utils/get-decorator-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedDecoratorsOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedDecoratorsGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenDecorators' +diff --git a/dist/rules/sort-enums/compute-dependencies-by-sorting-node.js b/dist/rules/sort-enums/compute-dependencies-by-sorting-node.js +index 6210ed9f7b..aac8e7fd54 100644 +--- a/dist/rules/sort-enums/compute-dependencies-by-sorting-node.js ++++ b/dist/rules/sort-enums/compute-dependencies-by-sorting-node.js +@@ -1,7 +1,7 @@ + import { computeDependenciesBySortingNode as computeDependenciesBySortingNode$1 } from '../../utils/compute-dependencies-by-sorting-node.js' + import { computeParentNodesWithTypes } from '../../utils/compute-parent-nodes-with-types.js' + import { doesSortingNodeHaveOneOfDependencyNames } from '../../utils/does-sorting-node-have-one-of-dependency-names.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeDependenciesBySortingNode({ + sortingNodes, + sourceCode, +diff --git a/dist/rules/sort-enums/compute-dependencies.js b/dist/rules/sort-enums/compute-dependencies.js +index ba822be2d7..c52275c945 100644 +--- a/dist/rules/sort-enums/compute-dependencies.js ++++ b/dist/rules/sort-enums/compute-dependencies.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Extract dependencies from an enum. + * +diff --git a/dist/rules/sort-enums/compute-expression-number-value.js b/dist/rules/sort-enums/compute-expression-number-value.js +index 99cb641d3d..c015ea2b74 100644 +--- a/dist/rules/sort-enums/compute-expression-number-value.js ++++ b/dist/rules/sort-enums/compute-expression-number-value.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Extracts a numeric value from an AST expression node. + * +diff --git a/dist/rules/sort-enums/compute-node-name.js b/dist/rules/sort-enums/compute-node-name.js +index 9b3fc7c0d8..02bfaf629b 100644 +--- a/dist/rules/sort-enums/compute-node-name.js ++++ b/dist/rules/sort-enums/compute-node-name.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an enum member node. + * +diff --git a/dist/rules/sort-enums/sort-enum.js b/dist/rules/sort-enums/sort-enum.js +index 5f67268981..48ca40376e 100644 +--- a/dist/rules/sort-enums/sort-enum.js ++++ b/dist/rules/sort-enums/sort-enum.js +@@ -29,7 +29,7 @@ import { computeExpressionNumberValue } from './compute-expression-number-value. + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' + import { computeDependencies } from './compute-dependencies.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var defaultOptions = { + useExperimentalDependencyDetection: true, + fallbackSort: { type: 'unsorted' }, +diff --git a/dist/rules/sort-enums.js b/dist/rules/sort-enums.js +index 17bdf99938..6dba46cdf5 100644 +--- a/dist/rules/sort-enums.js ++++ b/dist/rules/sort-enums.js +@@ -27,7 +27,7 @@ import { + additionalCustomGroupMatchOptionsJsonSchema, + } from './sort-enums/types.js' + import { defaultOptions, sortEnum } from './sort-enums/sort-enum.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_enums_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-export-attributes.js b/dist/rules/sort-export-attributes.js +index b39aedf042..938a171a23 100644 +--- a/dist/rules/sort-export-attributes.js ++++ b/dist/rules/sort-export-attributes.js +@@ -8,7 +8,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortImportOrExportAttributes } from './sort-import-attributes/sort-import-or-export-attributes.js' + import { jsonSchema } from './sort-import-attributes.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedExportAttributesOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedExportAttributesGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenExportAttributes' +diff --git a/dist/rules/sort-exports.js b/dist/rules/sort-exports.js +index 10485817eb..0e3bb50790 100644 +--- a/dist/rules/sort-exports.js ++++ b/dist/rules/sort-exports.js +@@ -35,7 +35,7 @@ import { + allModifiers, + allSelectors, + } from './sort-exports/types.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-heritage-clauses/compute-node-name.js b/dist/rules/sort-heritage-clauses/compute-node-name.js +index d67bd3abe3..00b011c5f0 100644 +--- a/dist/rules/sort-heritage-clauses/compute-node-name.js ++++ b/dist/rules/sort-heritage-clauses/compute-node-name.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Extracts the name from a heritage clause expression. + * +diff --git a/dist/rules/sort-heritage-clauses/sort-heritage-clause.js b/dist/rules/sort-heritage-clauses/sort-heritage-clause.js +index 137849c1ba..48025567e3 100644 +--- a/dist/rules/sort-heritage-clauses/sort-heritage-clause.js ++++ b/dist/rules/sort-heritage-clauses/sort-heritage-clause.js +@@ -22,7 +22,7 @@ import { + } from './types.js' + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var defaultOptions = { + fallbackSort: { type: 'unsorted' }, + newlinesInside: 'newlinesBetween', +diff --git a/dist/rules/sort-heritage-clauses.js b/dist/rules/sort-heritage-clauses.js +index 255ac05bde..0518b75320 100644 +--- a/dist/rules/sort-heritage-clauses.js ++++ b/dist/rules/sort-heritage-clauses.js +@@ -26,7 +26,7 @@ import { + defaultOptions, + sortHeritageClause, + } from './sort-heritage-clauses/sort-heritage-clause.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_heritage_clauses_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-import-attributes/compute-node-name.js b/dist/rules/sort-import-attributes/compute-node-name.js +index 9c8a989e40..b40d684b54 100644 +--- a/dist/rules/sort-import-attributes/compute-node-name.js ++++ b/dist/rules/sort-import-attributes/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Extracts the name of an import attribute for sorting purposes. + * +diff --git a/dist/rules/sort-import-attributes.js b/dist/rules/sort-import-attributes.js +index c6b8959f1f..6d107302f6 100644 +--- a/dist/rules/sort-import-attributes.js ++++ b/dist/rules/sort-import-attributes.js +@@ -17,7 +17,7 @@ import { + import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortImportOrExportAttributes } from './sort-import-attributes/sort-import-or-export-attributes.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedImportAttributesOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedImportAttributesGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenImportAttributes' +diff --git a/dist/rules/sort-imports/compute-dependencies.js b/dist/rules/sort-imports/compute-dependencies.js +index 5982968376..5da84aa816 100644 +--- a/dist/rules/sort-imports/compute-dependencies.js ++++ b/dist/rules/sort-imports/compute-dependencies.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the dependencies of an import-like AST node. + * +diff --git a/dist/rules/sort-imports/compute-dependency-names.js b/dist/rules/sort-imports/compute-dependency-names.js +index 9248e7658d..7c0670e3a6 100644 +--- a/dist/rules/sort-imports/compute-dependency-names.js ++++ b/dist/rules/sort-imports/compute-dependency-names.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the dependency names of an import-like AST node. + * +diff --git a/dist/rules/sort-imports/compute-node-name.js b/dist/rules/sort-imports/compute-node-name.js +index 6ca311c474..2d477cf8f5 100644 +--- a/dist/rules/sort-imports/compute-node-name.js ++++ b/dist/rules/sort-imports/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an import-like AST node. + * +diff --git a/dist/rules/sort-imports/compute-specifier-modifiers.js b/dist/rules/sort-imports/compute-specifier-modifiers.js +index f948b386b3..2d171472c1 100644 +--- a/dist/rules/sort-imports/compute-specifier-modifiers.js ++++ b/dist/rules/sort-imports/compute-specifier-modifiers.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the specifier modifiers of an import-like AST node. + * +diff --git a/dist/rules/sort-imports/compute-specifier-name.js b/dist/rules/sort-imports/compute-specifier-name.js +index acb8a82401..eb47acf975 100644 +--- a/dist/rules/sort-imports/compute-specifier-name.js ++++ b/dist/rules/sort-imports/compute-specifier-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeSpecifierName({ sourceCode, node }) { + switch (node.type) { + case AST_NODE_TYPES.TSImportEqualsDeclaration: +diff --git a/dist/rules/sort-imports/is-non-external-reference-ts-import-equals.js b/dist/rules/sort-imports/is-non-external-reference-ts-import-equals.js +index f78378394e..2af4de4357 100644 +--- a/dist/rules/sort-imports/is-non-external-reference-ts-import-equals.js ++++ b/dist/rules/sort-imports/is-non-external-reference-ts-import-equals.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Determines whether the given AST node is a non-external-reference TS import + * equals declaration. +diff --git a/dist/rules/sort-imports/is-side-effect-import.js b/dist/rules/sort-imports/is-side-effect-import.js +index dbb5f5168d..703558e7ab 100644 +--- a/dist/rules/sort-imports/is-side-effect-import.js ++++ b/dist/rules/sort-imports/is-side-effect-import.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Determines whether the given AST node is a side-effect import. + * +diff --git a/dist/rules/sort-imports.js b/dist/rules/sort-imports.js +index 1b827e2f88..f6dae75e0b 100644 +--- a/dist/rules/sort-imports.js ++++ b/dist/rules/sort-imports.js +@@ -57,7 +57,7 @@ import { computeSpecifierName } from './sort-imports/compute-specifier-name.js' + import { computeDependencies } from './sort-imports/compute-dependencies.js' + import { isSideEffectImport } from './sort-imports/is-side-effect-import.js' + import { computeNodeName } from './sort-imports/compute-node-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-interfaces.js b/dist/rules/sort-interfaces.js +index 622f22a87b..6ea763c739 100644 +--- a/dist/rules/sort-interfaces.js ++++ b/dist/rules/sort-interfaces.js +@@ -8,7 +8,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortObjectTypeElements } from './sort-object-types/sort-object-type-elements.js' + import { defaultOptions, jsonSchema } from './sort-object-types.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedInterfacePropertiesOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedInterfacePropertiesGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenInterfaceMembers' +diff --git a/dist/rules/sort-intersection-types.js b/dist/rules/sort-intersection-types.js +index 0d53136389..febd11cc49 100644 +--- a/dist/rules/sort-intersection-types.js ++++ b/dist/rules/sort-intersection-types.js +@@ -8,7 +8,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortUnionOrIntersectionTypes } from './sort-union-or-intersection-types/sort-union-or-intersection-types.js' + import { buildJsonSchema } from './sort-union-or-intersection-types/build-json-schema.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-jsx-props/compute-matched-context-options.js b/dist/rules/sort-jsx-props/compute-matched-context-options.js +index 336e3ffcee..8555494c73 100644 +--- a/dist/rules/sort-jsx-props/compute-matched-context-options.js ++++ b/dist/rules/sort-jsx-props/compute-matched-context-options.js +@@ -2,7 +2,7 @@ import { matches } from '../../utils/matches.js' + import { passesAllNamesMatchPatternFilter } from '../../utils/context-matching/passes-all-names-match-pattern-filter.js' + import { passesAstSelectorFilter } from '../../utils/context-matching/passes-ast-selector-filter.js' + import { computeNodeName } from './compute-node-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for a given JSX element node. + * +diff --git a/dist/rules/sort-jsx-props/compute-node-name.js b/dist/rules/sort-jsx-props/compute-node-name.js +index 94d97b0250..b95a285644 100644 +--- a/dist/rules/sort-jsx-props/compute-node-name.js ++++ b/dist/rules/sort-jsx-props/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of a JSX attribute node. + * +diff --git a/dist/rules/sort-jsx-props/sort-jsx-object.js b/dist/rules/sort-jsx-props/sort-jsx-object.js +index 2a08d6021d..3b16ffb49f 100644 +--- a/dist/rules/sort-jsx-props/sort-jsx-object.js ++++ b/dist/rules/sort-jsx-props/sort-jsx-object.js +@@ -26,7 +26,7 @@ import { + } from './types.js' + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-jsx-props.js b/dist/rules/sort-jsx-props.js +index ad5f069892..aade3f4534 100644 +--- a/dist/rules/sort-jsx-props.js ++++ b/dist/rules/sort-jsx-props.js +@@ -25,7 +25,7 @@ import { + defaultOptions, + sortJsxObject, + } from './sort-jsx-props/sort-jsx-object.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_jsx_props_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-maps/compute-matched-context-options.js b/dist/rules/sort-maps/compute-matched-context-options.js +index 0c0f0b693c..c1c25a539a 100644 +--- a/dist/rules/sort-maps/compute-matched-context-options.js ++++ b/dist/rules/sort-maps/compute-matched-context-options.js +@@ -1,7 +1,7 @@ + import { passesAllNamesMatchPatternFilter } from '../../utils/context-matching/passes-all-names-match-pattern-filter.js' + import { passesAstSelectorFilter } from '../../utils/context-matching/passes-ast-selector-filter.js' + import { computeNodeName } from './compute-node-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for a given map node. + * +diff --git a/dist/rules/sort-maps/compute-node-name.js b/dist/rules/sort-maps/compute-node-name.js +index 0f3540ada6..b83dff2bf4 100644 +--- a/dist/rules/sort-maps/compute-node-name.js ++++ b/dist/rules/sort-maps/compute-node-name.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Extracts the name of a Map element for sorting purposes. + * +diff --git a/dist/rules/sort-maps/sort-potential-map.js b/dist/rules/sort-maps/sort-potential-map.js +index 2ca140ba39..5123b14f88 100644 +--- a/dist/rules/sort-maps/sort-potential-map.js ++++ b/dist/rules/sort-maps/sort-potential-map.js +@@ -21,7 +21,7 @@ import { + } from './types.js' + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var defaultOptions = { + fallbackSort: { type: 'unsorted' }, + newlinesInside: 'newlinesBetween', +diff --git a/dist/rules/sort-maps.js b/dist/rules/sort-maps.js +index b52e1053ca..790c016f52 100644 +--- a/dist/rules/sort-maps.js ++++ b/dist/rules/sort-maps.js +@@ -26,7 +26,7 @@ import { + defaultOptions, + sortPotentialMap, + } from './sort-maps/sort-potential-map.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_maps_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-modules/compute-dependencies-by-sorting-node.js b/dist/rules/sort-modules/compute-dependencies-by-sorting-node.js +index 788761427a..a34db37618 100644 +--- a/dist/rules/sort-modules/compute-dependencies-by-sorting-node.js ++++ b/dist/rules/sort-modules/compute-dependencies-by-sorting-node.js +@@ -4,7 +4,7 @@ import { computeParentNodesWithTypes } from '../../utils/compute-parent-nodes-wi + import { doesSortingNodeHaveOneOfDependencyNames } from '../../utils/does-sorting-node-have-one-of-dependency-names.js' + import { isPropertyOrAccessorNode } from './is-property-or-accessor-node.js' + import { isArrowFunctionNode } from './is-arrow-function-node.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeDependenciesBySortingNode({ + dependencyDetection, + sortingNodes, +diff --git a/dist/rules/sort-modules/compute-dependencies.js b/dist/rules/sort-modules/compute-dependencies.js +index 27dbc05f9a..f62b19ebf8 100644 +--- a/dist/rules/sort-modules/compute-dependencies.js ++++ b/dist/rules/sort-modules/compute-dependencies.js +@@ -1,7 +1,7 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' + import { isArrowFunctionNode } from './is-arrow-function-node.js' + import { getEnumMembers } from '../../utils/get-enum-members.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the dependencies of a given AST node. + * +diff --git a/dist/rules/sort-modules/compute-node-details.js b/dist/rules/sort-modules/compute-node-details.js +index 7f0d8e50a0..9de9f69078 100644 +--- a/dist/rules/sort-modules/compute-node-details.js ++++ b/dist/rules/sort-modules/compute-node-details.js +@@ -2,7 +2,7 @@ import { getNodeDecorators } from '../../utils/get-node-decorators.js' + import { isPropertyOrAccessorNode } from './is-property-or-accessor-node.js' + import { isArrowFunctionNode } from './is-arrow-function-node.js' + import { computeDependencies } from './compute-dependencies.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Compute details about a module-related node. + * +diff --git a/dist/rules/sort-modules/compute-overload-signature-groups.js b/dist/rules/sort-modules/compute-overload-signature-groups.js +index f6c11d1af9..00ac7a87eb 100644 +--- a/dist/rules/sort-modules/compute-overload-signature-groups.js ++++ b/dist/rules/sort-modules/compute-overload-signature-groups.js +@@ -1,6 +1,6 @@ + import { isSortable } from '../../utils/is-sortable.js' + import { OverloadSignatureGroup } from '../../utils/overload-signature/overload-signature-group.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Returns a list of groups of overload signatures. + * +diff --git a/dist/rules/sort-modules/is-arrow-function-node.js b/dist/rules/sort-modules/is-arrow-function-node.js +index f9590605b9..691c7eae4f 100644 +--- a/dist/rules/sort-modules/is-arrow-function-node.js ++++ b/dist/rules/sort-modules/is-arrow-function-node.js +@@ -1,5 +1,5 @@ + import { isPropertyOrAccessorNode } from './is-property-or-accessor-node.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks whether a node is a property or accessor node with an + * ArrowFunctionExpression value. +diff --git a/dist/rules/sort-modules/is-property-or-accessor-node.js b/dist/rules/sort-modules/is-property-or-accessor-node.js +index c85be07fbf..285322fabd 100644 +--- a/dist/rules/sort-modules/is-property-or-accessor-node.js ++++ b/dist/rules/sort-modules/is-property-or-accessor-node.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks whether a node is a PropertyDefinition or AccessorProperty. + * +diff --git a/dist/rules/sort-modules.js b/dist/rules/sort-modules.js +index c5516f62f9..f7a9c5359a 100644 +--- a/dist/rules/sort-modules.js ++++ b/dist/rules/sort-modules.js +@@ -50,7 +50,7 @@ import { computeDependenciesBySortingNode } from './sort-modules/compute-depende + import { buildComparatorByOptionsComputer } from './sort-modules/build-comparator-by-options-computer.js' + import { computeOverloadSignatureGroups } from './sort-modules/compute-overload-signature-groups.js' + import { computeNodeDetails } from './sort-modules/compute-node-details.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-named-exports/compute-node-name.js b/dist/rules/sort-named-exports/compute-node-name.js +index 022b1f12ce..6742fa5c3d 100644 +--- a/dist/rules/sort-named-exports/compute-node-name.js ++++ b/dist/rules/sort-named-exports/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an export specifier node. + * +diff --git a/dist/rules/sort-named-exports.js b/dist/rules/sort-named-exports.js +index 5ee52bc084..fc10141cc8 100644 +--- a/dist/rules/sort-named-exports.js ++++ b/dist/rules/sort-named-exports.js +@@ -27,7 +27,7 @@ import { + defaultOptions, + sortNamedExport, + } from './sort-named-exports/sort-named-export.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_named_exports_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-named-imports/compute-matched-context-options.js b/dist/rules/sort-named-imports/compute-matched-context-options.js +index 0ea41e17b2..efcb07cf1b 100644 +--- a/dist/rules/sort-named-imports/compute-matched-context-options.js ++++ b/dist/rules/sort-named-imports/compute-matched-context-options.js +@@ -1,7 +1,7 @@ + import { passesAllNamesMatchPatternFilter } from '../../utils/context-matching/passes-all-names-match-pattern-filter.js' + import { passesAstSelectorFilter } from '../../utils/context-matching/passes-ast-selector-filter.js' + import { computeNodeName } from './compute-node-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for a given named import node. + * +diff --git a/dist/rules/sort-named-imports/compute-node-name.js b/dist/rules/sort-named-imports/compute-node-name.js +index 0c2b193921..0fb85dff29 100644 +--- a/dist/rules/sort-named-imports/compute-node-name.js ++++ b/dist/rules/sort-named-imports/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an import specifier node. + * +diff --git a/dist/rules/sort-named-imports/sort-named-import.js b/dist/rules/sort-named-imports/sort-named-import.js +index b9ec4578fa..cd8730f7b0 100644 +--- a/dist/rules/sort-named-imports/sort-named-import.js ++++ b/dist/rules/sort-named-imports/sort-named-import.js +@@ -26,7 +26,7 @@ import { + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' + import { computeImportKindModifier } from './compute-import-kind-modifier.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-named-imports.js b/dist/rules/sort-named-imports.js +index 704064a4c2..15d95d690f 100644 +--- a/dist/rules/sort-named-imports.js ++++ b/dist/rules/sort-named-imports.js +@@ -27,7 +27,7 @@ import { + defaultOptions, + sortNamedImport, + } from './sort-named-imports/sort-named-import.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_named_imports_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-object-types/compute-matched-context-options.js b/dist/rules/sort-object-types/compute-matched-context-options.js +index c13175b319..961e63db0e 100644 +--- a/dist/rules/sort-object-types/compute-matched-context-options.js ++++ b/dist/rules/sort-object-types/compute-matched-context-options.js +@@ -4,7 +4,7 @@ import { passesAstSelectorFilter } from '../../utils/context-matching/passes-ast + import { passesDeclarationMatchesPatternFilter } from './passes-declaration-matches-pattern-filter.js' + import { passesDeclarationCommentMatchesFilter } from './passes-declaration-comment-matches-filter.js' + import { computeNodeName } from './compute-node-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for the given nodes. + * +diff --git a/dist/rules/sort-object-types/compute-node-name.js b/dist/rules/sort-object-types/compute-node-name.js +index 74929882d1..9bcd042fc0 100644 +--- a/dist/rules/sort-object-types/compute-node-name.js ++++ b/dist/rules/sort-object-types/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an object-type-like node. + * +diff --git a/dist/rules/sort-object-types/compute-node-parent-name.js b/dist/rules/sort-object-types/compute-node-parent-name.js +index 2c508490f4..36b9d3f45b 100644 +--- a/dist/rules/sort-object-types/compute-node-parent-name.js ++++ b/dist/rules/sort-object-types/compute-node-parent-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of an object-type-like parent node. + * +diff --git a/dist/rules/sort-object-types/is-member-optional.js b/dist/rules/sort-object-types/is-member-optional.js +index a7be1025aa..2420c1f8c5 100644 +--- a/dist/rules/sort-object-types/is-member-optional.js ++++ b/dist/rules/sort-object-types/is-member-optional.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if a TypeScript type member is marked as optional. + * +diff --git a/dist/rules/sort-object-types/is-node-function-type.js b/dist/rules/sort-object-types/is-node-function-type.js +index 2ae7b6e3ab..e3e817b06b 100644 +--- a/dist/rules/sort-object-types/is-node-function-type.js ++++ b/dist/rules/sort-object-types/is-node-function-type.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Determines if an AST node represents a function type. + * +diff --git a/dist/rules/sort-object-types/passes-declaration-comment-matches-filter.js b/dist/rules/sort-object-types/passes-declaration-comment-matches-filter.js +index b5eeb291b9..a38e4b0ea1 100644 +--- a/dist/rules/sort-object-types/passes-declaration-comment-matches-filter.js ++++ b/dist/rules/sort-object-types/passes-declaration-comment-matches-filter.js +@@ -1,7 +1,7 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' + import { objectTypeParentTypes } from './types.js' + import { matchesScopedExpressions } from '../../utils/scoped-regex/matches-scoped-expressions.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if the object passes the declaration comment matches filter. + * +diff --git a/dist/rules/sort-object-types/sort-object-type-elements.js b/dist/rules/sort-object-types/sort-object-type-elements.js +index 31ac275a42..bbfa204052 100644 +--- a/dist/rules/sort-object-types/sort-object-type-elements.js ++++ b/dist/rules/sort-object-types/sort-object-type-elements.js +@@ -23,7 +23,7 @@ import { isNodeOnSingleLine } from '../../utils/is-node-on-single-line.js' + import { isNodeFunctionType } from './is-node-function-type.js' + import { isMemberOptional } from './is-member-optional.js' + import { defaultOptions } from '../sort-object-types.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-object-types/types.js b/dist/rules/sort-object-types/types.js +index 5aa6fa3498..db0d7def1d 100644 +--- a/dist/rules/sort-object-types/types.js ++++ b/dist/rules/sort-object-types/types.js +@@ -3,7 +3,7 @@ import { + buildCustomGroupModifiersJsonSchema, + buildCustomGroupSelectorJsonSchema, + } from '../../utils/json-schemas/common-groups-json-schemas.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var objectTypeParentTypes = [ + AST_NODE_TYPES.TSTypeAliasDeclaration, + AST_NODE_TYPES.TSInterfaceDeclaration, +diff --git a/dist/rules/sort-object-types.js b/dist/rules/sort-object-types.js +index 87fcb7609d..71bb95b430 100644 +--- a/dist/rules/sort-object-types.js ++++ b/dist/rules/sort-object-types.js +@@ -24,7 +24,7 @@ import { + } from './sort-object-types/types.js' + import { scopedRegexJsonSchema } from '../utils/json-schemas/scoped-regex-json-schema.js' + import { sortObjectTypeElements } from './sort-object-types/sort-object-type-elements.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedObjectTypesOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedObjectTypesGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenObjectTypeMembers' +diff --git a/dist/rules/sort-objects/compute-dependencies.js b/dist/rules/sort-objects/compute-dependencies.js +index 20e0962c46..a842dca4de 100644 +--- a/dist/rules/sort-objects/compute-dependencies.js ++++ b/dist/rules/sort-objects/compute-dependencies.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeDependencies(node) { + if (node.value.type !== AST_NODE_TYPES.AssignmentPattern) { + return [] +diff --git a/dist/rules/sort-objects/compute-dependency-names.js b/dist/rules/sort-objects/compute-dependency-names.js +index 4bd80e8318..c62f84ce15 100644 +--- a/dist/rules/sort-objects/compute-dependency-names.js ++++ b/dist/rules/sort-objects/compute-dependency-names.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeDependencyNames(pattern) { + let currentPattern = pattern + while (currentPattern.type === AST_NODE_TYPES.AssignmentPattern) { +diff --git a/dist/rules/sort-objects/compute-matched-context-options.js b/dist/rules/sort-objects/compute-matched-context-options.js +index 2b2807a2e5..9ee3eebd43 100644 +--- a/dist/rules/sort-objects/compute-matched-context-options.js ++++ b/dist/rules/sort-objects/compute-matched-context-options.js +@@ -7,7 +7,7 @@ import { computePropertyOrVariableDeclaratorName } from './compute-property-or-v + import { passesCallingFunctionNamePatternFilter } from './passes-calling-function-name-pattern-filter.js' + import { passesDeclarationMatchesPatternFilter } from './passes-declaration-matches-pattern-filter.js' + import { passesDeclarationCommentMatchesFilter } from './passes-declaration-comment-matches-filter.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the matched context options for a given object node. + * +diff --git a/dist/rules/sort-objects/compute-node-value.js b/dist/rules/sort-objects/compute-node-value.js +index 12b33e9fb8..e9e6b6e218 100644 +--- a/dist/rules/sort-objects/compute-node-value.js ++++ b/dist/rules/sort-objects/compute-node-value.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeNodeValue({ isDestructuredObject, sourceCode, property }) { + switch (property.value.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: +diff --git a/dist/rules/sort-objects/compute-property-or-variable-declarator-name.js b/dist/rules/sort-objects/compute-property-or-variable-declarator-name.js +index 36705b8435..f4dcebb491 100644 +--- a/dist/rules/sort-objects/compute-property-or-variable-declarator-name.js ++++ b/dist/rules/sort-objects/compute-property-or-variable-declarator-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Compute the name for a property-like node. + * +diff --git a/dist/rules/sort-objects/is-style-component.js b/dist/rules/sort-objects/is-style-component.js +index 1147a008e8..b20666fd2f 100644 +--- a/dist/rules/sort-objects/is-style-component.js ++++ b/dist/rules/sort-objects/is-style-component.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if a node represents a style component. + * +diff --git a/dist/rules/sort-objects/passes-calling-function-name-pattern-filter.js b/dist/rules/sort-objects/passes-calling-function-name-pattern-filter.js +index c3fad546bb..89fc35e91b 100644 +--- a/dist/rules/sort-objects/passes-calling-function-name-pattern-filter.js ++++ b/dist/rules/sort-objects/passes-calling-function-name-pattern-filter.js +@@ -1,5 +1,5 @@ + import { matchesScopedExpressions } from '../../utils/scoped-regex/matches-scoped-expressions.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if the object passes the calling function name pattern filter. + * +diff --git a/dist/rules/sort-objects/passes-declaration-comment-matches-filter.js b/dist/rules/sort-objects/passes-declaration-comment-matches-filter.js +index 9e7017b8ee..7853b7d2da 100644 +--- a/dist/rules/sort-objects/passes-declaration-comment-matches-filter.js ++++ b/dist/rules/sort-objects/passes-declaration-comment-matches-filter.js +@@ -1,7 +1,7 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' + import { matchesScopedExpressions } from '../../utils/scoped-regex/matches-scoped-expressions.js' + import { objectParentTypes } from './types.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if the object passes the declaration comment matches filter. + * +diff --git a/dist/rules/sort-objects/passes-declaration-matches-pattern-filter.js b/dist/rules/sort-objects/passes-declaration-matches-pattern-filter.js +index 87e39954ba..760f03bf51 100644 +--- a/dist/rules/sort-objects/passes-declaration-matches-pattern-filter.js ++++ b/dist/rules/sort-objects/passes-declaration-matches-pattern-filter.js +@@ -1,6 +1,6 @@ + import { matchesScopedExpressions } from '../../utils/scoped-regex/matches-scoped-expressions.js' + import { computePropertyOrVariableDeclaratorName } from './compute-property-or-variable-declarator-name.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var allowedTypes = [AST_NODE_TYPES.VariableDeclarator, AST_NODE_TYPES.Property] + /** + * Checks whether the node parent names match the given pattern. +diff --git a/dist/rules/sort-objects/sort-object.js b/dist/rules/sort-objects/sort-object.js +index 950c4e1944..84437c9e78 100644 +--- a/dist/rules/sort-objects/sort-object.js ++++ b/dist/rules/sort-objects/sort-object.js +@@ -34,7 +34,7 @@ import { computeDependencyNames } from './compute-dependency-names.js' + import { computeDependencies } from './compute-dependencies.js' + import { isStyleComponent } from './is-style-component.js' + import { computeNodeValue } from './compute-node-value.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-objects/types.js b/dist/rules/sort-objects/types.js +index ae92ba15ee..7b9b2266ec 100644 +--- a/dist/rules/sort-objects/types.js ++++ b/dist/rules/sort-objects/types.js +@@ -3,7 +3,7 @@ import { + buildCustomGroupModifiersJsonSchema, + buildCustomGroupSelectorJsonSchema, + } from '../../utils/json-schemas/common-groups-json-schemas.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedObjectsOrder' + var GROUP_ORDER_ERROR_ID = 'unexpectedObjectsGroupOrder' + var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenObjectMembers' +diff --git a/dist/rules/sort-objects.js b/dist/rules/sort-objects.js +index 9c3f70e535..146fca40e8 100644 +--- a/dist/rules/sort-objects.js ++++ b/dist/rules/sort-objects.js +@@ -29,7 +29,7 @@ import { + additionalSortOptionsJsonSchema, + } from './sort-objects/types.js' + import { defaultOptions, sortObject } from './sort-objects/sort-object.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_objects_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/rules/sort-sets.js b/dist/rules/sort-sets.js +index f82c1da54f..2ea5e1d727 100644 +--- a/dist/rules/sort-sets.js ++++ b/dist/rules/sort-sets.js +@@ -8,7 +8,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortArray } from './sort-arrays/sort-array.js' + import { defaultOptions, jsonSchema } from './sort-array-includes.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-switch-case/is-condition-expression.js b/dist/rules/sort-switch-case/is-condition-expression.js +index d17aaf0dde..349f3024e3 100644 +--- a/dist/rules/sort-switch-case/is-condition-expression.js ++++ b/dist/rules/sort-switch-case/is-condition-expression.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if an expression is condition-shaped. + * +diff --git a/dist/rules/sort-switch-case.js b/dist/rules/sort-switch-case.js +index 3b2369dd72..f6fe23e8a5 100644 +--- a/dist/rules/sort-switch-case.js ++++ b/dist/rules/sort-switch-case.js +@@ -18,7 +18,7 @@ import { isSortable } from '../utils/is-sortable.js' + import { complete } from '../utils/complete.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { isConditionExpression } from './sort-switch-case/is-condition-expression.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var ORDER_ERROR_ID = 'unexpectedSwitchCaseOrder' + var defaultOptions = { + fallbackSort: { type: 'unsorted' }, +diff --git a/dist/rules/sort-union-or-intersection-types/compute-node-name.js b/dist/rules/sort-union-or-intersection-types/compute-node-name.js +index 3d547f8d57..846f47e6cd 100644 +--- a/dist/rules/sort-union-or-intersection-types/compute-node-name.js ++++ b/dist/rules/sort-union-or-intersection-types/compute-node-name.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of a union/intersection member. + * +diff --git a/dist/rules/sort-union-or-intersection-types/sort-union-or-intersection-types.js b/dist/rules/sort-union-or-intersection-types/sort-union-or-intersection-types.js +index 70728334a5..eec0edd691 100644 +--- a/dist/rules/sort-union-or-intersection-types/sort-union-or-intersection-types.js ++++ b/dist/rules/sort-union-or-intersection-types/sort-union-or-intersection-types.js +@@ -18,7 +18,7 @@ import { allSelectors } from '../sort-union-types/types.js' + import { computeNodeName } from './compute-node-name.js' + import { computeMatchedContextOptions } from './compute-matched-context-options.js' + import { typeContainsCallableType } from './type-contains-callable-type.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function sortUnionOrIntersectionTypes({ + cachedGroupsByModifiersAndSelectors, + tokenValueToIgnoreBefore, +diff --git a/dist/rules/sort-union-or-intersection-types/type-contains-callable-type.js b/dist/rules/sort-union-or-intersection-types/type-contains-callable-type.js +index db6f128c0a..c64832c977 100644 +--- a/dist/rules/sort-union-or-intersection-types/type-contains-callable-type.js ++++ b/dist/rules/sort-union-or-intersection-types/type-contains-callable-type.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks if a type node contains a callable type (function or constructor). + * +diff --git a/dist/rules/sort-union-types.js b/dist/rules/sort-union-types.js +index a18fa14eb6..afbcd84e40 100644 +--- a/dist/rules/sort-union-types.js ++++ b/dist/rules/sort-union-types.js +@@ -8,7 +8,7 @@ import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' + import { sortUnionOrIntersectionTypes } from './sort-union-or-intersection-types/sort-union-or-intersection-types.js' + import { buildJsonSchema } from './sort-union-or-intersection-types/build-json-schema.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Cache computed groups by modifiers and selectors for performance. + */ +diff --git a/dist/rules/sort-variable-declarations/compute-dependencies.js b/dist/rules/sort-variable-declarations/compute-dependencies.js +index 1bfb9fc53a..e6ccbe7431 100644 +--- a/dist/rules/sort-variable-declarations/compute-dependencies.js ++++ b/dist/rules/sort-variable-declarations/compute-dependencies.js +@@ -1,5 +1,5 @@ + import { isNodeImmediatelyCalled } from '../../utils/is-node-immediately-called.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the dependencies of a variable declaration node. + * +diff --git a/dist/rules/sort-variable-declarations/compute-node-name.js b/dist/rules/sort-variable-declarations/compute-node-name.js +index 783093d4d8..c32a79c0ae 100644 +--- a/dist/rules/sort-variable-declarations/compute-node-name.js ++++ b/dist/rules/sort-variable-declarations/compute-node-name.js +@@ -1,5 +1,5 @@ + import { UnreachableCaseError } from '../../utils/unreachable-case-error.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Computes the name of a variable declaration. + * +diff --git a/dist/rules/sort-variable-declarations.js b/dist/rules/sort-variable-declarations.js +index 6e5ef6b495..a3b16ae144 100644 +--- a/dist/rules/sort-variable-declarations.js ++++ b/dist/rules/sort-variable-declarations.js +@@ -30,7 +30,7 @@ import { + } from './sort-variable-declarations/sort-variable-declaration.js' + import { buildAstListeners } from '../utils/build-ast-listeners.js' + import { createEslintRule } from '../utils/create-eslint-rule.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + var sort_variable_declarations_default = createEslintRule({ + meta: { + schema: { +diff --git a/dist/utils/compute-dependencies-outside-functions-by-sorting-node.js b/dist/utils/compute-dependencies-outside-functions-by-sorting-node.js +index 83b7b1345b..a7aaf976b9 100644 +--- a/dist/utils/compute-dependencies-outside-functions-by-sorting-node.js ++++ b/dist/utils/compute-dependencies-outside-functions-by-sorting-node.js +@@ -1,7 +1,7 @@ + import { computeDependenciesBySortingNode } from './compute-dependencies-by-sorting-node.js' + import { computeParentNodesWithTypes } from './compute-parent-nodes-with-types.js' + import { isNodeImmediatelyCalled } from './is-node-immediately-called.js' +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + function computeDependenciesOutsideFunctionsBySortingNode({ + sortingNodes, + sourceCode, +diff --git a/dist/utils/create-eslint-rule.js b/dist/utils/create-eslint-rule.js +index 7253d86e12..d10e38bf9f 100644 +--- a/dist/utils/create-eslint-rule.js ++++ b/dist/utils/create-eslint-rule.js +@@ -1,4 +1,4 @@ +-import { ESLintUtils } from '@typescript-eslint/utils' ++import * as ESLintUtils from '@typescript-eslint/utils/eslint-utils' + /** + * Factory function for creating ESLint rules with consistent structure and + * documentation. +diff --git a/dist/utils/get-node-range.js b/dist/utils/get-node-range.js +index 1ac032221d..c2773c1618 100644 +--- a/dist/utils/get-node-range.js ++++ b/dist/utils/get-node-range.js +@@ -1,7 +1,7 @@ + import { getEslintDisabledRules } from './get-eslint-disabled-rules.js' + import { isPartitionComment } from './is-partition-comment.js' + import { getCommentsBefore } from './get-comments-before.js' +-import { ASTUtils } from '@typescript-eslint/utils' ++import * as ASTUtils from '@typescript-eslint/utils/ast-utils' + /** + * Determines the complete range of a node including its associated comments. + * +diff --git a/dist/utils/is-node-immediately-called.js b/dist/utils/is-node-immediately-called.js +index dc4ded4e3e..9c10c2ecf0 100644 +--- a/dist/utils/is-node-immediately-called.js ++++ b/dist/utils/is-node-immediately-called.js +@@ -1,4 +1,4 @@ +-import { AST_NODE_TYPES } from '@typescript-eslint/utils' ++import { AST_NODE_TYPES } from '@typescript-eslint/types' + /** + * Checks whether a node is the callee of a call or `new` expression. + * diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 276541f65..8d93e4a4d 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -16,6 +16,8 @@ overrides: msgpackr: 2.1.0 zod: 4.5.4 +packageExtensionsChecksum: sha256-F3whp1/Z67s2/nCP/MLbO5ghyRkhOXgl4TpHpshbulg= + patchedDependencies: '@better-fetch/fetch@1.3.1': 9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747 '@module-federation/bridge-react@2.9.0': 8c084f41790295af8fd015b897c6298bbc13d927b796c624ac96cb2bdb4bc87c @@ -24,6 +26,7 @@ patchedDependencies: '@module-federation/runtime-core@2.9.0': b241be221397f0e07dbe6c515725e12eaf3b418469bb7dd21750e6e4b215dd8d '@vercel/nft@0.29.2': c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7 drizzle-orm@1.0.0-rc.5-ab785fc: b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe + eslint-plugin-perfectionist@5.10.1: 9e69fb6189199155ccf29de79c5127492dcebff0b0a1fdf79bb3cd72704501a7 msgpackr@2.1.0: de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a zod@4.5.4: 30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6 @@ -67,10 +70,10 @@ importers: version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' '@modern-js/ultramodern-create': specifier: npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2 - version: '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' + version: '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' '@nkzw/eslint-plugin': specifier: 2.0.0 - version: 2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + version: 2.0.0 '@noble/hashes': specifier: 2.2.0 version: 2.2.0 @@ -98,18 +101,15 @@ importers: esbuild: specifier: 0.28.1 version: 0.28.1 - eslint: - specifier: 10.10.0 - version: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-plugin-github: specifier: 6.1.2 - version: 6.1.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + version: 6.1.2(supports-color@10.2.2) eslint-plugin-perfectionist: specifier: 5.10.1 - version: 5.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) + version: 5.10.1(patch_hash=9e69fb6189199155ccf29de79c5127492dcebff0b0a1fdf79bb3cd72704501a7)(supports-color@10.2.2)(typescript@7.0.2) eslint-plugin-sonarjs: specifier: 4.2.0 - version: 4.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + version: 4.2.0 fallow: specifier: 3.22.0 version: 3.22.0 @@ -148,7 +148,7 @@ importers: version: 7.0.2001 ultracite: specifier: 7.11.0 - version: 7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) + version: 7.11.0(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) apps/shell-super-app: dependencies: @@ -909,12 +909,6 @@ packages: '@bufbuild/protobuf@2.13.0': resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} - '@cacheable/memory@2.2.0': - resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} - - '@cacheable/utils@2.5.0': - resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} - '@clack/core@1.4.3': resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} @@ -1617,14 +1611,6 @@ packages: eslint: optional: true - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.7.0': - resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1637,14 +1623,6 @@ packages: resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.3': - resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@fallow-cli/darwin-arm64@3.22.0': resolution: {integrity: sha512-X74T3osJZqFjNAM4w9OK4PWMPiubNyotDQ5OGYyvU9nNejJ8Ov9MH37H/9HE9hjdw4PxtPWlvl3LbatmuV5WMg==} cpu: [arm64] @@ -1706,26 +1684,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@iconify-json/mdi-light@1.2.2': resolution: {integrity: sha512-86UV9uyNve8zRFWiPrOrrDp9GDzsZM7plYV/on4VjgLLqXlyriuy541eHZB7LIOzTUyIPVli7QiUpBbTtBhsFw==} @@ -2095,15 +2053,6 @@ packages: peerDependencies: tslib: '2' - '@keyv/bigmap@1.3.1': - resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} - engines: {node: '>= 18'} - peerDependencies: - keyv: ^5.6.0 - - '@keyv/serialize@1.1.1': - resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@loadable/component@5.16.7': resolution: {integrity: sha512-XvkFixLUOTEaj8lI7uwc4nf8Wmq3IulYG7SZHCWcPm/Li5gjJDFfIkgWOLPnD7jqPJVtAG9bEz4SCek+SpHYYg==} engines: {node: '>=8'} @@ -4692,9 +4641,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.5.0: - resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -5050,9 +4996,6 @@ packages: supports-color: optional: true - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -5366,9 +5309,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -5411,10 +5351,6 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true @@ -5541,28 +5477,10 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.10.0: - resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5636,9 +5554,6 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -5677,9 +5592,6 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@11.1.5: - resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} - file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -5694,16 +5606,9 @@ packages: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - find-workspaces@0.3.1: resolution: {integrity: sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w==} - flat-cache@6.1.23: - resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} - flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -5823,10 +5728,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - glob-to-regex.js@1.2.0: resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} engines: {node: '>=10.0'} @@ -5891,10 +5792,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hashery@1.5.1: - resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} - engines: {node: '>=20'} - hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -5906,12 +5803,6 @@ packages: resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} - hookified@1.15.1: - resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} - - hookified@2.2.0: - resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} - html-minifier-terser@7.2.0: resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} engines: {node: ^14.13.1 || >=16.0.0} @@ -5998,10 +5889,6 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -6324,9 +6211,6 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -6350,9 +6234,6 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - keyv@5.6.0: - resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -6448,10 +6329,6 @@ packages: engines: {node: '>=18'} hasBin: true - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - libphonenumber-js@1.13.9: resolution: {integrity: sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==} @@ -6626,10 +6503,6 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -7033,10 +6906,6 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -7102,10 +6971,6 @@ packages: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -7149,10 +7014,6 @@ packages: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -7624,10 +7485,6 @@ packages: peerDependencies: postcss: ^8.5.28 - postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} - engines: {node: '>=4'} - postcss-selector-parser@7.1.6: resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} @@ -7659,10 +7516,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} @@ -7702,10 +7555,6 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.1: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} @@ -7760,10 +7609,6 @@ packages: pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} - qified@0.10.1: - resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} - engines: {node: '>=20'} - qs@6.16.0: resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} @@ -8460,10 +8305,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-fest@5.9.0: resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==} engines: {node: '>=20'} @@ -8657,10 +8498,6 @@ packages: engines: {node: '>= 8'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - workerd@1.20260730.1: resolution: {integrity: sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==} engines: {node: '>=16'} @@ -8860,7 +8697,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.8 + browserslist: 4.28.9 lru-cache: 5.1.1 semver: 6.3.1 @@ -9743,14 +9580,14 @@ snapshots: dependencies: '@jest/types': 30.5.1 - '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': + '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': dependencies: '@modern-js/codesmith': 2.6.9(supports-color@10.2.2) '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' esbuild: 0.28.2 oxfmt: 0.66.0 - ultracite: 7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) + ultracite: 7.11.0(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6) transitivePeerDependencies: - '@biomejs/biome' - eslint @@ -9777,18 +9614,6 @@ snapshots: '@bufbuild/protobuf@2.13.0': {} - '@cacheable/memory@2.2.0': - dependencies: - '@cacheable/utils': 2.5.0 - '@keyv/bigmap': 1.3.1(keyv@5.6.0) - hookified: 1.15.1 - keyv: 5.6.0 - - '@cacheable/utils@2.5.0': - dependencies: - hashery: 1.5.1 - keyv: 5.6.0 - '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 @@ -9845,13 +9670,13 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.4)': + '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.6)': dependencies: - postcss-selector-parser: 7.1.4 + postcss-selector-parser: 7.1.6 - '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.4)': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.6)': dependencies: - postcss-selector-parser: 7.1.4 + postcss-selector-parser: 7.1.6 '@csstools/utilities@3.0.0(postcss@8.5.28)': dependencies: @@ -10207,28 +10032,13 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint-community/eslint-utils@4.10.1': dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': - dependencies: - '@eslint/core': 1.2.1 - optionalDependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - - '@eslint/config-array@0.23.5(supports-color@10.2.2)': - dependencies: - '@eslint/object-schema': 3.0.5 - debug: 4.4.3(supports-color@10.2.2) - minimatch: 10.2.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.7.0': + '@eslint/compat@2.1.0': dependencies: '@eslint/core': 1.2.1 @@ -10252,13 +10062,6 @@ snapshots: '@eslint/js@9.39.5': {} - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.3': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - '@fallow-cli/darwin-arm64@3.22.0': optional: true @@ -10308,22 +10111,6 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.3 - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/mdi-light@1.2.2': dependencies: '@iconify/types': 2.0.0 @@ -10685,14 +10472,6 @@ snapshots: '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) tslib: 2.8.1 - '@keyv/bigmap@1.3.1(keyv@5.6.0)': - dependencies: - hashery: 1.5.1 - hookified: 1.15.1 - keyv: 5.6.0 - - '@keyv/serialize@1.1.1': {} - '@loadable/component@5.16.7(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 @@ -11044,9 +10823,7 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@nkzw/eslint-plugin@2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': - dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + '@nkzw/eslint-plugin@2.0.0': {} '@noble/ciphers@2.2.0': {} @@ -11681,7 +11458,7 @@ snapshots: dependencies: deepmerge: 4.3.1 loader-utils: 2.0.4 - postcss: 8.5.26 + postcss: 8.5.28 reduce-configs: 2.0.1 sass-embedded: 1.100.0 optionalDependencies: @@ -12335,15 +12112,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/type-utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.69.0 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 7.0.8 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -12351,14 +12127,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -12394,13 +12169,12 @@ snapshots: dependencies: typescript: 7.0.2 - '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.69.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -12438,24 +12212,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/utils@8.69.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1 '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2)': + '@typescript-eslint/utils@8.69.0(supports-color@10.2.2)(typescript@7.0.2)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1 '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@7.0.2) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 7.0.2 transitivePeerDependencies: - supports-color @@ -13202,7 +12974,7 @@ snapshots: browserslist-to-es-version@1.4.2: dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 browserslist@4.28.8: dependencies: @@ -13244,14 +13016,6 @@ snapshots: bytes@3.1.2: {} - cacheable@2.5.0: - dependencies: - '@cacheable/memory': 2.2.0 - '@cacheable/utils': 2.5.0 - hookified: 1.15.1 - keyv: 5.6.0 - qified: 0.10.1 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -13280,14 +13044,14 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-lite: 1.0.30001810 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-api@4.0.0: dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-lite: 1.0.30001810 caniuse-lite@1.0.30001810: {} @@ -13435,16 +13199,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.4.0(postcss@8.5.26): + css-declaration-sorter@7.4.0(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 7.1.9(postcss@8.5.26) + cssnano: 7.1.9(postcss@8.5.28) jest-worker: 30.4.1 - postcss: 8.5.26 + postcss: 8.5.28 schema-utils: 4.3.3 serialize-javascript: 7.0.7 webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) @@ -13457,9 +13221,9 @@ snapshots: css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 7.1.9(postcss@8.5.26) + cssnano: 7.1.9(postcss@8.5.28) jest-worker: 30.4.1 - postcss: 8.5.26 + postcss: 8.5.28 schema-utils: 4.3.3 serialize-javascript: 7.0.7 webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) @@ -13506,39 +13270,39 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.17(postcss@8.5.26): + cssnano-preset-default@7.0.17(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - css-declaration-sorter: 7.4.0(postcss@8.5.26) - cssnano-utils: 5.0.3(postcss@8.5.26) - postcss: 8.5.26 - postcss-calc: 10.1.1(postcss@8.5.26) - postcss-colormin: 7.0.10(postcss@8.5.26) - postcss-convert-values: 7.0.12(postcss@8.5.26) - postcss-discard-comments: 7.0.8(postcss@8.5.26) - postcss-discard-duplicates: 7.0.4(postcss@8.5.26) - postcss-discard-empty: 7.0.3(postcss@8.5.26) - postcss-discard-overridden: 7.0.3(postcss@8.5.26) - postcss-merge-longhand: 7.0.7(postcss@8.5.26) - postcss-merge-rules: 7.0.11(postcss@8.5.26) - postcss-minify-font-values: 7.0.3(postcss@8.5.26) - postcss-minify-gradients: 7.0.5(postcss@8.5.26) - postcss-minify-params: 7.0.9(postcss@8.5.26) - postcss-minify-selectors: 7.1.2(postcss@8.5.26) - postcss-normalize-charset: 7.0.3(postcss@8.5.26) - postcss-normalize-display-values: 7.0.3(postcss@8.5.26) - postcss-normalize-positions: 7.0.4(postcss@8.5.26) - postcss-normalize-repeat-style: 7.0.4(postcss@8.5.26) - postcss-normalize-string: 7.0.3(postcss@8.5.26) - postcss-normalize-timing-functions: 7.0.3(postcss@8.5.26) - postcss-normalize-unicode: 7.0.9(postcss@8.5.26) - postcss-normalize-url: 7.0.3(postcss@8.5.26) - postcss-normalize-whitespace: 7.0.3(postcss@8.5.26) - postcss-ordered-values: 7.0.4(postcss@8.5.26) - postcss-reduce-initial: 7.0.9(postcss@8.5.26) - postcss-reduce-transforms: 7.0.3(postcss@8.5.26) - postcss-svgo: 7.1.3(postcss@8.5.26) - postcss-unique-selectors: 7.0.7(postcss@8.5.26) + browserslist: 4.28.9 + css-declaration-sorter: 7.4.0(postcss@8.5.28) + cssnano-utils: 5.0.3(postcss@8.5.28) + postcss: 8.5.28 + postcss-calc: 10.1.1(postcss@8.5.28) + postcss-colormin: 7.0.10(postcss@8.5.28) + postcss-convert-values: 7.0.12(postcss@8.5.28) + postcss-discard-comments: 7.0.8(postcss@8.5.28) + postcss-discard-duplicates: 7.0.4(postcss@8.5.28) + postcss-discard-empty: 7.0.3(postcss@8.5.28) + postcss-discard-overridden: 7.0.3(postcss@8.5.28) + postcss-merge-longhand: 7.0.7(postcss@8.5.28) + postcss-merge-rules: 7.0.11(postcss@8.5.28) + postcss-minify-font-values: 7.0.3(postcss@8.5.28) + postcss-minify-gradients: 7.0.5(postcss@8.5.28) + postcss-minify-params: 7.0.9(postcss@8.5.28) + postcss-minify-selectors: 7.1.2(postcss@8.5.28) + postcss-normalize-charset: 7.0.3(postcss@8.5.28) + postcss-normalize-display-values: 7.0.3(postcss@8.5.28) + postcss-normalize-positions: 7.0.4(postcss@8.5.28) + postcss-normalize-repeat-style: 7.0.4(postcss@8.5.28) + postcss-normalize-string: 7.0.3(postcss@8.5.28) + postcss-normalize-timing-functions: 7.0.3(postcss@8.5.28) + postcss-normalize-unicode: 7.0.9(postcss@8.5.28) + postcss-normalize-url: 7.0.3(postcss@8.5.28) + postcss-normalize-whitespace: 7.0.3(postcss@8.5.28) + postcss-ordered-values: 7.0.4(postcss@8.5.28) + postcss-reduce-initial: 7.0.9(postcss@8.5.28) + postcss-reduce-transforms: 7.0.3(postcss@8.5.28) + postcss-svgo: 7.1.3(postcss@8.5.28) + postcss-unique-selectors: 7.0.7(postcss@8.5.28) cssnano-preset-default@9.0.3(postcss@8.5.28): dependencies: @@ -13573,19 +13337,19 @@ snapshots: postcss-svgo: 9.0.2(postcss@8.5.28) postcss-unique-selectors: 9.0.2(postcss@8.5.28) - cssnano-utils@5.0.3(postcss@8.5.26): + cssnano-utils@5.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 cssnano-utils@7.0.2(postcss@8.5.28): dependencies: postcss: 8.5.28 - cssnano@7.1.9(postcss@8.5.26): + cssnano@7.1.9(postcss@8.5.28): dependencies: - cssnano-preset-default: 7.0.17(postcss@8.5.26) + cssnano-preset-default: 7.0.17(postcss@8.5.28) lilconfig: 3.1.3 - postcss: 8.5.26 + postcss: 8.5.28 cssnano@9.0.3(postcss@8.5.28): dependencies: @@ -13647,8 +13411,6 @@ snapshots: optionalDependencies: supports-color: 10.2.2 - deep-is@0.1.4: {} - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -13872,8 +13634,6 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.3.1: {} - es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: @@ -13991,11 +13751,7 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): - dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + eslint-config-prettier@10.1.8: {} eslint-import-resolver-node@0.3.10(supports-color@10.2.2): dependencies: @@ -14005,72 +13761,65 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(supports-color@10.2.2): dependencies: debug: 3.2.7(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/parser': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) transitivePeerDependencies: - supports-color - eslint-plugin-escompat@3.12.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-escompat@3.12.0: dependencies: browserslist: 4.28.8 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-eslint-comments@3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-eslint-comments@3.2.0: dependencies: escape-string-regexp: 1.0.5 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-filenames@1.3.2: dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.1.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-github@6.1.2(supports-color@10.2.2): dependencies: - '@eslint/compat': 2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint/compat': 2.1.0 '@eslint/eslintrc': 3.3.7(supports-color@10.2.2) '@eslint/js': 9.39.5 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) aria-query: 5.3.0 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-escompat: 3.12.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-filenames: 1.3.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-config-prettier: 10.1.8 + eslint-plugin-escompat: 3.12.0 + eslint-plugin-eslint-comments: 3.2.0 + eslint-plugin-filenames: 1.3.2 + eslint-plugin-i18n-text: 1.0.1 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2) + eslint-plugin-jsx-a11y: 6.10.2 eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8)(prettier@3.9.6) eslint-rule-documentation: 1.0.23 globals: 17.12.0 jsx-ast-utils: 3.3.5 prettier: 3.9.6 svg-element-attributes: 1.3.1 typescript: 6.0.3 - typescript-eslint: 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + typescript-eslint: 8.69.0(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): - dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + eslint-plugin-i18n-text@1.0.1: {} - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14079,9 +13828,8 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@10.2.2) doctrine: 2.1.0 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(supports-color@10.2.2) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -14093,13 +13841,13 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-jsx-a11y@6.10.2: dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -14109,7 +13857,6 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -14120,30 +13867,28 @@ snapshots: eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-perfectionist@5.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2): + eslint-plugin-perfectionist@5.10.1(patch_hash=9e69fb6189199155ccf29de79c5127492dcebff0b0a1fdf79bb3cd72704501a7)(supports-color@10.2.2)(typescript@7.0.2): dependencies: - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@7.0.2) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(supports-color@10.2.2)(typescript@7.0.2) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(prettier@3.9.6): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8)(prettier@3.9.6): dependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) prettier: 3.9.6 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-config-prettier: 10.1.8 - eslint-plugin-sonarjs@4.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-sonarjs@4.2.0: dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) functional-red-black-tree: 1.0.1 globals: 17.12.0 jsx-ast-utils-x: 0.1.0 @@ -14175,59 +13920,12 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2): - dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5(supports-color@10.2.2) - '@eslint/config-helpers': 0.7.0 - '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.3 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@10.2.2) - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 11.1.5 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.7.0 - transitivePeerDependencies: - - supports-color - espree@10.4.0: dependencies: acorn: 8.17.0 acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 4.2.1 - espree@11.2.0: - dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -14316,8 +14014,6 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -14357,10 +14053,6 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@11.1.5: - dependencies: - flat-cache: 6.1.23 - file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -14373,23 +14065,12 @@ snapshots: dependencies: locate-path: 3.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - find-workspaces@0.3.1: dependencies: fast-glob: 3.3.3 pkg-types: 1.3.1 yaml: 2.9.0 - flat-cache@6.1.23: - dependencies: - cacheable: 2.5.0 - flatted: 3.4.4 - hookified: 1.15.1 - flatted@3.4.4: {} follow-redirects@1.16.0(debug@4.3.7(supports-color@10.2.2)): @@ -14522,10 +14203,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - glob-to-regex.js@1.2.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -14593,10 +14270,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hashery@1.5.1: - dependencies: - hookified: 1.15.1 - hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -14607,10 +14280,6 @@ snapshots: hono@4.13.7: {} - hookified@1.15.1: {} - - hookified@2.2.0: {} - html-minifier-terser@7.2.0: dependencies: camel-case: 4.1.2 @@ -14692,8 +14361,6 @@ snapshots: import-meta-resolve@4.2.0: {} - imurmurhash@0.1.4: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -14980,8 +14647,6 @@ snapshots: json-schema-traverse@1.0.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -15005,10 +14670,6 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - keyv@5.6.0: - dependencies: - '@keyv/serialize': 1.1.1 - kleur@4.1.5: {} knip@6.34.0: @@ -15109,11 +14770,6 @@ snapshots: transitivePeerDependencies: - supports-color - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - libphonenumber-js@1.13.9: {} lightningcss-android-arm64@1.32.0: @@ -15233,10 +14889,6 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash-es@4.18.1: {} lodash.camelcase@4.3.0: {} @@ -15503,7 +15155,7 @@ snapshots: dependencies: debug: 3.2.7(supports-color@10.2.2) iconv-lite: 0.4.24 - sax: 1.6.0 + sax: 1.6.1 transitivePeerDependencies: - supports-color optional: true @@ -15511,7 +15163,7 @@ snapshots: needle@3.5.0: dependencies: iconv-lite: 0.6.3 - sax: 1.6.0 + sax: 1.6.1 optional: true neo-async@2.6.2: {} @@ -15644,15 +15296,6 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - ora@5.4.1: dependencies: bl: 4.1.0 @@ -15819,10 +15462,6 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -15865,8 +15504,6 @@ snapshots: path-exists@3.0.0: {} - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -15991,10 +15628,10 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.5.26): + postcss-calc@10.1.1(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 postcss-calc@11.1.0(postcss@8.5.28): @@ -16002,12 +15639,12 @@ snapshots: '@csstools/css-tokenizer': 4.0.0 postcss: 8.5.28 - postcss-colormin@7.0.10(postcss@8.5.26): + postcss-colormin@7.0.10(postcss@8.5.28): dependencies: '@colordx/core': 5.5.0 - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 3.0.0 - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-colormin@9.0.2(postcss@8.5.28): @@ -16018,10 +15655,10 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.12(postcss@8.5.26): + postcss-convert-values@7.0.12(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-convert-values@9.0.2(postcss@8.5.28): @@ -16039,35 +15676,35 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.8(postcss@8.5.26): + postcss-discard-comments@7.0.8(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-discard-comments@9.0.2(postcss@8.5.28): dependencies: postcss: 8.5.28 postcss-selector-parser: 7.1.6 - postcss-discard-duplicates@7.0.4(postcss@8.5.26): + postcss-discard-duplicates@7.0.4(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-discard-duplicates@9.0.2(postcss@8.5.28): dependencies: postcss: 8.5.28 - postcss-discard-empty@7.0.3(postcss@8.5.26): + postcss-discard-empty@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-discard-empty@9.0.2(postcss@8.5.28): dependencies: postcss: 8.5.28 - postcss-discard-overridden@7.0.3(postcss@8.5.26): + postcss-discard-overridden@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-discard-overridden@9.0.2(postcss@8.5.28): dependencies: @@ -16089,11 +15726,11 @@ snapshots: dependencies: postcss: 8.5.28 - postcss-merge-longhand@7.0.7(postcss@8.5.26): + postcss-merge-longhand@7.0.7(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - stylehacks: 7.0.11(postcss@8.5.26) + stylehacks: 7.0.11(postcss@8.5.28) postcss-merge-longhand@9.0.3(postcss@8.5.28): dependencies: @@ -16101,13 +15738,13 @@ snapshots: postcss-value-parser: 4.2.0 stylehacks: 9.0.3(postcss@8.5.28) - postcss-merge-rules@7.0.11(postcss@8.5.26): + postcss-merge-rules@7.0.11(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 3.0.0 - cssnano-utils: 5.0.3(postcss@8.5.26) - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + cssnano-utils: 5.0.3(postcss@8.5.28) + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-merge-rules@9.0.3(postcss@8.5.28): dependencies: @@ -16117,9 +15754,9 @@ snapshots: postcss: 8.5.28 postcss-selector-parser: 7.1.6 - postcss-minify-font-values@7.0.3(postcss@8.5.26): + postcss-minify-font-values@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-font-values@9.0.2(postcss@8.5.28): @@ -16127,11 +15764,11 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.5(postcss@8.5.26): + postcss-minify-gradients@7.0.5(postcss@8.5.28): dependencies: '@colordx/core': 5.5.0 - cssnano-utils: 5.0.3(postcss@8.5.26) - postcss: 8.5.26 + cssnano-utils: 5.0.3(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-gradients@9.0.2(postcss@8.5.28): @@ -16141,11 +15778,11 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.9(postcss@8.5.26): + postcss-minify-params@7.0.9(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - cssnano-utils: 5.0.3(postcss@8.5.26) - postcss: 8.5.26 + browserslist: 4.28.9 + cssnano-utils: 5.0.3(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-minify-params@9.0.2(postcss@8.5.28): @@ -16155,13 +15792,13 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.1.2(postcss@8.5.26): + postcss-minify-selectors@7.1.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 3.0.0 cssesc: 3.0.0 - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-minify-selectors@9.0.3(postcss@8.5.28): dependencies: @@ -16173,22 +15810,22 @@ snapshots: postcss-nesting@14.0.1(postcss@8.5.28): dependencies: - '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.4) - '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4) + '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.6) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.6) postcss: 8.5.28 - postcss-selector-parser: 7.1.4 + postcss-selector-parser: 7.1.6 - postcss-normalize-charset@7.0.3(postcss@8.5.26): + postcss-normalize-charset@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-normalize-charset@9.0.2(postcss@8.5.28): dependencies: postcss: 8.5.28 - postcss-normalize-display-values@7.0.3(postcss@8.5.26): + postcss-normalize-display-values@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-display-values@9.0.2(postcss@8.5.28): @@ -16196,9 +15833,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.4(postcss@8.5.26): + postcss-normalize-positions@7.0.4(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-positions@9.0.2(postcss@8.5.28): @@ -16206,9 +15843,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.4(postcss@8.5.26): + postcss-normalize-repeat-style@7.0.4(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-repeat-style@9.0.2(postcss@8.5.28): @@ -16216,9 +15853,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.3(postcss@8.5.26): + postcss-normalize-string@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-string@9.0.2(postcss@8.5.28): @@ -16226,9 +15863,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.3(postcss@8.5.26): + postcss-normalize-timing-functions@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-timing-functions@9.0.2(postcss@8.5.28): @@ -16236,10 +15873,10 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.9(postcss@8.5.26): + postcss-normalize-unicode@7.0.9(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-unicode@9.0.2(postcss@8.5.28): @@ -16248,9 +15885,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.3(postcss@8.5.26): + postcss-normalize-url@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-url@9.0.2(postcss@8.5.28): @@ -16258,9 +15895,9 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.3(postcss@8.5.26): + postcss-normalize-whitespace@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-normalize-whitespace@9.0.2(postcss@8.5.28): @@ -16268,10 +15905,10 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.4(postcss@8.5.26): + postcss-ordered-values@7.0.4(postcss@8.5.28): dependencies: - cssnano-utils: 5.0.3(postcss@8.5.26) - postcss: 8.5.26 + cssnano-utils: 5.0.3(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-ordered-values@9.0.2(postcss@8.5.28): @@ -16284,11 +15921,11 @@ snapshots: dependencies: postcss: 8.5.28 - postcss-reduce-initial@7.0.9(postcss@8.5.26): + postcss-reduce-initial@7.0.9(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 3.0.0 - postcss: 8.5.26 + postcss: 8.5.28 postcss-reduce-initial@9.0.2(postcss@8.5.28): dependencies: @@ -16296,9 +15933,9 @@ snapshots: caniuse-api: 4.0.0 postcss: 8.5.28 - postcss-reduce-transforms@7.0.3(postcss@8.5.26): + postcss-reduce-transforms@7.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-reduce-transforms@9.0.2(postcss@8.5.28): @@ -16306,21 +15943,16 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-selector-parser@7.1.4: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.3(postcss@8.5.26): + postcss-svgo@7.1.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - svgo: 4.0.2 + svgo: 4.1.0 postcss-svgo@9.0.2(postcss@8.5.28): dependencies: @@ -16328,10 +15960,10 @@ snapshots: postcss-value-parser: 4.2.0 svgo: 4.1.0 - postcss-unique-selectors@7.0.7(postcss@8.5.26): + postcss-unique-selectors@7.0.7(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-unique-selectors@9.0.2(postcss@8.5.28): dependencies: @@ -16340,12 +15972,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.28: dependencies: nanoid: 3.3.18 @@ -16374,8 +16000,6 @@ snapshots: postgres-range@1.1.4: {} - prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 @@ -16442,10 +16066,6 @@ snapshots: pure-rand@8.4.2: {} - qified@0.10.1: - dependencies: - hookified: 2.2.0 - qs@6.16.0: dependencies: es-define-property: 1.0.1 @@ -16999,11 +16619,11 @@ snapshots: strip-json-comments@5.0.3: {} - stylehacks@7.0.11(postcss@8.5.26): + stylehacks@7.0.11(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 - postcss-selector-parser: 7.1.4 + browserslist: 4.28.9 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 stylehacks@9.0.3(postcss@8.5.28): dependencies: @@ -17035,7 +16655,7 @@ snapshots: css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.6.0 + sax: 1.6.1 svgo@4.0.2: dependencies: @@ -17161,10 +16781,6 @@ snapshots: tslib@2.8.1: {} - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-fest@5.9.0: dependencies: tagged-tag: 1.0.0 @@ -17208,13 +16824,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): + typescript-eslint@8.69.0(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/utils': 8.69.0(supports-color@10.2.2)(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -17246,7 +16861,7 @@ snapshots: ufo@1.6.4: {} - ultracite@7.11.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6): + ultracite@7.11.0(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6): dependencies: '@clack/prompts': 1.7.0 cli-truncate: 6.1.1 @@ -17266,7 +16881,6 @@ snapshots: yaml: 2.9.0 zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) optionalDependencies: - eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) oxfmt: 0.66.0 oxlint: 1.81.0(oxlint-tsgolint@7.0.2001) prettier: 3.9.6 @@ -17357,10 +16971,10 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.17.0 acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.8 + browserslist: 4.28.9 chrome-trace-event: 1.0.4 enhanced-resolve: 5.24.3 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 @@ -17395,10 +17009,10 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.17.0 acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.8 + browserslist: 4.28.9 chrome-trace-event: 1.0.4 enhanced-resolve: 5.24.3 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 @@ -17473,8 +17087,6 @@ snapshots: dependencies: isexe: 2.0.0 - word-wrap@1.2.5: {} - workerd@1.20260730.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260730.1 diff --git a/app/pnpm-workspace.yaml b/app/pnpm-workspace.yaml index 334b6d8bc..6425b4bd7 100644 --- a/app/pnpm-workspace.yaml +++ b/app/pnpm-workspace.yaml @@ -81,6 +81,7 @@ allowBuilds: sharp: true workerd: true patchedDependencies: + 'eslint-plugin-perfectionist@5.10.1': patches/eslint-plugin-perfectionist@5.10.1.patch '@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch '@better-fetch/fetch@1.3.1': patches/@better-fetch__fetch@1.3.1.patch '@module-federation/dts-plugin@2.9.0': patches/@module-federation__dts-plugin@2.9.0.patch @@ -90,3 +91,7 @@ patchedDependencies: msgpackr@2.1.0: patches/msgpackr@2.1.0.patch zod@4.5.4: patches/zod@4.5.4.patch drizzle-orm@1.0.0-rc.5-ab785fc: patches/drizzle-orm-rc5-declarations.patch +packageExtensions: + 'eslint-plugin-perfectionist@5.10.1': + dependencies: + '@typescript-eslint/types': 8.69.0 diff --git a/app/tools/oxlint/effect-native/tests/native-sorting-plugins.test.mts b/app/tools/oxlint/effect-native/tests/native-sorting-plugins.test.mts new file mode 100644 index 000000000..261fc340b --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/native-sorting-plugins.test.mts @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict'; +import { writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { appRoot, runOxlint } from './oxlint.mts'; +import { withTemporaryWorkspace } from './temporary-workspace.mts'; + +const plugin = fileURLToPath( + import.meta.resolve('eslint-plugin-perfectionist') +); +const applicationRequire = createRequire(join(appRoot, 'package.json')); +const cases = [ + { + rule: 'sort-enums', + options: { partitionByComment: true, sortByValue: 'always' }, + invalid: 'enum Status { Alpha = 20, Zulu = 1 }', + valid: 'enum Status { Zulu = 1, Alpha = 20 }', + }, + { + rule: 'sort-heritage-clauses', + invalid: 'interface View extends Zebra, Alpha {}', + valid: 'interface View extends Alpha, Zebra {}', + }, + { + rule: 'sort-interfaces', + invalid: 'interface View { zebra: string; alpha: string }', + valid: 'interface View { alpha: string; zebra: string }', + }, + { + rule: 'sort-jsx-props', + invalid: 'const view = ;', + valid: 'const view = ;', + }, + { + rule: 'sort-object-types', + invalid: 'type View = { zebra: string; alpha: string };', + valid: 'type View = { alpha: string; zebra: string };', + }, + { + rule: 'sort-objects', + options: { partitionByComment: true }, + invalid: 'const view = { zebra: 1, alpha: 2 };', + valid: 'const view = { alpha: 2, zebra: 1 };', + }, +]; + +test('native sorting integration does not resolve the ESLint runner', () => { + assert.throws( + () => applicationRequire.resolve('eslint'), + /Cannot find module 'eslint'/u + ); +}); + +for (const fixture of cases) { + test(`Oxlint executes ${fixture.rule} positives and negatives without ESLint`, () => { + withTemporaryWorkspace((directory) => { + const config = join(directory, 'oxlint.json'); + writeFileSync( + config, + JSON.stringify({ + // Isolate each actual plugin rule; the application rule configuration is untouched. + categories: { correctness: 'off' }, + jsPlugins: [{ name: 'perfectionist', specifier: plugin }], + rules: { + [`perfectionist/${fixture.rule}`]: ['error', fixture.options ?? {}], + }, + }) + ); + const source = join(directory, 'fixture.tsx'); + writeFileSync(source, fixture.invalid); + const negative = runOxlint(config, [source], directory); + assert.equal(negative.exitCode, 1); + assert.ok( + negative.diagnostics.some( + ({ code }) => code === `perfectionist(${fixture.rule})` + ) + ); + writeFileSync(source, fixture.valid); + const positive = runOxlint(config, [source], directory); + assert.equal(positive.exitCode, 0, JSON.stringify(positive.diagnostics)); + assert.deepEqual(positive.diagnostics, []); + }); + }); +} + +test('native enum and object sorting preserves explicit comment partitions', () => { + withTemporaryWorkspace((directory) => { + const config = join(directory, 'oxlint.json'); + writeFileSync( + config, + JSON.stringify({ + categories: { correctness: 'off' }, + jsPlugins: [{ name: 'perfectionist', specifier: plugin }], + rules: { + 'perfectionist/sort-enums': [ + 'error', + { partitionByComment: true, sortByValue: 'always' }, + ], + 'perfectionist/sort-objects': ['error', { partitionByComment: true }], + }, + }) + ); + const source = join(directory, 'fixture.ts'); + writeFileSync( + source, + `enum Status { + Alpha = 20, + // separate partition + Zulu = 1, +} +const value = { + zebra: 1, + // separate partition + alpha: 2, +}; +` + ); + const result = runOxlint(config, [source], directory); + assert.equal(result.exitCode, 0, JSON.stringify(result.diagnostics)); + assert.deepEqual(result.diagnostics, []); + }); +}); From 9a3e098607f9e559014e59c30d6b6b852d5dc939 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 17:05:50 +0200 Subject: [PATCH 04/11] test: share localized link doubles Keep per-file hoisted state while sharing typed recording and destination resolution. Preserve all route assertions; 32 tests and scoped native lint pass. Co-Authored-By: Claude Code --- .../tests/support/localized-link-double.tsx | 84 ++++++++ .../tests/unit/routes/home/page.test.tsx | 180 +++++++++--------- .../tests/unit/routes/login/page.test.tsx | 145 ++++++-------- .../tests/unit/routes/search/page.test.tsx | 124 +++++------- 4 files changed, 289 insertions(+), 244 deletions(-) create mode 100644 app/apps/shell-super-app/tests/support/localized-link-double.tsx diff --git a/app/apps/shell-super-app/tests/support/localized-link-double.tsx b/app/apps/shell-super-app/tests/support/localized-link-double.tsx new file mode 100644 index 000000000..4c833f906 --- /dev/null +++ b/app/apps/shell-super-app/tests/support/localized-link-double.tsx @@ -0,0 +1,84 @@ +import type { ComponentProps, ReactElement, ReactNode } from 'react'; + +import { ultramodernLocalisedUrls } from '../../src/routes/ultramodern-route-metadata.ts'; + +/** Props the localised framework link receives from the pages under test. */ +export type LocalizedLinkDoubleProps = Omit, 'href'> & { + readonly children?: ReactNode; + readonly href?: string | undefined; + readonly params?: Readonly>; + readonly to: string; +}; + +/** One canonical navigation target a page handed to the framework link. */ +export interface LocalizedLinkCall { + readonly href: string | undefined; + readonly params: Readonly> | undefined; + readonly to: string; +} + +/** + * Recording state supplied by a single test file. Each file owns its own + * array and language holder, so navigation evidence never leaks between + * suites. + */ +export interface LocalizedLinkRecording { + readonly calls: LocalizedLinkCall[]; + readonly language: { readonly current: string }; +} + +const localisedUrlPatterns = new Map>>( + Object.entries(ultramodernLocalisedUrls).map( + ([canonicalPattern, localisedPatterns]): readonly [ + string, + Readonly>, + ] => [ + canonicalPattern, + { cs: localisedPatterns.cs, en: localisedPatterns.en }, + ] + ) +); + +/** + * Resolves the destination the framework link would produce, using the + * application's own canonical-to-localised route map instead of a hand-written + * expectation, so the page is proven to hand over a language-agnostic target. + */ +const resolveLocalizedHref = ( + to: string, + params: Readonly> | undefined, + language: string +): string => { + const canonicalPattern = to.replaceAll('$', ':'); + const localisedPattern = + localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; + const segments = localisedPattern + .split('/') + .filter(Boolean) + .map((segment) => + segment.startsWith(':') + ? encodeURIComponent(params?.[segment.slice(1)] ?? '') + : segment + ); + return `/${[language, ...segments].join('/')}`; +}; + +/** + * Stands in for the localised framework link: it records the canonical target + * the page handed over and renders the destination the framework would resolve + * for the file's current language. + */ +export const renderLocalizedLinkDouble = ( + { children, href, params, to, ...anchorProps }: LocalizedLinkDoubleProps, + recording: LocalizedLinkRecording +): ReactElement => { + recording.calls.push({ href, params, to }); + return ( + + {children} + + ); +}; diff --git a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx index 4082659cc..11b74c815 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx @@ -3,7 +3,6 @@ import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Effect, Schema } from 'effect'; -import type { ComponentProps, ReactNode } from 'react'; import { AppIdSchema, @@ -18,14 +17,11 @@ import { } from '../../../../shared/api.ts'; import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; import { HomeView } from '../../../../src/routes/[lang]/page.tsx'; -import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; - -type LocalizedLinkDoubleProps = Omit, 'href'> & { - readonly children?: ReactNode; - readonly href?: string | undefined; - readonly params?: Readonly>; - readonly to: string; -}; +import type { + LocalizedLinkCall, + LocalizedLinkDoubleProps, +} from '../../../support/localized-link-double.tsx'; +import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; const { languageState, @@ -36,11 +32,7 @@ const { switchLegalEntityMock, switchTenantMock, } = rstest.hoisted(() => { - const recordedLinkCalls: { - href: string | undefined; - params: Readonly> | undefined; - to: string; - }[] = []; + const recordedLinkCalls: LocalizedLinkCall[] = []; return { languageState: { current: 'en' }, localizedLinkCalls: recordedLinkCalls, @@ -52,37 +44,6 @@ const { }; }); -const localisedUrlPatterns = new Map>>( - Object.entries(ultramodernLocalisedUrls).map( - ([canonicalPattern, localisedPatterns]): readonly [ - string, - Readonly>, - ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], - ), -); - -/** - * Resolves the destination the framework link would produce, using the - * application's own canonical-to-localised route map instead of a hand-written - * expectation, so the page is proven to hand over a language-agnostic target. - */ -const resolveLocalizedHref = ( - to: string, - params: Readonly> | undefined, - language: string, -): string => { - const canonicalPattern = to.replaceAll('$', ':'); - const localisedPattern = - localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; - const segments = localisedPattern - .split('/') - .filter(Boolean) - .map((segment) => - segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, - ); - return `/${[language, ...segments].join('/')}`; -}; - const translations = new Map( Object.entries({ 'shell.auth.identity.displayName': 'Name', @@ -115,18 +76,15 @@ const translations = new Map( 'shell.modules.unavailable': 'Module access unavailable', 'shell.search.label': 'Search this legal entity', 'shell.search.submit': 'Search', - }), + }) ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ - Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { - localizedLinkCalls.push({ href, params, to }); - return ( - - {children} - - ); - }, + Link: (props: LocalizedLinkDoubleProps) => + renderLocalizedLinkDouble(props, { + calls: localizedLinkCalls, + language: languageState, + }), useLocalizedLocation: () => ({ alternates: { cs: '/cs/', en: '/en/' }, canonical: '/en/', @@ -153,21 +111,30 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ })); const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( - '00000000-0000-4000-8000-000000000001', + '00000000-0000-4000-8000-000000000001' +); +const tenantId1 = Schema.decodeUnknownSync(TenantIdSchema)( + '00000000-0000-4000-8000-000000000101' +); +const tenantId2 = Schema.decodeUnknownSync(TenantIdSchema)( + '00000000-0000-4000-8000-000000000102' ); -const tenantId1 = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101'); -const tenantId2 = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000102'); const legalEntityId1 = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000201', + '00000000-0000-4000-8000-000000000201' ); const legalEntityId2 = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000202', + '00000000-0000-4000-8000-000000000202' ); const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); -const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); -const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); +const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)( + 'shell.navigation.modules' +); +const inventoryModuleId = + Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); -const authenticatedModel = (options?: { readonly moduleEnabled?: boolean }): HomePageModel => ({ +const authenticatedModel = (options?: { + readonly moduleEnabled?: boolean; +}): HomePageModel => ({ contextState: 'authenticated', identity: { displayName: 'Ada Lovelace', @@ -214,11 +181,16 @@ const authenticatedModel = (options?: { readonly moduleEnabled?: boolean }): Hom beforeEach(() => { navigateMock.mockResolvedValue(undefined); runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), + async (effect: Effect.Effect) => + await runEffectTestPromise(effect) ); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); - switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId2 })); - switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId2 })); + switchTenantMock.mockReturnValue( + Effect.succeed({ selectedTenantId: tenantId2 }) + ); + switchLegalEntityMock.mockReturnValue( + Effect.succeed({ selectedLegalEntityId: legalEntityId2 }) + ); }); afterEach(() => { @@ -230,7 +202,9 @@ afterEach(() => { test('anonymous home exposes only the localized login action', () => { render(); - expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/en/login'); + expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe( + '/en/login' + ); expect(screen.queryByRole('banner')).toBeNull(); }); @@ -246,7 +220,9 @@ test('the anonymous login action resolves Czech from the same canonical target', languageState.current = 'cs'; render(); expect(localizedLinkCalls.map((call) => call.to)).toContain('/login'); - expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/cs/login'); + expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe( + '/cs/login' + ); }); test('the unavailable dashboard exposes no navigable affordance', () => { @@ -256,17 +232,21 @@ test('the unavailable dashboard exposes no navigable affordance', () => { }); test('a disabled module affordance stays non-interactive text', () => { - render(); + render( + + ); expect(screen.queryByRole('link', { name: 'Inventory' })).toBeNull(); expect(screen.getByText('Inventory')).toBeTruthy(); - expect(localizedLinkCalls.map((call) => call.to)).not.toContain('/modules/inventory.stock'); + expect(localizedLinkCalls.map((call) => call.to)).not.toContain( + '/modules/inventory.stock' + ); }); test('authenticated home renders server-composed navigation and selected legal context', () => { render(); - expect(screen.getByRole('link', { name: 'Inventory' }).getAttribute('href')).toBe( - '/en/modules/inventory.stock', - ); + expect( + screen.getByRole('link', { name: 'Inventory' }).getAttribute('href') + ).toBe('/en/modules/inventory.stock'); expect(screen.getByText('Read only')).toBeTruthy(); expect(screen.getByText(legalEntityId1)).toBeTruthy(); expect(screen.queryByText('inventory.stock')).toBeNull(); @@ -278,23 +258,32 @@ test('successful tenant switch performs a full document reload', async () => { await user.click(screen.getByRole('combobox', { name: 'Current tenant' })); await user.click(await screen.findByRole('option', { name: 'Zeta tenant' })); await waitFor(() => - expect(switchTenantMock).toHaveBeenCalledWith({ tenantId: tenantId2 }, { locale: 'en' }), + expect(switchTenantMock).toHaveBeenCalledWith( + { tenantId: tenantId2 }, + { locale: 'en' } + ) + ); + await waitFor(() => + expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' }) ); - await waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })); }); test('successful legal-entity switch performs a full document reload', async () => { const user = userEvent.setup(); render(); - await user.click(screen.getByRole('combobox', { name: 'Current legal entity' })); + await user.click( + screen.getByRole('combobox', { name: 'Current legal entity' }) + ); await user.click(await screen.findByRole('option', { name: 'Beta company' })); await waitFor(() => expect(switchLegalEntityMock).toHaveBeenCalledWith( { legalEntityId: legalEntityId2 }, - { locale: 'en' }, - ), + { locale: 'en' } + ) + ); + await waitFor(() => + expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' }) ); - await waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })); }); test('search submission navigates to the localized Shell search route', async () => { @@ -314,12 +303,12 @@ test('logout clears the authenticated composition together', async () => { expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '/en/login', - }), + }) ); }); const tenantAuthenticationRequired = Schema.decodeUnknownSync( - TenantAuthenticationRequiredProblemSchema, + TenantAuthenticationRequiredProblemSchema )({ _tag: 'TenantAuthenticationRequiredProblem', detail: 'The tenant session expired.', @@ -327,7 +316,9 @@ const tenantAuthenticationRequired = Schema.decodeUnknownSync( title: 'Tenant authentication required', type: 'https://ontos.dev/problems/tenant-authentication-required', }); -const tenantAccessForbidden = Schema.decodeUnknownSync(TenantAccessForbiddenProblemSchema)({ +const tenantAccessForbidden = Schema.decodeUnknownSync( + TenantAccessForbiddenProblemSchema +)({ _tag: 'TenantAccessForbiddenProblem', detail: 'The principal cannot use this tenant.', status: 403, @@ -335,7 +326,7 @@ const tenantAccessForbidden = Schema.decodeUnknownSync(TenantAccessForbiddenProb type: 'https://ontos.dev/problems/tenant-access-forbidden', }); const legalEntityAccessForbidden = Schema.decodeUnknownSync( - LegalEntityAccessForbiddenProblemSchema, + LegalEntityAccessForbiddenProblemSchema )({ _tag: 'LegalEntityAccessForbiddenProblem', detail: 'The principal cannot use this legal entity.', @@ -403,7 +394,15 @@ const switchFailureCases: SwitchFailureCase[] = [ test.each(switchFailureCases)( 'settles $name into its own selector without leaving it pending', - async ({ comboboxName, failedText, failure, optionName, pendingText, reloads, switchMock }) => { + async ({ + comboboxName, + failedText, + failure, + optionName, + pendingText, + reloads, + switchMock, + }) => { switchMock.mockReturnValue(Effect.fail(failure)); const user = userEvent.setup(); render(); @@ -414,7 +413,10 @@ test.each(switchFailureCases)( if (reloads) { await waitFor(() => - expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' }), + expect(navigateMock).toHaveBeenCalledWith({ + reloadDocument: true, + to: '.', + }) ); await waitFor(() => expect(screen.queryByText(pendingText)).toBeNull()); expect(screen.queryByText(failedText)).toBeNull(); @@ -424,8 +426,10 @@ test.each(switchFailureCases)( expect(screen.queryByText(pendingText)).toBeNull(); } - expect(screen.getByRole('combobox', { name: comboboxName }).hasAttribute('disabled')).toBe( - false, - ); - }, + expect( + screen + .getByRole('combobox', { name: comboboxName }) + .hasAttribute('disabled') + ).toBe(false); + } ); diff --git a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx index 9529db26d..41dedfd84 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx @@ -4,68 +4,35 @@ import { toaster } from '@techsio/ui-kit/molecules/toast'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Effect, Redacted } from 'effect'; -import type { ComponentProps, ReactNode } from 'react'; import LoginPage from '../../../../src/routes/[lang]/login/page'; -import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; - -type LocalizedLinkDoubleProps = Omit, 'href'> & { - readonly children?: ReactNode; - readonly href?: string | undefined; - readonly params?: Readonly>; - readonly to: string; -}; - -const { languageState, localizedLinkCalls, navigateMock, runBrowserEffectMock, signInMock } = - rstest.hoisted(() => { - const recordedLinkCalls: { - href: string | undefined; - params: Readonly> | undefined; - to: string; - }[] = []; - return { - languageState: { current: 'en' }, - localizedLinkCalls: recordedLinkCalls, - navigateMock: rstest.fn(async () => {}), - runBrowserEffectMock: rstest.fn(), - signInMock: rstest.fn(), - }; - }); - -const localisedUrlPatterns = new Map>>( - Object.entries(ultramodernLocalisedUrls).map( - ([canonicalPattern, localisedPatterns]): readonly [ - string, - Readonly>, - ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], - ), -); - -/** - * Resolves the destination the framework link would produce, using the - * application's own canonical-to-localised route map instead of a hand-written - * expectation, so the page is proven to hand over a language-agnostic target. - */ -const resolveLocalizedHref = ( - to: string, - params: Readonly> | undefined, - language: string, -): string => { - const canonicalPattern = to.replaceAll('$', ':'); - const localisedPattern = - localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; - const segments = localisedPattern - .split('/') - .filter(Boolean) - .map((segment) => - segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, - ); - return `/${[language, ...segments].join('/')}`; -}; +import type { + LocalizedLinkCall, + LocalizedLinkDoubleProps, +} from '../../../support/localized-link-double.tsx'; +import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; + +const { + languageState, + localizedLinkCalls, + navigateMock, + runBrowserEffectMock, + signInMock, +} = rstest.hoisted(() => { + const recordedLinkCalls: LocalizedLinkCall[] = []; + return { + languageState: { current: 'en' }, + localizedLinkCalls: recordedLinkCalls, + navigateMock: rstest.fn(async () => {}), + runBrowserEffectMock: rstest.fn(), + signInMock: rstest.fn(), + }; +}); beforeEach(() => { runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), + async (effect: Effect.Effect) => + await runEffectTestPromise(effect) ); signInMock.mockReturnValue( Effect.succeed({ @@ -75,7 +42,7 @@ beforeEach(() => { principalId: 'principal-1', tenantId: 'tenant-1', }, - }), + }) ); }); @@ -90,18 +57,15 @@ const translations = new Map( 'shell.login.title': 'Login', 'shell.login.toast.description': 'Fill in both required fields.', 'shell.login.toast.title': 'Login details are incomplete', - }), + }) ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ - Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { - localizedLinkCalls.push({ href, params, to }); - return ( - - {children} - - ); - }, + Link: (props: LocalizedLinkDoubleProps) => + renderLocalizedLinkDouble(props, { + calls: localizedLinkCalls, + language: languageState, + }), useLocalizedLocation: () => ({ alternates: { cs: '/cs/login', @@ -128,7 +92,8 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ })); const getLogin = () => screen.getByRole('textbox', { name: 'Login *' }); -const getPassword = () => screen.getByLabelText(/^Password/u, { selector: 'input' }); +const getPassword = () => + screen.getByLabelText(/^Password/u, { selector: 'input' }); const getSubmit = () => screen.getByRole('button', { name: 'Login' }); const renderLogin = () => render(); @@ -157,9 +122,11 @@ test('shows the required login controls through the UI kit', () => { expect(password.getAttribute('autocomplete')).toBe('current-password'); expect(password.hasAttribute('required')).toBe(true); expect(submit.getAttribute('type')).toBe('submit'); - expect(screen.getByRole('link', { name: '← Back to the home page' }).getAttribute('href')).toBe( - '/en', - ); + expect( + screen + .getByRole('link', { name: '← Back to the home page' }) + .getAttribute('href') + ).toBe('/en'); }); test('the back link hands the canonical home target to the framework link', () => { @@ -176,9 +143,11 @@ test('the back link resolves Czech from the same canonical target', () => { renderLogin(); expect(localizedLinkCalls.map((call) => call.to)).toContain('/'); - expect(screen.getByRole('link', { name: '← Back to the home page' }).getAttribute('href')).toBe( - '/cs', - ); + expect( + screen + .getByRole('link', { name: '← Back to the home page' }) + .getAttribute('href') + ).toBe('/cs'); }); interface LoginValidationCase { @@ -258,14 +227,26 @@ test.each(validationCases)( await submitLogin(login, password); const incompleteToasts = loginInvalid || passwordInvalid ? 1 : 0; - expect(getLogin().getAttribute('aria-invalid')).toBe(loginInvalid ? 'true' : null); - expect(getPassword().getAttribute('aria-invalid')).toBe(passwordInvalid ? 'true' : null); - expect(screen.queryAllByText('Enter your login.')).toHaveLength(loginInvalid ? 1 : 0); - expect(screen.queryAllByText('Enter your password.')).toHaveLength(passwordInvalid ? 1 : 0); - expect(screen.queryAllByText('Login details are incomplete')).toHaveLength(incompleteToasts); - expect(screen.queryAllByText('Fill in both required fields.')).toHaveLength(incompleteToasts); + expect(getLogin().getAttribute('aria-invalid')).toBe( + loginInvalid ? 'true' : null + ); + expect(getPassword().getAttribute('aria-invalid')).toBe( + passwordInvalid ? 'true' : null + ); + expect(screen.queryAllByText('Enter your login.')).toHaveLength( + loginInvalid ? 1 : 0 + ); + expect(screen.queryAllByText('Enter your password.')).toHaveLength( + passwordInvalid ? 1 : 0 + ); + expect(screen.queryAllByText('Login details are incomplete')).toHaveLength( + incompleteToasts + ); + expect(screen.queryAllByText('Fill in both required fields.')).toHaveLength( + incompleteToasts + ); expect(document.activeElement).toBe(focusTargets[focus]()); - }, + } ); test('creates one Toast per repeated invalid submission', async () => { @@ -312,7 +293,7 @@ test('submits valid values through the Shell authentication client and navigates email: 'admin', password: Redacted.make('secret'), }, - { locale: 'en' }, + { locale: 'en' } ); expect(runBrowserEffectMock).toHaveBeenCalledTimes(1); expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); diff --git a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx index 0e8759484..66d4ef88c 100644 --- a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx @@ -2,7 +2,6 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; import { cleanup, render, screen } from '@testing-library/react'; import { Effect, Schema } from 'effect'; -import type { ComponentProps, ReactNode } from 'react'; import { AppIdSchema, @@ -16,14 +15,11 @@ import { import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; import type { SearchPageModel } from '../../../../src/routes/[lang]/search/page.data.ts'; import SearchPage from '../../../../src/routes/[lang]/search/page.tsx'; -import { ultramodernLocalisedUrls } from '../../../../src/routes/ultramodern-route-metadata.ts'; - -type LocalizedLinkDoubleProps = Omit, 'href'> & { - readonly children?: ReactNode; - readonly href?: string | undefined; - readonly params?: Readonly>; - readonly to: string; -}; +import type { + LocalizedLinkCall, + LocalizedLinkDoubleProps, +} from '../../../support/localized-link-double.tsx'; +import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; const { languageState, @@ -35,11 +31,7 @@ const { switchTenantMock, useLoaderDataMock, } = rstest.hoisted(() => { - const recordedLinkCalls: { - href: string | undefined; - params: Readonly> | undefined; - to: string; - }[] = []; + const recordedLinkCalls: LocalizedLinkCall[] = []; return { languageState: { current: 'en' }, localizedLinkCalls: recordedLinkCalls, @@ -52,37 +44,6 @@ const { }; }); -const localisedUrlPatterns = new Map>>( - Object.entries(ultramodernLocalisedUrls).map( - ([canonicalPattern, localisedPatterns]): readonly [ - string, - Readonly>, - ] => [canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }], - ), -); - -/** - * Resolves the destination the framework link would produce, using the - * application's own canonical-to-localised route map instead of a hand-written - * expectation, so the page is proven to hand over a language-agnostic target. - */ -const resolveLocalizedHref = ( - to: string, - params: Readonly> | undefined, - language: string, -): string => { - const canonicalPattern = to.replaceAll('$', ':'); - const localisedPattern = - localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; - const segments = localisedPattern - .split('/') - .filter(Boolean) - .map((segment) => - segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment, - ); - return `/${[language, ...segments].join('/')}`; -}; - const translations = new Map( Object.entries({ 'shell.auth.identity.title': 'Authenticated identity', @@ -104,18 +65,15 @@ const translations = new Map( 'shell.search.submit': 'Search', 'shell.search.title': 'Search', 'shell.search.unavailable': 'Search unavailable', - }), + }) ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ - Link: ({ children, href, params, to, ...props }: LocalizedLinkDoubleProps) => { - localizedLinkCalls.push({ href, params, to }); - return ( - - {children} - - ); - }, + Link: (props: LocalizedLinkDoubleProps) => + renderLocalizedLinkDouble(props, { + calls: localizedLinkCalls, + language: languageState, + }), useLocalizedLocation: () => ({ alternates: { cs: '/cs/hledat', en: '/en/search' }, }), @@ -141,17 +99,23 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ })); const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( - '00000000-0000-4000-8000-000000000001', + '00000000-0000-4000-8000-000000000001' +); +const tenantId = Schema.decodeUnknownSync(TenantIdSchema)( + '00000000-0000-4000-8000-000000000101' ); -const tenantId = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101'); const legalEntityId = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000201', + '00000000-0000-4000-8000-000000000201' ); const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); -const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); -const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); +const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)( + 'shell.navigation.modules' +); +const inventoryModuleId = + Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); const plainResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit-1'); -const awkwardResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2'); +const awkwardResourceId = + Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2'); const authenticatedShell = (): HomePageModel => ({ contextState: 'authenticated', @@ -191,7 +155,10 @@ const authenticatedShell = (): HomePageModel => ({ }, }); -const readyModel = (resourceType: string, resourceId: typeof plainResourceId): SearchPageModel => ({ +const readyModel = ( + resourceType: string, + resourceId: typeof plainResourceId +): SearchPageModel => ({ query: 'unit', response: { partial: false, @@ -212,11 +179,16 @@ const resourceLinkCalls = () => beforeEach(() => { runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), + async (effect: Effect.Effect) => + await runEffectTestPromise(effect) ); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); - switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId })); - switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId })); + switchTenantMock.mockReturnValue( + Effect.succeed({ selectedTenantId: tenantId }) + ); + switchLegalEntityMock.mockReturnValue( + Effect.succeed({ selectedLegalEntityId: legalEntityId }) + ); useLoaderDataMock.mockReturnValue(readyModel('stock-item', plainResourceId)); }); @@ -239,23 +211,27 @@ test('a search result hands the canonical resource route to the framework link', resourceType: 'stock-item', }); expect(resultCall?.href).toBeUndefined(); - expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( - '/en/resources/inventory.stock/stock-item/unit-1', - ); + expect( + screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') + ).toBe('/en/resources/inventory.stock/stock-item/unit-1'); }); test('a search result resolves the Czech resource route from the same canonical target', () => { languageState.current = 'cs'; render(); - expect(resourceLinkCalls()[0]?.to).toBe('/resources/$moduleId/$resourceType/$resourceId'); - expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( - '/cs/zdroje/inventory.stock/stock-item/unit-1', + expect(resourceLinkCalls()[0]?.to).toBe( + '/resources/$moduleId/$resourceType/$resourceId' ); + expect( + screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') + ).toBe('/cs/zdroje/inventory.stock/stock-item/unit-1'); }); test('resource path segments stay percent-encoded per segment', () => { - useLoaderDataMock.mockReturnValue(readyModel('stock item', awkwardResourceId)); + useLoaderDataMock.mockReturnValue( + readyModel('stock item', awkwardResourceId) + ); render(); expect(resourceLinkCalls()[0]?.params).toEqual({ @@ -263,9 +239,9 @@ test('resource path segments stay percent-encoded per segment', () => { resourceId: 'unit #1/2', resourceType: 'stock item', }); - expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( - '/en/resources/inventory.stock/stock%20item/unit%20%231%2F2', - ); + expect( + screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') + ).toBe('/en/resources/inventory.stock/stock%20item/unit%20%231%2F2'); }); test('an empty result set exposes no resource affordance', () => { From 035ce581cfbef7b29b1d6b35a26f166cd3c78a1a Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 17:18:38 +0200 Subject: [PATCH 05/11] fix: complete native Effect tooling adoption Preserve the merged strict declaration repair on rc.112, use native reference installation and CLI defaults, and complete Context.Service consumers. Keep profiling through the supported Rsbuild Rsdoctor interface. Full native lint passes across 928 files. Reference, planner, command, search and strict declaration regressions pass; published framework adoption remains pending. Co-Authored-By: Claude Code --- app/apps/shell-super-app/modern.config.ts | 4 - app/package.json | 8 +- .../effect-cli-metadata@4.0.0-rc.112.patch | 20 + app/pnpm-lock.yaml | 644 +++- app/pnpm-workspace.yaml | 1 + .../generate-outbox-worker-deployment.mjs | 5 +- app/scripts/generate-tanstack-routes.mts | 252 +- app/scripts/outbox-worker-delivery.mjs | 21 +- app/scripts/plan-deployment-impact.mts | 476 ++- .../tests/scaffold-generators.test.mts | 2818 ++++++++++----- app/scripts/setup-agent-reference-repos.mts | 845 +++-- app/scripts/tests/api-only-tooling.test.mts | 1681 ++++++--- .../setup-agent-reference-repos.test.mts | 217 ++ .../tests/ultramodern-command.test.mts | 131 +- .../validate-ultramodern-workspace.mts | 3218 +++++++++++------ app/verticals/party-registry/modern.config.ts | 4 - .../party-registry/src/worker-host/layer.ts | 55 +- .../tests/unit/search-identifier-sync.test.ts | 147 +- .../tests/unit/search-projector.test.ts | 157 +- 19 files changed, 7336 insertions(+), 3368 deletions(-) create mode 100644 app/patches/effect-cli-metadata@4.0.0-rc.112.patch create mode 100644 app/scripts/tests/setup-agent-reference-repos.test.mts diff --git a/app/apps/shell-super-app/modern.config.ts b/app/apps/shell-super-app/modern.config.ts index 2a84a3d0c..074de416a 100644 --- a/app/apps/shell-super-app/modern.config.ts +++ b/app/apps/shell-super-app/modern.config.ts @@ -289,10 +289,6 @@ export default defineConfig( cacheDigest: [appId, buildTarget], cacheDirectory: buildCacheDirectory, }, - rsdoctor: { - disableClientServer: true, - enabled: getBuildBoolean('ULTRAMODERN_RSDOCTOR'), - }, }, plugins: [ appTools(), diff --git a/app/package.json b/app/package.json index b9b9cd064..9b877b8cc 100644 --- a/app/package.json +++ b/app/package.json @@ -24,7 +24,7 @@ "local:initialize": "node ./scripts/initialize-local-development.mts", "test:unit": "pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component", "test:integration": "pnpm -r --if-present run test:integration", - "test:scripts": "node --test scripts/tests/boundary-source-structure.test.mts scripts/local-environment-values.test.mts scripts/tests/audit-database-trust-boundaries.test.mts scripts/tests/authorization-rollout-contract.test.mts scripts/tests/check-authorization-readiness.test.mts scripts/tests/database-access-boundaries.test.mts scripts/tests/initialize-local-development.test.mts scripts/tests/locki-feature.test.mts scripts/tests/migrate-contacts-authorization.test.mts scripts/tests/module-entrypoint-boundaries.test.mts scripts/tests/plan-deployment-impact.test.mts scripts/tests/protected-entrypoint-inventory.test.mts scripts/tests/provision-current-action-authorization.test.mts scripts/tests/report-fail-closed-authorization-impact.test.mts scripts/tests/api-only-tooling.test.mts scripts/tests/generated-slot-entries.test.mts scripts/tests/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts scripts/tests/ultramodern-command.test.mts scripts/tests/code-tools-i18n.test.mts scripts/tests/dependency-declarations.test.mts", + "test:scripts": "node --test scripts/tests/boundary-source-structure.test.mts scripts/local-environment-values.test.mts scripts/tests/audit-database-trust-boundaries.test.mts scripts/tests/authorization-rollout-contract.test.mts scripts/tests/check-authorization-readiness.test.mts scripts/tests/database-access-boundaries.test.mts scripts/tests/initialize-local-development.test.mts scripts/tests/locki-feature.test.mts scripts/tests/migrate-contacts-authorization.test.mts scripts/tests/module-entrypoint-boundaries.test.mts scripts/tests/plan-deployment-impact.test.mts scripts/tests/protected-entrypoint-inventory.test.mts scripts/tests/provision-current-action-authorization.test.mts scripts/tests/report-fail-closed-authorization-impact.test.mts scripts/tests/api-only-tooling.test.mts scripts/tests/generated-slot-entries.test.mts scripts/tests/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts scripts/tests/ultramodern-command.test.mts scripts/tests/code-tools-i18n.test.mts scripts/tests/dependency-declarations.test.mts scripts/tests/setup-agent-reference-repos.test.mts", "test:lint-rules": "node --test tools/oxlint/effect-native/tests/*.test.mts", "typecheck:lint-rules": "tsc -p tools/oxlint/effect-native/tsconfig.json", "lint:effect": "node tools/oxlint/effect-native/report.mts", @@ -87,7 +87,8 @@ "quality:audit": "node ./scripts/quality-audit.mts", "quality:audit:gate": "node ./scripts/quality-audit-gate.mts", "quality:check": "pnpm quality:audit && pnpm quality:audit:gate", - "quality:audit:test": "node --test ./scripts/tests/quality-audit.test.mts ./scripts/tests/quality-audit-model.test.mts ./scripts/tests/quality-audit-runtime-model.test.mts ./scripts/tests/quality-audit-gate.test.mts ./scripts/tests/quality-cli-lifecycle.test.mts ./scripts/tests/quality-audit-count-domain.test.mts" + "quality:audit:test": "node --test ./scripts/tests/quality-audit.test.mts ./scripts/tests/quality-audit-model.test.mts ./scripts/tests/quality-audit-runtime-model.test.mts ./scripts/tests/quality-audit-gate.test.mts ./scripts/tests/quality-cli-lifecycle.test.mts ./scripts/tests/quality-audit-count-domain.test.mts", + "build:analyze": "cross-env RSDOCTOR=true pnpm build" }, "dependencies": { "@authzed/authzed-node": "1.6.1", @@ -129,7 +130,8 @@ "fallow": "3.22.0", "@modern-js/ultramodern-create": "npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2", "cross-env": "10.1.0", - "@effect/opentelemetry": "4.0.0-rc.112" + "@effect/opentelemetry": "4.0.0-rc.112", + "@rsdoctor/rspack-plugin": "1.6.3" }, "engines": { "node": ">=26", diff --git a/app/patches/effect-cli-metadata@4.0.0-rc.112.patch b/app/patches/effect-cli-metadata@4.0.0-rc.112.patch new file mode 100644 index 000000000..9908e9f30 --- /dev/null +++ b/app/patches/effect-cli-metadata@4.0.0-rc.112.patch @@ -0,0 +1,20 @@ +diff --git a/dist/unstable/cli/Param.d.ts b/dist/unstable/cli/Param.d.ts +--- a/dist/unstable/cli/Param.d.ts ++++ b/dist/unstable/cli/Param.d.ts +@@ -2310,5 +2310,16 @@ + */ + (self: Param, orElse: LazyArg>): Param>; + }; ++/** ++ * Gets param metadata by traversing the structure. ++ * ++ * @internal ++ */ ++export declare const getParamMetadata: (param: Param) => { ++ readonly isOptional: boolean; ++ readonly isVariadic: boolean; ++ readonly variadicMin: Option.Option; ++ readonly variadicMax: Option.Option; ++}; + export {}; + //# sourceMappingURL=Param.d.ts.map \ No newline at end of file diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 8d93e4a4d..a680e2ed8 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -26,6 +26,7 @@ patchedDependencies: '@module-federation/runtime-core@2.9.0': b241be221397f0e07dbe6c515725e12eaf3b418469bb7dd21750e6e4b215dd8d '@vercel/nft@0.29.2': c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7 drizzle-orm@1.0.0-rc.5-ab785fc: b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe + effect@4.0.0-rc.112: b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada eslint-plugin-perfectionist@5.10.1: 9e69fb6189199155ccf29de79c5127492dcebff0b0a1fdf79bb3cd72704501a7 msgpackr@2.1.0: de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a zod@4.5.4: 30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6 @@ -39,23 +40,23 @@ importers: version: 1.6.1 '@effect/sql-pg': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) better-auth: specifier: 1.7.2 - version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) pg: specifier: 8.22.0 version: 8.22.0 devDependencies: '@effect/opentelemetry': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@effect/platform-node': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/tsgo': specifier: 0.41.0 version: 0.41.0 @@ -67,7 +68,7 @@ importers: version: 2.6.9(supports-color@10.2.2) '@modern-js/plugin-bff': specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 - version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(d452e5f08ab79bb3c831c122a00a8816)' '@modern-js/ultramodern-create': specifier: npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2 version: '@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.2(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))(prettier@3.9.6)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' @@ -80,6 +81,9 @@ importers: '@oxlint/plugins': specifier: 1.81.0 version: 1.81.0 + '@rsdoctor/rspack-plugin': + specifier: 1.6.3 + version: 1.6.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) '@types/node': specifier: ^26.4.1 version: 26.4.1 @@ -97,7 +101,7 @@ importers: version: 10.1.0 effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) esbuild: specifier: 0.28.1 version: 0.28.1 @@ -169,19 +173,19 @@ importers: version: 1.6.1 '@better-auth/api-key': specifier: 1.7.2 - version: 1.7.2(266de75404526095522e68ad80df5da2) + version: 1.7.2(039240e5509f6fa39698d3890ae80b72) '@better-auth/drizzle-adapter': specifier: 1.7.2 - version: 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) + version: 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) '@effect/opentelemetry': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@effect/sql-pg': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@modern-js/plugin-bff': specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 - version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(d452e5f08ab79bb3c831c122a00a8816)' '@modern-js/plugin-i18n': specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2 version: '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -202,13 +206,13 @@ importers: version: 0.25.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) better-auth: specifier: 1.7.2 - version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) i18next: specifier: 26.4.2 version: 26.4.2(typescript@7.0.2) @@ -296,16 +300,16 @@ importers: version: 1.6.1 '@effect/platform-node': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/sql-pg': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) pg: specifier: 8.22.0 version: 8.22.0 @@ -330,7 +334,7 @@ importers: version: link:../shared-contracts effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) jose: specifier: 6.2.5 version: 6.2.5 @@ -346,13 +350,13 @@ importers: version: link:../core-runtime '@effect/opentelemetry': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@modern-js/plugin-bff': specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 - version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)' + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(d452e5f08ab79bb3c831c122a00a8816)' effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) devDependencies: '@types/node': specifier: ^26.4.1 @@ -376,13 +380,13 @@ importers: version: link:../../packages/shared-design-tokens '@effect/opentelemetry': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@effect/sql-pg': specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@modern-js/plugin-bff': specifier: npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2 - version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(10f4d6332468433e4185bc862591bbd4)' + version: '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(bb815c3ad485ef27295f54b2cf2c8961)' '@modern-js/plugin-i18n': specifier: npm:@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2 version: '@bleedingdev/modern-js-plugin-i18n@3.9.0-ultramodern.2(@bleedingdev/modern-js-runtime@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -403,10 +407,10 @@ importers: version: 1.170.33(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + version: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) effect: specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + version: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) i18next: specifier: 26.4.2 version: 26.4.2(typescript@7.0.2) @@ -496,6 +500,10 @@ packages: '@authzed/authzed-node@1.6.1': resolution: {integrity: sha512-Rj3rMtWOjo3igxY/2fpPrIedCTfDq3e+weykuxNBzV/y6azBCoXp8SzpjCEJXVcWyBD8bu/EKY3cye3kOLsKpQ==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -3176,6 +3184,14 @@ packages: '@rsbuild/core': optional: true + '@rsbuild/plugin-check-syntax@1.6.1': + resolution: {integrity: sha512-26xtEYN0QjZYoyt0lWnvIztBWjEZJvcfw7MN4f5B4SpNggmnF7F7aNPrgkY3EccXVFx1VGQBhnCkBV//OoS07Q==} + peerDependencies: + '@rsbuild/core': ^1.0.0 || ^2.0.0-0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@rsbuild/plugin-check-syntax@2.0.1': resolution: {integrity: sha512-z+NMAUXEbM4fhoQlKJNgTjsf+O1tknBihsXj4JnSFlwpfoY5uDjKC6D+StATATvUilSKXFrk4WDhcIyoxkj1gg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3268,6 +3284,40 @@ packages: '@rsbuild/core': optional: true + '@rsdoctor/client@1.6.3': + resolution: {integrity: sha512-Vj1YCR8/amxlgjXn1rHOG56c6BBjEwiVM+qiQDRUBmn26oDxBsPwE0iCdn05ROyspepUXhuyE/4wBdYY7UtbUA==} + + '@rsdoctor/core@1.6.3': + resolution: {integrity: sha512-P96dNevDmlMbHBZ+G9c3G3D2kC/Bx6bHgkTz0DIJcsA/6QKQCwHO5bGlBqVndOlIzNV2b8nONQdaMLUujq5iaw==} + + '@rsdoctor/graph@1.6.3': + resolution: {integrity: sha512-lsW+DGiwSjeBt3sQh9eU9/kd1LjizjE+esrW7nud0cmRTzneHZ9TYM+v/WAaWlk2o7kvS7NEN7NLC/qqwKDZEQ==} + + '@rsdoctor/rspack-plugin@1.6.3': + resolution: {integrity: sha512-pM80ts4BRN+iXSXA0YNXK5G6PvkiH0hytC91Ww8bcoDrdl3gIrx1inhFNmbwArDVNaRRFDFJDr/zyfatTMDr4Q==} + peerDependencies: + '@rspack/core': '*' + peerDependenciesMeta: + '@rspack/core': + optional: true + + '@rsdoctor/sdk@1.6.3': + resolution: {integrity: sha512-UX+j+Tapz4VxT7DV2YLKP8eFXjmxHDxBSMH+WJLu0GaZv/ljHTjk+h/aJF1kOlA3gdXmbvas2vBQzTuEI9rKww==} + + '@rsdoctor/types@1.6.3': + resolution: {integrity: sha512-iCivPceJuCkUtxMswrUsIbKdARGyCnw3e1QEf8nTNWygWmud1nM8kr1Y8ZaqJGbe/g7c1KZebWpN5/hzUbBHPw==} + peerDependencies: + '@rspack/core': '*' + webpack: 5.x + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + '@rsdoctor/utils@1.6.3': + resolution: {integrity: sha512-xuZalAwSE8r/8Ro+N9RxyA+0OXvaSGXOaggmbmF89YGrYhQ4R3h05XvgZKQLtd//w8DxR6znUqb1xKp8a4NYDw==} + '@rspack/binding-darwin-arm64@2.2.0': resolution: {integrity: sha512-KAVVT7hp3NBjtc/RY2UtOjzzc8i+s4pIhW1p52UV+Aev6ywQCu3dXwkHTonpPvJO3hqLXc4zIMH5l4HbMqBm4g==} cpu: [arm64] @@ -3464,6 +3514,63 @@ packages: '@rspack/core': optional: true + '@rspack/resolver-binding-darwin-arm64@0.2.8': + resolution: {integrity: sha512-nTnK17kmxXEvR+WpOIZPSIzUFYeWCHoffgU9tvOLOwuTBH41kWnSQXXWu+AiMVwvJ6wdRO6Vo30hPhlXEG7Pyw==} + cpu: [arm64] + os: [darwin] + + '@rspack/resolver-binding-darwin-x64@0.2.8': + resolution: {integrity: sha512-Aqr4TK2rA6XVYUOmM5YCtYyCMZhOIR53P4cOGgGARg99A7OuMBMzUL4r1n0M0Fx35v6/sSx1OBe+odHmPxksEg==} + cpu: [x64] + os: [darwin] + + '@rspack/resolver-binding-linux-arm64-gnu@0.2.8': + resolution: {integrity: sha512-wGvkxm2G4mNTztslaOzLzx5JuySQSy5DcOWEZxHcjJJzp5L3ODbYLK18HtUc6cvmaVOmjaGrrYPrqJJ0hHTVFg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/resolver-binding-linux-arm64-musl@0.2.8': + resolution: {integrity: sha512-EqRJ9zLQsLAvyDKJKVZ45BSqRIMS12f5HtJdy3KkAHU14ZmsGv8e5IKkwUZN5CNBRad8xVlOMMx3dOfF4whJzg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/resolver-binding-linux-x64-gnu@0.2.8': + resolution: {integrity: sha512-eXbeotNCTntL4/+mxJRVCxK63YeWzTfp0F3POeHJFSs6Nt0f2J/mZNFlasJmd6xm7zvE80h/HWOwbwjRBLcElA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/resolver-binding-linux-x64-musl@0.2.8': + resolution: {integrity: sha512-KWFHlOWGkT+eMngoUgPGXrDi+rU04VCh9jyk0U6Ot2RTWvhGxwKykjmLS+CWZI/EBrzr9A6g2U3jzKTMNz9oCw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/resolver-binding-wasm32-wasi@0.2.8': + resolution: {integrity: sha512-I6GIhgICFViE88jejIV74oiiWHnpLpQ5ogaZM1ozM9KDnfqcHoX0IVEyrIh5KqA8iLDyhuoFSW+Hf0qN7VTBBQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rspack/resolver-binding-win32-arm64-msvc@0.2.8': + resolution: {integrity: sha512-ZXCt3qUfDAEbtc2sHpvxM7lNFZM+DxfblgXUIl3Jy6BuEZbHe1i6z+t9c34ayHoGTVbVSNCtaYuG/MaWdSnPHw==} + cpu: [arm64] + os: [win32] + + '@rspack/resolver-binding-win32-ia32-msvc@0.2.8': + resolution: {integrity: sha512-2LRymjDK8MpUERD8CL0PPae5y2crU5TAg4T4EzpeL5jLARVq6izsEruiWzB6Y+D15vUYlvmgs2370GXVSB861w==} + cpu: [ia32] + os: [win32] + + '@rspack/resolver-binding-win32-x64-msvc@0.2.8': + resolution: {integrity: sha512-hzRpfbtvv4M4EVrKKIAaHDs5wT8lVcbSUjtwPs5u4IeLEix45nQPQ6ZQjmE4lIH0GP/3L3XQhZroYmTcH/xdsQ==} + cpu: [x64] + os: [win32] + + '@rspack/resolver@0.2.8': + resolution: {integrity: sha512-FBWqdHhzS8mcf/WN4Ktzr7EaeaN+hsxbN98EweegX3924beZuY6H70CSFWCv1fIHAieCUv/9XCjKggHvhCsLwA==} + '@rstest/adapter-rsbuild@0.11.12': resolution: {integrity: sha512-0XXUMCSxAYK7zuBltAABSIIAcsCrvCywyRLIkicvJwIlvRGAwhRO5DSIZLo77y5lEYQAZl6uAHwczyZC9lZ1DA==} peerDependencies: @@ -3504,6 +3611,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@speed-highlight/core@1.2.17': resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} @@ -3858,12 +3968,21 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -3912,6 +4031,9 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/tapable@2.3.0': + resolution: {integrity: sha512-oMnbAXeVo+KUnje3hzdORXUbfnzTfqD0H92mLl19NE5hFqH9Q4ktq+xehNSxcNeeLm1COopYwa0zeP6Iz+oIXg==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -4312,6 +4434,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -4328,6 +4454,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -4480,6 +4610,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + baseline-browser-mapping@2.11.19: resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} engines: {node: '>=6.0.0'} @@ -4603,6 +4737,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist-load-config@1.0.3: + resolution: {integrity: sha512-boNaPS4KlW6AITZQ60G+1oDJLuxauljDd7QNQFOYpRtldzcTDknMZ8awbwI0BT/8h1/Y/CG4k/tDOLip9lAGcg==} + browserslist-to-es-version@1.4.2: resolution: {integrity: sha512-3NV13pCv0wmPxxZZcekHAG6vt8rQ94w2c4/UBe3ZU3NDUm5TP+QFK3rjS6XeKWSHWpnPYNfQzlhnljka0BrEOA==} hasBin: true @@ -4801,6 +4938,10 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -4816,6 +4957,10 @@ packages: core-js@3.50.0: resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -4996,6 +5141,10 @@ packages: supports-color: optional: true + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -5263,6 +5412,14 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.10: + resolution: {integrity: sha512-9/lX2bdlizlCXMHRMOIm03VBQHQYC7VvydcxtTAUJRxNW1QzM/2PMFSmr6h/lCiMHcyCP6abK+t9Q+j4vekk8Q==} + engines: {node: '>=10.2.0'} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -5271,6 +5428,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -5279,6 +5440,11 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + envinfo@7.21.0: + resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} + engines: {node: '>=4'} + hasBin: true + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -5328,6 +5494,9 @@ packages: resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -5595,6 +5764,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filesize@11.0.23: + resolution: {integrity: sha512-EzB9Km94xm3V7szupSlcGVJpkvXWuNtSZmr7qBoj229q7lEJEEYGMk8gwGnA4ZTAGQmHZigTDEIuOfCQXec1fQ==} + engines: {node: '>= 10.8.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5699,6 +5872,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -5808,6 +5985,9 @@ packages: engines: {node: ^14.13.1 || >=16.0.0} hasBin: true + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@12.0.0: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} engines: {node: '>=20.19.0'} @@ -6211,6 +6391,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stream-stringify@3.0.1: + resolution: {integrity: sha512-vuxs3G1ocFDiAQ/SX0okcZbtqXwgj1g71qE9+vrjJ2EkjKQlEFDAcUNRxRU8O+GekV4v5cM2qXP0Wyt/EMDBiQ==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -6257,6 +6440,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + lefthook-darwin-arm64@2.1.10: resolution: {integrity: sha512-nw+X8wRNDoUUV6WSteyKBbcLySq+fsmZt5WV/s50ZJpysmsDKJOUMln6SllNfP+60dzUahAO7REco/2633BsLg==} cpu: [arm64] @@ -6487,6 +6673,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} @@ -6785,6 +6975,10 @@ packages: engines: {node: '>= 4.4.x'} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -7010,6 +7204,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -8017,6 +8214,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} + engines: {node: '>= 0.4'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -8054,6 +8255,17 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + socket.io-adapter@2.5.8: + resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} + + socket.io-parser@4.2.7: + resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -8305,6 +8517,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + type-fest@5.9.0: resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==} engines: {node: '>=20'} @@ -8436,6 +8652,10 @@ packages: varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -8643,6 +8863,12 @@ snapshots: '@protobuf-ts/runtime-rpc': 2.11.1 google-protobuf: 4.0.2 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -8790,11 +9016,11 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.4 - '@better-auth/api-key@1.7.2(266de75404526095522e68ad80df5da2)': + '@better-auth/api-key@1.7.2(039240e5509f6fa39698d3890ae80b72)': dependencies: '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 - better-auth: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + better-auth: 1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) better-call: 1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) @@ -8813,12 +9039,12 @@ snapshots: '@cloudflare/workers-types': 5.20260810.1 '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))': + '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))': dependencies: '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) '@better-auth/utils': 0.4.2 optionalDependencies: - drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4)': dependencies: @@ -9104,7 +9330,7 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': + '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': dependencies: '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -9119,8 +9345,8 @@ snapshots: '@opentelemetry/sdk-trace-web': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 optionalDependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) - effect: 4.0.0-rc.112 + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) transitivePeerDependencies: - react - react-dom @@ -9266,10 +9492,10 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': + '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))': dependencies: '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' - '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.2' '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -9278,8 +9504,8 @@ snapshots: '@module-federation/runtime': 2.9.0 esbuild: 0.28.2 optionalDependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) - effect: 4.0.0-rc.112 + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) transitivePeerDependencies: - '@module-federation/runtime-tools' - core-js @@ -9288,12 +9514,12 @@ snapshots: - tsconfig-paths - zod - '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(10f4d6332468433e4185bc862591bbd4)': + '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(bb815c3ad485ef27295f54b2cf2c8961)': dependencies: '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' - '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -9301,9 +9527,9 @@ snapshots: qs: 6.16.0 type-is: 2.1.0 optionalDependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) transitivePeerDependencies: - '@module-federation/runtime-tools' - '@swc/helpers' @@ -9314,12 +9540,12 @@ snapshots: - tsconfig-paths - zod - '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(4a6c18a448029c3c53ff0312aa4451b7)': + '@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.2(d452e5f08ab79bb3c831c122a00a8816)': dependencies: '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' - '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/create-request': '@bleedingdev/modern-js-create-request@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' - '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' + '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.2(@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tsconfig-paths@4.2.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))' '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' '@modern-js/utils': '@bleedingdev/modern-js-utils@3.9.0-ultramodern.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -9327,9 +9553,9 @@ snapshots: qs: 6.16.0 type-is: 2.1.0 optionalDependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.2(@module-federation/runtime-tools@2.9.0)(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(core-js@3.50.0)(csso@5.0.5)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@7.0.2)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))' - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) transitivePeerDependencies: - '@module-federation/runtime-tools' - '@swc/helpers' @@ -9688,10 +9914,10 @@ snapshots: '@drizzle-team/brocli@0.12.0': {} - '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': + '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.222.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) optionalDependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.222.0 @@ -9702,19 +9928,19 @@ snapshots: '@opentelemetry/sdk-trace-node': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-web': 2.11.0(@opentelemetry/api@1.9.1) - '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': + '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1))': + '@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(redis@6.2.1(@opentelemetry/api@1.9.1))': dependencies: - '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) - effect: 4.0.0-rc.112 + '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) mime: 4.1.0 redis: 6.2.1(@opentelemetry/api@1.9.1) undici: 8.10.1 @@ -9722,9 +9948,9 @@ snapshots: - bufferutil - utf-8-validate - '@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112)': + '@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))': dependencies: - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) pg: 8.23.0 pg-connection-string: 2.14.0 pg-cursor: 2.22.0(pg@8.23.0) @@ -11373,6 +11599,16 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': + dependencies: + acorn: 8.17.0 + browserslist-to-es-version: 1.4.2 + htmlparser2: 10.0.0 + picocolors: 1.1.1 + source-map: 0.7.6 + optionalDependencies: + '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rsbuild/plugin-check-syntax@2.0.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))': dependencies: acorn: 8.17.0 @@ -11523,6 +11759,109 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) + '@rsdoctor/client@1.6.3': {} + + '@rsdoctor/core@1.6.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)) + '@rsdoctor/graph': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/sdk': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/types': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/utils': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rspack/resolver': 0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + browserslist-load-config: 1.0.3 + es-toolkit: 1.52.0 + filesize: 11.0.23 + fs-extra: 11.3.6 + semver: 7.8.5 + source-map: 0.7.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@rsbuild/core' + - '@rspack/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + + '@rsdoctor/graph@1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@rsdoctor/types': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/utils': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + es-toolkit: 1.52.0 + path-browserify: 1.0.1 + source-map: 0.7.6 + transitivePeerDependencies: + - '@rspack/core' + - webpack + + '@rsdoctor/rspack-plugin@1.6.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@rsdoctor/core': 1.6.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/graph': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/sdk': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/types': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/utils': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + optionalDependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@rsbuild/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + + '@rsdoctor/sdk@1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(supports-color@10.2.2)(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@rsdoctor/client': 1.6.3 + '@rsdoctor/graph': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/types': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@rsdoctor/utils': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + launch-editor: 2.14.1 + safer-buffer: 2.1.2 + socket.io: 4.8.1(supports-color@10.2.2) + tapable: 2.3.3 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + + '@rsdoctor/types@1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@types/connect': 3.4.38 + '@types/estree': 1.0.5 + '@types/tapable': 2.3.0 + source-map: 0.7.6 + optionalDependencies: + '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + webpack: 5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28) + + '@rsdoctor/utils@1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28))': + dependencies: + '@babel/code-frame': 7.26.2 + '@rsdoctor/types': 1.6.3(@rspack/core@2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.16.2(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@9.0.3(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)) + '@types/estree': 1.0.5 + acorn: 8.17.0 + acorn-import-attributes: 1.9.5(acorn@8.17.0) + acorn-walk: 8.3.5 + deep-eql: 4.1.4 + envinfo: 7.21.0 + fs-extra: 11.3.6 + get-port: 5.1.1 + json-stream-stringify: 3.0.1 + lines-and-columns: 2.0.4 + picocolors: 1.1.1 + rslog: 2.3.0 + strip-ansi: 7.2.0 + transitivePeerDependencies: + - '@rspack/core' + - webpack + '@rspack/binding-darwin-arm64@2.2.0': optional: true @@ -11671,6 +12010,57 @@ snapshots: optionalDependencies: '@rspack/core': 2.2.2(@module-federation/runtime-tools@2.9.0)(@swc/helpers@0.5.23) + '@rspack/resolver-binding-darwin-arm64@0.2.8': + optional: true + + '@rspack/resolver-binding-darwin-x64@0.2.8': + optional: true + + '@rspack/resolver-binding-linux-arm64-gnu@0.2.8': + optional: true + + '@rspack/resolver-binding-linux-arm64-musl@0.2.8': + optional: true + + '@rspack/resolver-binding-linux-x64-gnu@0.2.8': + optional: true + + '@rspack/resolver-binding-linux-x64-musl@0.2.8': + optional: true + + '@rspack/resolver-binding-wasm32-wasi@0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rspack/resolver-binding-win32-arm64-msvc@0.2.8': + optional: true + + '@rspack/resolver-binding-win32-ia32-msvc@0.2.8': + optional: true + + '@rspack/resolver-binding-win32-x64-msvc@0.2.8': + optional: true + + '@rspack/resolver@0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + optionalDependencies: + '@rspack/resolver-binding-darwin-arm64': 0.2.8 + '@rspack/resolver-binding-darwin-x64': 0.2.8 + '@rspack/resolver-binding-linux-arm64-gnu': 0.2.8 + '@rspack/resolver-binding-linux-arm64-musl': 0.2.8 + '@rspack/resolver-binding-linux-x64-gnu': 0.2.8 + '@rspack/resolver-binding-linux-x64-musl': 0.2.8 + '@rspack/resolver-binding-wasm32-wasi': 0.2.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@rspack/resolver-binding-win32-arm64-msvc': 0.2.8 + '@rspack/resolver-binding-win32-ia32-msvc': 0.2.8 + '@rspack/resolver-binding-win32-x64-msvc': 0.2.8 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + '@rstest/adapter-rsbuild@0.11.12(@rsbuild/core@2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0)(happy-dom@20.8.3))': dependencies: '@rsbuild/core': 2.2.3(@module-federation/runtime-tools@2.9.0)(core-js@3.50.0) @@ -11698,6 +12088,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@socket.io/component-emitter@3.1.2': {} + '@speed-highlight/core@1.2.17': {} '@standard-schema/spec@1.1.0': {} @@ -12044,10 +12436,20 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 26.4.1 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 26.4.1 + '@types/deep-eql@4.0.2': {} '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.5': {} + '@types/estree@1.0.9': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -12100,6 +12502,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/tapable@2.3.0': + dependencies: + tapable: 2.3.3 + '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': @@ -12702,6 +13108,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-import-attributes@1.9.5(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -12714,6 +13125,10 @@ snapshots: dependencies: acorn: 8.17.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.17.0 + acorn@8.17.0: {} adm-zip@0.6.0: {} @@ -12886,14 +13301,16 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + baseline-browser-mapping@2.11.19: {} baseline-browser-mapping@2.11.21: {} - better-auth@1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + better-auth@1.7.2(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(drizzle-kit@1.0.0-rc.5-ab785fc)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2) - '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) + '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6))) '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.4) '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747))(@cloudflare/workers-types@5.20260810.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)))(jose@6.2.5)(kysely@0.29.4)(nanostores@1.4.2))(@better-auth/utils@0.4.2) @@ -12911,7 +13328,7 @@ snapshots: zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) optionalDependencies: drizzle-kit: 1.0.0-rc.5-ab785fc - drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) + drizzle-orm: 1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)) pg: 8.22.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -12972,6 +13389,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist-load-config@1.0.3: {} + browserslist-to-es-version@1.4.2: dependencies: browserslist: 4.28.9 @@ -13169,6 +13588,8 @@ snapshots: cookie-es@3.1.1: {} + cookie@0.7.2: {} + cookie@1.1.1: {} cookie@2.0.1: {} @@ -13179,6 +13600,11 @@ snapshots: core-js@3.50.0: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@8.3.6(typescript@7.0.2): dependencies: import-fresh: 3.3.1 @@ -13411,6 +13837,10 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -13501,15 +13931,15 @@ snapshots: get-tsconfig: 4.14.3 jiti: 2.7.0 - drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112)(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)): + drizzle-orm@1.0.0-rc.5-ab785fc(patch_hash=b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe)(@cloudflare/workers-types@5.20260810.1)(@effect/sql-pg@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)))(@opentelemetry/api@1.9.1)(@sinclair/typebox@0.34.52)(@types/pg@8.20.0)(bun-types@1.4.0)(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada))(pg@8.22.0)(zod@4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6)): optionalDependencies: '@cloudflare/workers-types': 5.20260810.1 - '@effect/sql-pg': 4.0.0-rc.112(effect@4.0.0-rc.112) + '@effect/sql-pg': 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada)) '@opentelemetry/api': 1.9.1 '@sinclair/typebox': 0.34.52 '@types/pg': 8.20.0 bun-types: 1.4.0 - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada) pg: 8.22.0 zod: 4.5.4(patch_hash=30612645af6a21cba7258bc2be3b51d9bfc0841b78dc581a56a4283c3bb142b6) @@ -13521,7 +13951,7 @@ snapshots: eastasianwidth@0.2.0: {} - effect@4.0.0-rc.112: + effect@4.0.0-rc.112(patch_hash=b7489e5109ec4daf5d6c7ac30956ed95c312f4077cbc973b8c82f0f340108ada): dependencies: fast-check: 4.9.0 msgpackr: 2.1.0(patch_hash=de5c91fa6cfd894d171ed06673af40046ba97c7eb604409caf5f510e1a8a5b7a) @@ -13542,6 +13972,24 @@ snapshots: dependencies: iconv-lite: 0.6.3 + engine.io-parser@5.2.3: {} + + engine.io@6.6.10(supports-color@10.2.2): + dependencies: + '@types/cors': 2.8.19 + '@types/node': 26.4.1 + '@types/ws': 8.18.1 + accepts: 1.3.8 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3(supports-color@10.2.2) + engine.io-parser: 5.2.3 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -13549,10 +13997,14 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + entities@7.0.1: {} entities@8.0.0: {} + envinfo@7.21.0: {} + environment@1.1.0: {} errno@0.1.8: @@ -13660,6 +14112,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.52.0: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -14055,6 +14509,8 @@ snapshots: file-uri-to-path@1.0.0: {} + filesize@11.0.23: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -14168,6 +14624,8 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 + get-port@5.1.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -14290,6 +14748,13 @@ snapshots: relateurl: 0.2.7 terser: 5.49.0 + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.1 + htmlparser2@12.0.0: dependencies: domelementtype: 3.0.0 @@ -14647,6 +15112,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-stream-stringify@3.0.1: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -14698,6 +15165,11 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + launch-editor@2.14.1: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.10.0 + lefthook-darwin-arm64@2.1.10: optional: true @@ -14874,6 +15346,8 @@ snapshots: lines-and-columns@1.2.4: {} + lines-and-columns@2.0.4: {} + loader-runner@4.3.2: {} loader-utils@2.0.4: @@ -15166,6 +15640,8 @@ snapshots: sax: 1.6.1 optional: true + negotiator@0.6.3: {} + neo-async@2.6.2: {} no-case@3.0.4: @@ -15502,6 +15978,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-browserify@1.0.1: {} + path-exists@3.0.0: {} path-is-absolute@1.0.1: {} @@ -16477,6 +16955,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.10.0: {} + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -16523,6 +17003,36 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + socket.io-adapter@2.5.8(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.7(supports-color@10.2.2): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1(supports-color@10.2.2): + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.3.7(supports-color@10.2.2) + engine.io: 6.6.10(supports-color@10.2.2) + socket.io-adapter: 2.5.8(supports-color@10.2.2) + socket.io-parser: 4.2.7(supports-color@10.2.2) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -16781,6 +17291,8 @@ snapshots: tslib@2.8.1: {} + type-detect@4.1.0: {} + type-fest@5.9.0: dependencies: tagged-tag: 1.0.0 @@ -16942,6 +17454,8 @@ snapshots: varint@6.0.0: {} + vary@1.1.2: {} + walk-up-path@4.0.0: {} watchpack@2.5.2: diff --git a/app/pnpm-workspace.yaml b/app/pnpm-workspace.yaml index 6425b4bd7..061cf069b 100644 --- a/app/pnpm-workspace.yaml +++ b/app/pnpm-workspace.yaml @@ -81,6 +81,7 @@ allowBuilds: sharp: true workerd: true patchedDependencies: + 'effect@4.0.0-rc.112': patches/effect-cli-metadata@4.0.0-rc.112.patch 'eslint-plugin-perfectionist@5.10.1': patches/eslint-plugin-perfectionist@5.10.1.patch '@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch '@better-fetch/fetch@1.3.1': patches/@better-fetch__fetch@1.3.1.patch diff --git a/app/scripts/generate-outbox-worker-deployment.mjs b/app/scripts/generate-outbox-worker-deployment.mjs index 12fb9d605..e63e667c2 100644 --- a/app/scripts/generate-outbox-worker-deployment.mjs +++ b/app/scripts/generate-outbox-worker-deployment.mjs @@ -155,9 +155,6 @@ const command = Command.make( runCommand ); -/** @type {ImportMeta & { main?: boolean }} */ -const moduleMetadata = import.meta; - -if (moduleMetadata.main === true) { +if (import.meta.main) { void nodeRuntime.runPromise(Command.run(command, { version: '1.0.0' })); } diff --git a/app/scripts/generate-tanstack-routes.mts b/app/scripts/generate-tanstack-routes.mts index d49b55990..979e965bf 100644 --- a/app/scripts/generate-tanstack-routes.mts +++ b/app/scripts/generate-tanstack-routes.mts @@ -12,10 +12,16 @@ import { Schema, } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { launchUltramodern, resolveUltramodernInvocation } from './shared/ultramodern-command.mts'; + import { ModuleEntrypointSchema } from '../packages/core-runtime/src/modules/module-entrypoint.ts'; +import { + launchUltramodern, + resolveUltramodernInvocation, +} from './shared/ultramodern-command.mts'; -const RouteMetadataIdentifierSchema = Schema.String.pipe(Schema.brand('RouteMetadataIdentifier')); +const RouteMetadataIdentifierSchema = Schema.String.pipe( + Schema.brand('RouteMetadataIdentifier') +); const JsonPrimitiveSchema = Schema.Union([ Schema.Null, Schema.Number, @@ -23,7 +29,10 @@ const JsonPrimitiveSchema = Schema.Union([ Schema.String, ]); const RouteMetadataValueSchema = Schema.Tree(JsonPrimitiveSchema); -const RouteMetadataFieldsSchema = Schema.Record(Schema.String, RouteMetadataValueSchema); +const RouteMetadataFieldsSchema = Schema.Record( + Schema.String, + RouteMetadataValueSchema +); const RouteEntrypointSchema = Schema.StructWithRest(ModuleEntrypointSchema, [ RouteMetadataFieldsSchema, @@ -43,7 +52,7 @@ const RouteMetadataSchema = Schema.StructWithRest( public: Schema.Boolean, titleKey: RouteMetadataIdentifierSchema, }), - [RouteMetadataFieldsSchema], + [RouteMetadataFieldsSchema] ); type RouteMetadata = typeof RouteMetadataSchema.Type; @@ -60,10 +69,10 @@ const UltramodernConfigSchema = Schema.Struct({ Schema.Struct({ id: Schema.String, path: Schema.String, - }), - ), + }) + ) ), - }), + }) ), }); @@ -73,104 +82,144 @@ const PackageConfigSchema = Schema.Struct({ ontosModule: Schema.optionalKey( Schema.Struct({ moduleId: Schema.optionalKey(RouteMetadataIdentifierSchema), - }), + }) ), - }), + }) ), }); class RouteGenerationError extends Schema.TaggedError()( 'RouteGenerationError', - { reason: Schema.String }, + { reason: Schema.String } ) {} -const failure = (reason: string): RouteGenerationError => new RouteGenerationError({ reason }); +const failure = (reason: string): RouteGenerationError => + new RouteGenerationError({ reason }); const decodeUltramodernConfig = Schema.decodeUnknownEffect( - Schema.fromJsonString(UltramodernConfigSchema), + Schema.fromJsonString(UltramodernConfigSchema) +); +const decodePackageConfig = Schema.decodeUnknownEffect( + Schema.fromJsonString(PackageConfigSchema) +); +const encodeJsonString = Schema.encodeEffect( + Schema.fromJsonString(Schema.String) ); -const decodePackageConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(PackageConfigSchema)); -const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); const encodeJson = Schema.encodeEffect( - Schema.fromJsonString(RouteMetadataValueSchema, { space: 2 }), + Schema.fromJsonString(RouteMetadataValueSchema, { space: 2 }) ); const isJsonArray = Schema.is(Schema.Array(RouteMetadataValueSchema)); const isJsonObject = Schema.is(RouteMetadataFieldsSchema); const sortJsonValue = ( - value: typeof RouteMetadataValueSchema.Type, + value: typeof RouteMetadataValueSchema.Type ): typeof RouteMetadataValueSchema.Type => { if (isJsonArray(value)) { return value.map(sortJsonValue); } if (isJsonObject(value)) { - const sortedEntries = EffectArray.sortWith(Object.entries(value), ([key]) => key, Order.String); - return Object.fromEntries(sortedEntries.map(([key, entry]) => [key, sortJsonValue(entry)])); + const sortedEntries = EffectArray.sortWith( + Object.entries(value), + ([key]) => key, + Order.String + ); + return Object.fromEntries( + sortedEntries.map(([key, entry]) => [key, sortJsonValue(entry)]) + ); } return value; }; const findRouteMetadataFiles = ( - directory: string, -): Effect.Effect => + directory: string +): Effect.Effect< + string[], + RouteGenerationError, + FileSystem.FileSystem | Path.Path +> => Effect.gen(function* findRouteMetadataFilesEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const entries = yield* fileSystem.readDirectory(directory, { recursive: true }); + const entries = yield* fileSystem.readDirectory(directory, { + recursive: true, + }); const routeFiles = entries .filter((entry) => path.basename(entry) === 'route.meta.ts') .map((entry) => path.resolve(directory, entry)); return EffectArray.sort(routeFiles, Order.String); - }).pipe(Effect.mapError(() => failure(`Unable to discover route metadata beneath ${directory}`))); + }).pipe( + Effect.mapError(() => + failure(`Unable to discover route metadata beneath ${directory}`) + ) + ); const isGovernedPageEntrypoint = ( route: RouteMetadata, appId: string, moduleId: string, - expectedScope: 'system' | 'tenant', + expectedScope: 'system' | 'tenant' ): boolean => route.ownerAppId === appId && route.entrypoint.moduleKey === moduleId && route.entrypoint.role === 'page' && - (route.entrypoint.access === 'read' || route.entrypoint.access === 'historical_read') && + (route.entrypoint.access === 'read' || + route.entrypoint.access === 'historical_read') && route.entrypoint.scope === expectedScope && route.entrypoint.entrypointKey.startsWith(`${moduleId}.`); const loadRouteMetadataFile = ( metadataFile: string, appId: string, - moduleId: string, + moduleId: string ): Effect.Effect => Effect.gen(function* loadRouteMetadataFileEffect() { const path = yield* Path.Path; const moduleFileUrl = yield* path .toFileUrl(metadataFile) - .pipe(Effect.mapError(() => failure(`Unable to resolve ${metadataFile}`))); + .pipe( + Effect.mapError(() => failure(`Unable to resolve ${metadataFile}`)) + ); const cacheNonce = yield* Random.nextInt; const moduleUrl = `${moduleFileUrl.href}?generated=${cacheNonce}`; - const routeModule = yield* Effect.tryPromise({ - catch: () => failure(`Unable to import route metadata from ${metadataFile}`), - try: async () => - await Schema.decodeUnknownPromise(RouteMetadataModuleSchema)(await import(moduleUrl)), + const decodedModule = yield* Effect.tryPromise({ + catch: () => + failure(`Unable to import route metadata from ${metadataFile}`), + try: async () => { + const importedModule: unknown = await import(moduleUrl); + return Schema.decodeUnknownResult(RouteMetadataModuleSchema)( + importedModule + ); + }, }); + const routeModule = yield* Effect.fromResult(decodedModule).pipe( + Effect.mapError(() => + failure(`Invalid route metadata in ${metadataFile}`) + ) + ); const route = routeModule.routeMeta ?? routeModule.default; if (route === undefined) { return yield* Effect.fail( - failure(`${metadataFile} must export routeMeta or a default route metadata object`), + failure( + `${metadataFile} must export routeMeta or a default route metadata object` + ) ); } const expectedScope = appId.startsWith('shell-') ? 'system' : 'tenant'; if (!isGovernedPageEntrypoint(route, appId, moduleId, expectedScope)) { return yield* Effect.fail( failure( - `${metadataFile} must declare one governed ${expectedScope} page entrypoint owned by ${appId}`, - ), + `${metadataFile} must declare one governed ${expectedScope} page entrypoint owned by ${appId}` + ) ); } return route; }); -const loadRouteMetadata = (appDirectory: string, appId: string, moduleId: string) => +const loadRouteMetadata = ( + appDirectory: string, + appId: string, + moduleId: string +) => Effect.gen(function* loadRouteMetadataEffect() { const path = yield* Path.Path; const routeDirectory = path.join(appDirectory, 'src/routes'); @@ -178,13 +227,17 @@ const loadRouteMetadata = (appDirectory: string, appId: string, moduleId: string const routes = yield* Effect.forEach( metadataFiles, (metadataFile) => loadRouteMetadataFile(metadataFile, appId, moduleId), - { concurrency: 'unbounded' }, + { concurrency: 'unbounded' } + ); + return EffectArray.sortWith( + routes, + (route) => route.canonicalPath, + Order.String ); - return EffectArray.sortWith(routes, (route) => route.canonicalPath, Order.String); }); const createLocalisedUrls = ( - routes: readonly RouteMetadata[], + routes: readonly RouteMetadata[] ): Readonly>>> => Object.fromEntries( routes.flatMap((route) => { @@ -192,17 +245,19 @@ const createLocalisedUrls = ( return []; } return [[route.canonicalPath, route.localisedPaths]]; - }), + }) ); const runCommand = ( executable: string, args: readonly string[], - options: ChildProcess.CommandOptions, + options: ChildProcess.CommandOptions ) => Effect.gen(function* runCommandEffect() { const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const exitCode = yield* processSpawner.exitCode(ChildProcess.make(executable, args, options)); + const exitCode = yield* processSpawner.exitCode( + ChildProcess.make(executable, args, options) + ); return Number(exitCode); }); @@ -210,7 +265,7 @@ const generateRouteMetadataManifest = ( appDirectory: string, appId: string, moduleId: string, - workspaceRoot: string, + workspaceRoot: string ) => Effect.gen(function* generateRouteMetadataManifestEffect() { const fileSystem = yield* FileSystem.FileSystem; @@ -222,13 +277,21 @@ const generateRouteMetadataManifest = ( } const localisedUrls = createLocalisedUrls(routes); const encodedNamespace = yield* encodeJsonString(namespace).pipe( - Effect.mapError(() => failure(`Unable to encode the route namespace for ${appId}`)), + Effect.mapError(() => + failure(`Unable to encode the route namespace for ${appId}`) + ) ); const encodedRoutes = yield* encodeJson(sortJsonValue(routes)).pipe( - Effect.mapError(() => failure(`Unable to encode route metadata for ${appId}`)), + Effect.mapError(() => + failure(`Unable to encode route metadata for ${appId}`) + ) ); - const encodedLocalisedUrls = yield* encodeJson(sortJsonValue(localisedUrls)).pipe( - Effect.mapError(() => failure(`Unable to encode localised URLs for ${appId}`)), + const encodedLocalisedUrls = yield* encodeJson( + sortJsonValue(localisedUrls) + ).pipe( + Effect.mapError(() => + failure(`Unable to encode localised URLs for ${appId}`) + ) ); const content = `// @generated by @modern-js/ultramodern-create. // Author route metadata in colocated src/routes/**/route.meta.ts files. @@ -241,22 +304,33 @@ export const ultramodernRouteMetadata = ${encodedRoutes} as const; export const ultramodernLocalisedUrls = ${encodedLocalisedUrls} as const; `; - const manifestPath = path.join(appDirectory, 'src/routes/ultramodern-route-metadata.ts'); + const manifestPath = path.join( + appDirectory, + 'src/routes/ultramodern-route-metadata.ts' + ); yield* fileSystem .writeFileString(manifestPath, content) .pipe(Effect.mapError(() => failure(`Unable to write ${manifestPath}`))); - const formatStatus = yield* runCommand('pnpm', ['exec', 'oxfmt', manifestPath], { - cwd: workspaceRoot, - shell: path.sep === '\\', - stderr: 'inherit', - stdin: 'inherit', - stdout: 'inherit', - }).pipe(Effect.mapError(() => failure(`Unable to launch the formatter for ${manifestPath}`))); + const formatStatus = yield* runCommand( + 'pnpm', + ['exec', 'oxfmt', manifestPath], + { + cwd: workspaceRoot, + shell: path.sep === '\\', + stderr: 'inherit', + stdin: 'inherit', + stdout: 'inherit', + } + ).pipe( + Effect.mapError(() => + failure(`Unable to launch the formatter for ${manifestPath}`) + ) + ); if (formatStatus !== 0) { yield* Effect.fail( failure( - `Failed to format generated route metadata at ${manifestPath}: exit ${formatStatus}`, - ), + `Failed to format generated route metadata at ${manifestPath}: exit ${formatStatus}` + ) ); } }); @@ -272,53 +346,77 @@ const program = Effect.gen(function* generateTanstackRoutesEffect() { moduleUrl: import.meta.url, }); const { forwardedArgs, workspaceRoot } = invocation; - const ultramodernConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json'); + const ultramodernConfigPath = path.join( + workspaceRoot, + '.modernjs/ultramodern.json' + ); const ultramodernConfigText = yield* fileSystem .readFileString(ultramodernConfigPath) - .pipe(Effect.mapError(() => failure(`Unable to read ${ultramodernConfigPath}`))); - const ultramodernConfig = yield* decodeUltramodernConfig(ultramodernConfigText).pipe( - Effect.mapError(() => failure(`${ultramodernConfigPath} is invalid`)), - ); + .pipe( + Effect.mapError(() => failure(`Unable to read ${ultramodernConfigPath}`)) + ); + const ultramodernConfig = yield* decodeUltramodernConfig( + ultramodernConfigText + ).pipe(Effect.mapError(() => failure(`${ultramodernConfigPath} is invalid`))); const appFlagIndex = forwardedArgs.indexOf('--app'); - const selectedAppId = appFlagIndex === -1 ? undefined : forwardedArgs[appFlagIndex + 1]; + const selectedAppId = + appFlagIndex === -1 ? undefined : forwardedArgs[appFlagIndex + 1]; const selectedApps = (ultramodernConfig.topology?.apps ?? []).filter( - (app) => selectedAppId === undefined || selectedAppId === app.id, + (app) => selectedAppId === undefined || selectedAppId === app.id ); yield* Effect.forEach( selectedApps, (app) => Effect.gen(function* generateAppRouteMetadataEffect() { - const packageConfigPath = path.join(workspaceRoot, app.path, 'package.json'); + const packageConfigPath = path.join( + workspaceRoot, + app.path, + 'package.json' + ); const packageConfigText = yield* fileSystem .readFileString(packageConfigPath) - .pipe(Effect.mapError(() => failure(`Unable to read ${packageConfigPath}`))); - const packageConfig = yield* decodePackageConfig(packageConfigText).pipe( - Effect.mapError(() => failure(`${packageConfigPath} is invalid`)), + .pipe( + Effect.mapError(() => + failure(`Unable to read ${packageConfigPath}`) + ) + ); + const packageConfig = yield* decodePackageConfig( + packageConfigText + ).pipe( + Effect.mapError(() => failure(`${packageConfigPath} is invalid`)) ); - const moduleId = packageConfig.modernjs?.ontosModule?.moduleId ?? app.id; + const moduleId = + packageConfig.modernjs?.ontosModule?.moduleId ?? app.id; yield* generateRouteMetadataManifest( path.join(workspaceRoot, app.path), app.id, moduleId, - workspaceRoot, + workspaceRoot + ); + yield* Console.log( + `[ultramodern] Route metadata manifest generated: ${app.id}` ); - yield* Console.log(`[ultramodern] Route metadata manifest generated: ${app.id}`); }), - { concurrency: 1, discard: true }, + { concurrency: 1, discard: true } ); const generationStatus = yield* launchUltramodern(invocation); if (generationStatus !== 0) { - return yield* Effect.fail( - failure(`Framework route-artifact generation failed: exit ${generationStatus}`), + yield* Effect.fail( + failure( + `Framework route-artifact generation failed: exit ${generationStatus}` + ) ); } }); -const reportFailure = (error: RouteGenerationError) => Console.error(error.reason); -const MainLayer = Layer.effectDiscard(program.pipe(Effect.tapError(reportFailure))).pipe( - Layer.provide(NodeServices.layer), -); +const reportFailure = (error: RouteGenerationError) => + Console.error(error.reason); +const MainLayer = Layer.effectDiscard( + program.pipe(Effect.tapError(reportFailure)) +).pipe(Layer.provide(NodeServices.layer)); -NodeRuntime.runMain(Effect.scoped(Layer.build(MainLayer)), { disableErrorReporting: true }); +NodeRuntime.runMain(Effect.scoped(Layer.build(MainLayer)), { + disableErrorReporting: true, +}); diff --git a/app/scripts/outbox-worker-delivery.mjs b/app/scripts/outbox-worker-delivery.mjs index 7eb5644da..55f9d2895 100644 --- a/app/scripts/outbox-worker-delivery.mjs +++ b/app/scripts/outbox-worker-delivery.mjs @@ -8,7 +8,7 @@ const OwnerPackageSchema = Schema.Struct({ scripts: Schema.optional( Schema.Struct({ 'worker:start': Schema.optional(Schema.String), - }), + }) ), }); @@ -16,14 +16,17 @@ const OwnerPackageSchema = Schema.Struct({ /** @typedef {{ readonly _tag: 'OutboxWorkerDeliveryInvalid', readonly reason: string }} OutboxWorkerDeliveryInvalidValue */ /** @typedef {{ readonly entry: string, readonly id: string, readonly ownerId: string, readonly packageName: string, readonly path: string, readonly serviceIdEnv: string, readonly stageSetup: string }} OutboxWorkerDelivery */ -class OutboxWorkerDeliveryInvalid extends Schema.TaggedError()('OutboxWorkerDeliveryInvalid', { - reason: Schema.String, -}) {} +class OutboxWorkerDeliveryInvalid extends Schema.TaggedError()( + 'OutboxWorkerDeliveryInvalid', + { + reason: Schema.String, + } +) {} /** * A generated worker host is the deployment capability; topology owns its identity. * - * @type {(root: string, vertical: OutboxWorkerVertical) => Effect.Effect} + * @type {(root: string, vertical: OutboxWorkerVertical) => Effect.Effect} */ export const outboxWorkerDelivery = Effect.fn('outboxWorkerDelivery')( /** @@ -40,14 +43,16 @@ export const outboxWorkerDelivery = Effect.fn('outboxWorkerDelivery')( } const ownerPackage = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(OwnerPackageSchema), + Schema.fromJsonString(OwnerPackageSchema) )(yield* fileSystem.readFileString(packagePath)); const workerStart = ownerPackage.scripts?.['worker:start']; if (workerStart === undefined || workerStart.length === 0) { return yield* Effect.undefined; } - const host = yield* fileSystem.readFileString(path.join(root, vertical.path, workerEntry)); + const host = yield* fileSystem.readFileString( + path.join(root, vertical.path, workerEntry) + ); if ( ownerPackage.name !== vertical.package || workerStart !== workerStartCommand || @@ -68,5 +73,5 @@ export const outboxWorkerDelivery = Effect.fn('outboxWorkerDelivery')( serviceIdEnv: `ZEROPS_${vertical.id.replaceAll('-', '_').toUpperCase()}_WORKER_SERVICE_ID`, stageSetup: `${vertical.id}-worker`, }; - }, + } ); diff --git a/app/scripts/plan-deployment-impact.mts b/app/scripts/plan-deployment-impact.mts index 15baef1f0..b8ac230c4 100644 --- a/app/scripts/plan-deployment-impact.mts +++ b/app/scripts/plan-deployment-impact.mts @@ -28,13 +28,11 @@ import { hashAuthorizationEvidence } from './check-authorization-readiness.mts'; import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; import type { AuthorizationImpactReport } from './report-fail-closed-authorization-impact.mts'; -declare global { - interface ImportMeta { - readonly main?: boolean; - } -} - -export const DeploymentPhaseKindSchema = Schema.Literals(['infrastructure', 'provider', 'shell']); +export const DeploymentPhaseKindSchema = Schema.Literals([ + 'infrastructure', + 'provider', + 'shell', +]); export type DeploymentPhaseKind = typeof DeploymentPhaseKindSchema.Type; interface TopologyUnit { @@ -67,7 +65,7 @@ const ReferenceTopologySchema = Schema.Struct({ id: Schema.optional(Schema.String), package: Schema.optional(Schema.String), verticalRefs: Schema.optional(Schema.Array(Schema.String)), - }), + }) ), verticals: Schema.optional( Schema.Array( @@ -76,15 +74,17 @@ const ReferenceTopologySchema = Schema.Struct({ moduleFederation: Schema.optional( Schema.Struct({ remotes: Schema.optional( - Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) })), + Schema.Array( + Schema.Struct({ id: Schema.optional(Schema.String) }) + ) ), verticalRefs: Schema.optional(Schema.Array(Schema.String)), - }), + }) ), package: Schema.optional(Schema.String), path: Schema.optional(Schema.String), - }), - ), + }) + ) ), }); @@ -96,7 +96,11 @@ const OwnershipSchema = Schema.Struct({ type Ownership = typeof OwnershipSchema.Type; -const AuthorizationEnvironmentSchema = Schema.Literals(['development', 'production', 'stage']); +const AuthorizationEnvironmentSchema = Schema.Literals([ + 'development', + 'production', + 'stage', +]); const AuthorizationModeSchema = Schema.Literals(['enforced', 'report_only']); const AuthorizationCredentialSchema = Schema.Literals(['api_key', 'session']); const AuthorizationSurfaceSchema = Schema.Literals([ @@ -112,10 +116,10 @@ const CanonicalTimestampStringSchema = Schema.String.check( return Option.isSome(parsed) && DateTime.formatIso(parsed.value) === value ? undefined : 'timestamp must use canonical UTC ISO 8601 encoding'; - }), + }) ); const CanonicalTimestampCodec = CanonicalTimestampStringSchema.pipe( - Schema.decodeTo(Schema.DateTimeUtcFromString), + Schema.decodeTo(Schema.DateTimeUtcFromString) ); const CanonicalTimestampWireSchema = Schema.toEncoded(CanonicalTimestampCodec); @@ -187,7 +191,7 @@ const ProtectedEntrypointInventorySchema = Schema.Struct({ entrypointKey: EntrypointKeySchema, owner: Schema.String, surface: AuthorizationSurfaceSchema, - }), + }) ), inventoryHash: Schema.String, schemaVersion: Schema.Literal(1), @@ -229,7 +233,7 @@ const AuthorizationImpactReportSchema = Schema.Struct({ 'public', ]), surface: AuthorizationSurfaceSchema, - }), + }) ), inventoryHash: Schema.String, observation: Schema.Struct({ @@ -249,7 +253,7 @@ const AuthorizationNegativeSmokeEvidenceSchema = Schema.Struct({ credential: AuthorizationCredentialSchema, outcome: Schema.Literal('denied'), scenario: Schema.String, - }), + }) ), schemaVersion: Schema.Literal(1), sourceRevision: Schema.String, @@ -290,7 +294,7 @@ const DeploymentImpactPlanSchema = Schema.Struct({ environment: AuthorizationEnvironmentSchema, mode: AuthorizationModeSchema, status: Schema.Literals(['observing', 'ready']), - }), + }) ), changedPaths: Schema.Array(Schema.String), comparison: Schema.Struct({ @@ -313,7 +317,7 @@ class DeploymentImpactPlanningError extends Schema.TaggedError @@ -321,24 +325,36 @@ const fail = (message: string): never => Result.fail( new DeploymentImpactPlanningError({ message: `Deployment impact planning failed: ${message}`, - }), - ), + }) + ) ); const requireAuthorizationEvidence = ( - input: AuthorizationPromotionGateInput, -): Required> => { + input: AuthorizationPromotionGateInput +): Required< + Pick< + AuthorizationPromotionGateInput, + 'impact' | 'negativeSmoke' | 'readiness' + > +> => { const { impact, negativeSmoke, readiness } = input; - if (impact === undefined || negativeSmoke === undefined || readiness === undefined) { + if ( + impact === undefined || + negativeSmoke === undefined || + readiness === undefined + ) { return fail( - 'enforced authorization promotion requires impact, readiness, and negative-smoke evidence', + 'enforced authorization promotion requires impact, readiness, and negative-smoke evidence' ); } return { impact, negativeSmoke, readiness }; }; type PromotionEvidence = Required< - Pick + Pick< + AuthorizationPromotionGateInput, + 'impact' | 'negativeSmoke' | 'readiness' + > >; const evidenceHasInventoryIdentity = ( @@ -347,7 +363,7 @@ const evidenceHasInventoryIdentity = ( readonly inventoryHash: string; readonly schemaVersion: number; readonly sourceRevision: string; - }, + } ): boolean => evidence.schemaVersion === 1 && evidence.sourceRevision === inventory.sourceRevision && @@ -355,7 +371,7 @@ const evidenceHasInventoryIdentity = ( const readinessMatchesPromotion = ( input: AuthorizationPromotionGateInput, - evidence: PromotionEvidence, + evidence: PromotionEvidence ): boolean => { const { impact, negativeSmoke, readiness } = evidence; return ( @@ -369,27 +385,31 @@ const readinessMatchesPromotion = ( const authorizationEvidenceMatches = ( input: AuthorizationPromotionGateInput, - evidence: PromotionEvidence, + evidence: PromotionEvidence ): boolean => [evidence.impact, evidence.negativeSmoke, evidence.readiness].every((item) => - evidenceHasInventoryIdentity(input.inventory, item), + evidenceHasInventoryIdentity(input.inventory, item) ) && evidence.impact.totalWouldDeny === 0 && evidence.negativeSmoke.environment === input.environment && readinessMatchesPromotion(input, evidence); export const validateAuthorizationPromotionGate = ( - input: AuthorizationPromotionGateInput, + input: AuthorizationPromotionGateInput ): NonNullable => { const { inventory, rollout } = input; validateAuthorizationRolloutContract(rollout, { - entrypointKeys: new Set(inventory.entries.map(({ entrypointKey }) => entrypointKey)), + entrypointKeys: new Set( + inventory.entries.map(({ entrypointKey }) => entrypointKey) + ), inventoryHash: inventory.inventoryHash, nowEpochMs: input.nowEpochMs, }); if (rollout.mode === 'report_only') { if (input.environment === 'production') { - fail('production authorization promotion rejects report-only configuration'); + fail( + 'production authorization promotion rejects report-only configuration' + ); } return { environment: input.environment, @@ -397,8 +417,12 @@ export const validateAuthorizationPromotionGate = ( status: 'observing', }; } - if (!authorizationEvidenceMatches(input, requireAuthorizationEvidence(input))) { - fail('authorization promotion evidence is missing, stale, mismatched, or unresolved'); + if ( + !authorizationEvidenceMatches(input, requireAuthorizationEvidence(input)) + ) { + fail( + 'authorization promotion evidence is missing, stale, mismatched, or unresolved' + ); } return { environment: input.environment, @@ -426,12 +450,14 @@ const GIT_EXECUTABLE = '/usr/bin/git'; const readJson = >( schema: DocumentSchema, - filePath: string, + filePath: string ) => Effect.gen(function* readJsonEffect() { const fileSystem = yield* FileSystem.FileSystem; const source = yield* fileSystem.readFileString(filePath); - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(source); + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))( + source + ); }); const requireString = (value: string | undefined, area: string): string => { @@ -449,7 +475,9 @@ const toEnvironmentSegment = (value: string): string => const normalizeChangedPath = (changedPath: string): string => { const normalized = changedPath.replaceAll('\\', '/').replace(/^\.\//u, ''); - return normalized.startsWith('app/') ? normalized.slice('app/'.length) : normalized; + return normalized.startsWith('app/') + ? normalized.slice('app/'.length) + : normalized; }; const isWithin = (changedPath: string, ownerPath: string): boolean => @@ -458,7 +486,7 @@ const isWithin = (changedPath: string, ownerPath: string): boolean => const parseStageSetups = (zeropsSource: string): ReadonlySet => { const setups = new Set(); for (const match of zeropsSource.matchAll( - /^\s*-\s+setup:\s*['"]?(?[^'"\s]+)['"]?\s*$/gmu, + /^\s*-\s+setup:\s*['"]?(?[^'"\s]+)['"]?\s*$/gmu )) { const setup = match.groups?.setup; if (setup !== undefined && setup.length > 0) { @@ -472,13 +500,15 @@ type TopologyOwner = typeof TopologyOwnerSchema.Type; type ReferenceVertical = NonNullable[number]; const indexOwners = ( - ownerEntries: readonly TopologyOwner[], + ownerEntries: readonly TopologyOwner[] ): ReadonlyMap => { const ownersById = new Map(); for (const owner of ownerEntries) { const ownerId = requireString(owner.id, 'ownership owner.id'); if (ownersById.has(ownerId)) { - fail(`topology/ownership.json contains duplicate owner identity "${ownerId}"`); + fail( + `topology/ownership.json contains duplicate owner identity "${ownerId}"` + ); } ownersById.set(ownerId, owner); } @@ -487,18 +517,29 @@ const indexOwners = ( const readShellUnit = ( topology: ReferenceTopology, - ownersById: ReadonlyMap, + ownersById: ReadonlyMap ): Omit => { - const shellId = requireString(topology.shell?.id, 'reference topology shell.id'); - const shellPackage = requireString(topology.shell?.package, 'reference topology shell.package'); + const shellId = requireString( + topology.shell?.id, + 'reference topology shell.id' + ); + const shellPackage = requireString( + topology.shell?.package, + 'reference topology shell.package' + ); const shellOwner = ownersById.get(shellId); if (shellOwner === undefined) { - return fail(`topology delivery unit "${shellId}" is missing from topology/ownership.json`); + return fail( + `topology delivery unit "${shellId}" is missing from topology/ownership.json` + ); } - const shellPath = requireString(shellOwner.path, `ownership owner ${shellId}.path`); + const shellPath = requireString( + shellOwner.path, + `ownership owner ${shellId}.path` + ); if (shellOwner.package !== shellPackage) { fail( - `topology and ownership disagree for "${shellId}": topology package "${shellPackage}" versus ownership package "${String(shellOwner.package)}"`, + `topology and ownership disagree for "${shellId}": topology package "${shellPackage}" versus ownership package "${String(shellOwner.package)}"` ); } return { @@ -511,12 +552,19 @@ const readShellUnit = ( }; }; -const collectVerticalIds = (verticals: readonly ReferenceVertical[]): ReadonlySet => { +const collectVerticalIds = ( + verticals: readonly ReferenceVertical[] +): ReadonlySet => { const verticalIds = new Set(); for (const vertical of verticals) { - const verticalId = requireString(vertical.id, 'reference topology vertical.id'); + const verticalId = requireString( + vertical.id, + 'reference topology vertical.id' + ); if (verticalIds.has(verticalId)) { - fail(`reference topology contains duplicate vertical identity "${verticalId}"`); + fail( + `reference topology contains duplicate vertical identity "${verticalId}"` + ); } verticalIds.add(verticalId); } @@ -525,30 +573,37 @@ const collectVerticalIds = (verticals: readonly ReferenceVertical[]): ReadonlySe const validateSharedPackages = ( sharedPackages: readonly TopologyOwner[], - ownersById: ReadonlyMap, + ownersById: ReadonlyMap ): ReadonlySet => { const sharedPackageIds = new Set(); for (const sharedPackage of sharedPackages) { - const id = requireString(sharedPackage.id, 'reference topology shared package.id'); + const id = requireString( + sharedPackage.id, + 'reference topology shared package.id' + ); const packageName = requireString( sharedPackage.package, - `reference topology shared package ${id}.package`, + `reference topology shared package ${id}.package` ); const ownerPath = requireString( sharedPackage.path, - `reference topology shared package ${id}.path`, + `reference topology shared package ${id}.path` ); if (sharedPackageIds.has(id)) { - fail(`reference topology contains duplicate shared package identity "${id}"`); + fail( + `reference topology contains duplicate shared package identity "${id}"` + ); } sharedPackageIds.add(id); const owner = ownersById.get(id); if (owner === undefined) { - return fail(`topology shared package "${id}" is missing from topology/ownership.json`); + return fail( + `topology shared package "${id}" is missing from topology/ownership.json` + ); } if (owner.package !== packageName || owner.path !== ownerPath) { fail( - `topology and ownership disagree for shared package "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"`, + `topology and ownership disagree for shared package "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"` ); } } @@ -558,11 +613,13 @@ const validateSharedPackages = ( const validateDistinctTopologyIds = ( shellId: string, verticalIds: ReadonlySet, - sharedPackageIds: ReadonlySet, + sharedPackageIds: ReadonlySet ): void => { for (const id of [shellId, ...verticalIds]) { if (sharedPackageIds.has(id)) { - fail(`reference topology reuses delivery identity "${id}" for a shared package`); + fail( + `reference topology reuses delivery identity "${id}" for a shared package` + ); } } }; @@ -570,17 +627,19 @@ const validateDistinctTopologyIds = ( const verticalDependencies = ( vertical: ReferenceVertical, id: string, - verticalIds: ReadonlySet, + verticalIds: ReadonlySet ): readonly string[] => { const dependencies = [ ...(vertical.moduleFederation?.verticalRefs ?? []), ...(vertical.moduleFederation?.remotes ?? []).flatMap((remote) => - remote.id === undefined ? [] : [remote.id], + remote.id === undefined ? [] : [remote.id] ), ]; for (const dependency of dependencies) { if (!verticalIds.has(dependency)) { - fail(`topology delivery unit "${id}" references unknown provider "${dependency}"`); + fail( + `topology delivery unit "${id}" references unknown provider "${dependency}"` + ); } } return dependencies; @@ -589,23 +648,28 @@ const verticalDependencies = ( const buildVerticalUnits = ( verticals: readonly ReferenceVertical[], verticalIds: ReadonlySet, - ownersById: ReadonlyMap, + ownersById: ReadonlyMap ): readonly TopologyUnit[] => { const units: TopologyUnit[] = []; for (const vertical of verticals) { const id = requireString(vertical.id, 'reference topology vertical.id'); const packageName = requireString( vertical.package, - `reference topology vertical ${id}.package`, + `reference topology vertical ${id}.package` + ); + const ownerPath = requireString( + vertical.path, + `reference topology vertical ${id}.path` ); - const ownerPath = requireString(vertical.path, `reference topology vertical ${id}.path`); const owner = ownersById.get(id); if (owner === undefined) { - return fail(`topology delivery unit "${id}" is missing from topology/ownership.json`); + return fail( + `topology delivery unit "${id}" is missing from topology/ownership.json` + ); } if (owner.package !== packageName || owner.path !== ownerPath) { fail( - `topology and ownership disagree for "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"`, + `topology and ownership disagree for "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"` ); } const dependencies = verticalDependencies(vertical, id, verticalIds); @@ -626,17 +690,22 @@ const addShellUnit = ( units: readonly TopologyUnit[], shell: Omit, shellDependencies: readonly string[], - verticalIds: ReadonlySet, + verticalIds: ReadonlySet ): readonly TopologyUnit[] => { for (const dependency of shellDependencies) { if (!verticalIds.has(dependency)) { - fail(`topology shell "${shell.id}" references unknown provider "${dependency}"`); + fail( + `topology shell "${shell.id}" references unknown provider "${dependency}"` + ); } } return [ ...units, { - dependencies: EffectArray.sort([...new Set(shellDependencies)], Order.String), + dependencies: EffectArray.sort( + [...new Set(shellDependencies)], + Order.String + ), ...shell, }, ]; @@ -644,25 +713,30 @@ const addShellUnit = ( const validateOwnershipCoverage = ( ownerEntries: readonly TopologyOwner[], - topologyOwnerIds: ReadonlySet, + topologyOwnerIds: ReadonlySet ): void => { for (const owner of ownerEntries) { const id = requireString(owner.id, 'ownership owner.id'); const ownerPath = requireString(owner.path, `ownership owner ${id}.path`); - if (/^(?:apps|packages|verticals)\//u.test(ownerPath) && !topologyOwnerIds.has(id)) { - fail(`ownership entry "${id}" at "${ownerPath}" has no matching topology identity`); + if ( + /^(?:apps|packages|verticals)\//u.test(ownerPath) && + !topologyOwnerIds.has(id) + ) { + fail( + `ownership entry "${id}" at "${ownerPath}" has no matching topology identity` + ); } } }; const validateStageSetupCoverage = ( units: readonly TopologyUnit[], - stageSetups: ReadonlySet, + stageSetups: ReadonlySet ): void => { for (const phase of [...Object.values(INFRASTRUCTURE_PHASES), ...units]) { if (!stageSetups.has(phase.stageSetup)) { fail( - `topology delivery unit "${phase.id}" has unsupported stage setup "${phase.stageSetup}" in zerops.yaml`, + `topology delivery unit "${phase.id}" has unsupported stage setup "${phase.stageSetup}" in zerops.yaml` ); } } @@ -671,27 +745,35 @@ const validateStageSetupCoverage = ( const buildTopologyUnits = ( topology: ReferenceTopology, ownership: Ownership, - stageSetups: ReadonlySet, + stageSetups: ReadonlySet ): readonly TopologyUnit[] => { const ownerEntries = ownership.owners ?? []; const ownersById = indexOwners(ownerEntries); const shell = readShellUnit(topology, ownersById); const verticals = topology.verticals ?? []; const verticalIds = collectVerticalIds(verticals); - const sharedPackageIds = validateSharedPackages(topology.sharedPackages ?? [], ownersById); + const sharedPackageIds = validateSharedPackages( + topology.sharedPackages ?? [], + ownersById + ); validateDistinctTopologyIds(shell.id, verticalIds, sharedPackageIds); const units = addShellUnit( buildVerticalUnits(verticals, verticalIds, ownersById), shell, topology.shell?.verticalRefs ?? [], - verticalIds, + verticalIds + ); + validateOwnershipCoverage( + ownerEntries, + new Set([shell.id, ...verticalIds, ...sharedPackageIds]) ); - validateOwnershipCoverage(ownerEntries, new Set([shell.id, ...verticalIds, ...sharedPackageIds])); validateStageSetupCoverage(units, stageSetups); return units; }; -const orderUnits = (units: readonly TopologyUnit[]): readonly TopologyUnit[] => { +const orderUnits = ( + units: readonly TopologyUnit[] +): readonly TopologyUnit[] => { const unitsById = new Map(units.map((unit) => [unit.id, unit])); const ordered: TopologyUnit[] = []; const visiting = new Set(); @@ -705,7 +787,9 @@ const orderUnits = (units: readonly TopologyUnit[]): readonly TopologyUnit[] => } const unit = unitsById.get(id); if (unit === undefined) { - return fail(`topology delivery dependencies reference unknown unit "${id}"`); + return fail( + `topology delivery dependencies reference unknown unit "${id}"` + ); } visiting.add(id); for (const dependency of unit.dependencies) { @@ -715,7 +799,11 @@ const orderUnits = (units: readonly TopologyUnit[]): readonly TopologyUnit[] => visited.add(id); ordered.push(unit); }; - for (const unit of EffectArray.sortWith(units, (candidate) => candidate.id, Order.String)) { + for (const unit of EffectArray.sortWith( + units, + (candidate) => candidate.id, + Order.String + )) { visit(unit.id); } return ordered; @@ -724,24 +812,32 @@ const orderUnits = (units: readonly TopologyUnit[]): readonly TopologyUnit[] => const invalidBaseReason = ( rootDirectory: string, baseRevision: string | undefined, - headRevision: string, + headRevision: string ) => Effect.gen(function* invalidBaseReasonEffect() { - if (baseRevision === undefined || baseRevision.length === 0 || /^0+$/u.test(baseRevision)) { + if ( + baseRevision === undefined || + baseRevision.length === 0 || + /^0+$/u.test(baseRevision) + ) { return 'comparison base is unavailable or all-zero'; } const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const revisionExists = yield* spawner .exitCode( - ChildProcess.make(GIT_EXECUTABLE, ['cat-file', '-e', `${baseRevision}^{commit}`], { - cwd: rootDirectory, - stderr: 'ignore', - stdout: 'ignore', - }), + ChildProcess.make( + GIT_EXECUTABLE, + ['cat-file', '-e', `${baseRevision}^{commit}`], + { + cwd: rootDirectory, + stderr: 'ignore', + stdout: 'ignore', + } + ) ) .pipe( Effect.map((exitCode) => exitCode === 0), - Effect.catch(() => Effect.succeed(false)), + Effect.catch(() => Effect.succeed(false)) ); if (!revisionExists) { return `comparison base "${baseRevision}" is unavailable`; @@ -751,12 +847,12 @@ const invalidBaseReason = ( ChildProcess.make( GIT_EXECUTABLE, ['merge-base', '--is-ancestor', baseRevision, headRevision], - { cwd: rootDirectory, stderr: 'ignore', stdout: 'ignore' }, - ), + { cwd: rootDirectory, stderr: 'ignore', stdout: 'ignore' } + ) ) .pipe( Effect.map((exitCode) => exitCode === 0), - Effect.catch(() => Effect.succeed(false)), + Effect.catch(() => Effect.succeed(false)) ); if (!isAncestor) { return `comparison base "${baseRevision}" is not an ancestor of "${headRevision}"`; @@ -764,29 +860,44 @@ const invalidBaseReason = ( return yield* Effect.undefined; }); -const changedPathsFromGit = (rootDirectory: string, baseRevision: string, headRevision: string) => +const changedPathsFromGit = ( + rootDirectory: string, + baseRevision: string, + headRevision: string +) => Effect.gen(function* changedPathsFromGitEffect() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const output = yield* spawner.string( ChildProcess.make( GIT_EXECUTABLE, - ['diff', '--name-only', '--no-renames', '-z', baseRevision, headRevision], - { cwd: rootDirectory }, - ), + [ + 'diff', + '--name-only', + '--no-renames', + '-z', + baseRevision, + headRevision, + ], + { cwd: rootDirectory } + ) ); return output.split('\0').filter(Boolean); }); const isMigrationChange = (changedPath: string): boolean => /(?:^|\/)(?:drizzle(?:-auth)?\/|drizzle(?:\.auth)?\.config\.ts$|schema\.ts$|prepare-[^/]+-migration\.mts$|verify-(?:auth-)?db-schema\.mts$)/u.test( - changedPath, + changedPath ) || /^(?:scripts\/run-zerops-migrator\.mjs|scripts\/verify-application-db-schema\.mts|scripts\/postgres\/(?:bootstrap-runtime-role\.mts|bootstrap-spicedb-database\.mts|docker-init-runtime-role\.sh))$/u.test( - changedPath, + changedPath ) || - changedPath === 'packages/core-runtime/src/install/spicedb-database-config.ts'; + changedPath === + 'packages/core-runtime/src/install/spicedb-database-config.ts'; -const isPublicContractChange = (ownerPath: string, changedPath: string): boolean => { +const isPublicContractChange = ( + ownerPath: string, + changedPath: string +): boolean => { const relativePath = changedPath.slice(ownerPath.length + 1); return ( relativePath === 'package.json' || @@ -800,13 +911,16 @@ const isPublicContractChange = (ownerPath: string, changedPath: string): boolean const isSpiceDbChange = (changedPath: string): boolean => changedPath.startsWith('packages/core-runtime/spicedb/') || changedPath.startsWith('packages/core-runtime/src/permissions/') || - changedPath === 'packages/core-runtime/src/install/spicedb-database-config.ts' || + changedPath === + 'packages/core-runtime/src/install/spicedb-database-config.ts' || changedPath === 'scripts/postgres/bootstrap-spicedb-database.mts' || changedPath === 'scripts/run-zerops-spicedb.sh'; const isAuthorizationRolloutChange = (changedPath: string): boolean => changedPath.startsWith('packages/core-runtime/src/authorization/') || - changedPath.startsWith('packages/core-runtime/src/auth/gateway-assertion-redemption') || + changedPath.startsWith( + 'packages/core-runtime/src/auth/gateway-assertion-redemption' + ) || changedPath.startsWith('scripts/authorization/') || changedPath === 'scripts/check-authorization-readiness.mts' || changedPath === 'scripts/check-module-entrypoint-boundaries.mts' || @@ -827,7 +941,8 @@ const CONSERVATIVE_FULL_DEPLOY_PATHS = new Set([ 'zerops.yaml', ]); const isConservativeFullDeployChange = (changedPath: string): boolean => - CONSERVATIVE_FULL_DEPLOY_PATHS.has(changedPath) || changedPath.startsWith('topology/'); + CONSERVATIVE_FULL_DEPLOY_PATHS.has(changedPath) || + changedPath.startsWith('topology/'); const toPhase = (unit: TopologyUnit): DeploymentPhase => ({ id: unit.id, @@ -839,7 +954,7 @@ const toPhase = (unit: TopologyUnit): DeploymentPhase => ({ const makeComparison = ( options: PlanDeploymentImpactOptions, headRevision: string, - fallbackReason: string | undefined, + fallbackReason: string | undefined ): DeploymentImpactPlan['comparison'] => { const mode = fallbackReason === undefined ? 'diff' : 'full'; if (options.baseRevision === undefined) { @@ -863,7 +978,10 @@ interface DeploymentImpactState { spicedb: boolean; } -const addAllUnits = (impacted: Set, orderedUnits: readonly TopologyUnit[]): void => { +const addAllUnits = ( + impacted: Set, + orderedUnits: readonly TopologyUnit[] +): void => { for (const unit of orderedUnits) { impacted.add(unit.id); } @@ -872,7 +990,7 @@ const addAllUnits = (impacted: Set, orderedUnits: readonly TopologyUnit[ const addWithConsumers = ( unitId: string, impacted: Set, - orderedUnits: readonly TopologyUnit[], + orderedUnits: readonly TopologyUnit[] ): void => { impacted.add(unitId); let changed = true; @@ -895,21 +1013,26 @@ const applyOwnedPathImpact = ( ownerEntries: readonly TopologyOwner[], unitsById: ReadonlyMap, orderedUnits: readonly TopologyUnit[], - impacted: Set, + impacted: Set ): void => { if (!/^(?:apps|packages|verticals)\//u.test(changedPath)) { return; } const [owner] = EffectArray.sortWith( - ownerEntries.filter((entry) => entry.path !== undefined && isWithin(changedPath, entry.path)), + ownerEntries.filter( + (entry) => entry.path !== undefined && isWithin(changedPath, entry.path) + ), (entry) => String(entry.path).length, - Order.flip(Order.Number), + Order.flip(Order.Number) ); if (owner === undefined) { const [area] = changedPath.split('/'); fail(`unknown changed path "${changedPath}" in application area "${area}"`); } - const ownerId = requireString(owner.id, `owner for changed path ${changedPath}`); + const ownerId = requireString( + owner.id, + `owner for changed path ${changedPath}` + ); const topologyUnit = unitsById.get(ownerId); if (topologyUnit !== undefined) { impacted.add(ownerId); @@ -919,7 +1042,9 @@ const applyOwnedPathImpact = ( } else if (changedPath.startsWith('packages/')) { addAllUnits(impacted, orderedUnits); } else { - fail(`changed path "${changedPath}" maps to non-delivery owner "${ownerId}"`); + fail( + `changed path "${changedPath}" maps to non-delivery owner "${ownerId}"` + ); } }; @@ -928,9 +1053,15 @@ const applyChangedPathImpact = ( ownerEntries: readonly TopologyOwner[], unitsById: ReadonlyMap, orderedUnits: readonly TopologyUnit[], - state: DeploymentImpactState, + state: DeploymentImpactState ): void => { - applyOwnedPathImpact(changedPath, ownerEntries, unitsById, orderedUnits, state.impacted); + applyOwnedPathImpact( + changedPath, + ownerEntries, + unitsById, + orderedUnits, + state.impacted + ); if (isMigrationChange(changedPath)) { state.migrator = true; } @@ -954,7 +1085,7 @@ const deriveDeploymentImpact = ( changedPaths: readonly string[], fullDeploy: boolean, ownerEntries: readonly TopologyOwner[], - orderedUnits: readonly TopologyUnit[], + orderedUnits: readonly TopologyUnit[] ): DeploymentImpactState => { const state: DeploymentImpactState = { impacted: new Set(), @@ -967,17 +1098,30 @@ const deriveDeploymentImpact = ( } const unitsById = new Map(orderedUnits.map((unit) => [unit.id, unit])); for (const changedPath of changedPaths) { - applyChangedPathImpact(changedPath, ownerEntries, unitsById, orderedUnits, state); + applyChangedPathImpact( + changedPath, + ownerEntries, + unitsById, + orderedUnits, + state + ); } return state; }; -const deploymentComparison = (options: PlanDeploymentImpactOptions, rootDirectory: string) => +const deploymentComparison = ( + options: PlanDeploymentImpactOptions, + rootDirectory: string +) => Effect.gen(function* deploymentComparisonEffect() { const headRevision = options.headRevision ?? 'HEAD'; const fallbackReason = options.changedPaths === undefined - ? yield* invalidBaseReason(rootDirectory, options.baseRevision, headRevision) + ? yield* invalidBaseReason( + rootDirectory, + options.baseRevision, + headRevision + ) : undefined; const fullDeploy = fallbackReason !== undefined; const comparedPaths = @@ -987,18 +1131,18 @@ const deploymentComparison = (options: PlanDeploymentImpactOptions, rootDirector : yield* changedPathsFromGit( rootDirectory, requireString(options.baseRevision, 'base revision'), - headRevision, + headRevision )); const changedPaths = EffectArray.sort( [...new Set(comparedPaths.map(normalizeChangedPath))], - Order.String, + Order.String ); return { changedPaths, fallbackReason, fullDeploy, headRevision }; }); const validateWorkerStageSetups = ( workers: readonly { readonly stageSetup: string }[], - stageSetups: ReadonlySet, + stageSetups: ReadonlySet ): void => { for (const delivery of workers) { if (!stageSetups.has(delivery.stageSetup)) { @@ -1007,7 +1151,9 @@ const validateWorkerStageSetups = ( } }; -export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) => +export const planDeploymentImpact = ( + options: PlanDeploymentImpactOptions = {} +) => Effect.gen(function* planDeploymentImpactEffect() { const authorization = options.authorizationPromotion === undefined @@ -1016,45 +1162,50 @@ export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) const pathService = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const rootDirectory = - options.rootDirectory ?? (yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.'))); + options.rootDirectory ?? + (yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.'))); const topology = yield* readJson( ReferenceTopologySchema, - pathService.join(rootDirectory, 'topology/reference-topology.json'), + pathService.join(rootDirectory, 'topology/reference-topology.json') ); const ownership = yield* readJson( OwnershipSchema, - pathService.join(rootDirectory, 'topology/ownership.json'), + pathService.join(rootDirectory, 'topology/ownership.json') ); const stageSetups = parseStageSetups( - yield* fileSystem.readFileString(pathService.join(rootDirectory, 'zerops.yaml')), + yield* fileSystem.readFileString( + pathService.join(rootDirectory, 'zerops.yaml') + ) + ); + const orderedUnits = orderUnits( + buildTopologyUnits(topology, ownership, stageSetups) ); - const orderedUnits = orderUnits(buildTopologyUnits(topology, ownership, stageSetups)); const workerDeliveries = yield* Effect.all( (topology.verticals ?? []).map((vertical) => outboxWorkerDelivery(rootDirectory, { id: requireString(vertical.id, 'vertical id'), package: requireString(vertical.package, 'vertical package'), path: requireString(vertical.path, 'vertical path'), - }), - ), + }) + ) + ); + const workers = workerDeliveries.filter( + (delivery) => delivery !== undefined ); - const workers = workerDeliveries.filter((delivery) => delivery !== undefined); validateWorkerStageSetups(workers, stageSetups); const shell = orderedUnits.find((unit) => unit.kind === 'shell'); if (shell === undefined) { return fail('reference topology has no Shell delivery unit'); } - const { changedPaths, fallbackReason, fullDeploy, headRevision } = yield* deploymentComparison( - options, - rootDirectory, - ); + const { changedPaths, fallbackReason, fullDeploy, headRevision } = + yield* deploymentComparison(options, rootDirectory); const { impacted, migrator, spicedb } = deriveDeploymentImpact( changedPaths, fullDeploy, ownership.owners ?? [], - orderedUnits, + orderedUnits ); const selectedUnits = orderedUnits.filter((unit) => impacted.has(unit.id)); @@ -1075,7 +1226,7 @@ export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) serviceIdEnv: worker.serviceIdEnv, stageSetup: worker.stageSetup, })), - ...selectedUnits.filter((unit) => unit.kind === 'shell').map(toPhase), + ...selectedUnits.filter((unit) => unit.kind === 'shell').map(toPhase) ); const plan: DeploymentImpactPlan = { @@ -1086,7 +1237,9 @@ export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) schemaVersion: 1, units: { migrator, - providers: phases.filter((phase) => phase.kind === 'provider').map((phase) => phase.id), + providers: phases + .filter((phase) => phase.kind === 'provider') + .map((phase) => phase.id), shell: impacted.has(shell.id), spicedb, }, @@ -1097,18 +1250,21 @@ export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) const loadAuthorizationPromotionGate = ( rootDirectory: string, environment: AuthorizationPromotionGateInput['environment'], - nowEpochMs: number, + nowEpochMs: number ) => Effect.gen(function* loadAuthorizationPromotionGateEffect() { const pathService = yield* Path.Path; - const reportDirectory = pathService.join(rootDirectory, '.codex/reports/authorization'); + const reportDirectory = pathService.join( + rootDirectory, + '.codex/reports/authorization' + ); const rollout = yield* readJson( AuthorizationRolloutContractSchema, - pathService.join(rootDirectory, 'topology/authorization-rollout.json'), + pathService.join(rootDirectory, 'topology/authorization-rollout.json') ); const inventory = yield* readJson( ProtectedEntrypointInventorySchema, - pathService.join(reportDirectory, 'protected-entrypoints.json'), + pathService.join(reportDirectory, 'protected-entrypoints.json') ); if (rollout.mode === 'report_only') { return { environment, inventory, nowEpochMs, rollout }; @@ -1117,17 +1273,17 @@ const loadAuthorizationPromotionGate = ( environment, impact: yield* readJson( AuthorizationImpactReportSchema, - pathService.join(reportDirectory, 'fail-closed-impact.json'), + pathService.join(reportDirectory, 'fail-closed-impact.json') ), inventory, negativeSmoke: yield* readJson( AuthorizationNegativeSmokeEvidenceSchema, - pathService.join(reportDirectory, `negative-smoke.${environment}.json`), + pathService.join(reportDirectory, `negative-smoke.${environment}.json`) ), nowEpochMs, readiness: yield* readJson( AuthorizationReadinessEvidenceSchema, - pathService.join(reportDirectory, 'readiness.json'), + pathService.join(reportDirectory, 'readiness.json') ), rollout, }; @@ -1140,7 +1296,9 @@ const writeGitHubOutputs = (plan: DeploymentImpactPlan, outputPath: string) => Effect.gen(function* writeGitHubOutputsEffect() { const fileSystem = yield* FileSystem.FileSystem; const planJson = yield* Schema.encodeEffect(PlanJsonSchema)(plan); - const providersJson = yield* Schema.encodeEffect(ProvidersJsonSchema)(plan.units.providers); + const providersJson = yield* Schema.encodeEffect(ProvidersJsonSchema)( + plan.units.providers + ); const output = [ `any=${String(plan.any)}`, `migrator=${String(plan.units.migrator)}`, @@ -1155,7 +1313,7 @@ const writeGitHubOutputs = (plan: DeploymentImpactPlan, outputPath: string) => const parseAuthorizationNow = (value: string) => Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString)(value).pipe( - Effect.map(DateTime.toEpochMillis), + Effect.map(DateTime.toEpochMillis) ); const deploymentImpactCommand = Command.make( @@ -1171,9 +1329,17 @@ const deploymentImpactCommand = Command.make( changedPaths: Flag.string('changed-path').pipe(Flag.atLeast(0)), headRevision: Flag.string('head').pipe(Flag.optional), }, - ({ authorizationEnvironment, authorizationNow, baseRevision, changedPaths, headRevision }) => + ({ + authorizationEnvironment, + authorizationNow, + baseRevision, + changedPaths, + headRevision, + }) => Effect.gen(function* deploymentImpactCommandEffect() { - const rootDirectory = yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.')); + const rootDirectory = yield* Config.string('PWD').pipe( + Effect.orElseSucceed(() => '.') + ); const environment = Option.getOrUndefined(authorizationEnvironment); let authorizationPromotion: AuthorizationPromotionGateInput | undefined; if (environment !== undefined) { @@ -1185,7 +1351,7 @@ const deploymentImpactCommand = Command.make( authorizationPromotion = yield* loadAuthorizationPromotionGate( rootDirectory, environment, - nowEpochMs, + nowEpochMs ); } const options: PlanDeploymentImpactOptions = { @@ -1204,15 +1370,15 @@ const deploymentImpactCommand = Command.make( if (Option.isSome(outputPath)) { yield* writeGitHubOutputs(plan, outputPath.value); } - }), + }) ); export const main = Command.run({ version: '1.0.0' })(deploymentImpactCommand); -if (import.meta.main === true) { +if (import.meta.main) { NodeRuntime.runMain( - Layer.build(Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer))).pipe( - Effect.scoped, - ), + Layer.build( + Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer)) + ).pipe(Effect.scoped) ); } diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index c631a7de2..baa76c075 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,6 +1,15 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { + mkdtemp, + mkdir, + readFile, + readdir, + rm, + stat, + symlink, + writeFile, +} from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -21,7 +30,13 @@ import { Schema, } from '@modern-js/plugin-bff/effect-edge'; import { Clock, ConfigProvider, Predicate, Redacted } from 'effect'; -import { SignJWT, exportJWK, generateKeyPair, generateSecret, importJWK } from 'jose'; +import { + SignJWT, + exportJWK, + generateKeyPair, + generateSecret, + importJWK, +} from 'jose'; import type { JWK } from 'jose'; import type { GatewayIssuerConfigValue } from '../../../apps/shell-super-app/api/auth/gateway-issuer-config.ts'; @@ -85,40 +100,49 @@ const test = (name: string, handler: () => void | Promise): void => { class FirstScaffoldTestError extends Schema.TaggedError()( 'FirstScaffoldTestError', - { cause: Schema.optionalKey(Schema.Unknown), message: Schema.String }, + { cause: Schema.optionalKey(Schema.Unknown), message: Schema.String } ) {} const firstScaffoldErrors = createScaffoldErrorTools( FirstScaffoldTestError, Schema.is(FirstScaffoldTestError), - 'first update failed', + 'first update failed' ); const secondScaffoldErrors = createScaffoldErrorTools( ScaffoldFailure, Schema.is(ScaffoldFailure), - 'second update failed', + 'second update failed' ); test('scaffold error tools preserve success and own failure identity', async () => { const value = { unchanged: true }; - assert.equal(await runEffectTestPromise(firstScaffoldErrors.trySync(() => value)), value); + assert.equal( + await runEffectTestPromise(firstScaffoldErrors.trySync(() => value)), + value + ); const own = firstScaffoldErrors.scaffoldError('own failure'); const failure = await runEffectTestPromise( firstScaffoldErrors .trySync(() => { throw own; }) - .pipe(Effect.flip), + .pipe(Effect.flip) ); assert.equal(failure, own); }); test('scaffold error tools omit undefined causes and retain defined causes', () => { - assert.equal(Object.hasOwn(firstScaffoldErrors.scaffoldError('absent'), 'cause'), false); + assert.equal( + Object.hasOwn(firstScaffoldErrors.scaffoldError('absent'), 'cause'), + false + ); const absentCause = firstScaffoldErrors.scaffoldError('absent').cause; assert.equal( - Object.hasOwn(firstScaffoldErrors.scaffoldError('undefined', absentCause), 'cause'), - false, + Object.hasOwn( + firstScaffoldErrors.scaffoldError('undefined', absentCause), + 'cause' + ), + false ); for (const cause of [null, false, 0, '', { detail: 'retained' }]) { const failure = firstScaffoldErrors.scaffoldError('defined', cause); @@ -135,7 +159,11 @@ test('scaffold error tools normalize foreign errors without accepting another ow emptyMessageError.message = ''; await runEffectTestPromise( Effect.gen(function* foreignScaffoldErrors() { - for (const cause of [new Error('foreign error'), emptyMessageError, foreign]) { + for (const cause of [ + new Error('foreign error'), + emptyMessageError, + foreign, + ]) { const failure = yield* firstScaffoldErrors .trySync(() => { throw cause; @@ -147,7 +175,7 @@ test('scaffold error tools normalize foreign errors without accepting another ow assert.equal(failure.message, cause.message); assert.equal(failure.cause, cause); } - }), + }) ); }); @@ -166,10 +194,10 @@ for (const [index, cause] of [ return iterator.throw(cause); }; const first = await runEffectTestPromise( - firstScaffoldErrors.trySync(operation).pipe(Effect.flip), + firstScaffoldErrors.trySync(operation).pipe(Effect.flip) ); const second = await runEffectTestPromise( - secondScaffoldErrors.trySync(operation).pipe(Effect.flip), + secondScaffoldErrors.trySync(operation).pipe(Effect.flip) ); assert.equal(first.message, 'first update failed'); assert.equal(second.message, 'second update failed'); @@ -208,8 +236,11 @@ interface GeneratedPrincipalModule { readonly currentTimeSeconds: Effect.Effect; readonly environment: GeneratedPrincipalEnvironment; readonly redemption: { readonly consume: () => Effect.Effect }; - }, - ) => Effect.Effect; + } + ) => Effect.Effect< + TrustedPrincipalContext, + { readonly _tag: GeneratedPrincipalErrorTag } + >; } interface GeneratedActionHttpRunnerModule { @@ -218,10 +249,12 @@ interface GeneratedActionHttpRunnerModule { interface GeneratedOperationGatewayModule { readonly makeOperationGateway: ( - acquire: (payload: { readonly audience: string }) => Effect.Effect<{ readonly token: string }>, + acquire: (payload: { + readonly audience: string; + }) => Effect.Effect<{ readonly token: string }> ) => { readonly invoke: ( - attempt: (authorization: string) => Effect.Effect, + attempt: (authorization: string) => Effect.Effect ) => Effect.Effect; }; } @@ -230,23 +263,26 @@ const GeneratedPrincipalModuleSchema = Schema.Struct({ ActionPrincipalVerifierLive: Schema.declare< GeneratedPrincipalModule['ActionPrincipalVerifierLive'] >((value): value is GeneratedPrincipalModule['ActionPrincipalVerifierLive'] => - Predicate.isObject(value), + Predicate.isObject(value) ), - verifyActionPrincipal: Schema.declare( - (value): value is GeneratedPrincipalModule['verifyActionPrincipal'] => - Predicate.isFunction(value), + verifyActionPrincipal: Schema.declare< + GeneratedPrincipalModule['verifyActionPrincipal'] + >((value): value is GeneratedPrincipalModule['verifyActionPrincipal'] => + Predicate.isFunction(value) ), }); const GeneratedActionHttpRunnerModuleSchema = Schema.Struct({ - bindActionHttpRunner: Schema.declare( - (value): value is GeneratedActionHttpRunnerModule['bindActionHttpRunner'] => - Predicate.isFunction(value), + bindActionHttpRunner: Schema.declare< + GeneratedActionHttpRunnerModule['bindActionHttpRunner'] + >((value): value is GeneratedActionHttpRunnerModule['bindActionHttpRunner'] => + Predicate.isFunction(value) ), }); const GeneratedOperationGatewayModuleSchema = Schema.Struct({ - makeOperationGateway: Schema.declare( - (value): value is GeneratedOperationGatewayModule['makeOperationGateway'] => - Predicate.isFunction(value), + makeOperationGateway: Schema.declare< + GeneratedOperationGatewayModule['makeOperationGateway'] + >((value): value is GeneratedOperationGatewayModule['makeOperationGateway'] => + Predicate.isFunction(value) ), }); @@ -262,7 +298,7 @@ const EsbuildMetafileSchema = Schema.Struct({ Schema.String, Schema.Struct({ bytes: Schema.Number, - }), + }) ), }); const RetryableProblemSchema = Schema.Struct({ @@ -279,25 +315,29 @@ const asProblemDetails = HttpApiSchema.asJson({ }); const ActionAuthenticationProblemSchema = Schema.TaggedStruct( 'ActionAuthenticationProblem', - problemFields, + problemFields ).pipe(asProblemDetails, HttpApiSchema.status(401)); const ActionVerificationUnavailableProblemSchema = Schema.TaggedStruct( 'ActionVerificationUnavailableProblem', - { ...problemFields, retryable: Schema.Literal(true) }, + { ...problemFields, retryable: Schema.Literal(true) } ).pipe(asProblemDetails, HttpApiSchema.status(503)); type EndpointProblem = | typeof ActionAuthenticationProblemSchema.Type | typeof ActionVerificationUnavailableProblemSchema.Type; -const bearerChallenge = HttpEffect.appendPreResponseHandler((_request, response) => - Effect.succeed(HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer')), +const bearerChallenge = HttpEffect.appendPreResponseHandler( + (_request, response) => + Effect.succeed( + HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer') + ) ); -const actionAuthenticationProblem = (): typeof ActionAuthenticationProblemSchema.Type => ({ - _tag: 'ActionAuthenticationProblem', - detail: 'A valid Bearer assertion is required.', - status: 401, - title: 'Action authentication required', - type: 'https://ontos.dev/problems/action-authentication-required', -}); +const actionAuthenticationProblem = + (): typeof ActionAuthenticationProblemSchema.Type => ({ + _tag: 'ActionAuthenticationProblem', + detail: 'A valid Bearer assertion is required.', + status: 401, + title: 'Action authentication required', + type: 'https://ontos.dev/problems/action-authentication-required', + }); const actionVerificationUnavailableProblem = (): typeof ActionVerificationUnavailableProblemSchema.Type => ({ _tag: 'ActionVerificationUnavailableProblem', @@ -308,7 +348,9 @@ const actionVerificationUnavailableProblem = type: 'https://ontos.dev/problems/action-verification-unavailable', }); const failActionAuthentication = () => - bearerChallenge.pipe(Effect.andThen(Effect.fail(actionAuthenticationProblem()))); + bearerChallenge.pipe( + Effect.andThen(Effect.fail(actionAuthenticationProblem())) + ); const failActionVerificationUnavailable = () => Effect.fail(actionVerificationUnavailableProblem()); const generatedPrincipalErrorHandlers = { @@ -350,7 +392,7 @@ const generatedBindingAction = defineAction( resultSchema: GeneratedBindingResultSchema, schemaVersion: '1', }, - () => Effect.succeed({ accepted: true as const }), + () => Effect.succeed({ accepted: true as const }) ); const FixtureTsconfigSchema = Schema.Struct({ references: Schema.Array(Schema.Struct({ path: Schema.String })), @@ -358,7 +400,10 @@ const FixtureTsconfigSchema = Schema.Struct({ const InventoryLocaleSchema = Schema.Struct({ inventory: Schema.Struct({ existing: Schema.optional(Schema.String), - pages: Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String)), + pages: Schema.Record( + Schema.String, + Schema.Record(Schema.String, Schema.String) + ), }), }); @@ -381,7 +426,8 @@ const workspaceVersion = 'workspace:*'; const fixtureGatewayJti = '60000000-0000-4000-8000-000000000001'; const actionInvokeUrl = 'https://inventory.example.test/actions/invoke'; const generatedOutboxContractPath = './shared/outbox/orders-created.ts'; -const workerStartScript = 'node --experimental-strip-types ./src/worker-host/main.ts'; +const workerStartScript = + 'node --experimental-strip-types ./src/worker-host/main.ts'; const workerRegistryEntry = 'ordersCreatedProjectorWorker,'; const pagePlaceholder = 'This page is ready for implementation.'; const purchasingOrdersUrl = '/purchasing/orders'; @@ -426,39 +472,58 @@ const fixtureName = { } as const; const rootPackageFile = 'package.json'; const coreRuntimeIndexFile = 'packages/core-runtime/src/index.ts'; -const coreActionCatalogFile = 'packages/core-runtime/src/modules/actions/catalog.ts'; +const coreActionCatalogFile = + 'packages/core-runtime/src/modules/actions/catalog.ts'; const shellSentinelFile = 'apps/shell-super-app/src/sentinel.ts'; -const shellVerticalClientsFile = 'apps/shell-super-app/src/api/vertical-clients.ts'; +const shellVerticalClientsFile = + 'apps/shell-super-app/src/api/vertical-clients.ts'; const inventoryManifestFile = 'verticals/inventory-stock/vertical.manifest.ts'; -const inventoryRegistrationFile = 'verticals/inventory-stock/vertical.registration.ts'; -const inventoryFederationConfigFile = 'verticals/inventory-stock/module-federation.config.ts'; +const inventoryRegistrationFile = + 'verticals/inventory-stock/vertical.registration.ts'; +const inventoryFederationConfigFile = + 'verticals/inventory-stock/module-federation.config.ts'; const inventorySearchProviderFile = 'verticals/inventory-stock/src/search/inventory-items.provider.ts'; const inventorySearchContractFile = 'verticals/inventory-stock/shared/apis/inventory-items-search.ts'; -const inventoryModuleApiContractFile = 'verticals/inventory-stock/shared/apis/resource-detail.ts'; -const inventoryModuleApiReadFile = 'verticals/inventory-stock/src/api/resource-detail.read.ts'; -const inventoryModuleApiClientFile = 'verticals/inventory-stock/src/api/resource-detail-client.ts'; -const inventoryModuleApiServerFile = 'verticals/inventory-stock/api/resource-detail-read-server.ts'; +const inventoryModuleApiContractFile = + 'verticals/inventory-stock/shared/apis/resource-detail.ts'; +const inventoryModuleApiReadFile = + 'verticals/inventory-stock/src/api/resource-detail.read.ts'; +const inventoryModuleApiClientFile = + 'verticals/inventory-stock/src/api/resource-detail-client.ts'; +const inventoryModuleApiServerFile = + 'verticals/inventory-stock/api/resource-detail-read-server.ts'; const inventorySearchClientFile = 'verticals/inventory-stock/src/api/inventory-items-search-client.ts'; -const inventorySearchServerFile = 'verticals/inventory-stock/api/inventory-items-search-server.ts'; +const inventorySearchServerFile = + 'verticals/inventory-stock/api/inventory-items-search-server.ts'; const inventoryReportProviderFile = 'verticals/inventory-stock/src/reports/stock-levels.provider.ts'; -const inventoryReportContractFile = 'verticals/inventory-stock/shared/apis/stock-levels-report.ts'; -const inventoryReportClientFile = 'verticals/inventory-stock/src/api/stock-levels-report-client.ts'; -const inventoryReportServerFile = 'verticals/inventory-stock/api/stock-levels-report-server.ts'; -const inventoryActionPrincipalFile = 'verticals/inventory-stock/api/auth/action-principal.ts'; -const inventoryActionHttpRunnerFile = 'verticals/inventory-stock/api/action-http-runner.ts'; -const inventoryActionGatewayFile = 'verticals/inventory-stock/src/api/action-gateway.ts'; +const inventoryReportContractFile = + 'verticals/inventory-stock/shared/apis/stock-levels-report.ts'; +const inventoryReportClientFile = + 'verticals/inventory-stock/src/api/stock-levels-report-client.ts'; +const inventoryReportServerFile = + 'verticals/inventory-stock/api/stock-levels-report-server.ts'; +const inventoryActionPrincipalFile = + 'verticals/inventory-stock/api/auth/action-principal.ts'; +const inventoryActionHttpRunnerFile = + 'verticals/inventory-stock/api/action-http-runner.ts'; +const inventoryActionGatewayFile = + 'verticals/inventory-stock/src/api/action-gateway.ts'; const inventoryPackageFile = 'verticals/inventory-stock/package.json'; -const inventoryActionFile = 'verticals/inventory-stock/src/actions/create-order.action.ts'; -const inventoryOutboxContractFile = 'verticals/inventory-stock/shared/outbox/orders-created.ts'; +const inventoryActionFile = + 'verticals/inventory-stock/src/actions/create-order.action.ts'; +const inventoryOutboxContractFile = + 'verticals/inventory-stock/shared/outbox/orders-created.ts'; const billingApiIndexFile = 'verticals/billing/api/index.ts'; const billingWorkersIndexFile = 'verticals/billing/src/workers/index.ts'; const inventoryTsconfigFile = 'verticals/inventory-stock/tsconfig.json'; -const inventoryEnglishLocaleFile = 'verticals/inventory-stock/locales/en/inventory.json'; -const inventoryOrdersRouteFile = 'verticals/inventory-stock/src/routes/[lang]/orders/page.tsx'; +const inventoryEnglishLocaleFile = + 'verticals/inventory-stock/locales/en/inventory.json'; +const inventoryOrdersRouteFile = + 'verticals/inventory-stock/src/routes/[lang]/orders/page.tsx'; const effectNodeModulePath = 'node_modules/effect'; const pluginBffNodeModulePath = 'node_modules/@modern-js/plugin-bff'; @@ -494,7 +559,8 @@ const contactsVertical: FixtureVertical = { slug: 'contacts', }; -const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; +const json = (value: JsonValue): string => + `${JSON.stringify(value, null, 2)}\n`; const inventoryHandlerRootFile = 'verticals/inventory-stock/api/index.ts'; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const require = createRequire(import.meta.url); @@ -506,7 +572,7 @@ const oxfmtPath = path.join(appRoot, 'node_modules', '.bin', 'oxfmt'); const tscPath = path.join(appRoot, 'node_modules', '.bin', 'tsc'); const makeGatewayKey = async ( - kid: string, + kid: string ): Promise<{ configuration: GatewayIssuerConfigValue; publicJwk: JWK; @@ -537,18 +603,21 @@ const makeGatewayKey = async ( const writeFixtureFile = async ( root: string, relativePath: string, - content: string, + content: string ): Promise => { const filePath = path.join(root, relativePath); await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, content, 'utf-8'); }; -const createVertical = async (root: string, vertical: FixtureVertical): Promise => { +const createVertical = async ( + root: string, + vertical: FixtureVertical +): Promise => { await writeFixtureFile( root, `verticals/${vertical.slug}/module-federation.config.ts`, - 'export default { exposes: {} };\n', + 'export default { exposes: {} };\n' ); await writeFixtureFile( root, @@ -557,7 +626,7 @@ const createVertical = async (root: string, vertical: FixtureVertical): Promise< compilerOptions: { composite: true }, include: ['src', 'shared'], references: [], - }), + }) ); await writeFixtureFile( root, @@ -578,13 +647,14 @@ const createVertical = async (root: string, vertical: FixtureVertical): Promise< name: `@app/${vertical.slug}`, private: true, scripts: { - build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + build: + 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', 'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', existing: preservedFixtureValue, }, version: '0.1.0', - }), + }) ); await writeFixtureFile( root, @@ -601,7 +671,7 @@ const layer = HttpApiBuilder.layer(fixtureApi).pipe( ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: fixtureApi, layer }); -`, +` ); await writeFixtureFile( root, @@ -616,7 +686,7 @@ export const fixtureApi = HttpApi.make('FixtureApi').add( }), ), ); -`, +` ); await Promise.all( ['cs', 'en'].map( @@ -628,14 +698,16 @@ export const fixtureApi = HttpApi.make('FixtureApi').add( [vertical.namespace]: { existing: `${locale}-preserved`, }, - }), - ), - ), + }) + ) + ) ); const resourcesName = `${vertical.slug .split('-') .map((segment, index) => - index === 0 ? segment : `${segment[0]?.toUpperCase() ?? ''}${segment.slice(1)}`, + index === 0 + ? segment + : `${segment[0]?.toUpperCase() ?? ''}${segment.slice(1)}` ) .join('')}I18nResources`; await writeFixtureFile( @@ -665,22 +737,26 @@ export const ${resourcesName} = { cs: { ${vertical.namespace}: flattenLocaleResource(csResource) }, en: { ${vertical.namespace}: flattenLocaleResource(enResource) }, } as const; -`, +` ); await writeFixtureFile( root, `verticals/${vertical.slug}/src/routes/ultramodern-route-head.tsx`, - 'export const UltramodernRouteHead = () => null;\n', + 'export const UltramodernRouteHead = () => null;\n' ); }; const createFixture = async (): Promise => { const root = await mkdtemp(path.join(tmpdir(), 'ontos-scaffolding-')); - await writeFixtureFile(root, rootPackageFile, json({ name: 'fixture', private: true })); + await writeFixtureFile( + root, + rootPackageFile, + json({ name: 'fixture', private: true }) + ); await writeFixtureFile( root, coreRuntimeIndexFile, - `export const existingCoreSurface = true;\n\n// \n// \n\n// \n// \n`, + `export const existingCoreSurface = true;\n\n// \n// \n\n// \n// \n` ); await writeFixtureFile( root, @@ -694,9 +770,13 @@ export const coreActionCatalog = [ // // ]; -`, +` + ); + await writeFixtureFile( + root, + shellSentinelFile, + 'export const shell = true;\n' ); - await writeFixtureFile(root, shellSentinelFile, 'export const shell = true;\n'); await writeFixtureFile( root, shellVerticalClientsFile, @@ -704,7 +784,7 @@ export const coreActionCatalog = [ // @ontos-codegen-start shell-page-clients // @ontos-codegen-end shell-page-clients ] as const; -`, +` ); await createVertical(root, inventoryVertical); await createVertical(root, billingVertical); @@ -715,20 +795,23 @@ export const coreActionCatalog = [ topologyFile, json({ schemaVersion: 1, - verticals: [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( - (vertical) => ({ - domain: vertical.namespace, - id: vertical.appId, - kind: 'vertical', - moduleFederation: { - name: vertical.mfBoundaryId, - role: 'remote', - }, - package: `@app/${vertical.slug}`, - path: `verticals/${vertical.slug}`, - }), - ), - }), + verticals: [ + inventoryVertical, + billingVertical, + hrVertical, + contactsVertical, + ].map((vertical) => ({ + domain: vertical.namespace, + id: vertical.appId, + kind: 'vertical', + moduleFederation: { + name: vertical.mfBoundaryId, + role: 'remote', + }, + package: `@app/${vertical.slug}`, + path: `verticals/${vertical.slug}`, + })), + }) ); await Promise.all( [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( @@ -738,14 +821,16 @@ export const coreActionCatalog = [ [scaffoldFlag.vertical, vertical.slug, '--module', vertical.moduleId], { workspaceRoot: root, - }, - ), - ), + } + ) + ) ); return { root }; }; -const withFixture = async (run: (fixture: Fixture) => Promise): Promise => { +const withFixture = async ( + run: (fixture: Fixture) => Promise +): Promise => { const fixture = await createFixture(); try { await run(fixture); @@ -754,7 +839,9 @@ const withFixture = async (run: (fixture: Fixture) => Promise): Promise>> => { +const snapshotTree = async ( + root: string +): Promise>> => { const snapshot: Record = {}; const visit = async (directory: string): Promise => { const entries = await readdir(directory, { withFileTypes: true }); @@ -766,17 +853,22 @@ const snapshotTree = async (root: string): Promise => - await readFile(path.join(root, relativePath), 'utf-8'); +const readFixtureFile = async ( + root: string, + relativePath: string +): Promise => await readFile(path.join(root, relativePath), 'utf-8'); const contextPermissionCommands = new Set([ scaffoldCommand.microverticalPage, @@ -790,7 +882,7 @@ const run = async ( fixture: Fixture, command: ScaffoldCommand, scaffoldArguments: readonly string[], - routeRefresh?: (appId: string) => void, + routeRefresh?: (appId: string) => void ) => await runScaffold( command, @@ -813,7 +905,11 @@ const run = async ( 'tenant_membership_default', ]; } else if (command === scaffoldCommand.outboxWorker) { - flags = [...flags, scaffoldFlag.authorization, 'owner_local_background']; + flags = [ + ...flags, + scaffoldFlag.authorization, + 'owner_local_background', + ]; } else if (contextPermissionCommands.has(command)) { flags = [ ...flags, @@ -829,7 +925,7 @@ const run = async ( { routeRefresh: ({ appId }) => routeRefresh?.(appId), workspaceRoot: fixture.root, - }, + } ); /** @@ -840,14 +936,16 @@ const assertScaffoldRefused = async ( fixture: Fixture, command: ScaffoldCommand, commandArguments: readonly string[], - expected: RegExp, + expected: RegExp ): Promise => { const before = await snapshotTree(fixture.root); await assert.rejects(run(fixture, command, commandArguments), expected); assert.deepEqual(await snapshotTree(fixture.root), before); }; -const addInventoryItemResourceType = async (fixture: Fixture): Promise => { +const addInventoryItemResourceType = async ( + fixture: Fixture +): Promise => { const manifestPath = path.join(fixture.root, inventoryManifestFile); const manifest = await readFile(manifestPath, 'utf-8'); await writeFile( @@ -868,9 +966,9 @@ const addInventoryItemResourceType = async (fixture: Fixture): Promise => label: 'Inventory item', owningModuleId: 'inventory.stock', }, - ],`, + ],` ), - 'utf-8', + 'utf-8' ); }; @@ -899,25 +997,31 @@ test('documents every command and treats --help as a write-free operation', asyn }); assert.deepEqual(result, { help: getHelpText(command), kind: 'help' }); assert.match(result.help, new RegExp(`scaffold:${command}`, 'u')); - }), + }) ); assert.match(getHelpText('action'), /--vertical /u); assert.match(getHelpText('action'), /--scope core --module /u); assert.match(getHelpText(scaffoldCommand.microverticalPage), /--url /u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /defaults to \/\//u); + assert.match( + getHelpText(scaffoldCommand.microverticalPage), + /defaults to \/\//u + ); assert.match(getHelpText(scaffoldCommand.microverticalPage), /:parameter/u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /\/contacts\/customers\/:id\/edit/u); + assert.match( + getHelpText(scaffoldCommand.microverticalPage), + /\/contacts\/customers\/:id\/edit/u + ); assert.match( getHelpText(scaffoldCommand.externalHttpAdapter), - /scaffold:external-http-adapter -- --vertical --provider --operation /u, + /scaffold:external-http-adapter -- --vertical --provider --operation /u ); assert.match( getHelpText(scaffoldCommand.externalHttpAdapter), - /--vertical contacts --provider ares --operation subject/u, + /--vertical contacts --provider ares --operation subject/u ); assert.match( getHelpText(scaffoldCommand.searchProviderAccess), - /--tenant-permission read_party_identity/u, + /--tenant-permission read_party_identity/u ); }); @@ -957,12 +1061,18 @@ test('search-provider access updates only generated access metadata and fails at ]); assert.match( manifest, - /accessFiltering: 'tenant_scope'.*requestFilters: \['includeArchived'\].*tenantPermission: 'read_party_identity'/u, + /accessFiltering: 'tenant_scope'.*requestFilters: \['includeArchived'\].*tenantPermission: 'read_party_identity'/u ); assert.match(provider, /legalEntityScope: 'optional'/u); assert.match(provider, /permissionTarget: 'tenant'/u); - assert.match(provider, /kind: 'tenant', permission: 'read_party_identity'/u); - assert.match(contract, /includeArchived: Schema\.optionalKey\(Schema\.Boolean\)/u); + assert.match( + provider, + /kind: 'tenant', permission: 'read_party_identity'/u + ); + assert.match( + contract, + /includeArchived: Schema\.optionalKey\(Schema\.Boolean\)/u + ); const beforeProviderRerun = await snapshotTree(fixture.root); await run(fixture, scaffoldCommand.searchProvider, [ @@ -978,7 +1088,7 @@ test('search-provider access updates only generated access metadata and fails at const providerPath = path.join(fixture.root, inventorySearchProviderFile); await writeFile( providerPath, - `${provider}\n// Owner-customized searchable semantics remain untouched.\n`, + `${provider}\n// Owner-customized searchable semantics remain untouched.\n` ); const beforeIdempotentUpdate = await snapshotTree(fixture.root); await run(fixture, scaffoldCommand.searchProviderAccess, [ @@ -998,7 +1108,7 @@ test('search-provider access updates only generated access metadata and fails at assert.deepEqual(await snapshotTree(fixture.root), beforeIdempotentUpdate); await writeFile( providerPath, - provider.replace('// @generated by OntOS Codesmith ', '// custom '), + provider.replace('// @generated by OntOS Codesmith ', '// custom ') ); await assertScaffoldRefused( fixture, @@ -1015,7 +1125,7 @@ test('search-provider access updates only generated access metadata and fails at scaffoldFlag.requestFilters, 'includeArchived,role', ], - /Codesmith-owned provider/u, + /Codesmith-owned provider/u ); }); }); @@ -1036,11 +1146,14 @@ test('generated API owner slots sort property keys before suffix variants', asyn ]); const sources = await Promise.all( [inventoryManifestFile, inventoryRegistrationFile].map( - async (owner) => await readFixtureFile(fixture.root, owner), - ), + async (owner) => await readFixtureFile(fixture.root, owner) + ) ); for (const source of sources) { - assert.ok(source.indexOf("'party-match':") < source.indexOf("'party-match-decision':")); + assert.ok( + source.indexOf("'party-match':") < + source.indexOf("'party-match-decision':") + ); } }); }); @@ -1079,17 +1192,17 @@ test('generated read clients fetch mounted owner URLs and support separately dep await symlink( path.join(appRoot, sharedContractsPackagePath), path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, effectNodeModulePath), path.join(fixture.root, effectNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, pluginBffNodeModulePath), path.join(fixture.root, pluginBffNodeModulePath), - 'dir', + 'dir' ); const result = spawnSync( process.execPath, @@ -1201,9 +1314,10 @@ test('generated read clients fetch mounted owner URLs and support separately dep console.log(JSON.stringify({ calls, endpointRequestsAfterGatewayFailure, gatewayAttempts, gatewayUnavailable })); `, ], - { cwd: fixture.root, encoding: 'utf-8' }, + { cwd: fixture.root, encoding: 'utf-8' } ); - assert.equal(result.status, 0, result.stderr || result.error?.message); + assert.ifError(result.error); + assert.equal(result.status, 0, result.stderr); const proof = Schema.decodeUnknownSync( Schema.fromJsonString( Schema.Struct({ @@ -1211,8 +1325,8 @@ test('generated read clients fetch mounted owner URLs and support separately dep endpointRequestsAfterGatewayFailure: Schema.Number, gatewayAttempts: Schema.Number, gatewayUnavailable: Schema.Boolean, - }), - ), + }) + ) )(result.stdout); assert.deepEqual( proof.calls, @@ -1231,7 +1345,7 @@ test('generated read clients fetch mounted owner URLs and support separately dep correlationId: 'correlation-proof', method: 'POST', url, - })), + })) ); assert.equal(proof.gatewayAttempts, 4); assert.equal(proof.gatewayUnavailable, true); @@ -1262,15 +1376,18 @@ test('all live Party read and search transports match actual scaffold output', a ]); const owner = path.join(appRoot, 'verticals/party-registry'); const ownerFiles = await readdir(path.join(owner, 'api')); - const serverNames = ownerFiles.filter((name) => /-(?:read|search)-server\.ts$/u.test(name)); + const serverNames = ownerFiles.filter((name) => + /-(?:read|search)-server\.ts$/u.test(name) + ); assert.equal(serverNames.length, 18); await Promise.all( serverNames.map(async (serverName) => { const search = serverName.endsWith('-search-server.ts'); const suffix = search ? 'search' : 'read'; const name = serverName.slice(0, -`-${suffix}-server.ts`.length); - const camel = name.replaceAll(/-(?[a-z])/gu, (_, letter: string) => - letter.toUpperCase(), + const camel = name.replaceAll( + /-(?[a-z])/gu, + (_, letter: string) => letter.toUpperCase() ); const pascal = `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`; const fixtureNameValue = search ? 'inventory-items' : 'resource-detail'; @@ -1283,50 +1400,71 @@ test('all live Party read and search transports match actual scaffold output', a .replaceAll(`/${name}`, `/${fixtureNameValue}`) .replaceAll(pascal, fixturePascal) .replaceAll(camel, fixtureCamel) - .replaceAll(`${fixturePascal}SearchClientOptions`, `${fixturePascal}ClientOptions`) + .replaceAll( + `${fixturePascal}SearchClientOptions`, + `${fixturePascal}ClientOptions` + ) .replaceAll('partyRegistryApi', 'fixtureApi') - .replaceAll('/party-registry-api', '/inventory-stock-api'), + .replaceAll('/party-registry-api', '/inventory-stock-api') ); const expectedServer = await readFixtureFile( fixture.root, - `verticals/inventory-stock/api/${fixtureNameValue}-${suffix}-server.ts`, + `verticals/inventory-stock/api/${fixtureNameValue}-${suffix}-server.ts` ); const expectedClient = await readFixtureFile( fixture.root, - `verticals/inventory-stock/src/api/${fixtureNameValue}${search ? '-search' : ''}-client.ts`, + `verticals/inventory-stock/src/api/${fixtureNameValue}${search ? '-search' : ''}-client.ts` ); assert.equal( - normalize(await readFile(path.join(owner, 'api', serverName), 'utf-8')), + normalize( + await readFile(path.join(owner, 'api', serverName), 'utf-8') + ), compactGovernedSource(expectedServer), - serverName, + serverName ); assert.equal( - normalize(await readFile(path.join(owner, 'src/api', clientName), 'utf-8')), + normalize( + await readFile(path.join(owner, 'src/api', clientName), 'utf-8') + ), compactGovernedSource(expectedClient), - clientName, + clientName ); - }), + }) + ); + const sharedApi = await readFixtureFile( + fixture.root, + inventorySharedApiFile ); - const sharedApi = await readFixtureFile(fixture.root, inventorySharedApiFile); assert.doesNotMatch(sharedApi, /governedHttpApi/u); }); }); test('the migrated Party governed API slot accepts future generated additions', async () => { - const source = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); + const source = await readFile( + path.join(appRoot, partyGovernedContractPath), + 'utf-8' + ); const next = insertSortedSlot( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ['.addHttpApi(FutureReadApi)'], - (candidate) => candidate.startsWith('.addHttpApi(') && candidate.endsWith(')'), + (candidate) => + candidate.startsWith('.addHttpApi(') && candidate.endsWith(')') ); assert.match(next, /\.addHttpApi\(FutureReadApi\)/u); }); -const requiredGeneratedSlot = (source: string, start: string, end: string): string => { +const requiredGeneratedSlot = ( + source: string, + start: string, + end: string +): string => { const slot = new RegExp(`${start}[\\s\\S]*?${end}`, 'u').exec(source)?.[0]; - assert.ok(slot !== undefined, `expected the generated slot between ${start} and ${end}`); + assert.ok( + slot !== undefined, + `expected the generated slot between ${start} and ${end}` + ); return slot; }; @@ -1338,19 +1476,24 @@ const assertRelocatedSlotRefused = async ( fixture: Fixture, file: string, validSource: string, - [slotStart, slotEnd]: readonly [string, string], + [slotStart, slotEnd]: readonly [string, string] ): Promise => { const slot = requiredGeneratedSlot(validSource, slotStart, slotEnd); await writeFile( file, `${validSource.replace(slot, '')}\nconst relocatedSlot = String.raw\`${slot}\`;\n`, - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /composition slots are not bound/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ], + /composition slots are not bound/u ); await writeFile(file, validSource, 'utf-8'); }; @@ -1363,11 +1506,11 @@ const assertGovernedReadClients = (clients: readonly string[]): void => { assert.match(client, /WithAuthorization/u); assert.match( client, - /credential,\s+defaultApiPrefix: '\/inventory-stock-api',\s+requestCorrelation,/u, + /credential,\s+defaultApiPrefix: '\/inventory-stock-api',\s+requestCorrelation,/u ); assert.doesNotMatch( client, - /makeEffectHttpApiClient|Context\.Reference|HttpClientRequest|HttpClient\.mapRequest/u, + /makeEffectHttpApiClient|Context\.Reference|HttpClientRequest|HttpClient\.mapRequest/u ); } }; @@ -1376,44 +1519,67 @@ const assertGovernedReadProviders = (providers: readonly string[]): void => { assert.match(provider, /defineRead\(/u); assert.match(provider, /legalEntityScope: 'required'/u); assert.match(provider, /permissionTarget: 'module'/u); - assert.doesNotMatch(provider, /CoreDatabase|ScopedTransactionExecutor|from 'pg'/u); + assert.doesNotMatch( + provider, + /CoreDatabase|ScopedTransactionExecutor|from 'pg'/u + ); } }; const assertGovernedReadServers = (servers: readonly string[]): void => { for (const server of servers) { assert.match(server, /makeGovernedReadHttpHandler\(\{/u); - assert.match(server, /authenticatePrincipal: authenticateOperationPrincipal/u); + assert.match( + server, + /authenticatePrincipal: authenticateOperationPrincipal/u + ); assert.match(server, /registration: \w+Read/u); - assert.doesNotMatch(server, /ReadRuntime|Match\.tags|catchTags|bearerChallenge/u); - assert.doesNotMatch(server, /tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u); + assert.doesNotMatch( + server, + /ReadRuntime|Match\.tags|catchTags|bearerChallenge/u + ); + assert.doesNotMatch( + server, + /tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u + ); } }; -const assertComposedGovernedReads = (composedApi: string, composedHandlers: string): void => { +const assertComposedGovernedReads = ( + composedApi: string, + composedHandlers: string +): void => { for (const [contract, layer] of [ ['InventoryItemsSearchApi', 'inventoryItemsReadApiLive'], ['ResourceDetailApi', 'resourceDetailReadApiLive'], ['StockLevelsReportApi', 'stockLevelsReadApiLive'], ] as const) { assert.match(composedApi, new RegExp(`import \\{ ${contract} \\}`, 'u')); - assert.match(composedApi, new RegExp(`\\.addHttpApi\\(${contract}\\)`, 'u')); + assert.match( + composedApi, + new RegExp(`\\.addHttpApi\\(${contract}\\)`, 'u') + ); assert.match(composedHandlers, new RegExp(`import \\{ ${layer} \\}`, 'u')); assert.match( composedHandlers, new RegExp( `${layer}\\.pipe\\([\\s\\S]*?GovernedReadLayer\\.provide\\(governedReadRuntimeLive\\)`, - 'u', - ), + 'u' + ) ); } }; -const assertGovernedProblemDetailsContracts = (contracts: readonly string[]): void => { +const assertGovernedProblemDetailsContracts = ( + contracts: readonly string[] +): void => { for (const contract of contracts) { assert.match( contract, - /import \{\s*makeProblemDetailsSchema,\s*makeRetryableProblemDetailsSchema,?\s*\} from '@app\/shared-contracts\/problem-details';/u, + /import \{\s*makeProblemDetailsSchema,\s*makeRetryableProblemDetailsSchema,?\s*\} from '@app\/shared-contracts\/problem-details';/u ); assert.match(contract, /makeProblemDetailsSchema\([^)]*,\s*409,?\s*\)/u); - assert.match(contract, /makeRetryableProblemDetailsSchema\([^)]*,\s*503,?\s*\)/u); + assert.match( + contract, + /makeRetryableProblemDetailsSchema\([^)]*,\s*503,?\s*\)/u + ); assert.doesNotMatch(contract, /application\/problem\+json|HttpApiSchema/u); } }; @@ -1477,55 +1643,112 @@ test('governed contribution generators patch owner contracts and lazy adapters a readFixtureFile(fixture.root, inventoryRegistrationFile), readFixtureFile(fixture.root, inventoryFederationConfigFile), ]); - assert.match(nextManifest, /inventory\.stock\.component\.inventory-summary/u); + assert.match( + nextManifest, + /inventory\.stock\.component\.inventory-summary/u + ); assert.match(nextManifest, /inventory\.stock\.search\.inventory-items/u); assert.match(nextManifest, /inventory\.stock\.report\.stock-levels/u); - assert.match(registration, /import\('\.\/src\/api\/resource-detail-client\.ts'\)/u); - assert.match(registration, /import\('\.\/src\/api\/inventory-items-search-client\.ts'\)/u); - assert.match(registration, /import\('\.\/src\/api\/stock-levels-report-client\.ts'\)/u); + assert.match( + registration, + /import\('\.\/src\/api\/resource-detail-client\.ts'\)/u + ); + assert.match( + registration, + /import\('\.\/src\/api\/inventory-items-search-client\.ts'\)/u + ); + assert.match( + registration, + /import\('\.\/src\/api\/stock-levels-report-client\.ts'\)/u + ); assert.match(federation, /\.\/InventoryAlerts/u); assert.match(federation, /\.\/InventorySummary/u); assert.doesNotMatch(nextManifest, /import\('/u); const searchClient = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/api/inventory-items-search-client.ts', + 'verticals/inventory-stock/src/api/inventory-items-search-client.ts' + ); + const reportClient = await readFixtureFile( + fixture.root, + inventoryReportClientFile ); - const reportClient = await readFixtureFile(fixture.root, inventoryReportClientFile); assert.match(searchClient, /export interface InventoryItemsClientOptions/u); - assert.doesNotMatch(searchClient, /export interface InventoryItemsSearchClientOptions/u); + assert.doesNotMatch( + searchClient, + /export interface InventoryItemsSearchClientOptions/u + ); assert.match(reportClient, /export interface StockLevelsClientOptions/u); - assert.doesNotMatch(reportClient, /export interface StockLevelsReportClientOptions/u); - const moduleApiClient = await readFixtureFile(fixture.root, inventoryModuleApiClientFile); - const moduleApiContract = await readFixtureFile(fixture.root, inventoryModuleApiContractFile); + assert.doesNotMatch( + reportClient, + /export interface StockLevelsReportClientOptions/u + ); + const moduleApiClient = await readFixtureFile( + fixture.root, + inventoryModuleApiClientFile + ); + const moduleApiContract = await readFixtureFile( + fixture.root, + inventoryModuleApiContractFile + ); const secondModuleApiContract = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/shared/apis/resource-history.ts', + 'verticals/inventory-stock/shared/apis/resource-history.ts' ); const secondModuleApiClient = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/api/resource-history-client.ts', - ); - const searchProvider = await readFixtureFile(fixture.root, inventorySearchProviderFile); - const reportProvider = await readFixtureFile(fixture.root, inventoryReportProviderFile); - const moduleApiRead = await readFixtureFile(fixture.root, inventoryModuleApiReadFile); - const searchServer = await readFixtureFile(fixture.root, inventorySearchServerFile); - const reportServer = await readFixtureFile(fixture.root, inventoryReportServerFile); - const moduleApiServer = await readFixtureFile(fixture.root, inventoryModuleApiServerFile); - const operationBoundary = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - const composedApi = await readFixtureFile(fixture.root, inventorySharedApiFile); - const composedHandlers = await readFixtureFile(fixture.root, inventoryHandlerRootFile); + 'verticals/inventory-stock/src/api/resource-history-client.ts' + ); + const searchProvider = await readFixtureFile( + fixture.root, + inventorySearchProviderFile + ); + const reportProvider = await readFixtureFile( + fixture.root, + inventoryReportProviderFile + ); + const moduleApiRead = await readFixtureFile( + fixture.root, + inventoryModuleApiReadFile + ); + const searchServer = await readFixtureFile( + fixture.root, + inventorySearchServerFile + ); + const reportServer = await readFixtureFile( + fixture.root, + inventoryReportServerFile + ); + const moduleApiServer = await readFixtureFile( + fixture.root, + inventoryModuleApiServerFile + ); + const operationBoundary = await readFixtureFile( + fixture.root, + inventoryActionPrincipalFile + ); + const composedApi = await readFixtureFile( + fixture.root, + inventorySharedApiFile + ); + const composedHandlers = await readFixtureFile( + fixture.root, + inventoryHandlerRootFile + ); assert.match(searchClient, /api: InventoryItemsSearchApi,/u); assert.match(reportClient, /api: StockLevelsReportApi,/u); assert.match( moduleApiContract, - /headers: \{\},\s+params: \{\},\s+payload: ResourceDetailRequestSchema,\s+query: \{\}/u, + /headers: \{\},\s+params: \{\},\s+payload: ResourceDetailRequestSchema,\s+query: \{\}/u ); assert.match( moduleApiClient, - /client\.resourceDetail\.execute\(\{\s+headers: \{\},\s+params: \{\},\s+payload,\s+query: \{\},?\s+\}\)/u, + /client\.resourceDetail\.execute\(\{\s+headers: \{\},\s+params: \{\},\s+payload,\s+query: \{\},?\s+\}\)/u ); assert.match(moduleApiContract, /HttpApiGroup\.make\('resourceDetail'\)/u); - assert.match(secondModuleApiContract, /HttpApiGroup\.make\('resourceHistory'\)/u); + assert.match( + secondModuleApiContract, + /HttpApiGroup\.make\('resourceHistory'\)/u + ); assert.match(secondModuleApiClient, /client\.resourceHistory\.execute\(/u); assertGovernedReadClients([moduleApiClient, searchClient, reportClient]); assert.doesNotMatch(searchClient, /\.provider\.ts|import\(/u); @@ -1535,51 +1758,67 @@ test('governed contribution generators patch owner contracts and lazy adapters a assert.match(moduleApiRead, /defineRead\(/u); assert.match(moduleApiRead, /legalEntityScope: 'required'/u); assertGovernedReadServers([moduleApiServer, searchServer, reportServer]); - assert.match(operationBoundary, /export const authenticateOperationPrincipal/u); + assert.match( + operationBoundary, + /export const authenticateOperationPrincipal/u + ); assertComposedGovernedReads(composedApi, composedHandlers); - const searchContract = await readFixtureFile(fixture.root, inventorySearchContractFile); + const searchContract = await readFixtureFile( + fixture.root, + inventorySearchContractFile + ); const reportContract = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', + 'verticals/inventory-stock/shared/apis/stock-levels-report.ts' ); - assertGovernedProblemDetailsContracts([moduleApiContract, searchContract, reportContract]); + assertGovernedProblemDetailsContracts([ + moduleApiContract, + searchContract, + reportContract, + ]); assert.match( searchContract, - /HttpApiEndpoint\.post\('execute', '\/inventory\.stock\/search\/inventory-items'/u, + /HttpApiEndpoint\.post\('execute', '\/inventory\.stock\/search\/inventory-items'/u ); assert.doesNotMatch(searchContract, /tenantId|legalEntityId|principalId/u); assert.match(searchContract, /PolicyConflictProblem/u); assert.match( searchContract, - /makeProblemDetailsSchema\(\s*'InventoryItemsProviderPolicyConflictProblem',\s*409,?\s*\)/u, + /makeProblemDetailsSchema\(\s*'InventoryItemsProviderPolicyConflictProblem',\s*409,?\s*\)/u + ); + assert.match( + searchContract, + /HttpApiGroup\.make\('inventoryItemsSearch'\)/u ); - assert.match(searchContract, /HttpApiGroup\.make\('inventoryItemsSearch'\)/u); await mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true, }); - await mkdir(path.dirname(path.join(fixture.root, pluginBffNodeModulePath)), { - recursive: true, - }); + await mkdir( + path.dirname(path.join(fixture.root, pluginBffNodeModulePath)), + { + recursive: true, + } + ); await symlink( path.join(appRoot, 'packages/core-runtime'), path.join(fixture.root, 'node_modules/@app/core-runtime'), - 'dir', + 'dir' ); await symlink( path.join(appRoot, sharedContractsPackagePath), path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, effectNodeModulePath), path.join(fixture.root, effectNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, pluginBffNodeModulePath), path.join(fixture.root, pluginBffNodeModulePath), - 'dir', + 'dir' ); await writeFixtureFile( fixture.root, @@ -1599,7 +1838,7 @@ export const authenticateOperationPrincipal = (authorization, problems) => Redacted.value(authorization) === 'Bearer proof' ? Effect.succeed(principal) : Effect.fail(problems.authentication()); -`, +` ); await writeFixtureFile( fixture.root, @@ -1701,7 +1940,7 @@ try { await generatedServer.dispose(); await server.dispose(); } -`, +` ); const execution = spawnSync( process.execPath, @@ -1710,10 +1949,12 @@ try { cwd: fixture.root, encoding: 'utf-8', env: { - DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', - DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', + DATABASE_ADMIN_URL: + 'postgresql://ontos_admin:admin@localhost:5433/ontos', + DATABASE_URL: + 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', }, - }, + } ); assert.equal(execution.status, 0, execution.stderr); const expectedGeneratedPrincipal = { @@ -1722,7 +1963,9 @@ try { principalId: '00000000-0000-4000-8000-000000000001', tenantId: '00000000-0000-4000-8000-000000000002', }; - const expectedGeneratedTransport = { correlationId: 'generated-correlation' }; + const expectedGeneratedTransport = { + correlationId: 'generated-correlation', + }; const lastGeneratedLine = execution.stdout.trim().split('\n').at(-1); assert.ok(lastGeneratedLine !== undefined); assert.deepEqual(JSON.parse(lastGeneratedLine), { @@ -1765,11 +2008,18 @@ try { // Restore the generated contract after the execution-only authentication stub. Reruns // must validate the real owned boundary, not silently accept handwritten fixture code. - await writeFixtureFile(fixture.root, inventoryActionPrincipalFile, operationBoundary); + await writeFixtureFile( + fixture.root, + inventoryActionPrincipalFile, + operationBoundary + ); const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), + await readFixtureFile(fixture.root, inventoryPackageFile) + ); + assert.equal( + packageJson.dependencies['@app/shared-contracts'], + workspaceVersion ); - assert.equal(packageJson.dependencies['@app/shared-contracts'], workspaceVersion); // Owner contracts, reads, and clients remain adaptable; thin HTTP adapters stay generator-owned. const adaptedGeneratedArtifacts = [ @@ -1781,7 +2031,10 @@ try { inventoryModuleApiReadFile, 'export const resourceDetailOwnerProjection = (value: string) => value;', ], - [inventoryModuleApiClientFile, '// Owner-maintained client documentation.'], + [ + inventoryModuleApiClientFile, + '// Owner-maintained client documentation.', + ], [ inventorySearchProviderFile, 'export const inventoryItemsOwnerRanking = (score: number) => score;', @@ -1790,7 +2043,10 @@ try { inventorySearchContractFile, 'export const InventoryItemsOwnerFilterSchema = Schema.Struct({ tag: Schema.String });', ], - [inventorySearchClientFile, '// Owner-maintained search client documentation.'], + [ + inventorySearchClientFile, + '// Owner-maintained search client documentation.', + ], [ inventoryReportProviderFile, 'export const stockLevelsOwnerProjection = (column: string) => column;', @@ -1799,16 +2055,26 @@ try { inventoryReportContractFile, 'export const StockLevelsOwnerColumnSchema = Schema.Struct({ column: Schema.String });', ], - [inventoryReportClientFile, '// Owner-maintained report client documentation.'], + [ + inventoryReportClientFile, + '// Owner-maintained report client documentation.', + ], ] as const; await Promise.all( adaptedGeneratedArtifacts.map(async ([relativePath, ownerAddition]) => { const generated = await readFixtureFile(fixture.root, relativePath); - await writeFixtureFile(fixture.root, relativePath, `${generated}\n${ownerAddition}\n`); - }), + await writeFixtureFile( + fixture.root, + relativePath, + `${generated}\n${ownerAddition}\n` + ); + }) ); - const adaptedManifest = await readFixtureFile(fixture.root, inventoryManifestFile); + const adaptedManifest = await readFixtureFile( + fixture.root, + inventoryManifestFile + ); assert.match(adaptedManifest, /dimensions: \[\]/u); assert.match(adaptedManifest, /label: 'Stock Levels'/u); await writeFixtureFile( @@ -1816,7 +2082,7 @@ try { inventoryManifestFile, adaptedManifest .replace('dimensions: []', "dimensions: ['warehouse']") - .replace("label: 'Stock Levels'", "label: 'Warehouse stock'"), + .replace("label: 'Stock Levels'", "label: 'Warehouse stock'") ); const beforeRepeat = await snapshotTree(fixture.root); @@ -1850,7 +2116,7 @@ try { '--name', 'inventory-summary', ]), - /refusing to overwrite/u, + /refusing to overwrite/u ); assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); for (const generated of [ @@ -1884,11 +2150,19 @@ try { ]); // eslint-disable-next-line no-await-in-loop assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - const serverPath = path.join(fixture.root, 'verticals/inventory-stock/api', generated.server); + const serverPath = path.join( + fixture.root, + 'verticals/inventory-stock/api', + generated.server + ); // eslint-disable-next-line no-await-in-loop const ownedServer = await readFile(serverPath, 'utf-8'); // eslint-disable-next-line no-await-in-loop - await writeFile(serverPath, `${ownedServer}// owner customization\n`, 'utf-8'); + await writeFile( + serverPath, + `${ownedServer}// owner customization\n`, + 'utf-8' + ); // eslint-disable-next-line no-await-in-loop await assert.rejects( run(fixture, generated.command, [ @@ -1898,7 +2172,7 @@ try { generated.name, ...(generated.resource ? [scaffoldFlag.resource, 'item'] : []), ]), - /refusing to overwrite/u, + /refusing to overwrite/u ); // eslint-disable-next-line no-await-in-loop await writeFile(serverPath, ownedServer, 'utf-8'); @@ -1909,15 +2183,20 @@ try { sharedApiPath, validSharedApi.replace( '// ', - 'ownerCustomLayer()\n // ', + 'ownerCustomLayer()\n // ' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /composition slots are not bound|unsupported developer content/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ], + /composition slots are not bound|unsupported developer content/u ); await writeFile(sharedApiPath, validSharedApi, 'utf-8'); @@ -1928,20 +2207,20 @@ try { const registrationPath = path.join( fixture.root, - 'verticals/inventory-stock/vertical.registration.ts', + 'verticals/inventory-stock/vertical.registration.ts' ); const validRegistration = await readFile(registrationPath, 'utf-8'); const resourceDetailRegistration = " 'resource-detail': () => import('./src/api/resource-detail-client.ts'),\n"; const wrongCategoryRegistration = validRegistration.replace( '// ', - `${resourceDetailRegistration} // `, + `${resourceDetailRegistration} // ` ); for (const invalidRegistration of [ wrongCategoryRegistration, wrongCategoryRegistration.replace( /^\s*'resource-detail': \(\) => import\('\.\/src\/api\/resource-detail-client\.ts'\),\n/mu, - '', + '' ), ]) { // eslint-disable-next-line no-await-in-loop @@ -1950,8 +2229,13 @@ try { await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /wrong contribution category/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ], + /wrong contribution category/u ); } await writeFile(registrationPath, validRegistration, 'utf-8'); @@ -1962,21 +2246,31 @@ try { handlerRootPath, validHandlerRoot.replace( /resourceDetailReadApiLive\.pipe\(\s*GovernedReadLayer\.provide\(governedReadRuntimeLive\),?\s*\),/u, - 'resourceDetailReadApiLive.pipe(\n GovernedReadLayer.provide(governedReadRuntimeLive),\n GovernedReadLayer.provide(ownerCustomizedRuntime),\n ),', + 'resourceDetailReadApiLive.pipe(\n GovernedReadLayer.provide(governedReadRuntimeLive),\n GovernedReadLayer.provide(ownerCustomizedRuntime),\n ),' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /contains drift/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ], + /contains drift/u ); await writeFile(handlerRootPath, validHandlerRoot, 'utf-8'); - await assertRelocatedSlotRefused(fixture, handlerRootPath, validHandlerRoot, [ - GOVERNED_HTTP_HANDLER_LAYER_SLOT_START, - GOVERNED_HTTP_HANDLER_LAYER_SLOT_END, - ]); + await assertRelocatedSlotRefused( + fixture, + handlerRootPath, + validHandlerRoot, + [ + GOVERNED_HTTP_HANDLER_LAYER_SLOT_START, + GOVERNED_HTTP_HANDLER_LAYER_SLOT_END, + ] + ); await assert.rejects( run(fixture, scaffoldCommand.moduleApi, [ scaffoldFlag.vertical, @@ -1984,12 +2278,12 @@ try { '--name', '../unsafe', ]), - /lower-kebab-case/u, + /lower-kebab-case/u ); assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); const billingFederationPath = path.join( fixture.root, - 'verticals/billing/module-federation.config.ts', + 'verticals/billing/module-federation.config.ts' ); await writeFile( billingFederationPath, @@ -2003,7 +2297,7 @@ export default { }; void ignored; `, - 'utf-8', + 'utf-8' ); await run(fixture, scaffoldCommand.publicComponent, [ scaffoldFlag.vertical, @@ -2011,7 +2305,10 @@ void ignored; '--name', 'billing-summary', ]); - const commentSafeFederation = await readFile(billingFederationPath, 'utf-8'); + const commentSafeFederation = await readFile( + billingFederationPath, + 'utf-8' + ); assert.match(commentSafeFederation, /\/exposes: \\\{\\\}\/u/u); assert.match(commentSafeFederation, /\.\/BillingSummary/u); await writeFile(billingFederationPath, 'export default {};\n', 'utf-8'); @@ -2019,7 +2316,7 @@ void ignored; fixture, scaffoldCommand.publicComponent, [scaffoldFlag.vertical, 'billing', '--name', 'billing-details'], - /exposes object is missing/u, + /exposes object is missing/u ); }); }); @@ -2033,81 +2330,115 @@ test('governed contribution reruns cannot be spoofed by comments or corrupt owne fixtureName.resourceDetail, ] as const; await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const apiContract = await readFixtureFile(fixture.root, inventoryModuleApiContractFile); + const apiContract = await readFixtureFile( + fixture.root, + inventoryModuleApiContractFile + ); const assertInvalidApiContractRerunRejected = async ( - invalidApiContract: string, + invalidApiContract: string ): Promise => { - await writeFixtureFile(fixture.root, inventoryModuleApiContractFile, invalidApiContract); + await writeFixtureFile( + fixture.root, + inventoryModuleApiContractFile, + invalidApiContract + ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, scaffoldArguments, - /refusing to overwrite existing business file/u, + /refusing to overwrite existing business file/u ); }; await assertInvalidApiContractRerunRejected( - apiContract.replace('/reads/resource-detail', '/reads/wrong'), + apiContract.replace('/reads/resource-detail', '/reads/wrong') ); await assertInvalidApiContractRerunRejected( apiContract.replace( "HttpApiEndpoint.post('execute', '/reads/resource-detail', {", - "HttpApiEndpoint.post('wrong', '/reads/resource-detail', {", - ), + "HttpApiEndpoint.post('wrong', '/reads/resource-detail', {" + ) ); await assertInvalidApiContractRerunRejected( apiContract.replace( /\.add\(\n {2}HttpApiGroup\.make\('resourceDetail'\)\.add\([\s\S]*?\n {2}\),\n\);\n$/u, - ".add(HttpApiGroup.make('resourceDetail'));\n", - ), + ".add(HttpApiGroup.make('resourceDetail'));\n" + ) + ); + await writeFixtureFile( + fixture.root, + inventoryModuleApiContractFile, + apiContract ); - await writeFixtureFile(fixture.root, inventoryModuleApiContractFile, apiContract); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const ownerImport = "import { ResourceDetailApi } from './shared/apis/resource-detail.ts';"; + const ownerImport = + "import { ResourceDetailApi } from './shared/apis/resource-detail.ts';"; await writeFixtureFile( fixture.root, inventoryManifestFile, - `${manifest.replace(ownerImport, '')}\n/* ${ownerImport} */\n`, + `${manifest.replace(ownerImport, '')}\n/* ${ownerImport} */\n` ); await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const repairedManifest = await readFixtureFile(fixture.root, inventoryManifestFile); - assert.equal(repairedManifest.split(/\r?\n/u).filter((line) => line === ownerImport).length, 1); + const repairedManifest = await readFixtureFile( + fixture.root, + inventoryManifestFile + ); + assert.equal( + repairedManifest.split(/\r?\n/u).filter((line) => line === ownerImport) + .length, + 1 + ); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const entry = "'resource-detail': () => import('./src/api/resource-detail-client.ts'),"; + const registration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); + const entry = + "'resource-detail': () => import('./src/api/resource-detail-client.ts'),"; const corrupted = registration.replace(entry, `${entry}\n${entry}`); await writeFixtureFile(fixture.root, inventoryRegistrationFile, corrupted); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, scaffoldArguments, - /generated export already exists|generated owner slot/u, + /generated export already exists|generated owner slot/u ); - await writeFixtureFile(fixture.root, inventoryRegistrationFile, registration); await writeFixtureFile( fixture.root, inventoryRegistrationFile, - registration.replace(entry, "'resource-detail': () => import('./src/api/evil-client.ts'),"), + registration + ); + await writeFixtureFile( + fixture.root, + inventoryRegistrationFile, + registration.replace( + entry, + "'resource-detail': () => import('./src/api/evil-client.ts')," + ) ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, scaffoldArguments, - /generated owner slot contains mismatched identity/u, + /generated owner slot contains mismatched identity/u ); const wrongSlotRegistration = registration .replace(`${entry}\n`, '') .replace( ' // ', - ` ${entry}\n // `, + ` ${entry}\n // ` ); - await writeFixtureFile(fixture.root, inventoryRegistrationFile, wrongSlotRegistration); + await writeFixtureFile( + fixture.root, + inventoryRegistrationFile, + wrongSlotRegistration + ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, scaffoldArguments, - /generated owner slot contains mismatched identity/u, + /generated owner slot contains mismatched identity/u ); await writeFixtureFile( @@ -2115,11 +2446,14 @@ test('governed contribution reruns cannot be spoofed by comments or corrupt owne inventoryRegistrationFile, registration.replace( entry, - "'unrelated': () => import('./src/api/unrelated-client.ts') /* 'resource-detail': spoof */,", - ), + "'unrelated': () => import('./src/api/unrelated-client.ts') /* 'resource-detail': spoof */," + ) ); await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const commentSafeRegistration = await readFixtureFile(fixture.root, inventoryRegistrationFile); + const commentSafeRegistration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); assert.equal(commentSafeRegistration.split(entry).length - 1, 1); await writeFixtureFile( @@ -2127,14 +2461,14 @@ test('governed contribution reruns cannot be spoofed by comments or corrupt owne inventoryManifestFile, repairedManifest.replace( ownerImport, - "import { ResourceDetailApi } from './shared/apis/evil.ts';", - ), + "import { ResourceDetailApi } from './shared/apis/evil.ts';" + ) ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, scaffoldArguments, - /generated owner import binding conflicts/u, + /generated owner import binding conflicts/u ); }); }); @@ -2144,7 +2478,7 @@ test('adapted governed artifacts require executable owner identity instead of co const assertSpoofsRejected = async ( spoofs: readonly (readonly [string, string])[], command: Parameters[1], - commandArguments: readonly string[], + commandArguments: readonly string[] ): Promise => { const [spoof, ...remaining] = spoofs; if (spoof === undefined) { @@ -2156,13 +2490,13 @@ test('adapted governed artifacts require executable owner identity instead of co await writeFixtureFile( fixture.root, file, - `${current.replace(identity, removedIdentity)}\n/* ${identity} */\nconst identitySpoof = ${JSON.stringify(identity)};\n`, + `${current.replace(identity, removedIdentity)}\n/* ${identity} */\nconst identitySpoof = ${JSON.stringify(identity)};\n` ); await assertScaffoldRefused( fixture, command, commandArguments, - /refusing to overwrite existing business file/u, + /refusing to overwrite existing business file/u ); await writeFixtureFile(fixture.root, file, current); await assertSpoofsRejected(remaining, command, commandArguments); @@ -2180,17 +2514,24 @@ test('adapted governed artifacts require executable owner identity instead of co inventoryModuleApiContractFile, "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", ], - [inventoryModuleApiReadFile, 'export const resourceDetailRead = defineRead('], + [ + inventoryModuleApiReadFile, + 'export const resourceDetailRead = defineRead(', + ], [ inventoryModuleApiServerFile, 'export const resourceDetailReadApiLive = HttpApiBuilder.group(', ], ] as const; - await assertSpoofsRejected(moduleSpoofs, scaffoldCommand.moduleApi, moduleArguments); + await assertSpoofsRejected( + moduleSpoofs, + scaffoldCommand.moduleApi, + moduleArguments + ); const assertAdaptationRejected = async ( file: string, - adapt: (source: string) => string, + adapt: (source: string) => string ): Promise => { const current = await readFixtureFile(fixture.root, file); await writeFixtureFile(fixture.root, file, adapt(current)); @@ -2198,7 +2539,7 @@ test('adapted governed artifacts require executable owner identity instead of co fixture, scaffoldCommand.moduleApi, moduleArguments, - /refusing to overwrite existing business file/u, + /refusing to overwrite existing business file/u ); await writeFixtureFile(fixture.root, file, current); }; @@ -2207,32 +2548,32 @@ test('adapted governed artifacts require executable owner identity instead of co (source) => `${source.replace( "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", - "namespace Decoy { export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", - )}\n}`, + "namespace Decoy { export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')" + )}\n}` ); await assertAdaptationRejected(inventoryActionGatewayFile, (source) => source.replace( 'export const operationGateway = makeOperationGateway();', - "namespace Decoy { export const operationGateway = makeOperationGateway(); }\nconst spoof = 'export const operationGateway = actionGateway';", - ), + "namespace Decoy { export const operationGateway = makeOperationGateway(); }\nconst spoof = 'export const operationGateway = actionGateway';" + ) ); await assertAdaptationRejected( inventoryModuleApiServerFile, (source) => `${source.replace( 'authenticatePrincipal: authenticateOperationPrincipal', - 'authenticatePrincipal: unverifiedPrincipal', - )}\nconst unverifiedPrincipal = authenticateOperationPrincipal;`, + 'authenticatePrincipal: unverifiedPrincipal' + )}\nconst unverifiedPrincipal = authenticateOperationPrincipal;` ); await assertAdaptationRejected( inventoryModuleApiServerFile, (source) => - `${source.replace('registration: resourceDetailRead', 'registration: otherRead')}\nvoid ReadRuntime;`, + `${source.replace('registration: resourceDetailRead', 'registration: otherRead')}\nvoid ReadRuntime;` ); await assertAdaptationRejected( inventoryModuleApiServerFile, (source) => - `${source.replace('makeGovernedReadHttpHandler({', 'unsafeReadHandler({')}\nconst spoof = '.runRead({';`, + `${source.replace('makeGovernedReadHttpHandler({', 'unsafeReadHandler({')}\nconst spoof = '.runRead({';` ); const searchArguments = [ @@ -2245,14 +2586,24 @@ test('adapted governed artifacts require executable owner identity instead of co ] as const; await run(fixture, scaffoldCommand.searchProvider, searchArguments); const providerSpoofs = [ - [inventorySearchProviderFile, 'export const inventoryItemsRead = defineRead('], [ - inventorySearchContractFile, + inventorySearchProviderFile, + 'export const inventoryItemsRead = defineRead(', + ], + [ + inventorySearchContractFile, "export const InventoryItemsSearchApi = HttpApi.make('InventoryItemsSearchApi')", ], - [inventorySearchServerFile, 'export const inventoryItemsReadApiLive = HttpApiBuilder.group('], + [ + inventorySearchServerFile, + 'export const inventoryItemsReadApiLive = HttpApiBuilder.group(', + ], ] as const; - await assertSpoofsRejected(providerSpoofs, scaffoldCommand.searchProvider, searchArguments); + await assertSpoofsRejected( + providerSpoofs, + scaffoldCommand.searchProvider, + searchArguments + ); }); }); @@ -2263,7 +2614,9 @@ test('governed client generation rejects an incompatible shared runtime dependen inventorySlug, ]); const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); + const packageJson = decodeFixturePackage( + await readFile(packagePath, 'utf-8') + ); await writeFile( packagePath, json({ @@ -2273,13 +2626,18 @@ test('governed client generation rejects an incompatible shared runtime dependen '@app/shared-contracts': '^1.0.0', }, }), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /incompatible @app\/shared-contracts dependency/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ], + /incompatible @app\/shared-contracts dependency/u ); }); }); @@ -2299,9 +2657,15 @@ test('governed client generation restores its missing owner-local operation gate fixtureName.resourceDetail, ]); - const gateway = await readFixtureFile(fixture.root, inventoryActionGatewayFile); + const gateway = await readFixtureFile( + fixture.root, + inventoryActionGatewayFile + ); assert.match(gateway, /@ontos-action-boundary-owner inventory-stock/u); - assert.match(gateway, /export const operationGateway = makeOperationGateway\(\)/u); + assert.match( + gateway, + /export const operationGateway = makeOperationGateway\(\)/u + ); }); }); @@ -2313,13 +2677,15 @@ test('recognizes only exact schema-only Outbox package subpaths as cross-vertica './workers': './src/workers/index.ts', }, }; - assert.deepEqual(publishedOutboxContractExports(producerPackage), ['./outbox/orders-created']); + assert.deepEqual(publishedOutboxContractExports(producerPackage), [ + './outbox/orders-created', + ]); assert.doesNotThrow(() => assertPublishedOutboxDependencyUsage({ dependencyPackageJson: producerPackage, dependencyPackageName: inventoryPackageName, moduleSpecifiers: ['@app/inventory-stock/outbox/orders-created'], - }), + }) ); assert.throws( () => @@ -2328,7 +2694,7 @@ test('recognizes only exact schema-only Outbox package subpaths as cross-vertica dependencyPackageName: inventoryPackageName, moduleSpecifiers: ['@app/inventory-stock/workers'], }), - /not a published schema-only Outbox contract subpath/u, + /not a published schema-only Outbox contract subpath/u ); assert.throws( () => @@ -2337,7 +2703,7 @@ test('recognizes only exact schema-only Outbox package subpaths as cross-vertica dependencyPackageName: inventoryPackageName, moduleSpecifiers: [inventoryPackageName], }), - /not a published schema-only Outbox contract dependency/u, + /not a published schema-only Outbox contract dependency/u ); }); @@ -2357,13 +2723,21 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy '--provisioning', 'tenant_membership_default', ], - { workspaceRoot: fixture.root }, + { workspaceRoot: fixture.root } ), - /missing required flag --legal-entity-scope/u, + /missing required flag --legal-entity-scope/u ); assert.deepEqual(await snapshotTree(fixture.root), before); - const invalidCalls: readonly [ScaffoldCommand, readonly string[], RegExp][] = [ - ['action', [scaffoldFlag.vertical, inventorySlug], /missing required flag --action/u], + const invalidCalls: readonly [ + ScaffoldCommand, + readonly string[], + RegExp, + ][] = [ + [ + 'action', + [scaffoldFlag.vertical, inventorySlug], + /missing required flag --action/u, + ], [ 'action', [ @@ -2378,15 +2752,33 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy ], [ 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--unknown', 'x'], + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--unknown', + 'x', + ], /unknown flag --unknown/u, ], [ 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--action', 'again'], + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--action', + 'again', + ], /only once/u, ], - ['action', [scaffoldFlag.vertical, '', '--action', fixtureName.action], /non-empty value/u], + [ + 'action', + [scaffoldFlag.vertical, '', '--action', fixtureName.action], + /non-empty value/u, + ], [ 'action', [scaffoldFlag.vertical, '../billing', '--action', fixtureName.action], @@ -2394,7 +2786,12 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy ], [ 'action', - [scaffoldFlag.vertical, '/absolute/billing', '--action', fixtureName.action], + [ + scaffoldFlag.vertical, + '/absolute/billing', + '--action', + fixtureName.action, + ], /lower-kebab-case/u, ], [ @@ -2411,20 +2808,45 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy ], /mutually exclusive/u, ], - ['action', ['--scope', 'core', '--action', fixtureName.action], /--module is required/u], [ 'action', - ['--scope', 'other', '--module', fixtureName.actionModule, '--action', fixtureName.action], + ['--scope', 'core', '--action', fixtureName.action], + /--module is required/u, + ], + [ + 'action', + [ + '--scope', + 'other', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], /--scope core is required/u, ], [ 'action', - ['--scope', 'core', '--module', 'billing.modules', '--action', fixtureName.action], + [ + '--scope', + 'core', + '--module', + 'billing.modules', + '--action', + fixtureName.action, + ], /stable lowercase core/u, ], [ 'action', - ['--scope', 'core', '--module', 'core.../modules', '--action', fixtureName.action], + [ + '--scope', + 'core', + '--module', + 'core.../modules', + '--action', + fixtureName.action, + ], /stable lowercase core/u, ], [ @@ -2444,11 +2866,26 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy ], [ 'policy', - ['--scope', 'global', '--policy', fixtureName.policy, scaffoldFlag.vertical, inventorySlug], + [ + '--scope', + 'global', + '--policy', + fixtureName.policy, + scaffoldFlag.vertical, + inventorySlug, + ], /forbidden/u, ], - ['policy', ['--scope', 'microvertical', '--policy', fixtureName.policy], /required/u], - ['policy', ['--scope', 'other', '--policy', fixtureName.policy], /global or microvertical/u], + [ + 'policy', + ['--scope', 'microvertical', '--policy', fixtureName.policy], + /required/u, + ], + [ + 'policy', + ['--scope', 'other', '--policy', fixtureName.policy], + /global or microvertical/u, + ], [ scaffoldCommand.outboxMessage, [ @@ -2478,9 +2915,12 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy ]; await Promise.all( invalidCalls.map(async ([command, generatorArguments, expected]) => { - await assert.rejects(run(fixture, command, generatorArguments), expected); + await assert.rejects( + run(fixture, command, generatorArguments), + expected + ); assert.deepEqual(await snapshotTree(fixture.root), before); - }), + }) ); }); }); @@ -2489,17 +2929,27 @@ test('generates one immutable Action identity boundary and exact direct dependen await withFixture(async (fixture) => { const shellBefore = await readFixtureFile(fixture.root, shellSentinelFile); const topologyBefore = await readFixtureFile(fixture.root, topologyFile); - const result = await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); + const result = await run( + fixture, + scaffoldCommand.microverticalActionBoundary, + [scaffoldFlag.vertical, inventorySlug] + ); assert.equal(result.kind, 'generated'); - const server = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - const actionHttpRunner = await readFixtureFile(fixture.root, inventoryActionHttpRunnerFile); - const client = await readFixtureFile(fixture.root, inventoryActionGatewayFile); + const server = await readFixtureFile( + fixture.root, + inventoryActionPrincipalFile + ); + const actionHttpRunner = await readFixtureFile( + fixture.root, + inventoryActionHttpRunnerFile + ); + const client = await readFixtureFile( + fixture.root, + inventoryActionGatewayFile + ); const redemption = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/api/auth/gateway-assertion-redemption.ts', + 'verticals/inventory-stock/api/auth/gateway-assertion-redemption.ts' ); for (const source of [server, client]) { assert.match(source, /@ontos-action-boundary-owner inventory-stock/u); @@ -2507,32 +2957,47 @@ test('generates one immutable Action identity boundary and exact direct dependen assert.match(source, /ACTION_GATEWAY_AUDIENCE = 'inventory-stock'/u); } assert.match(server, /@app\/gateway-principal-verifier\/server/u); - assert.match(server, /bindGatewayPrincipalVerifier\(ACTION_GATEWAY_AUDIENCE\)/u); + assert.match( + server, + /bindGatewayPrincipalVerifier\(ACTION_GATEWAY_AUDIENCE\)/u + ); assert.doesNotMatch( server, - /createLocalJWKSet|decodeProtectedHeader|jwtVerify|PublicVerificationKeySchema/u, + /createLocalJWKSet|decodeProtectedHeader|jwtVerify|PublicVerificationKeySchema/u ); assert.match(client, /makeOperationGateway as makeSharedOperationGateway/u); - assert.match(client, /makeSharedOperationGateway\(ACTION_GATEWAY_AUDIENCE, acquire\)/u); - assert.match(client, /export const operationGateway = makeOperationGateway\(\)/u); + assert.match( + client, + /makeSharedOperationGateway\(ACTION_GATEWAY_AUDIENCE, acquire\)/u + ); + assert.match( + client, + /export const operationGateway = makeOperationGateway\(\)/u + ); + assert.doesNotMatch( + client, + /ActionGatewayIssuer|ActionGatewayAttempt|makeActionGateway|\bactionGateway\b/u + ); assert.doesNotMatch( client, - /ActionGatewayIssuer|ActionGatewayAttempt|makeActionGateway|\bactionGateway\b/u, + /Effect\.flatMap|Bearer \$\{|acquire\(\{ audience/u ); - assert.doesNotMatch(client, /Effect\.flatMap|Bearer \$\{|acquire\(\{ audience/u); assert.doesNotMatch( client, - /api\/auth\/action-principal|gateway-assertion-redemption|GatewayContextProtectedHeader|verticals\//u, + /api\/auth\/action-principal|gateway-assertion-redemption|GatewayContextProtectedHeader|verticals\//u ); assert.doesNotMatch(client, /localStorage|sessionStorage/u); assert.match(server, /verifyAndRedeem/u); assert.match(actionHttpRunner, /bindGovernedActionHttp/u); assert.match(actionHttpRunner, /bindActionHttpRunner/u); assert.match(actionHttpRunner, /authenticateOperationPrincipal/u); - assert.doesNotMatch(actionHttpRunner, /ActionRuntime|ActionCoreError|HttpApiEndpoint/u); + assert.doesNotMatch( + actionHttpRunner, + /ActionRuntime|ActionCoreError|HttpApiEndpoint/u + ); assert.match(redemption, /GatewayAssertionRedemptionUnavailableError/u); const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), + await readFixtureFile(fixture.root, inventoryPackageFile) ); assert.deepEqual(packageJson.dependencies, { '@app/core-runtime': workspaceVersion, @@ -2542,8 +3007,14 @@ test('generates one immutable Action identity boundary and exact direct dependen zeta: '1.0.0', }); assert.equal(packageJson.scripts['existing'], preservedFixtureValue); - assert.equal(await readFixtureFile(fixture.root, shellSentinelFile), shellBefore); - assert.equal(await readFixtureFile(fixture.root, topologyFile), topologyBefore); + assert.equal( + await readFixtureFile(fixture.root, shellSentinelFile), + shellBefore + ); + assert.equal( + await readFixtureFile(fixture.root, topologyFile), + topologyBefore + ); }); }); @@ -2566,13 +3037,13 @@ test('Action identity boundary preflight refuses unsafe writes', async () => { inventoryActionPrincipalFile, `// Owner-authored identity adapter export const ownerCode = true; -`, +` ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalActionBoundary, [scaffoldFlag.vertical, inventorySlug], - /refusing to overwrite existing business file/u, + /refusing to overwrite existing business file/u ); }); }); @@ -2584,10 +3055,13 @@ test('governed generators reject legacy principal boundaries before writing file scaffoldFlag.vertical, inventorySlug, ]); - const generated = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); + const generated = await readFixtureFile( + fixture.root, + inventoryActionPrincipalFile + ); const legacy = generated.replace( /const verifyOperationPrincipal =[\s\S]*$/u, - 'export const verifyOperationPrincipal = verifyActionPrincipal;\n', + 'export const verifyOperationPrincipal = verifyActionPrincipal;\n' ); assert.doesNotMatch(legacy, /export const authenticateOperationPrincipal/u); await writeFixtureFile(fixture.root, inventoryActionPrincipalFile, legacy); @@ -2599,16 +3073,23 @@ test('governed generators reject legacy principal boundaries before writing file scaffoldCommand.searchProvider, ['--name', fixtureName.inventoryItems, scaffoldFlag.resource, 'item'], ], - ['report', ['--name', fixtureName.stockLevels, scaffoldFlag.resource, 'item']], + [ + 'report', + ['--name', fixtureName.stockLevels, scaffoldFlag.resource, 'item'], + ], ]; await Promise.all( calls.map(async ([command, args]) => { await assert.rejects( - run(fixture, command, [scaffoldFlag.vertical, inventorySlug, ...args]), - /incompatible generated Action boundary:.*export authenticateOperationPrincipal.*provide ActionPrincipalVerifierLive|refusing to overwrite existing business file: operation boundary/u, + run(fixture, command, [ + scaffoldFlag.vertical, + inventorySlug, + ...args, + ]), + /incompatible generated Action boundary:.*export authenticateOperationPrincipal.*provide ActionPrincipalVerifierLive|refusing to overwrite existing business file: operation boundary/u ); assert.deepEqual(await snapshotTree(fixture.root), before); - }), + }) ); }); }); @@ -2631,7 +3112,10 @@ test('governed generation preserves compatible owner principal adaptations', asy '--name', fixtureName.resourceDetail, ]); - assert.equal(await readFixtureFile(fixture.root, inventoryActionPrincipalFile), adapted); + assert.equal( + await readFixtureFile(fixture.root, inventoryActionPrincipalFile), + adapted + ); }); }); @@ -2651,27 +3135,27 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 await symlink( path.join(appRoot, 'packages/core-runtime'), path.join(fixture.root, 'node_modules/@app/core-runtime'), - 'dir', + 'dir' ); await symlink( path.join(appRoot, sharedContractsPackagePath), path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, 'packages/gateway-principal-verifier'), path.join(fixture.root, 'node_modules/@app/gateway-principal-verifier'), - 'dir', + 'dir' ); await symlink( path.join(appRoot, 'packages/core-runtime/node_modules/effect'), path.join(fixture.root, effectNodeModulePath), - 'dir', + 'dir' ); await symlink( path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), path.join(fixture.root, 'node_modules/jose'), - 'dir', + 'dir' ); const edgeBundleDirectory = path.join(fixture.root, 'edge-bundle'); await mkdir(edgeBundleDirectory, { recursive: true }); @@ -2686,38 +3170,67 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 `--outfile=${path.join(edgeBundleDirectory, 'action-principal.mjs')}`, '--platform=browser', ], - { encoding: 'utf-8' }, + { encoding: 'utf-8' } ); const edgeBundleErrorMessage = edgeBundle.error?.message; let edgeBundleFailureMessage = 'edge bundle command did not start'; if (edgeBundle.stderr.length > 0) { edgeBundleFailureMessage = edgeBundle.stderr; - } else if (edgeBundleErrorMessage !== undefined && edgeBundleErrorMessage.length > 0) { + } else if ( + edgeBundleErrorMessage !== undefined && + edgeBundleErrorMessage.length > 0 + ) { edgeBundleFailureMessage = edgeBundleErrorMessage; } assert.equal(edgeBundle.status, 0, edgeBundleFailureMessage); const edgeInputs = Object.keys( Schema.decodeUnknownSync(EsbuildMetafileSchema)( - JSON.parse(await readFile(edgeMetafile, 'utf-8')), - ).inputs, + JSON.parse(await readFile(edgeMetafile, 'utf-8')) + ).inputs ).join('\n'); - assert.match(edgeInputs, /core-runtime\/src\/auth\/gateway-assertion-redemption\.ts/u); - assert.doesNotMatch(edgeInputs, /core-runtime\/src\/db|node:(?:crypto|path)|\/pg\//u); - const generatedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( - await import(pathToFileURL(path.join(fixture.root, inventoryActionPrincipalFile)).href), + assert.match( + edgeInputs, + /core-runtime\/src\/auth\/gateway-assertion-redemption\.ts/u ); - const billingGeneratedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( + assert.doesNotMatch( + edgeInputs, + /core-runtime\/src\/db|node:(?:crypto|path)|\/pg\//u + ); + const generatedModule = Schema.decodeUnknownSync( + GeneratedPrincipalModuleSchema + )( await import( - pathToFileURL(path.join(fixture.root, 'verticals/billing/api/auth/action-principal.ts')) + pathToFileURL(path.join(fixture.root, inventoryActionPrincipalFile)) .href - ), + ) ); - const generatedClientModule = Schema.decodeUnknownSync(GeneratedOperationGatewayModuleSchema)( - await import(pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).href), + const billingGeneratedModule = Schema.decodeUnknownSync( + GeneratedPrincipalModuleSchema + )( + await import( + pathToFileURL( + path.join( + fixture.root, + 'verticals/billing/api/auth/action-principal.ts' + ) + ).href + ) + ); + const generatedClientModule = Schema.decodeUnknownSync( + GeneratedOperationGatewayModuleSchema + )( + await import( + pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).href + ) ); const generatedActionHttpRunnerModule = Schema.decodeUnknownSync( - GeneratedActionHttpRunnerModuleSchema, - )(await import(pathToFileURL(path.join(fixture.root, inventoryActionHttpRunnerFile)).href)); + GeneratedActionHttpRunnerModuleSchema + )( + await import( + pathToFileURL(path.join(fixture.root, inventoryActionHttpRunnerFile)) + .href + ) + ); const current = await makeGatewayKey('current'); const retiring = await makeGatewayKey('retiring'); const principal = { @@ -2730,7 +3243,7 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 const issue = async ( configuration: GatewayIssuerConfigValue, issuedAt: number, - audience = inventorySlug, + audience = inventorySlug ) => await runEffectTestPromise( issueGatewayContextAssertion({ audience, principal }).pipe( @@ -2740,9 +3253,9 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 generateJti: Effect.succeed(fixtureGatewayJti), loadAudiences: Effect.succeed(new Set([audience])), loadConfig: Effect.succeed(configuration), - }), - ), - ), + }) + ) + ) ); const environment = { ONTOS_GATEWAY_ISSUER: fixtureGatewayIssuer, @@ -2751,53 +3264,73 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 }), }; const currentAssertion = await issue(current.configuration, 1_700_000_000); - const billingAssertion = await issue(current.configuration, 1_700_000_000, 'billing'); - const retiringAssertion = await issue(retiring.configuration, 1_700_000_000); + const billingAssertion = await issue( + current.configuration, + 1_700_000_000, + 'billing' + ); + const retiringAssertion = await issue( + retiring.configuration, + 1_700_000_000 + ); const testRedemption = { consume: () => Effect.void }; - const verify = async (token: string, override = environment, now = 1_700_000_001) => + const verify = async ( + token: string, + override = environment, + now = 1_700_000_001 + ) => await runEffectTestPromise( generatedModule.verifyActionPrincipal(`Bearer ${token}`, { currentTimeSeconds: Effect.succeed(now), environment: override, redemption: testRedemption, - }), + }) ); assert.deepEqual(await verify(currentAssertion.token), principal); assert.deepEqual( await runEffectTestPromise( - billingGeneratedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), + billingGeneratedModule.verifyActionPrincipal( + `Bearer ${billingAssertion.token}`, + { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + } + ) ), - principal, + principal ); await assert.rejects( runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), + generatedModule.verifyActionPrincipal( + `Bearer ${billingAssertion.token}`, + { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + } + ) ), - isGeneratedPrincipalError('ActionPrincipalScopeError'), + isGeneratedPrincipalError('ActionPrincipalScopeError') ); await assert.rejects( runEffectTestPromise( - billingGeneratedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), + billingGeneratedModule.verifyActionPrincipal( + `Bearer ${currentAssertion.token}`, + { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + } + ) ), - isGeneratedPrincipalError('ActionPrincipalScopeError'), + isGeneratedPrincipalError('ActionPrincipalScopeError') ); assert.deepEqual(await verify(retiringAssertion.token), principal); await assert.rejects( verify('not-a-jwt'), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); await Promise.all( [ @@ -2814,16 +3347,16 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 ...environment, ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify(jwks), }), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), - ), - ), + isGeneratedPrincipalError('ActionPrincipalConfigurationError') + ) + ) ); await assert.rejects( verify(currentAssertion.token, { ...environment, ONTOS_GATEWAY_ISSUER: 'file:///not-an-http-issuer', }), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), + isGeneratedPrincipalError('ActionPrincipalConfigurationError') ); await assert.rejects( verify( @@ -2834,45 +3367,55 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 keys: [current.publicJwk], }), }, - 1_700_000_000 + GATEWAY_ASSERTION_TTL_SECONDS + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS + 1, + 1_700_000_000 + + GATEWAY_ASSERTION_TTL_SECONDS + + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS + + 1 ), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') + ); + const wrongAudience = await issue( + current.configuration, + 1_700_000_000, + 'billing' ); - const wrongAudience = await issue(current.configuration, 1_700_000_000, 'billing'); await assert.rejects( verify(wrongAudience.token), - isGeneratedPrincipalError('ActionPrincipalScopeError'), + isGeneratedPrincipalError('ActionPrincipalScopeError') ); const wrongIssuer = await issue( { ...current.configuration, issuer: 'https://other.example.test' }, - 1_700_000_000, + 1_700_000_000 ); await assert.rejects( verify(wrongIssuer.token), - isGeneratedPrincipalError('ActionPrincipalScopeError'), + isGeneratedPrincipalError('ActionPrincipalScopeError') ); const unknownKid = await issue( { ...current.configuration, privateJwk: { ...current.configuration.privateJwk, kid: 'unknown' }, }, - 1_700_000_000, + 1_700_000_000 ); await assert.rejects( verify(unknownKid.token), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); const expired = await issue(current.configuration, 1_699_999_000); await assert.rejects( verify(expired.token), - isGeneratedPrincipalError('ActionPrincipalExpiredError'), + isGeneratedPrincipalError('ActionPrincipalExpiredError') ); const future = await issue(current.configuration, 1_700_000_032); await assert.rejects( verify(future.token), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') + ); + const signingKey = await importJWK( + current.configuration.privateJwk, + 'EdDSA' ); - const signingKey = await importJWK(current.configuration.privateJwk, 'EdDSA'); const mismatchedSubject = await new SignJWT({ principal, ver: 1 }) .setProtectedHeader({ alg: 'EdDSA', kid: 'current', typ: 'JWT' }) .setIssuer(fixtureGatewayIssuer) @@ -2884,7 +3427,7 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 .sign(signingKey); await assert.rejects( verify(mismatchedSubject), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); const invalidContext = await new SignJWT({ principal: { ...principal, principalId: 'not-a-uuid' }, @@ -2900,7 +3443,7 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 .sign(signingKey); await assert.rejects( verify(invalidContext), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); const hmacToken = await new SignJWT({ principal, ver: 1 }) .setProtectedHeader({ alg: 'HS256', kid: 'current', typ: 'JWT' }) @@ -2913,14 +3456,14 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 .sign(await generateSecret('HS256')); await assert.rejects( verify(hmacToken), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); const tokenParts = currentAssertion.token.split('.'); const encodedPayload = tokenParts[1] ?? ''; const tampered = `${tokenParts[0]}.${encodedPayload.startsWith('a') ? 'b' : 'a'}${encodedPayload.slice(1)}.${tokenParts[2]}`; await assert.rejects( verify(tampered), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); await assert.rejects( runEffectTestPromise( @@ -2928,9 +3471,9 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 currentTimeSeconds: Effect.succeed(1_700_000_001), environment, redemption: testRedemption, - }), + }) ), - isGeneratedPrincipalError('ActionPrincipalMissingError'), + isGeneratedPrincipalError('ActionPrincipalMissingError') ); await assert.rejects( runEffectTestPromise( @@ -2938,74 +3481,96 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 currentTimeSeconds: Effect.succeed(1_700_000_001), environment, redemption: testRedemption, - }), + }) ), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + isGeneratedPrincipalError('ActionPrincipalInvalidError') ); await assert.rejects( runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: {}, - redemption: testRedemption, - }), + generatedModule.verifyActionPrincipal( + `Bearer ${currentAssertion.token}`, + { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment: {}, + redemption: testRedemption, + } + ) ), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), + isGeneratedPrincipalError('ActionPrincipalConfigurationError') ); let acquisitions = 0; const authorizations: string[] = []; const idempotencyKey = 'caller-owned-idempotency-key'; - const operationGateway = generatedClientModule.makeOperationGateway(({ audience }) => { - acquisitions += 1; - assert.equal(audience, inventorySlug); - return Effect.succeed({ token: `attempt-${acquisitions}` }); - }); + const operationGateway = generatedClientModule.makeOperationGateway( + ({ audience }) => { + acquisitions += 1; + assert.equal(audience, inventorySlug); + return Effect.succeed({ token: `attempt-${acquisitions}` }); + } + ); const attempt = (authorization: string) => { authorizations.push(authorization); return Effect.succeed(idempotencyKey); }; - assert.equal(await runEffectTestPromise(operationGateway.invoke(attempt)), idempotencyKey); - assert.equal(await runEffectTestPromise(operationGateway.invoke(attempt)), idempotencyKey); + assert.equal( + await runEffectTestPromise(operationGateway.invoke(attempt)), + idempotencyKey + ); + assert.equal( + await runEffectTestPromise(operationGateway.invoke(attempt)), + idempotencyKey + ); assert.deepEqual(authorizations, ['Bearer attempt-1', 'Bearer attempt-2']); const actionApi = HttpApi.make('generatedActionIdentityFixture').add( HttpApiGroup.make('action').add( HttpApiEndpoint.post('invoke', '/actions/invoke', { - error: [ActionAuthenticationProblemSchema, ActionVerificationUnavailableProblemSchema], + error: [ + ActionAuthenticationProblemSchema, + ActionVerificationUnavailableProblemSchema, + ], success: TrustedPrincipalContextSchema, - }), - ), + }) + ) ); let actionReached = false; let endpointEnvironment: GeneratedPrincipalEnvironment = environment; const markActionReached = Effect.sync(() => { actionReached = true; }); - const actionGroupLive = HttpApiBuilder.group(actionApi, 'action', (handlers) => - handlers.handle('invoke', ({ request }) => - generatedModule - .verifyActionPrincipal(request.headers['authorization'], { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: endpointEnvironment, - redemption: testRedemption, - }) - .pipe(Effect.tap(markActionReached), Effect.catchTags(generatedPrincipalErrorHandlers)), - ), + const actionGroupLive = HttpApiBuilder.group( + actionApi, + 'action', + (handlers) => + handlers.handle('invoke', ({ request }) => + generatedModule + .verifyActionPrincipal(request.headers['authorization'], { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment: endpointEnvironment, + redemption: testRedemption, + }) + .pipe( + Effect.tap(markActionReached), + Effect.catchTags(generatedPrincipalErrorHandlers) + ) + ) ); const actionRuntime = defineEffectBff({ api: actionApi, - layer: HttpApiBuilder.layer(actionApi).pipe(Layer.provide(actionGroupLive)), + layer: HttpApiBuilder.layer(actionApi).pipe( + Layer.provide(actionGroupLive) + ), }); const actionHandler = actionRuntime.createHandler(); try { const missingResponse = await actionHandler.handler( - new Request(actionInvokeUrl, { method: 'POST' }), + new Request(actionInvokeUrl, { method: 'POST' }) ); assert.equal(missingResponse.status, 401); assert.equal(missingResponse.headers.get('www-authenticate'), 'Bearer'); assert.match( missingResponse.headers.get('content-type') ?? '', - /application\/problem\+json/u, + /application\/problem\+json/u ); assert.equal(actionReached, false); @@ -3014,13 +3579,14 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 new Request(actionInvokeUrl, { headers: { authorization: `Bearer ${currentAssertion.token}` }, method: 'POST', - }), + }) ); assert.equal(unavailableResponse.status, 503); assert.equal( - Schema.decodeUnknownSync(RetryableProblemSchema)(await unavailableResponse.json()) - .retryable, - true, + Schema.decodeUnknownSync(RetryableProblemSchema)( + await unavailableResponse.json() + ).retryable, + true ); assert.equal(actionReached, false); @@ -3029,7 +3595,7 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 new Request(actionInvokeUrl, { headers: { authorization: `Bearer ${currentAssertion.token}` }, method: 'POST', - }), + }) ); assert.equal(successResponse.status, 200); assert.deepEqual(await successResponse.json(), principal); @@ -3038,18 +3604,24 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 await actionHandler.dispose(); } - const generatedBindingApi = HttpApi.make('generatedActionRunnerFixture').add( + const generatedBindingApi = HttpApi.make( + 'generatedActionRunnerFixture' + ).add( HttpApiGroup.make('action').add( HttpApiEndpoint.post('invoke', '/actions/generated-runner', { - error: [ActionAuthenticationProblemSchema, ActionVerificationUnavailableProblemSchema], + error: [ + ActionAuthenticationProblemSchema, + ActionVerificationUnavailableProblemSchema, + ], success: GeneratedBindingResultSchema, - }), - ), + }) + ) ); - const runGeneratedActionHttp = generatedActionHttpRunnerModule.bindActionHttpRunner({ - authentication: actionAuthenticationProblem, - unavailable: actionVerificationUnavailableProblem, - }); + const runGeneratedActionHttp = + generatedActionHttpRunnerModule.bindActionHttpRunner({ + authentication: actionAuthenticationProblem, + unavailable: actionVerificationUnavailableProblem, + }); const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', @@ -3073,24 +3645,30 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 authorization: Redacted.make(request.headers['authorization']), 'x-correlation-id': request.headers['x-correlation-id'], }, - }), - ), + }) + ) ).pipe( Layer.provide(generatedModule.ActionPrincipalVerifierLive), - Layer.provide(Layer.succeed(GatewayAssertionRedemptionService, testRedemption)), - Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(environment))), - Layer.provide(harness.layer), + Layer.provide( + Layer.succeed(GatewayAssertionRedemptionService, testRedemption) + ), + Layer.provide( + ConfigProvider.layer(ConfigProvider.fromUnknown(environment)) + ), + Layer.provide(harness.layer) ); const generatedBindingRuntime = defineEffectBff({ api: generatedBindingApi, layer: HttpApiBuilder.layer(generatedBindingApi).pipe( Layer.provide(generatedBindingGroupLive), - Layer.provideMerge(harness.layer), + Layer.provideMerge(harness.layer) ), }); const generatedBindingHandler = generatedBindingRuntime.createHandler(); try { - const liveIssuedAt = Math.floor((await runEffectTestPromise(Clock.currentTimeMillis)) / 1000); + const liveIssuedAt = Math.floor( + (await runEffectTestPromise(Clock.currentTimeMillis)) / 1000 + ); const liveAssertion = await issue(current.configuration, liveIssuedAt); const generatedBindingResponse = await generatedBindingHandler.handler( new Request('https://inventory.example.test/actions/generated-runner', { @@ -3099,18 +3677,18 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 'x-correlation-id': 'generated-runner-correlation', }, method: 'POST', - }), - ); - const generatedBindingBody = Schema.decodeUnknownSync(GeneratedBindingResultSchema)( - await generatedBindingResponse.json(), + }) ); + const generatedBindingBody = Schema.decodeUnknownSync( + GeneratedBindingResultSchema + )(await generatedBindingResponse.json()); assert.equal( generatedBindingResponse.status, 200, JSON.stringify({ body: generatedBindingBody, snapshot: harness.snapshot(), - }), + }) ); assert.deepEqual(generatedBindingBody, { accepted: true }); assert.equal(harness.snapshot().invocations.length, 1); @@ -3131,7 +3709,7 @@ test('generates one self-contained typed fail-closed Action and preserves packag ]); const action = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/actions/create-order2.action.ts', + 'verticals/inventory-stock/src/actions/create-order2.action.ts' ); assert.equal( action, @@ -3193,10 +3771,10 @@ export const createOrder2Action = defineAction( // // -`, +` ); const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), + await readFixtureFile(fixture.root, inventoryPackageFile) ); assert.deepEqual(packageJson.dependencies, { '@app/core-runtime': workspaceVersion, @@ -3207,7 +3785,7 @@ export const createOrder2Action = defineAction( fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order2'], - /refusing to overwrite/u, + /refusing to overwrite/u ); }); }); @@ -3222,7 +3800,7 @@ test('generates an owner-local Action service without overwriting business logic ]); const service = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/services/inventory-persistence.service.ts', + 'verticals/inventory-stock/src/services/inventory-persistence.service.ts' ); assert.equal( service, @@ -3230,13 +3808,18 @@ test('generates an owner-local Action service without overwriting business logic import { Effect } from 'effect'; export const inventoryPersistenceService = () => Effect.succeed({}); -`, +` ); await assertScaffoldRefused( fixture, scaffoldCommand.actionService, - [scaffoldFlag.vertical, inventorySlug, '--service', 'inventory-persistence'], - /refusing to overwrite/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--service', + 'inventory-persistence', + ], + /refusing to overwrite/u ); }); }); @@ -3254,7 +3837,7 @@ test('generates exactly one private owner-local external HTTP adapter', async () ]); const adapterPath = path.join( fixture.root, - 'verticals/contacts/src/integrations/ares/ares-subject.service.ts', + 'verticals/contacts/src/integrations/ares/ares-subject.service.ts' ); assert.deepEqual(result, { kind: 'generated', @@ -3267,7 +3850,7 @@ test('generates exactly one private owner-local external HTTP adapter', async () ]); assert.deepEqual( [...changedPaths], - ['verticals/contacts/src/integrations/ares/ares-subject.service.ts'], + ['verticals/contacts/src/integrations/ares/ares-subject.service.ts'] ); assert.equal( after['verticals/contacts/src/integrations/ares/ares-subject.service.ts'], @@ -3308,14 +3891,17 @@ const makeAresSubjectService = Effect.gen(function* () { }); export const AresSubjectServiceLive = Layer.effect(AresSubjectService, makeAresSubjectService); -`, +` ); - const source = after['verticals/contacts/src/integrations/ares/ares-subject.service.ts'] ?? ''; + const source = + after[ + 'verticals/contacts/src/integrations/ares/ares-subject.service.ts' + ] ?? ''; assert.match(source, /HttpClient\.HttpClient/u); assert.match(source, /Layer\.effect/u); assert.doesNotMatch( source, - /fetch\(|httpClient\.(?:execute|get|head|post|patch|put|del|options)\(|https?:\/\//u, + /fetch\(|httpClient\.(?:execute|get|head|post|patch|put|del|options)\(|https?:\/\//u ); await assertScaffoldRefused( @@ -3329,7 +3915,7 @@ export const AresSubjectServiceLive = Layer.effect(AresSubjectService, makeAresS scaffoldFlag.operation, 'subject', ], - /refusing to overwrite/u, + /refusing to overwrite/u ); }); }); @@ -3462,7 +4048,7 @@ test('rejects unsafe external HTTP adapter command input without writing', async const [generatorArguments, expected] = invalidCall; await assert.rejects( run(fixture, scaffoldCommand.externalHttpAdapter, generatorArguments), - expected, + expected ); assert.deepEqual(await snapshotTree(fixture.root), before); await assertInvalidCall(index + 1); @@ -3473,15 +4059,18 @@ test('rejects unsafe external HTTP adapter command input without writing', async test('external HTTP adapter planner rejects malformed OntOS ownership atomically', async () => { await withFixture(async (fixture) => { - const manifestPath = path.join(fixture.root, 'verticals/contacts/vertical.manifest.ts'); + const manifestPath = path.join( + fixture.root, + 'verticals/contacts/vertical.manifest.ts' + ); const manifest = await readFile(manifestPath, 'utf-8'); await writeFile( manifestPath, manifest.replace( '// @generated by OntOS Codesmith Module Contract v1', - '// developer-owned manifest', + '// developer-owned manifest' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, @@ -3494,7 +4083,7 @@ test('external HTTP adapter planner rejects malformed OntOS ownership atomically scaffoldFlag.operation, 'subject', ], - /is not a generated module owner/u, + /is not a generated module owner/u ); }); @@ -3502,7 +4091,7 @@ test('external HTTP adapter planner rejects malformed OntOS ownership atomically await writeFixtureFile( fixture.root, 'verticals/contacts/src/integrations', - 'planner fixture blocks the required directory\n', + 'planner fixture blocks the required directory\n' ); await assertScaffoldRefused( fixture, @@ -3515,7 +4104,7 @@ test('external HTTP adapter planner rejects malformed OntOS ownership atomically scaffoldFlag.operation, 'subject', ], - /ENOTDIR|not a directory/u, + /ENOTDIR|not a directory/u ); }); }); @@ -3529,15 +4118,15 @@ test('Action generation rejects unrelated imports in its governed owner slots', manifest.replace( '// ', `// -import { fakeRead } from './src/api/fake.read.ts';`, +import { fakeRead } from './src/api/fake.read.ts';` ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order3'], - /generated owner slot contains unsupported developer content/u, + /generated owner slot contains unsupported developer content/u ); }); }); @@ -3563,7 +4152,7 @@ test('generates Core-owned Actions only through the Core owner slot with atomic const action = await readFixtureFile( fixture.root, - 'packages/core-runtime/src/modules/actions/account-change.action.ts', + 'packages/core-runtime/src/modules/actions/account-change.action.ts' ); assert.match(action, /@ontos-action-owner core\.modules/u); assert.match(action, /actionKey: 'core\.modules\.account-change'/u); @@ -3583,25 +4172,39 @@ test('generates Core-owned Actions only through the Core owner slot with atomic assert.ok(coreIndex.indexOf(accountExport) < coreIndex.indexOf(zExport)); assert.match(coreIndex, /export const existingCoreSurface = true/u); - const coreCatalog = await readFixtureFile(fixture.root, coreActionCatalogFile); - const accountImport = "import { accountChangeAction } from './account-change.action.ts';"; - const zImport = "import { zLastChangeAction } from './z-last-change.action.ts';"; + const coreCatalog = await readFixtureFile( + fixture.root, + coreActionCatalogFile + ); + const accountImport = + "import { accountChangeAction } from './account-change.action.ts';"; + const zImport = + "import { zLastChangeAction } from './z-last-change.action.ts';"; assert.ok(coreCatalog.includes(accountImport)); assert.ok(coreCatalog.includes(zImport)); assert.ok(coreCatalog.includes('accountChangeAction.descriptor,')); assert.ok(coreCatalog.includes('zLastChangeAction.descriptor,')); - assert.ok(coreCatalog.indexOf(accountImport) < coreCatalog.indexOf(zImport)); + assert.ok( + coreCatalog.indexOf(accountImport) < coreCatalog.indexOf(zImport) + ); assert.ok( coreCatalog.indexOf('accountChangeAction.descriptor,') < - coreCatalog.indexOf('zLastChangeAction.descriptor,'), + coreCatalog.indexOf('zLastChangeAction.descriptor,') ); assert.match(coreCatalog, /export const existingCatalogSurface = true/u); await assertScaffoldRefused( fixture, 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', 'account-change'], - /refusing to overwrite/u, + [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + 'account-change', + ], + /refusing to overwrite/u ); }); @@ -3610,13 +4213,20 @@ test('generates Core-owned Actions only through the Core owner slot with atomic await writeFile( indexPath, `export const existingCoreSurface = true;\n\n// \n// \n`, - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /generated owner file does not contain one valid/u, + [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], + /generated owner file does not contain one valid/u ); }); @@ -3627,15 +4237,22 @@ test('generates Core-owned Actions only through the Core owner slot with atomic indexPath, index.replace( '// \n', - '// \nexport const developerOwned = true;\n', + '// \nexport const developerOwned = true;\n' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /unsupported developer content/u, + [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], + /unsupported developer content/u ); }); @@ -3646,15 +4263,22 @@ test('generates Core-owned Actions only through the Core owner slot with atomic catalogPath, catalog.replace( '// \n', - '// \n developerOwned.descriptor,\n', + '// \n developerOwned.descriptor,\n' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /unsupported developer content/u, + [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], + /unsupported developer content/u ); }); }); @@ -3662,28 +4286,35 @@ test('generates Core-owned Actions only through the Core owner slot with atomic test('preflights the Action dependency patch before creating a file', async () => { await withFixture(async (fixture) => { const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); + const packageJson = decodeFixturePackage( + await readFile(packagePath, 'utf-8') + ); await writeFile( packagePath, json({ ...packageJson, dependencies: { '@app/core-runtime': '^1.0.0', zeta: '1.0.0' }, }), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /incompatible/u, + /incompatible/u ); }); }); test('rejects Action generation when a vertical app identity is duplicated', async () => { await withFixture(async (fixture) => { - const billingPackagePath = path.join(fixture.root, 'verticals/billing/package.json'); - const billingPackage = decodeFixturePackage(await readFile(billingPackagePath, 'utf-8')); + const billingPackagePath = path.join( + fixture.root, + 'verticals/billing/package.json' + ); + const billingPackage = decodeFixturePackage( + await readFile(billingPackagePath, 'utf-8') + ); await writeFile( billingPackagePath, json({ @@ -3693,13 +4324,13 @@ test('rejects Action generation when a vertical app identity is duplicated', asy appId: inventoryVertical.appId, }, }), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /duplicate generated appId inventory-stock/u, + /duplicate generated appId inventory-stock/u ); }); }); @@ -3707,20 +4338,22 @@ test('rejects Action generation when a vertical app identity is duplicated', asy test('rejects Action generation when the target identity is absent from topology', async () => { await withFixture(async (fixture) => { const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); + const packageJson = decodeFixturePackage( + await readFile(packagePath, 'utf-8') + ); await writeFile( packagePath, json({ ...packageJson, modernjs: { ...packageJson.modernjs, appId: 'inventory-shadow' }, }), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /must have exactly one matching generated topology entry/u, + /must have exactly one matching generated topology entry/u ); }); }); @@ -3728,8 +4361,13 @@ test('rejects Action generation when the target identity is absent from topology test('preserves owner JSON document style while patching the Core dependency', async () => { await withFixture(async (fixture) => { const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - const styledPackage = JSON.stringify(packageJson, null, 4).replaceAll('\n', '\r\n'); + const packageJson = decodeFixturePackage( + await readFile(packagePath, 'utf-8') + ); + const styledPackage = JSON.stringify(packageJson, null, 4).replaceAll( + '\n', + '\r\n' + ); await writeFile(packagePath, styledPackage, 'utf-8'); await run(fixture, 'action', [ @@ -3760,7 +4398,7 @@ test('generates Action-owned Outbox Messages and sorts only the owned export slo await writeFile( actionPath, `${generatedAction}\nexport const developerOwned = true;\n`, - 'utf-8', + 'utf-8' ); await run(fixture, scaffoldCommand.outboxMessage, [ scaffoldFlag.vertical, @@ -3780,7 +4418,7 @@ test('generates Action-owned Outbox Messages and sorts only the owned export slo ]); const message = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts', + 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts' ); assert.equal( message, @@ -3804,7 +4442,7 @@ export const createCreateOrderOrdersCreatedOutboxMessage = ( producerModuleKey: CreateOrderOrdersCreatedOutboxProducerModuleKey, topic: CreateOrderOrdersCreatedOutboxTopic, }); -`, +` ); assert.equal( await readFixtureFile(fixture.root, inventoryOutboxContractFile), @@ -3820,12 +4458,15 @@ export type OutboxPayload = Schema.Schema.Type; export const outboxTopic = 'orders.created' as const; export const outboxProducerModuleKey = 'inventory.stock' as const; -`, +` ); const producerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), + await readFixtureFile(fixture.root, inventoryPackageFile) + ); + assert.equal( + producerPackage.exports['./outbox/orders-created'], + generatedOutboxContractPath ); - assert.equal(producerPackage.exports['./outbox/orders-created'], generatedOutboxContractPath); const action = await readFile(actionPath, 'utf-8'); const createdExport = "export { CreateOrderOrdersCreatedOutboxPayloadSchema } from './create-order.orders-created.outbox-message.ts';"; @@ -3835,7 +4476,7 @@ export const outboxProducerModuleKey = 'inventory.stock' as const; assert.match(action, /export const developerOwned = true;/u); assert.doesNotMatch( message, - /addDomainEvent|addOutboxMessage|subjectResource|transport|worker/u, + /addDomainEvent|addOutboxMessage|subjectResource|transport|worker/u ); await run(fixture, scaffoldCommand.outboxMessage, [ @@ -3857,7 +4498,7 @@ export const outboxProducerModuleKey = 'inventory.stock' as const; '--topic', 'events.foo1-bar', ], - /Outbox identifier CreateOrderEventsFoo1BarOutbox already exists/u, + /Outbox identifier CreateOrderEventsFoo1BarOutbox already exists/u ); }); }); @@ -3875,13 +4516,13 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t '--topic', fixtureName.ordersCreated, ], - /requires the generated Action/u, + /requires the generated Action/u ); await writeFixtureFile( fixture.root, 'verticals/inventory-stock/src/actions/handwritten.action.ts', - `// \n// \n`, + `// \n// \n` ); await assertScaffoldRefused( fixture, @@ -3894,7 +4535,7 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t '--topic', fixtureName.ordersCreated, ], - /only the matching generated Action/u, + /only the matching generated Action/u ); await run(fixture, 'action', [ @@ -3904,11 +4545,14 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t fixtureName.action, ]); const governedActionPath = inventoryActionFile; - const governedAction = await readFixtureFile(fixture.root, governedActionPath); + const governedAction = await readFixtureFile( + fixture.root, + governedActionPath + ); await writeFixtureFile( fixture.root, governedActionPath, - governedAction.replace(" access: 'write',", " access: 'read',"), + governedAction.replace(" access: 'write',", " access: 'read',") ); await assertScaffoldRefused( fixture, @@ -3921,7 +4565,7 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t '--topic', fixtureName.ordersCreated, ], - /matching generated Action with its governed write entrypoint/u, + /matching generated Action with its governed write entrypoint/u ); await writeFixtureFile(fixture.root, governedActionPath, governedAction); await run(fixture, scaffoldCommand.outboxMessage, [ @@ -3944,17 +4588,20 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t '--topic', topic, ]), - /already exists/u, + /already exists/u ); assert.deepEqual(await snapshotTree(fixture.root), beforeCollision); - }), + }) ); }); }); test('generates isolated Outbox Workers from published contracts and composes a stable registry', async () => { await withFixture(async (fixture) => { - const billingApiBefore = await readFixtureFile(fixture.root, billingApiIndexFile); + const billingApiBefore = await readFixtureFile( + fixture.root, + billingApiIndexFile + ); await run(fixture, 'action', [ scaffoldFlag.vertical, inventorySlug, @@ -3971,8 +4618,8 @@ test('generates isolated Outbox Workers from published contracts and composes a ]); const producerBefore = Object.fromEntries( Object.entries(await snapshotTree(fixture.root)).filter(([file]) => - file.startsWith('verticals/inventory-stock/'), - ), + file.startsWith('verticals/inventory-stock/') + ) ); await run(fixture, scaffoldCommand.outboxWorker, [ @@ -3987,7 +4634,7 @@ test('generates isolated Outbox Workers from published contracts and composes a ]); const worker = await readFixtureFile( fixture.root, - 'verticals/billing/src/workers/orders-created-logger.worker.ts', + 'verticals/billing/src/workers/orders-created-logger.worker.ts' ); assert.equal( worker, @@ -4044,7 +4691,7 @@ export const ordersCreatedLoggerWorker = defineOutboxWorker( }, handleOrdersCreatedLogger, ); -`, +` ); assert.equal( await readFixtureFile(fixture.root, billingWorkersIndexFile), @@ -4059,10 +4706,13 @@ export const outboxWorkers = Object.freeze([ ordersCreatedLoggerWorker, // ]) satisfies readonly AnyOutboxWorkerRegistration[]; -`, +` ); assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/layer.ts'), + await readFixtureFile( + fixture.root, + 'verticals/billing/src/worker-host/layer.ts' + ), `// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { Layer } from 'effect'; @@ -4079,19 +4729,25 @@ export const outboxWorkerLayer = Layer.merge( OutboxWorkerInfrastructureLive, outboxWorkerHandlerLayer, ); -`, +` ); assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/main.ts'), + await readFixtureFile( + fixture.root, + 'verticals/billing/src/worker-host/main.ts' + ), `// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { startBillingOutboxWorker } from '../../scripts/outbox-worker.ts'; startBillingOutboxWorker(); -`, +` ); assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/scripts/outbox-worker.ts'), + await readFixtureFile( + fixture.root, + 'verticals/billing/scripts/outbox-worker.ts' + ), `// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { Layer } from 'effect'; @@ -4122,25 +4778,38 @@ export const startBillingOutboxWorker = (): void => registrations: outboxWorkers, subscriptions: outboxSubscriptions, }); -`, +` + ); + assert.equal( + await readFixtureFile(fixture.root, billingApiIndexFile), + billingApiBefore ); - assert.equal(await readFixtureFile(fixture.root, billingApiIndexFile), billingApiBefore); const consumerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, 'verticals/billing/package.json'), + await readFixtureFile(fixture.root, 'verticals/billing/package.json') + ); + assert.equal( + consumerPackage.dependencies['@app/core-runtime'], + workspaceVersion + ); + assert.equal( + consumerPackage.dependencies[inventoryPackageName], + workspaceVersion ); - assert.equal(consumerPackage.dependencies['@app/core-runtime'], workspaceVersion); - assert.equal(consumerPackage.dependencies[inventoryPackageName], workspaceVersion); assert.equal(consumerPackage.exports['./workers'], undefined); assert.equal(consumerPackage.scripts['dev:worker'], workerStartScript); assert.equal(consumerPackage.scripts['worker:start'], workerStartScript); const consumerTsconfig = Schema.decodeUnknownSync(FixtureTsconfigSchema)( - JSON.parse(await readFixtureFile(fixture.root, 'verticals/billing/tsconfig.json')), + JSON.parse( + await readFixtureFile(fixture.root, 'verticals/billing/tsconfig.json') + ) ); - assert.deepEqual(consumerTsconfig.references, [{ path: '../inventory-stock' }]); + assert.deepEqual(consumerTsconfig.references, [ + { path: '../inventory-stock' }, + ]); const producerAfter = Object.fromEntries( Object.entries(await snapshotTree(fixture.root)).filter(([file]) => - file.startsWith('verticals/inventory-stock/'), - ), + file.startsWith('verticals/inventory-stock/') + ) ); assert.deepEqual(producerAfter, producerBefore); @@ -4162,10 +4831,13 @@ export const startBillingOutboxWorker = (): void => '--topic', fixtureName.ordersShipped, ]); - const registry = await readFixtureFile(fixture.root, billingWorkersIndexFile); + const registry = await readFixtureFile( + fixture.root, + billingWorkersIndexFile + ); assert.ok( registry.indexOf('ordersCreatedLoggerWorker') < - registry.indexOf('ordersShippedProjectorWorker'), + registry.indexOf('ordersShippedProjectorWorker') ); await assertScaffoldRefused( fixture, @@ -4180,7 +4852,7 @@ export const startBillingOutboxWorker = (): void => '--topic', fixtureName.ordersCreated, ], - /refusing to overwrite/u, + /refusing to overwrite/u ); }); }); @@ -4201,8 +4873,14 @@ test('generates self-consuming Outbox Workers without circular project or packag '--topic', fixtureName.ordersCreated, ]); - const manifestBefore = await readFixtureFile(fixture.root, inventoryManifestFile); - const tsconfigBefore = await readFixtureFile(fixture.root, inventoryTsconfigFile); + const manifestBefore = await readFixtureFile( + fixture.root, + inventoryManifestFile + ); + const tsconfigBefore = await readFixtureFile( + fixture.root, + inventoryTsconfigFile + ); const args = [ scaffoldFlag.vertical, inventorySlug, @@ -4216,42 +4894,61 @@ test('generates self-consuming Outbox Workers without circular project or packag await run(fixture, scaffoldCommand.outboxWorker, args); const worker = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/workers/orders-created-projector.worker.ts', + 'verticals/inventory-stock/src/workers/orders-created-projector.worker.ts' ); assert.ok(worker.includes('// @ontos-outbox-worker-owner inventory.stock')); - assert.ok(worker.includes('// @ontos-outbox-worker-producer inventory.stock')); - assert.ok(worker.includes("from '@app/inventory-stock/outbox/orders-created'")); + assert.ok( + worker.includes('// @ontos-outbox-worker-producer inventory.stock') + ); + assert.ok( + worker.includes("from '@app/inventory-stock/outbox/orders-created'") + ); const registry = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/workers/index.ts', + 'verticals/inventory-stock/src/workers/index.ts' ); const hostLayer = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/worker-host/layer.ts', + 'verticals/inventory-stock/src/worker-host/layer.ts' ); const hostMain = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/worker-host/main.ts', + 'verticals/inventory-stock/src/worker-host/main.ts' ); const hostScript = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/scripts/outbox-worker.ts', + 'verticals/inventory-stock/scripts/outbox-worker.ts' ); assert.ok(registry.includes(workerRegistryEntry)); assert.ok(hostLayer.includes('OutboxWorkerInfrastructureLive')); assert.ok(hostMain.includes('startInventoryStockOutboxWorker();')); assert.ok(hostScript.includes('startOutboxWorkerProcess({')); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); + const registration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); assert.ok(registration.includes('createOrderAction,')); assert.ok(registration.includes(workerRegistryEntry)); - assert.equal(await readFixtureFile(fixture.root, inventoryManifestFile), manifestBefore); - assert.equal(await readFixtureFile(fixture.root, inventoryTsconfigFile), tsconfigBefore); + assert.equal( + await readFixtureFile(fixture.root, inventoryManifestFile), + manifestBefore + ); + assert.equal( + await readFixtureFile(fixture.root, inventoryTsconfigFile), + tsconfigBefore + ); const ownerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), + await readFixtureFile(fixture.root, inventoryPackageFile) + ); + assert.equal( + ownerPackage.dependencies['@app/core-runtime'], + workspaceVersion ); - assert.equal(ownerPackage.dependencies['@app/core-runtime'], workspaceVersion); assert.equal(ownerPackage.dependencies[inventoryPackageName], undefined); - assert.equal(ownerPackage.exports['./outbox/orders-created'], generatedOutboxContractPath); + assert.equal( + ownerPackage.exports['./outbox/orders-created'], + generatedOutboxContractPath + ); for (const script of ['dev:worker', 'worker:start']) { assert.equal(ownerPackage.scripts[script], workerStartScript); } @@ -4259,7 +4956,7 @@ test('generates self-consuming Outbox Workers without circular project or packag fixture, scaffoldCommand.outboxWorker, args, - /refusing to overwrite/u, + /refusing to overwrite/u ); await run(fixture, 'action', [ scaffoldFlag.vertical, @@ -4267,13 +4964,16 @@ test('generates self-consuming Outbox Workers without circular project or packag '--action', 'request-rebuild', ]); - const registrationAfterAction = await readFixtureFile(fixture.root, inventoryRegistrationFile); + const registrationAfterAction = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); assert.ok(registrationAfterAction.includes('requestRebuildAction,')); assert.ok(registrationAfterAction.includes(workerRegistryEntry)); await writeFixtureFile( fixture.root, inventoryTsconfigFile, - JSON.stringify({ references: [{ path: '../inventory-stock' }] }), + JSON.stringify({ references: [{ path: '../inventory-stock' }] }) ); await assertScaffoldRefused( fixture, @@ -4288,7 +4988,7 @@ test('generates self-consuming Outbox Workers without circular project or packag '--topic', fixtureName.ordersCreated, ], - /circular self project reference/u, + /circular self project reference/u ); }); }); @@ -4308,7 +5008,7 @@ test('refuses unpublished or malformed Outbox contracts without partial consumer '--topic', 'orders.missing', ], - /published producer Outbox contract is missing/u, + /published producer Outbox contract is missing/u ); await run(fixture, 'action', [ @@ -4331,9 +5031,9 @@ test('refuses unpublished or malformed Outbox contracts without partial consumer contractPath, validContract.replace( '// @ontos-outbox-producer inventory.stock', - '// @ontos-outbox-producer billing', + '// @ontos-outbox-producer billing' ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, @@ -4348,15 +5048,25 @@ test('refuses unpublished or malformed Outbox contracts without partial consumer '--topic', fixtureName.ordersCreated, ], - /owner\/topic\/schema mismatch/u, + /owner\/topic\/schema mismatch/u ); }); }); test('generates fail-closed global and owner-local Policies with narrow exports', async () => { await withFixture(async (fixture) => { - await run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]); - await run(fixture, 'policy', ['--scope', 'global', '--policy', 'account-open']); + await run(fixture, 'policy', [ + '--scope', + 'global', + '--policy', + fixtureName.policy, + ]); + await run(fixture, 'policy', [ + '--scope', + 'global', + '--policy', + 'account-open', + ]); await run(fixture, 'policy', [ '--scope', 'microvertical', @@ -4369,7 +5079,7 @@ test('generates fail-closed global and owner-local Policies with narrow exports' assert.equal( await readFixtureFile( fixture.root, - 'packages/core-runtime/src/policies/tenant-active.policy.ts', + 'packages/core-runtime/src/policies/tenant-active.policy.ts' ), `import { Effect } from 'effect'; import { defineGlobalPolicy, denyPolicy } from '../actions/policy.ts'; @@ -4381,12 +5091,12 @@ export const tenantActivePolicy = defineGlobalPolicy({ ), policyKey: 'global.tenant-active.v1', }); -`, +` ); assert.equal( await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/policies/stock-available.policy.ts', + 'verticals/inventory-stock/src/policies/stock-available.policy.ts' ), `import { Effect } from 'effect'; import { defineMicroverticalPolicy, denyPolicy } from '@app/core-runtime'; @@ -4399,7 +5109,7 @@ export const stockAvailablePolicy = defineMicroverticalPolicy -`, +` ); assert.doesNotMatch(coreIndex, /stockAvailablePolicy/u); assert.equal( - decodeFixturePackage(await readFixtureFile(fixture.root, inventoryPackageFile)).dependencies[ - '@app/core-runtime' - ], - workspaceVersion, + decodeFixturePackage( + await readFixtureFile(fixture.root, inventoryPackageFile) + ).dependencies['@app/core-runtime'], + workspaceVersion ); await assertScaffoldRefused( fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy], - /refusing to overwrite/u, + /refusing to overwrite/u ); - await run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo-1-bar']); + await run(fixture, 'policy', [ + '--scope', + 'global', + '--policy', + 'foo-1-bar', + ]); await assertScaffoldRefused( fixture, 'policy', ['--scope', 'global', '--policy', 'foo1-bar'], - /Policy identifier foo1BarPolicy already exists/u, + /Policy identifier foo1BarPolicy already exists/u ); }); }); @@ -4442,25 +5157,33 @@ export { tenantActivePolicy } from './policies/tenant-active.policy.ts'; test('generates a title-only authenticated page at the default MicroVertical URL', async () => { await withFixture(async (fixture) => { const shellBefore = await readFixtureFile(fixture.root, shellSentinelFile); - const englishLocalePath = path.join(fixture.root, inventoryEnglishLocaleFile); + const englishLocalePath = path.join( + fixture.root, + inventoryEnglishLocaleFile + ); await writeFile( englishLocalePath, '{\r\n "inventory": {"existing":"en-preserved"}\r\n}', - 'utf-8', + 'utf-8' ); const refreshes: string[] = []; await run( fixture, scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + ], (appId) => { refreshes.push(appId); - }, + } ); assert.deepEqual(refreshes, [inventorySlug, shellAppId]); const page = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx' ); assert.equal( page, @@ -4490,56 +5213,65 @@ export const PurchaseOrdersPage = () => { }; export default PurchaseOrdersPage; -`, +` ); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const federation = await readFixtureFile(fixture.root, inventoryFederationConfigFile); + const registration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); + const federation = await readFixtureFile( + fixture.root, + inventoryFederationConfigFile + ); const federatedPage = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/federation/page-purchase-orders.tsx', + 'verticals/inventory-stock/src/federation/page-purchase-orders.tsx' + ); + const shellClients = await readFixtureFile( + fixture.root, + shellVerticalClientsFile ); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); assert.match(manifest, /inventory\.stock\.navigation\.purchase-orders/u); assert.match(manifest, /inventory\.stock\.page\.purchase-orders/u); assert.match(manifest, /routePath: '\/inventory-stock\/purchase-orders'/u); assert.match(registration, /page-purchase-orders/u); assert.match( federation, - /'\.\/PagePurchaseOrders': '\.\/src\/federation\/page-purchase-orders\.tsx'/u, + /'\.\/PagePurchaseOrders': '\.\/src\/federation\/page-purchase-orders\.tsx'/u ); assert.match(federatedPage, / import\('inventoryStock\/PagePurchaseOrders'\)/u, + /appId: 'inventory-stock', componentKey: 'inventory\.stock\.page-purchase-orders', load: \(\) => import\('inventoryStock\/PagePurchaseOrders'\)/u ); assert.equal( await readFixtureFile( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx' ), `export { default } from '../../modules/[moduleId]/page.tsx'; -`, +` ); assert.match( await readFixtureFile( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.data.ts', + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.data.ts' ), - /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u, + /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u ); assert.match( await readFixtureFile( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts' ), - /canonicalPath: '\/inventory-stock\/purchase-orders'/u, + /canonicalPath: '\/inventory-stock\/purchase-orders'/u ); assert.equal( await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts' ), `import { defineTenantModuleEntrypoint } from '@app/core-runtime'; @@ -4570,15 +5302,21 @@ const routeMeta = { export default routeMeta; export { routeMeta }; -`, +` ); const englishContent = await readFile(englishLocalePath, 'utf-8'); const english = decodeInventoryLocale(englishContent); const czech = decodeInventoryLocale( - await readFixtureFile(fixture.root, 'verticals/inventory-stock/locales/cs/inventory.json'), + await readFixtureFile( + fixture.root, + 'verticals/inventory-stock/locales/cs/inventory.json' + ) ); assert.equal(english.inventory.existing, 'en-preserved'); - assert.match(englishContent, /"inventory": \{"existing":"en-preserved", "pages":/u); + assert.match( + englishContent, + /"inventory": \{"existing":"en-preserved", "pages":/u + ); assert.doesNotMatch(englishContent, /(? { key, }; }; -`, +` ); const runnerPath = path.join(fixture.root, 'render-generated-page.tsx'); await writeFile( @@ -4681,7 +5430,7 @@ import Page from './verticals/inventory-stock/src/federation/page-customers.tsx' process.stdout.write(renderToStaticMarkup()); `, - 'utf-8', + 'utf-8' ); const bundlePath = path.join(fixture.root, 'render-generated-page.cjs'); const bundle = spawnSync( @@ -4694,9 +5443,10 @@ process.stdout.write(renderToStaticMarkup()); '--platform=node', `--outfile=${bundlePath}`, ], - { cwd: fixture.root, encoding: 'utf-8' }, + { cwd: fixture.root, encoding: 'utf-8' } ); - assert.equal(bundle.status, 0, bundle.stderr || bundle.error?.message); + assert.ifError(bundle.error); + assert.equal(bundle.status, 0, bundle.stderr); const renderLanguage = (language: 'cs' | 'en') => spawnSync(process.execPath, [bundlePath], { cwd: fixture.root, @@ -4723,14 +5473,18 @@ test('adds further pages after generated owner files have been formatted', async await Promise.all( formattedOwnerPaths.map(async (relativePath) => { const filePath = path.join(fixture.root, relativePath); - const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { - cwd: appRoot, - encoding: 'utf-8', - input: await readFile(filePath, 'utf-8'), - }); + const formatted = spawnSync( + oxfmtPath, + [`--stdin-filepath=${relativePath}`], + { + cwd: appRoot, + encoding: 'utf-8', + input: await readFile(filePath, 'utf-8'), + } + ); assert.equal(formatted.status, 0, formatted.stderr); await writeFile(filePath, formatted.stdout, 'utf-8'); - }), + }) ); }; @@ -4758,20 +5512,34 @@ test('adds further pages after generated owner files have been formatted', async ]); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); + const registration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); + const shellClients = await readFixtureFile( + fixture.root, + shellVerticalClientsFile + ); await Promise.all( - ['customer-notes', 'customers', fixtureName.purchaseOrdersPage].map(async (page) => { - assert.match(manifest, new RegExp(`inventory\\.stock\\.page\\.${page}`, 'u')); - assert.match(registration, new RegExp(`'page-${page}'`, 'u')); - assert.match(shellClients, new RegExp(`inventory\\.stock\\.page-${page}`, 'u')); - await stat( - path.join( - fixture.root, - `verticals/inventory-stock/src/routes/[lang]/inventory-stock/${page}/page.tsx`, - ), - ); - }), + ['customer-notes', 'customers', fixtureName.purchaseOrdersPage].map( + async (page) => { + assert.match( + manifest, + new RegExp(`inventory\\.stock\\.page\\.${page}`, 'u') + ); + assert.match(registration, new RegExp(`'page-${page}'`, 'u')); + assert.match( + shellClients, + new RegExp(`inventory\\.stock\\.page-${page}`, 'u') + ); + await stat( + path.join( + fixture.root, + `verticals/inventory-stock/src/routes/[lang]/inventory-stock/${page}/page.tsx` + ) + ); + } + ) ); }); }); @@ -4788,7 +5556,7 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica ]); const page = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/purchasing/orders/page.tsx', + 'verticals/inventory-stock/src/routes/[lang]/purchasing/orders/page.tsx' ); assert.match(page, /from '\.\.\/\.\.\/\.\.\/ultramodern-route-head'/u); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); @@ -4796,9 +5564,9 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica assert.match( await readFixtureFile( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/purchasing/orders/page.data.ts', + 'apps/shell-super-app/src/routes/[lang]/purchasing/orders/page.data.ts' ), - /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u, + /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u ); const beforeRerun = await snapshotTree(fixture.root); await run(fixture, scaffoldCommand.microverticalPage, [ @@ -4819,7 +5587,7 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica '--url', '/different/orders', ]), - /already exists at another URL/u, + /already exists at another URL/u ); await assert.rejects( run(fixture, scaffoldCommand.microverticalPage, [ @@ -4830,7 +5598,7 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica '--url', purchasingOrdersUrl, ]), - /already exists|collides/u, + /already exists|collides/u ); assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); }); @@ -4847,7 +5615,7 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica await stat(path.join(fixture.root, inventoryOrdersRouteFile)); assert.match( await readFixtureFile(fixture.root, inventoryManifestFile), - /routePath: '\/orders'/u, + /routePath: '\/orders'/u ); }); @@ -4868,11 +5636,18 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', url], - /--url/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + url, + ], + /--url/u ); - }), - ), + }) + ) ); }); @@ -4889,7 +5664,7 @@ test('generates a non-navigational dynamic page with canonical parameters and ro additionalData: ({ stats }) => ({ exposes: stats.exposes }), }, }; -`, +` ); const generatorArguments = [ scaffoldFlag.vertical, @@ -4901,55 +5676,102 @@ test('generates a non-navigational dynamic page with canonical parameters and ro ]; await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const ownerRoute = 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/edit'; - const shellRoute = 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/edit'; + const ownerRoute = + 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/edit'; + const shellRoute = + 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/edit'; const page = await readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); - const ownerMetadata = await readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); - const shellLoader = await readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); - const shellMetadata = await readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); + const ownerMetadata = await readFixtureFile( + fixture.root, + `${ownerRoute}/route.meta.ts` + ); + const shellLoader = await readFixtureFile( + fixture.root, + `${shellRoute}/page.data.ts` + ); + const shellMetadata = await readFixtureFile( + fixture.root, + `${shellRoute}/route.meta.ts` + ); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const federation = await readFixtureFile(fixture.root, inventoryFederationConfigFile); + const registration = await readFixtureFile( + fixture.root, + inventoryRegistrationFile + ); + const federation = await readFixtureFile( + fixture.root, + inventoryFederationConfigFile + ); const federatedPage = await readFixtureFile( fixture.root, - 'verticals/inventory-stock/src/federation/page-customer-edit.tsx', + 'verticals/inventory-stock/src/federation/page-customer-edit.tsx' + ); + const shellClients = await readFixtureFile( + fixture.root, + shellVerticalClientsFile ); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); - assert.match(page, /export const CustomerEditPageRouteParams = Schema\.Struct/u); assert.match( page, - /id: Schema\.String\.pipe\(Schema\.brand\('CustomerEditPageIdRouteParameter'\)\)/u, + /export const CustomerEditPageRouteParams = Schema\.Struct/u + ); + assert.match( + page, + /id: Schema\.String\.pipe\(Schema\.brand\('CustomerEditPageIdRouteParameter'\)\)/u ); assert.match( page, - /export type CustomerEditPageRouteParams = typeof CustomerEditPageRouteParams\.Type/u, + /export type CustomerEditPageRouteParams = typeof CustomerEditPageRouteParams\.Type/u + ); + assert.match( + page, + /Schema\.toStandardSchemaV1\(\s*CustomerEditPageRouteParams,?\s*\)/u ); - assert.match(page, /Schema\.toStandardSchemaV1\(\s*CustomerEditPageRouteParams,?\s*\)/u); assert.match(page, /CustomerEditPage = \(\{ routeParams \}/u); assert.match(page, /void routeParams;/u); - assert.match(ownerMetadata, /canonicalPath: '\/contacts\/customers\/:id\/edit'/u); + assert.match( + ownerMetadata, + /canonicalPath: '\/contacts\/customers\/:id\/edit'/u + ); assert.match(ownerMetadata, /en: '\/contacts\/customers\/:id\/edit'/u); - assert.match(shellMetadata, /canonicalPath: '\/contacts\/customers\/:id\/edit'/u); + assert.match( + shellMetadata, + /canonicalPath: '\/contacts\/customers\/:id\/edit'/u + ); assert.match(manifest, /routePath: '\/contacts\/customers\/:id\/edit'/u); assert.match(manifest, /inventory\.stock\.page\.customer-edit/u); - assert.doesNotMatch(manifest, /inventory\.stock\.navigation\.customer-edit/u); + assert.doesNotMatch( + manifest, + /inventory\.stock\.navigation\.customer-edit/u + ); assert.match(registration, /'page-customer-edit'/u); assert.match(federation, /'\.\/PageCustomerEdit'/u); assert.match(federatedPage, /type CustomerEditPageRouteParams/u); assert.doesNotMatch(federatedPage, /Schema\.Struct/u); - assert.match(federatedPage, //u); + assert.match( + federatedPage, + //u + ); assert.match(shellClients, /inventory\.stock\.page-customer-edit/u); assert.match(shellLoader, /selectRouteParams/u); - assert.match(shellLoader, /const routeParameterNames = \['id'\] as const;/u); - assert.match(shellLoader, /routeParams: selectRouteParams\(params, routeParameterNames\)/u); + assert.match( + shellLoader, + /const routeParameterNames = \['id'\] as const;/u + ); + assert.match( + shellLoader, + /routeParams: selectRouteParams\(params, routeParameterNames\)/u + ); assert.match( await readFixtureFile(fixture.root, inventoryEnglishLocaleFile), - /"customerEdit"/u, + /"customerEdit"/u ); assert.match( - await readFixtureFile(fixture.root, 'verticals/inventory-stock/locales/cs/inventory.json'), - /"customerEdit"/u, + await readFixtureFile( + fixture.root, + 'verticals/inventory-stock/locales/cs/inventory.json' + ), + /"customerEdit"/u ); const afterFirstRun = await snapshotTree(fixture.root); @@ -4977,38 +5799,65 @@ test('generates the Contacts Contact-detail two-parameter page atomically and sa await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); const page = await readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); - const ownerMetadata = await readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); - const shellLoader = await readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); - const shellMetadata = await readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); + const ownerMetadata = await readFixtureFile( + fixture.root, + `${ownerRoute}/route.meta.ts` + ); + const shellLoader = await readFixtureFile( + fixture.root, + `${shellRoute}/page.data.ts` + ); + const shellMetadata = await readFixtureFile( + fixture.root, + `${shellRoute}/route.meta.ts` + ); const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - assert.match(page, /export const ContactDetailPageRouteParams = Schema\.Struct/u); assert.match( page, - /id: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageIdRouteParameter'\)\)/u, + /export const ContactDetailPageRouteParams = Schema\.Struct/u + ); + assert.match( + page, + /id: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageIdRouteParameter'\)\)/u + ); + assert.match( + page, + /contactId: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageContactIdRouteParameter'\)\)/u ); assert.match( page, - /contactId: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageContactIdRouteParameter'\)\)/u, + /export type ContactDetailPageRouteParams = typeof ContactDetailPageRouteParams\.Type/u ); assert.match( page, - /export type ContactDetailPageRouteParams = typeof ContactDetailPageRouteParams\.Type/u, + /Schema\.toStandardSchemaV1\(\s*ContactDetailPageRouteParams,?\s*\)/u ); - assert.match(page, /Schema\.toStandardSchemaV1\(\s*ContactDetailPageRouteParams,?\s*\)/u); assert.match( ownerMetadata, - /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, + /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u ); assert.match( shellMetadata, - /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, + /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u + ); + assert.match( + manifest, + /routePath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u ); - assert.match(manifest, /routePath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u); assert.match(manifest, /inventory\.stock\.page\.contact-detail/u); - assert.doesNotMatch(manifest, /inventory\.stock\.navigation\.contact-detail/u); - assert.match(shellLoader, /const routeParameterNames = \['id', 'contactId'\] as const;/u); - assert.match(shellLoader, /routeParams: selectRouteParams\(params, routeParameterNames\)/u); + assert.doesNotMatch( + manifest, + /inventory\.stock\.navigation\.contact-detail/u + ); + assert.match( + shellLoader, + /const routeParameterNames = \['id', 'contactId'\] as const;/u + ); + assert.match( + shellLoader, + /routeParams: selectRouteParams\(params, routeParameterNames\)/u + ); await stat(path.join(fixture.root, ownerRoute)); await stat(path.join(fixture.root, shellRoute)); @@ -5021,13 +5870,13 @@ test('generates the Contacts Contact-detail two-parameter page atomically and sa await writeFixtureFile( fixture.root, 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]/page.tsx', - 'export default function DeveloperOwnedPage() { return null; }\n', + 'export default function DeveloperOwnedPage() { return null; }\n' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /refusing to overwrite|already exists/u, + /refusing to overwrite|already exists/u ); }); }); @@ -5058,10 +5907,10 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri '--url', url, ], - /--url/u, + /--url/u ); - }), - ), + }) + ) ); await Promise.all([ @@ -5085,7 +5934,7 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri '--url', '/inventory/customers/:customerId', ], - /routing collision|already registered|collides/u, + /routing collision|already registered|collides/u ); }), withFixture(async (fixture) => { @@ -5100,25 +5949,25 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); const pagePath = path.join( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', + 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx' ); await writeFile( pagePath, `${await readFile(pagePath, 'utf-8')}\n// developer edit\n`, - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /collides/u, + /collides/u ); }), withFixture(async (fixture) => { await writeFixtureFile( fixture.root, 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', - 'export default function PartialPage() { return null; }\n', + 'export default function PartialPage() { return null; }\n' ); await assertScaffoldRefused( fixture, @@ -5131,7 +5980,7 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri '--url', customerEditUrl, ], - /collides with nested content/u, + /collides with nested content/u ); }), withFixture(async (fixture) => { @@ -5154,7 +6003,7 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri '--url', customerDetailUrl, ], - /static route segment|collides/u, + /static route segment|collides/u ); }), withFixture(async (fixture) => { @@ -5169,9 +6018,9 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri await rm( path.join( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/shared/customers/[id]/edit', + 'apps/shell-super-app/src/routes/[lang]/shared/customers/[id]/edit' ), - { recursive: true }, + { recursive: true } ); await assertScaffoldRefused( fixture, @@ -5184,7 +6033,7 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri '--url', '/shared/customers/:id/edit', ], - /already registered by billing/u, + /already registered by billing/u ); }), ]); @@ -5203,7 +6052,7 @@ test('extends an existing dynamic route branch without reclassifying an existing await writeFixtureFile( fixture.root, 'apps/shell-super-app/src/routes/[lang]/inventory/customers/new/page.tsx', - 'export default function ExistingStaticSibling() { return null; }\n', + 'export default function ExistingStaticSibling() { return null; }\n' ); await run(fixture, scaffoldCommand.microverticalPage, [ @@ -5218,8 +6067,8 @@ test('extends an existing dynamic route branch without reclassifying an existing await stat( path.join( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', - ), + 'apps/shell-super-app/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx' + ) ); }); }); @@ -5230,7 +6079,7 @@ test('rejects reserved, dynamic, and cross-owner page URLs before writing', asyn await writeFixtureFile( fixture.root, 'apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx', - 'export default function ModulePage() { return null; }\n', + 'export default function ModulePage() { return null; }\n' ); await assertScaffoldRefused( fixture, @@ -5243,20 +6092,27 @@ test('rejects reserved, dynamic, and cross-owner page URLs before writing', asyn '--url', '/modules/customers', ], - /collides with dynamic route segment \[moduleId\]/u, + /collides with dynamic route segment \[moduleId\]/u ); }), withFixture(async (fixture) => { await writeFixtureFile( fixture.root, 'apps/shell-super-app/src/routes/[lang]/login/page.tsx', - 'export default function LoginPage() { return null; }\n', + 'export default function LoginPage() { return null; }\n' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'customers', '--url', '/login/customers'], - /reserved route prefix \/login/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + '--url', + '/login/customers', + ], + /reserved route prefix \/login/u ); }), withFixture(async (fixture) => { @@ -5268,9 +6124,15 @@ test('rejects reserved, dynamic, and cross-owner page URLs before writing', asyn '--url', '/shared/customers', ]); - await rm(path.join(fixture.root, 'apps/shell-super-app/src/routes/[lang]/shared/customers'), { - recursive: true, - }); + await rm( + path.join( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/shared/customers' + ), + { + recursive: true, + } + ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, @@ -5282,7 +6144,7 @@ test('rejects reserved, dynamic, and cross-owner page URLs before writing', asyn '--url', '/shared/customers', ], - /already registered by billing/u, + /already registered by billing/u ); }), ]); @@ -5305,8 +6167,8 @@ test('uses exact page identities and rejects edited generated wiring', async () await stat( path.join( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/order/page.tsx', - ), + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/order/page.tsx' + ) ); }); @@ -5326,31 +6188,55 @@ test('uses exact page identities and rejects edited generated wiring', async () manifestPath, manifest .replaceAll("'page-orders'", '"page-orders"') - .replaceAll("'inventory.stock.page.orders'", '"inventory.stock.page.orders"'), - 'utf-8', + .replaceAll( + "'inventory.stock.page.orders'", + '"inventory.stock.page.orders"' + ), + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', '/second/orders'], - /page identity inventory\.stock\.page\.orders already exists/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + '/second/orders', + ], + /page identity inventory\.stock\.page\.orders already exists/u ); }), withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]; await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); const manifestPath = path.join(fixture.root, inventoryManifestFile); const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile(manifestPath, manifest.replace('order: 100', 'order: 101'), 'utf-8'); + await writeFile( + manifestPath, + manifest.replace('order: 100', 'order: 101'), + 'utf-8' + ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /already exists|collides/u, + /already exists|collides/u ); }), withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]; await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); const manifestPath = path.join(fixture.root, inventoryManifestFile); const manifest = await readFile(manifestPath, 'utf-8'); @@ -5359,50 +6245,63 @@ test('uses exact page identities and rejects edited generated wiring', async () manifest.replace( '// ', `{ contributionKey : "inventory.stock.navigation.orders", entrypoint: { access: 'read', entrypointKey: 'inventory.stock.page.orders', moduleKey: 'inventory.stock', role: 'page', scope: 'tenant' }, groupKey: 'shell.navigation.modules', order: 101, pageKey: 'inventory.stock.page.orders' }, - // `, + // ` ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /already exists|collides/u, + /already exists|collides/u ); }), withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]; await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const federationPath = path.join(fixture.root, inventoryFederationConfigFile); + const federationPath = path.join( + fixture.root, + inventoryFederationConfigFile + ); const federation = await readFile(federationPath, 'utf-8'); await writeFile( federationPath, federation.replace( "'./src/federation/page-orders.tsx'", - "'./src/federation/page-other.tsx'", + "'./src/federation/page-other.tsx'" ), - 'utf-8', + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /already exists|collides/u, + /already exists|collides/u ); }), withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]; await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); await writeFixtureFile( fixture.root, 'apps/shell-super-app/src/routes/[lang]/inventory-stock/orders/developer-note.ts', - 'export const developerNote = true;\n', + 'export const developerNote = true;\n' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /already exists|collides/u, + /already exists|collides/u ); }), ]); @@ -5458,7 +6357,7 @@ export const OrdersPage = () => { }; export default OrdersPage; -`, +` ); await writeFixtureFile( fixture.root, @@ -5471,15 +6370,17 @@ interface ShellPageLoaderArguments { export const loader = ({ request }: ShellPageLoaderArguments) => loadModuleTarget({ params: { moduleId: 'inventory.stock' }, request }); -`, +` ); await Promise.all( ['cs', 'en'].map(async (locale) => { const localePath = path.join( fixture.root, - `verticals/inventory-stock/locales/${locale}/inventory.json`, + `verticals/inventory-stock/locales/${locale}/inventory.json` + ); + const catalog = decodeInventoryLocale( + await readFile(localePath, 'utf-8') ); - const catalog = decodeInventoryLocale(await readFile(localePath, 'utf-8')); const ordersPage = locale === 'cs' ? { @@ -5500,14 +6401,14 @@ export const loader = ({ request }: ShellPageLoaderArguments) => }, }); await writeFile(localePath, json(nextCatalog), 'utf-8'); - }), + }) ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, generatorArguments, - /page route already exists or collides/u, + /page route already exists or collides/u ); }); }); @@ -5515,7 +6416,9 @@ export const loader = ({ request }: ShellPageLoaderArguments) => test('rejects page generation when an owning locale has no truthful starter translation', async () => { await withFixture(async (fixture) => { const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); + const packageJson = decodeFixturePackage( + await readFile(packagePath, 'utf-8') + ); await writeFile( packagePath, json({ @@ -5525,18 +6428,23 @@ test('rejects page generation when an owning locale has no truthful starter tran './locales/de': './locales/de/inventory.json', }, }), - 'utf-8', + 'utf-8' ); await writeFixtureFile( fixture.root, 'verticals/inventory-stock/locales/de/inventory.json', - json({ inventory: { existing: 'de-preserved' } }), + json({ inventory: { existing: 'de-preserved' } }) ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], - /no starter translation for locale de/u, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + ], + /no starter translation for locale de/u ); }); }); @@ -5544,13 +6452,16 @@ test('rejects page generation when an owning locale has no truthful starter tran test('page prerequisite and nested-route failures are preflighted, while refresh failure is safely rerunnable', async () => { await withFixture(async (fixture) => { await rm( - path.join(fixture.root, 'verticals/inventory-stock/src/routes/ultramodern-route-head.tsx'), + path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/ultramodern-route-head.tsx' + ) ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - /UltramodernRouteHead is missing/u, + /UltramodernRouteHead is missing/u ); }); @@ -5558,13 +6469,13 @@ test('page prerequisite and nested-route failures are preflighted, while refresh await writeFixtureFile( fixture.root, 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/nested.ts', - 'export {};\n', + 'export {};\n' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalPage, [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - /collides with nested content/u, + /collides with nested content/u ); }); @@ -5587,15 +6498,15 @@ test('page prerequisite and nested-route failures are preflighted, while refresh throw new Error('route refresh fixture failure'); }, workspaceRoot: fixture.root, - }, + } ), - /route refresh fixture failure/u, + /route refresh fixture failure/u ); await stat( path.join( fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', - ), + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx' + ) ); const afterRefreshFailure = await snapshotTree(fixture.root); const refreshes: string[] = []; @@ -5605,14 +6516,16 @@ test('page prerequisite and nested-route failures are preflighted, while refresh [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], (appId) => { refreshes.push(appId); - }, + } ); assert.deepEqual(refreshes, [inventorySlug, shellAppId]); assert.deepEqual(await snapshotTree(fixture.root), afterRefreshFailure); }); }); -const runCombinedScenario = async (fixture: Fixture): Promise>> => { +const runCombinedScenario = async ( + fixture: Fixture +): Promise>> => { await addInventoryItemResourceType(fixture); await run(fixture, scaffoldCommand.microverticalActionBoundary, [ scaffoldFlag.vertical, @@ -5648,7 +6561,12 @@ const runCombinedScenario = async (fixture: Fixture): Promise assert.ok([inventorySlug, shellAppId].includes(appId)), + (appId) => assert.ok([inventorySlug, shellAppId].includes(appId)) ); await run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, @@ -5702,8 +6620,8 @@ test('all generators compose deterministically without crossing owner boundaries try { const billingBefore = Object.fromEntries( Object.entries(await snapshotTree(first.root)).filter(([file]) => - file.startsWith('verticals/billing/'), - ), + file.startsWith('verticals/billing/') + ) ); const shellBefore = await readFixtureFile(first.root, shellSentinelFile); const topologyBefore = await readFixtureFile(first.root, topologyFile); @@ -5711,13 +6629,24 @@ test('all generators compose deterministically without crossing owner boundaries const secondTree = await runCombinedScenario(second); assert.deepEqual(firstTree, secondTree); const billingAfter = Object.fromEntries( - Object.entries(firstTree).filter(([file]) => file.startsWith('verticals/billing/')), + Object.entries(firstTree).filter(([file]) => + file.startsWith('verticals/billing/') + ) ); assert.deepEqual(billingAfter, billingBefore); - assert.equal(await readFixtureFile(first.root, shellSentinelFile), shellBefore); - assert.equal(await readFixtureFile(first.root, topologyFile), topologyBefore); + assert.equal( + await readFixtureFile(first.root, shellSentinelFile), + shellBefore + ); + assert.equal( + await readFixtureFile(first.root, topologyFile), + topologyBefore + ); const combinedSource = Object.values(firstTree).join('\n'); - assert.doesNotMatch(combinedSource, /from ['"]\.\.\/\.\.\/billing|fetch\(/u); + assert.doesNotMatch( + combinedSource, + /from ['"]\.\.\/\.\.\/billing|fetch\(/u + ); } finally { await rm(first.root, { force: true, recursive: true }); await rm(second.root, { force: true, recursive: true }); @@ -5774,14 +6703,22 @@ test('every generated TypeScript file is already formatter-stable', async () => await Promise.all( generatedFiles.map(async (relativePath) => { const source = await readFixtureFile(fixture.root, relativePath); - const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { - cwd: appRoot, - encoding: 'utf-8', - input: source, - }); + const formatted = spawnSync( + oxfmtPath, + [`--stdin-filepath=${relativePath}`], + { + cwd: appRoot, + encoding: 'utf-8', + input: source, + } + ); assert.equal(formatted.status, 0, formatted.stderr); - assert.equal(formatted.stdout, source, `${relativePath} must be formatter-stable`); - }), + assert.equal( + formatted.stdout, + source, + `${relativePath} must be formatter-stable` + ); + }) ); }); }); @@ -5799,16 +6736,28 @@ test('all generated files typecheck against the real workspace contracts', async '--topic', fixtureName.ordersCreated, ]); - await mkdir(path.join(fixture.root, 'node_modules', '@authzed'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@effect'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@types'), { recursive: true }); + await mkdir(path.join(fixture.root, 'node_modules', '@authzed'), { + recursive: true, + }); + await mkdir(path.join(fixture.root, 'node_modules', '@effect'), { + recursive: true, + }); + await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { + recursive: true, + }); + await mkdir(path.join(fixture.root, 'node_modules', '@types'), { + recursive: true, + }); // Every generated-runtime dependency is linked from the real workspace so the fixture // typechecks and runs against the same modules the shipped verticals resolve. await Promise.all( ( [ - ['packages/core-runtime/node_modules/effect', effectNodeModulePath, 'dir'], + [ + 'packages/core-runtime/node_modules/effect', + effectNodeModulePath, + 'dir', + ], [ 'packages/core-runtime/node_modules/@effect/sql-pg', 'node_modules/@effect/sql-pg', @@ -5819,9 +6768,21 @@ test('all generated files typecheck against the real workspace contracts', async 'node_modules/@effect/platform-node', 'dir', ], - ['apps/shell-super-app/node_modules/jose', 'node_modules/jose', 'dir'], - ['packages/core-runtime/node_modules/drizzle-orm', 'node_modules/drizzle-orm', 'dir'], - ['packages/core-runtime/node_modules/dotenv', 'node_modules/dotenv', 'dir'], + [ + 'apps/shell-super-app/node_modules/jose', + 'node_modules/jose', + 'dir', + ], + [ + 'packages/core-runtime/node_modules/drizzle-orm', + 'node_modules/drizzle-orm', + 'dir', + ], + [ + 'packages/core-runtime/node_modules/dotenv', + 'node_modules/dotenv', + 'dir', + ], ['packages/core-runtime/node_modules/pg', 'node_modules/pg', 'dir'], [ 'packages/core-runtime/node_modules/@authzed/authzed-node', @@ -5838,16 +6799,52 @@ test('all generated files typecheck against the real workspace contracts', async pluginBffNodeModulePath, 'dir', ], - ['apps/shell-super-app/node_modules/@types/react', 'node_modules/@types/react', 'dir'], - ['packages/core-runtime/node_modules/@types/pg', 'node_modules/@types/pg', 'dir'], + [ + 'apps/shell-super-app/node_modules/@types/react', + 'node_modules/@types/react', + 'dir', + ], + [ + 'packages/core-runtime/node_modules/@types/pg', + 'node_modules/@types/pg', + 'dir', + ], ['node_modules/@types/node', 'node_modules/@types/node', 'dir'], - ['packages/core-runtime/src/actions', 'packages/core-runtime/src/actions', 'dir'], - ['packages/core-runtime/src/db', 'packages/core-runtime/src/db', 'dir'], - ['packages/core-runtime/src/operations', 'packages/core-runtime/src/operations', 'dir'], - ['packages/core-runtime/src/database', 'packages/core-runtime/src/database', 'dir'], - ['packages/core-runtime/src/environment', 'packages/core-runtime/src/environment', 'dir'], - ['packages/core-runtime/src/permissions', 'packages/core-runtime/src/permissions', 'dir'], - ['packages/core-runtime/src/auth', 'packages/core-runtime/src/auth', 'dir'], + [ + 'packages/core-runtime/src/actions', + 'packages/core-runtime/src/actions', + 'dir', + ], + [ + 'packages/core-runtime/src/db', + 'packages/core-runtime/src/db', + 'dir', + ], + [ + 'packages/core-runtime/src/operations', + 'packages/core-runtime/src/operations', + 'dir', + ], + [ + 'packages/core-runtime/src/database', + 'packages/core-runtime/src/database', + 'dir', + ], + [ + 'packages/core-runtime/src/environment', + 'packages/core-runtime/src/environment', + 'dir', + ], + [ + 'packages/core-runtime/src/permissions', + 'packages/core-runtime/src/permissions', + 'dir', + ], + [ + 'packages/core-runtime/src/auth', + 'packages/core-runtime/src/auth', + 'dir', + ], [ 'packages/core-runtime/src/authorization', 'packages/core-runtime/src/authorization', @@ -5861,8 +6858,12 @@ test('all generated files typecheck against the real workspace contracts', async ] as const ).map( async ([source, target, kind]) => - await symlink(path.join(appRoot, source), path.join(fixture.root, target), kind), - ), + await symlink( + path.join(appRoot, source), + path.join(fixture.root, target), + kind + ) + ) ); await Promise.all( [ @@ -5877,10 +6878,14 @@ test('all generated files typecheck against the real workspace contracts', async async (moduleFile) => await symlink( path.join(appRoot, 'packages/core-runtime/src/modules', moduleFile), - path.join(fixture.root, 'packages/core-runtime/src/modules', moduleFile), - 'file', - ), - ), + path.join( + fixture.root, + 'packages/core-runtime/src/modules', + moduleFile + ), + 'file' + ) + ) ); const fixtureTsconfig = path.join(fixture.root, 'tsconfig.generated.json'); await writeFile( @@ -5895,39 +6900,76 @@ test('all generated files typecheck against the real workspace contracts', async paths: { '@app/core-runtime': [path.join(appRoot, coreRuntimeIndexFile)], '@app/core-runtime/actions/principal-context': [ - path.join(appRoot, 'packages/core-runtime/src/actions/principal-context.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/actions/principal-context.ts' + ), ], '@app/core-runtime/actions/runtime-wiring': [ - path.join(appRoot, 'packages/core-runtime/src/actions/runtime-wiring.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/actions/runtime-wiring.ts' + ), ], '@app/core-runtime/auth/gateway-assertion-redemption': [ - path.join(appRoot, 'packages/core-runtime/src/auth/gateway-assertion-redemption.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/auth/gateway-assertion-redemption.ts' + ), ], '@app/core-runtime/http/action-runner': [ - path.join(appRoot, 'packages/core-runtime/src/http/http-instrumentation-seam.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/http/http-instrumentation-seam.ts' + ), ], '@app/core-runtime/http/governed-read': [ - path.join(appRoot, 'packages/core-runtime/src/http/governed-read.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/http/governed-read.ts' + ), ], '@app/core-runtime/http/principal-authentication': [ - path.join(appRoot, 'packages/core-runtime/src/http/principal-authentication.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/http/principal-authentication.ts' + ), ], '@app/core-runtime/outbox/worker': [ - path.join(appRoot, 'packages/core-runtime/src/outbox/worker-entrypoint.ts'), + path.join( + appRoot, + 'packages/core-runtime/src/outbox/worker-entrypoint.ts' + ), ], '@app/gateway-principal-verifier/server': [ - path.join(appRoot, 'packages/gateway-principal-verifier/src/server.ts'), + path.join( + appRoot, + 'packages/gateway-principal-verifier/src/server.ts' + ), + ], + '@app/inventory-stock/outbox/*': [ + './verticals/inventory-stock/shared/outbox/*.ts', + ], + '@app/shared-contracts': [ + path.join(appRoot, 'packages/shared-contracts/src/index.ts'), ], - '@app/inventory-stock/outbox/*': ['./verticals/inventory-stock/shared/outbox/*.ts'], - '@app/shared-contracts': [path.join(appRoot, 'packages/shared-contracts/src/index.ts')], '@app/shared-contracts/client-runtime': [ - path.join(appRoot, 'packages/shared-contracts/src/client-runtime.ts'), + path.join( + appRoot, + 'packages/shared-contracts/src/client-runtime.ts' + ), ], '@app/shared-contracts/problem-details': [ - path.join(appRoot, 'packages/shared-contracts/src/problem-details.ts'), + path.join( + appRoot, + 'packages/shared-contracts/src/problem-details.ts' + ), ], '@app/shared-contracts/server/effect-bff-runtime': [ - path.join(appRoot, 'packages/shared-contracts/src/effect-bff-runtime.ts'), + path.join( + appRoot, + 'packages/shared-contracts/src/effect-bff-runtime.ts' + ), ], }, resolveJsonModule: true, @@ -5958,7 +7000,7 @@ test('all generated files typecheck against the real workspace contracts', async 'verticals/inventory-stock/src/reports/**/*.ts', ], }), - 'utf-8', + 'utf-8' ); const result = spawnSync(tscPath, ['-p', fixtureTsconfig], { @@ -5986,24 +7028,25 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - [nestedEntry, neighborEntry], + [nestedEntry, neighborEntry] ); const next = insertSortedSlot( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, [addedEntry], - (entry) => entry === nestedEntry || entry === neighborEntry || entry === addedEntry, + (entry) => + entry === nestedEntry || entry === neighborEntry || entry === addedEntry ); assert.deepEqual( readGeneratedSlotEntries( next, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - [nestedEntry, neighborEntry, addedEntry], + [nestedEntry, neighborEntry, addedEntry] ); }); @@ -6022,15 +7065,16 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - [protectedEntry, '.addHttpApi(SecondApi)'], + [protectedEntry, '.addHttpApi(SecondApi)'] ); }); } test('generated fluent slots preserve terminated statements and reset tail boundaries', () => { - const statement = '.addHttpApi(StatementApi)\n.addHttpApi(StatementNeighbor);'; + const statement = + '.addHttpApi(StatementApi)\n.addHttpApi(StatementNeighbor);'; const tailEntries = ['.addHttpApi(TailApi)', '.addHttpApi(TailNeighbor)']; const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${statement} @@ -6040,14 +7084,17 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - [statement, ...tailEntries], + [statement, ...tailEntries] ); }); test('generated fluent slots preserve nonfluent multiline statement continuations', () => { - const entries = ['const api = FirstApi\n .addGroup(FirstGroup);', 'SecondApi,']; + const entries = [ + 'const api = FirstApi\n .addGroup(FirstGroup);', + 'SecondApi,', + ]; const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${entries.join('\n')} ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; @@ -6055,9 +7102,9 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - entries, + entries ); }); @@ -6078,9 +7125,9 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ), - /generated owner slot contains unsupported developer content/u, + /generated owner slot contains unsupported developer content/u ); }); } @@ -6097,27 +7144,37 @@ test('generated fluent slots preserve multiline call entries', () => { GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ['.addHttpApi(ThirdApi)'], - (entry) => entry.startsWith('.addHttpApi(') && entry.endsWith(')'), + (entry) => entry.startsWith('.addHttpApi(') && entry.endsWith(')') ); const entries = readGeneratedSlotEntries( next, GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, + GOVERNED_HTTP_API_ADDITION_SLOT_END ); assert.equal(entries.length, 3); assert.match(entries[0] ?? '', /FirstApi/u); }); for (const [start, end] of [ - [GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END], - [GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END], + [ + GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START, + GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END, + ], + [ + GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START, + GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END, + ], ] as const) { test(`governed generation accepts the independent support slot without ${start}`, async () => { await withFixture(async (fixture) => { const rootPath = path.join(fixture.root, inventoryHandlerRootFile); const source = await readFile(rootPath, 'utf-8'); assert.ok(source.includes(start)); - await writeFile(rootPath, source.replace(start, '').replace(end, ''), 'utf-8'); + await writeFile( + rootPath, + source.replace(start, '').replace(end, ''), + 'utf-8' + ); await run(fixture, scaffoldCommand.moduleApi, [ scaffoldFlag.vertical, inventorySlug, @@ -6140,23 +7197,29 @@ test('Action identity boundary rejects an owned file without the authentication const source = await readFile(serverPath, 'utf-8'); await writeFile( serverPath, - source.replaceAll('authenticateOperationPrincipal', 'removedAuthenticationAdapter'), - 'utf-8', + source.replaceAll( + 'authenticateOperationPrincipal', + 'removedAuthenticationAdapter' + ), + 'utf-8' ); await assertScaffoldRefused( fixture, scaffoldCommand.microverticalActionBoundary, [scaffoldFlag.vertical, inventorySlug], - /refusing|owned|boundary/u, + /refusing|owned|boundary/u ); }); }); test('typed injected governed runtime stays bound to the exported owner composition', async () => { - const shared = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); + const shared = await readFile( + path.join(appRoot, partyGovernedContractPath), + 'utf-8' + ); const handler = await readFile( path.join(appRoot, 'verticals/party-registry/api/index.ts'), - 'utf-8', + 'utf-8' ); assert.equal(hasValidGovernedHttpCompositionRoot(shared, handler), true); assert.equal( @@ -6164,52 +7227,67 @@ test('typed injected governed runtime stays bound to the exported owner composit shared, handler.replace( 'readRuntime: Layer.Layer { - const shared = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); + const shared = await readFile( + path.join(appRoot, partyGovernedContractPath), + 'utf-8' + ); const handler = await readFile( path.join(appRoot, 'verticals/party-registry/api/index.ts'), - 'utf-8', + 'utf-8' ); for (const [before, after] of [ [ 'const resolvedApiHandlersLive = apiHandlersLive.pipe(', 'const resolvedApiHandlersLive = unrelatedHandlers.pipe(', ], - ["'@app/shared-contracts/server/effect-bff-runtime'", "'./counterfeit-assembler.ts'"], + [ + "'@app/shared-contracts/server/effect-bff-runtime'", + "'./counterfeit-assembler.ts'", + ], ['handlers: resolvedApiHandlersLive,', 'handlers: unrelatedHandlers,'], ] as const) { assert.ok(handler.includes(before)); assert.equal( - hasValidGovernedHttpCompositionRoot(shared, handler.replace(before, after)), - false, + hasValidGovernedHttpCompositionRoot( + shared, + handler.replace(before, after) + ), + false ); } }); diff --git a/app/scripts/setup-agent-reference-repos.mts b/app/scripts/setup-agent-reference-repos.mts index 4628de1cf..accb96d95 100644 --- a/app/scripts/setup-agent-reference-repos.mts +++ b/app/scripts/setup-agent-reference-repos.mts @@ -1,368 +1,565 @@ -import { spawnSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; - -const root = process.cwd(); -const args = new Set(process.argv.slice(2)); -const checkOnly = args.has('--check'); -const configPath = path.join(root, '.agents', 'agent-reference-repos.json'); -const manifestPath = path.join(root, '.modernjs', 'agent-reference-repos.json'); - -const truthy = (value) => /^(1|true|yes|on)$/i.test(String(value ?? '')); -const falsy = (value) => /^(0|false|no|off)$/i.test(String(value ?? '')); - -const skipRequested = - truthy(process.env.ULTRAMODERN_SKIP_AGENT_REPOS) || - falsy(process.env.ULTRAMODERN_AGENT_REPOS); -const required = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REQUIRED); -const refresh = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REFRESH); - -const gitIdentityEnv = { - GIT_AUTHOR_NAME: - process.env.GIT_AUTHOR_NAME || 'UltraModern Agent Reference Setup', - GIT_AUTHOR_EMAIL: - process.env.GIT_AUTHOR_EMAIL || 'ultramodern-agent-refs@local', - GIT_COMMITTER_NAME: - process.env.GIT_COMMITTER_NAME || 'UltraModern Agent Reference Setup', - GIT_COMMITTER_EMAIL: - process.env.GIT_COMMITTER_EMAIL || 'ultramodern-agent-refs@local', -}; - -const log = (message) => console.log(`[agent-reference-repos] ${message}`); -const warn = (message) => console.warn(`[agent-reference-repos] ${message}`); - -function fail(message) { - if (required || checkOnly) { - throw new Error(message); - } - warn(message); +#!/usr/bin/env node +import { NodeRuntime, NodeServices } from '@effect/platform-node'; +import { + Config, + Context, + DateTime, + Effect, + FileSystem, + Layer, + Option, + Path, + Schema, + Stream, +} from 'effect'; +import { Command, Flag } from 'effect/unstable/cli'; +import { ChildProcess } from 'effect/unstable/process'; + +const LOG_PREFIX = '[agent-reference-repos]'; +const REPOSITORY_STRATEGY = 'git-subtree-squash' as const; +const WORKSPACE_ROOT = '.'; + +const ReferenceRepositorySchema = Schema.Struct({ + id: Schema.String, + name: Schema.String, + path: Schema.String, + readOnly: Schema.optionalKey(Schema.Boolean), + ref: Schema.String, + url: Schema.String, +}); +const ReferenceRepositoryConfigSchema = Schema.Struct({ + defaultEnabled: Schema.Boolean, + installDir: Schema.Literal('repos'), + repositories: Schema.Array(ReferenceRepositorySchema), + schemaVersion: Schema.Literal(1), + strategy: Schema.Literal(REPOSITORY_STRATEGY), +}); +const InstalledRepositorySchema = Schema.Struct({ + commit: Schema.optionalKey(Schema.String), + id: Schema.String, + installedAt: Schema.optionalKey(Schema.DateTimeUtcFromString), + name: Schema.String, + path: Schema.String, + readOnly: Schema.Boolean, + ref: Schema.String, + schemaVersion: Schema.optionalKey(Schema.Literal(1)), + status: Schema.Literals(['installed', 'present']), + strategy: Schema.Literal(REPOSITORY_STRATEGY), + url: Schema.String, +}); +const InstalledManifestSchema = Schema.Struct({ + generatedAt: Schema.DateTimeUtcFromString, + installDir: Schema.Literal('repos'), + repositories: Schema.Array(InstalledRepositorySchema), + schemaVersion: Schema.Literal(1), + strategy: Schema.Literal(REPOSITORY_STRATEGY), +}); +const ReferenceRepositoryConfigJsonSchema = Schema.fromJsonString( + ReferenceRepositoryConfigSchema +); +const InstalledManifestJsonSchema = Schema.fromJsonString( + InstalledManifestSchema, + { space: 2 } +); +type ReferenceRepository = typeof ReferenceRepositorySchema.Type; +type InstalledRepository = typeof InstalledRepositorySchema.Type; + +class AgentReferenceRepoSetupError extends Schema.TaggedError()( + 'AgentReferenceRepoSetupError', + { reason: Schema.String } +) {} +const setupError = (reason: string) => + new AgentReferenceRepoSetupError({ reason }); +const truthy = (value: string): boolean => /^(?:1|true|yes|on)$/iu.test(value); +const falsy = (value: string): boolean => /^(?:0|false|no|off)$/iu.test(value); +const environmentValue = (name: string) => + Config.string(name).pipe(Config.withDefault('')); +const identityValue = (value: string, fallback: string): string => + value.length > 0 ? value : fallback; +const SetupEnvironment = Config.all({ + agentRepos: environmentValue('ULTRAMODERN_AGENT_REPOS'), + authorEmail: environmentValue('GIT_AUTHOR_EMAIL'), + authorName: environmentValue('GIT_AUTHOR_NAME'), + committerEmail: environmentValue('GIT_COMMITTER_EMAIL'), + committerName: environmentValue('GIT_COMMITTER_NAME'), + refresh: environmentValue('ULTRAMODERN_AGENT_REPOS_REFRESH'), + required: environmentValue('ULTRAMODERN_AGENT_REPOS_REQUIRED'), + skipAgentRepos: environmentValue('ULTRAMODERN_SKIP_AGENT_REPOS'), +}); +interface RuntimeSettings { + readonly gitIdentity: Readonly>; + readonly refresh: boolean; + readonly required: boolean; + readonly skipRequested: boolean; } - -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, 'utf-8')); -} - -function run(command, commandArgs, options = {}) { - const result = spawnSync(command, commandArgs, { - cwd: options.cwd ?? root, - encoding: 'utf-8', - env: { - ...process.env, - ...gitIdentityEnv, - ...(options.env ?? {}), - }, - stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'], - timeout: options.timeout ?? 120000, - }); - - if (result.error) { - throw result.error; - } - if (result.status !== 0) { - const stderr = result.stderr?.trim(); - throw new Error( - `${command} ${commandArgs.join(' ')} failed${stderr ? `: ${stderr}` : ''}` - ); +const RuntimeConfiguration = Context.Service( + 'scripts/setup-agent-reference-repos/RuntimeConfiguration' +); +const loadRuntimeSettings = Effect.fn('loadRuntimeSettings')( + function* loadRuntimeSettingsEffect() { + const environment = yield* SetupEnvironment; + return { + gitIdentity: { + GIT_AUTHOR_EMAIL: identityValue( + environment.authorEmail, + 'ultramodern-agent-refs@local' + ), + GIT_AUTHOR_NAME: identityValue( + environment.authorName, + 'UltraModern Agent Reference Setup' + ), + GIT_COMMITTER_EMAIL: identityValue( + environment.committerEmail, + 'ultramodern-agent-refs@local' + ), + GIT_COMMITTER_NAME: identityValue( + environment.committerName, + 'UltraModern Agent Reference Setup' + ), + }, + refresh: truthy(environment.refresh), + required: truthy(environment.required), + skipRequested: + truthy(environment.skipAgentRepos) || falsy(environment.agentRepos), + } satisfies RuntimeSettings; } - return result.stdout?.trim() ?? ''; -} - -function assertSafeRepoPath(relativePath) { - if ( - typeof relativePath !== 'string' || - relativePath.length === 0 || - path.isAbsolute(relativePath) || - relativePath.split(/[\\/]+/).includes('..') || - !relativePath.startsWith('repos/') +); + +const commandFailure = ( + command: string, + commandArguments: readonly string[], + detail: string +): AgentReferenceRepoSetupError => { + const invocation = [command, ...commandArguments].join(' '); + const detailSuffix = detail.length > 0 ? `: ${detail}` : ''; + return setupError(`${invocation} failed${detailSuffix}`); +}; +const executeCommand = Effect.fn('executeCommand')( + function* executeCommandEffect( + command: string, + commandArguments: readonly string[], + timeoutMilliseconds: number ) { - throw new Error(`Unsafe reference repository path: ${relativePath}`); + const settings = yield* RuntimeConfiguration; + const invocation = ChildProcess.make(command, commandArguments, { + cwd: WORKSPACE_ROOT, + env: settings.gitIdentity, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }); + return yield* Effect.scoped( + Effect.gen(function* collectCommandResultEffect() { + const handle = yield* invocation; + const [status, stdout, stderr] = yield* Effect.all( + [ + handle.exitCode.pipe(Effect.map(Number)), + handle.stdout.pipe(Stream.decodeText(), Stream.mkString), + handle.stderr.pipe(Stream.decodeText(), Stream.mkString), + ], + { concurrency: 'unbounded' } + ); + return { status, stderr: stderr.trim(), stdout: stdout.trim() }; + }) + ).pipe( + Effect.timeout(timeoutMilliseconds), + Effect.mapError((error) => + commandFailure(command, commandArguments, String(error)) + ) + ); } -} - -function hasGit() { - const result = spawnSync('git', ['--version'], { - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - return result.status === 0; -} - -function hasGitSubtree() { - const result = spawnSync('git', ['subtree', '-h'], { - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - return ( - (result.status === 0 || result.status === 129) && - result.stdout.includes('usage: git subtree') +); +const runCommand = Effect.fn('runCommand')(function* runCommandEffect( + command: string, + commandArguments: readonly string[], + timeoutMilliseconds: number +) { + const result = yield* executeCommand( + command, + commandArguments, + timeoutMilliseconds ); -} - -function isGitWorkTree() { - const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { - cwd: root, - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - return result.status === 0 && result.stdout.trim() === 'true'; -} - -function hasCommits() { - const result = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], { - cwd: root, - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - return result.status === 0; -} - -function porcelainStatus() { - return run('git', ['status', '--porcelain'], { timeout: 30000 }); -} - -function commitInstallerChanges(message) { - run('git', ['commit', '-m', message], { - timeout: 120000, - }); -} - -function ensureGitRepository() { - if (!isGitWorkTree()) { - if (checkOnly) { - fail('workspace is not a git repository'); - return false; - } - log('initializing git repository for agent reference subtrees'); - run('git', ['init'], { timeout: 30000 }); + if (result.status !== 0) { + return yield* commandFailure(command, commandArguments, result.stderr); } - - if (!hasCommits()) { - if (checkOnly) { - fail('workspace has no initial git commit'); - return false; + return result.stdout; +}); +const assertSafeRepoPath = Effect.fn('assertSafeRepoPath')( + function* assertSafeRepoPathEffect(relativePath: string) { + const path = yield* Path.Path; + if ( + relativePath.length === 0 || + path.isAbsolute(relativePath) || + relativePath.split(/[\\/]+/u).includes('..') || + !relativePath.startsWith('repos/') || + path.resolve(relativePath) === path.resolve('repos') + ) { + return yield* setupError( + `Unsafe reference repository path: ${relativePath}` + ); } - log('creating initial workspace commit before adding reference subtrees'); - run('git', ['add', '-A'], { timeout: 30000 }); - commitInstallerChanges('Initialize UltraModern workspace'); - return true; + return yield* Effect.void; } - - const status = porcelainStatus(); - if (status) { - fail( - 'workspace has uncommitted changes; commit or stash them before installing reference subtrees' +); +const hasGit = Effect.fn('hasGit')(function* hasGitEffect() { + const result = yield* executeCommand('git', ['--version'], 30_000); + return result.status === 0; +}); +const hasGitSubtree = Effect.fn('hasGitSubtree')( + function* hasGitSubtreeEffect() { + const result = yield* executeCommand('git', ['subtree', '-h'], 30_000); + return ( + (result.status === 0 || result.status === 129) && + result.stdout.includes('usage: git subtree') ); - return false; } - - return true; -} - -function remoteCommit(repo) { - let output = run('git', ['ls-remote', repo.url, `refs/heads/${repo.ref}`], { - timeout: 120000, - }); - if (!output) { - output = run('git', ['ls-remote', repo.url, repo.ref], { - timeout: 120000, - }); - } - const [commit] = output.split(/\s+/); - if (!/^[a-f0-9]{40}$/i.test(commit ?? '')) { - throw new Error(`Could not resolve ${repo.url}#${repo.ref}`); +); +const isGitWorkTree = Effect.fn('isGitWorkTree')( + function* isGitWorkTreeEffect() { + const result = yield* executeCommand( + 'git', + ['rev-parse', '--is-inside-work-tree'], + 30_000 + ); + return result.status === 0 && result.stdout === 'true'; } - return commit; -} - -function subtreeCommitExists(repo) { - const result = spawnSync( +); +const hasCommits = Effect.fn('hasCommits')(function* hasCommitsEffect() { + const result = yield* executeCommand( 'git', - [ - 'log', - '--grep', - `git-subtree-dir: ${repo.path}`, - '--format=%H', - '-n', - '1', - ], - { - cwd: root, - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - } + ['rev-parse', '--verify', 'HEAD'], + 30_000 ); - return result.status === 0 && result.stdout.trim().length > 0; -} - -function installedManifestEntry(repo) { - if (!fs.existsSync(manifestPath)) { - return undefined; + return result.status === 0; +}); +const commitInstallerChanges = Effect.fn('commitInstallerChanges')( + function* commitInstallerChangesEffect(message: string) { + return yield* runCommand('git', ['commit', '-m', message], 120_000); } - try { - const manifest = readJson(manifestPath); - return manifest.repositories?.find((entry) => entry.id === repo.id); - } catch { - return undefined; +); +const ensureGitRepository = Effect.fn('ensureGitRepository')( + function* ensureGitRepositoryEffect(checkOnly: boolean) { + if (!(yield* isGitWorkTree())) { + if (checkOnly) { + return yield* setupError('workspace is not a git repository'); + } + yield* Effect.logInfo( + `${LOG_PREFIX} initializing git repository for agent reference subtrees` + ); + yield* runCommand('git', ['init'], 30_000); + } + if (!(yield* hasCommits())) { + if (checkOnly) { + return yield* setupError('workspace has no initial git commit'); + } + yield* Effect.logInfo( + `${LOG_PREFIX} creating initial workspace commit before adding reference subtrees` + ); + yield* runCommand('git', ['add', '-A'], 30_000); + yield* commitInstallerChanges('Initialize UltraModern workspace'); + return yield* Effect.void; + } + const status = yield* runCommand('git', ['status', '--porcelain'], 30_000); + if (status.length > 0) { + return yield* setupError( + 'workspace has uncommitted changes; commit or stash them before installing reference subtrees' + ); + } + return yield* Effect.void; } -} - -function assertSubtreePresent(repo) { - assertSafeRepoPath(repo.path); - const targetPath = path.join(root, repo.path); - if (!fs.existsSync(targetPath)) { - fail(`${repo.path} is missing`); - return undefined; +); +const remoteCommit = Effect.fn('remoteCommit')(function* remoteCommitEffect( + repository: ReferenceRepository +) { + const branchOutput = yield* runCommand( + 'git', + ['ls-remote', repository.url, `refs/heads/${repository.ref}`], + 120_000 + ); + const output = + branchOutput.length > 0 + ? branchOutput + : yield* runCommand( + 'git', + ['ls-remote', repository.url, repository.ref], + 120_000 + ); + const commit = output.split(/\s+/u).at(0) ?? ''; + if (!/^[a-f\d]{40}$/iu.test(commit)) { + return yield* setupError( + `Could not resolve ${repository.url}#${repository.ref}` + ); } - if (!subtreeCommitExists(repo)) { - fail(`${repo.path} is present but has no git-subtree commit evidence`); - return undefined; + return commit; +}); +const subtreeCommitExists = Effect.fn('subtreeCommitExists')( + function* subtreeCommitExistsEffect(repository: ReferenceRepository) { + const result = yield* executeCommand( + 'git', + [ + 'log', + '--grep', + `git-subtree-dir: ${repository.path}`, + '--format=%H', + '-n', + '1', + ], + 30_000 + ); + return result.status === 0 && result.stdout.length > 0; } - return ( - installedManifestEntry(repo) ?? { - id: repo.id, - name: repo.name, - url: repo.url, - ref: repo.ref, - path: repo.path, - readOnly: repo.readOnly !== false, - status: 'present', - strategy: 'git-subtree-squash', +); +const installedManifestEntry = Effect.fn('installedManifestEntry')( + function* installedManifestEntryEffect( + manifestPath: string, + repository: ReferenceRepository + ) { + const fileSystem = yield* FileSystem.FileSystem; + if (!(yield* fileSystem.exists(manifestPath))) { + return Option.none(); } - ); -} - -function addSubtree(repo) { - assertSafeRepoPath(repo.path); - const targetPath = path.join(root, repo.path); - const existing = fs.existsSync(targetPath); - - if (existing && !refresh) { - return assertSubtreePresent(repo); + const repositories = yield* fileSystem.readFileString(manifestPath).pipe( + Effect.flatMap(Schema.decodeUnknownEffect(InstalledManifestJsonSchema)), + Effect.map((manifest) => manifest.repositories), + Effect.option + ); + return repositories.pipe( + Option.flatMap((entries) => + Option.fromUndefinedOr( + entries.find((entry) => entry.id === repository.id) + ) + ) + ); } - - if (existing && refresh) { - fail( - `${repo.path} already exists; refresh for subtree references is intentionally manual` +); +const assertSubtreePresent = Effect.fn('assertSubtreePresent')( + function* assertSubtreePresentEffect( + manifestPath: string, + repository: ReferenceRepository + ) { + yield* assertSafeRepoPath(repository.path); + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + if ( + !(yield* fileSystem.exists(path.join(WORKSPACE_ROOT, repository.path))) + ) { + return yield* setupError(`${repository.path} is missing`); + } + if (!(yield* subtreeCommitExists(repository))) { + return yield* setupError( + `${repository.path} is present but has no git-subtree commit evidence` + ); + } + const installedEntry = yield* installedManifestEntry( + manifestPath, + repository ); - return undefined; + return Option.getOrElse(installedEntry, (): InstalledRepository => ({ + id: repository.id, + name: repository.name, + path: repository.path, + readOnly: repository.readOnly !== false, + ref: repository.ref, + status: 'present', + strategy: REPOSITORY_STRATEGY, + url: repository.url, + })); } - - if (checkOnly) { - fail(`${repo.path} is missing`); - return undefined; +); +const addSubtree = Effect.fn('addSubtree')(function* addSubtreeEffect( + manifestPath: string, + repository: ReferenceRepository +) { + yield* assertSafeRepoPath(repository.path); + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const settings = yield* RuntimeConfiguration; + const existing = yield* fileSystem.exists( + path.join(WORKSPACE_ROOT, repository.path) + ); + if (existing && !settings.refresh) { + return yield* assertSubtreePresent(manifestPath, repository); } - - const commit = remoteCommit(repo); - log(`adding ${repo.name} as git subtree at ${repo.path} (${commit})`); - run('git', ['fetch', '--depth', '1', repo.url, repo.ref], { - timeout: 300000, - }); - run( + if (existing) { + return yield* setupError( + `${repository.path} already exists; refresh for subtree references is intentionally manual` + ); + } + const commit = yield* remoteCommit(repository); + yield* Effect.logInfo( + `${LOG_PREFIX} adding ${repository.name} as git subtree at ${repository.path} (${commit})` + ); + yield* runCommand( + 'git', + ['fetch', '--depth', '1', repository.url, repository.ref], + 300_000 + ); + yield* runCommand( 'git', [ 'subtree', 'add', '--prefix', - repo.path, + repository.path, 'FETCH_HEAD', '--squash', '-m', - `Add ${repo.name} agent reference repo`, + `Add ${repository.name} agent reference repo`, ], - { timeout: 600000 } + 600_000 ); - + const installedAt = yield* DateTime.now; return { - schemaVersion: 1, - id: repo.id, - name: repo.name, - url: repo.url, - ref: repo.ref, commit, - path: repo.path, - readOnly: repo.readOnly !== false, - strategy: 'git-subtree-squash', + id: repository.id, + installedAt, + name: repository.name, + path: repository.path, + readOnly: repository.readOnly !== false, + ref: repository.ref, + schemaVersion: 1, status: 'installed', - installedAt: new Date().toISOString(), - }; -} - -function writeManifest(entries) { - fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); - fs.writeFileSync( - manifestPath, - `${JSON.stringify( - { - schemaVersion: 1, - generatedAt: new Date().toISOString(), - strategy: 'git-subtree-squash', - installDir: 'repos', - repositories: entries, - }, - null, - 2 - )}\n` + strategy: REPOSITORY_STRATEGY, + url: repository.url, + } satisfies InstalledRepository; +}); +const writeManifest = Effect.fn('writeManifest')(function* writeManifestEffect( + manifestPath: string, + entries: readonly InstalledRepository[] +) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const generatedAt = yield* DateTime.now; + const contents = yield* Schema.encodeEffect(InstalledManifestJsonSchema)({ + generatedAt, + installDir: 'repos', + repositories: entries, + schemaVersion: 1, + strategy: REPOSITORY_STRATEGY, + }).pipe( + Effect.mapError(() => + setupError('Unable to encode the agent reference manifest') + ) ); -} - -function commitManifestIfChanged() { - const status = run('git', ['status', '--porcelain', '--', manifestPath], { - timeout: 30000, + yield* fileSystem.makeDirectory(path.dirname(manifestPath), { + recursive: true, }); - if (!status) { - return; - } - run('git', ['add', manifestPath], { timeout: 30000 }); - commitInstallerChanges('Record agent reference repo manifest'); -} - -function main() { - if (!fs.existsSync(configPath)) { - fail('Missing .agents/agent-reference-repos.json'); - return; - } - - const config = readJson(configPath); - const enabled = config.defaultEnabled !== false && !skipRequested; - - if (!enabled) { - log('setup skipped; set ULTRAMODERN_SKIP_AGENT_REPOS=0 to enable it again'); - return; - } - - if (!hasGit()) { - fail('git is required to install agent reference repositories'); - return; + yield* fileSystem.writeFileString(manifestPath, `${contents}\n`); +}); +const commitManifestIfChanged = Effect.fn('commitManifestIfChanged')( + function* commitManifestIfChangedEffect(manifestPath: string) { + const status = yield* runCommand( + 'git', + ['status', '--porcelain', '--', manifestPath], + 30_000 + ); + if (status.length === 0) { + return yield* Effect.void; + } + yield* runCommand('git', ['add', manifestPath], 30_000); + yield* commitInstallerChanges('Record agent reference repo manifest'); + return yield* Effect.void; } - if (!hasGitSubtree()) { - fail('git subtree is required to install agent reference repositories'); - return; +); +const runSetup = Effect.fn('runSetup')(function* runSetupEffect( + checkOnly: boolean +) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const settings = yield* RuntimeConfiguration; + const configPath = path.join( + WORKSPACE_ROOT, + '.agents', + 'agent-reference-repos.json' + ); + const manifestPath = path.join( + WORKSPACE_ROOT, + '.modernjs', + 'agent-reference-repos.json' + ); + if (!(yield* fileSystem.exists(configPath))) { + return yield* setupError('Missing .agents/agent-reference-repos.json'); } - if (!ensureGitRepository()) { - return; + const config = yield* fileSystem.readFileString(configPath).pipe( + Effect.flatMap( + Schema.decodeUnknownEffect(ReferenceRepositoryConfigJsonSchema) + ), + Effect.mapError(() => + setupError(`Invalid reference repository configuration at ${configPath}`) + ) + ); + if (!config.defaultEnabled || settings.skipRequested) { + yield* Effect.logInfo( + `${LOG_PREFIX} setup skipped; set ULTRAMODERN_SKIP_AGENT_REPOS=0 to enable it again` + ); + return yield* Effect.void; } - - const entries = []; - for (const repo of config.repositories ?? []) { - const result = checkOnly ? assertSubtreePresent(repo) : addSubtree(repo); - if (result) { - entries.push(result); + // Validate all destinations before any Git mutation, including initial commits. + yield* Effect.forEach( + config.repositories, + (repository) => assertSafeRepoPath(repository.path), + { + discard: true, } + ); + if (!(yield* hasGit())) { + return yield* setupError( + 'git is required to install agent reference repositories' + ); } - - if (!checkOnly) { - writeManifest(entries); - commitManifestIfChanged(); + if (!(yield* hasGitSubtree())) { + return yield* setupError( + 'git subtree is required to install agent reference repositories' + ); } -} - -try { - main(); -} catch (error) { - if (required || checkOnly) { - console.error(`[agent-reference-repos] ${error.message}`); - process.exitCode = 1; - } else { - warn(error.message); + yield* ensureGitRepository(checkOnly); + const entries = yield* Effect.forEach( + config.repositories, + (repository) => + checkOnly + ? assertSubtreePresent(manifestPath, repository) + : addSubtree(manifestPath, repository), + { concurrency: 1 } + ); + if (!checkOnly) { + yield* writeManifest(manifestPath, entries); + yield* commitManifestIfChanged(manifestPath); } -} + return yield* Effect.void; +}); +const reportSetupFailure = + (checkOnly: boolean) => (error: AgentReferenceRepoSetupError) => + Effect.gen(function* reportSetupFailureEffect() { + const settings = yield* RuntimeConfiguration; + if (settings.required || checkOnly) { + yield* Effect.logError(`${LOG_PREFIX} ${error.reason}`); + return yield* error; + } + yield* Effect.logWarning(`${LOG_PREFIX} ${error.reason}`); + return yield* Effect.void; + }); +const setupCommand = Command.make( + 'setup-agent-reference-repos', + { checkOnly: Flag.boolean('check').pipe(Flag.withDefault(false)) }, + ({ checkOnly }) => + runSetup(checkOnly).pipe( + Effect.mapError((cause) => + Schema.is(AgentReferenceRepoSetupError)(cause) + ? cause + : setupError(String(cause)) + ), + Effect.catchTag( + 'AgentReferenceRepoSetupError', + reportSetupFailure(checkOnly) + ) + ) +); +const applicationLayer = Layer.merge( + NodeServices.layer, + Layer.effect(RuntimeConfiguration, loadRuntimeSettings()) +); +const executableLayer = Layer.effectDiscard( + Command.run(setupCommand, { version: '1.0.0' }) +).pipe(Layer.provide(applicationLayer)); +NodeRuntime.runMain(Effect.scoped(Layer.build(executableLayer))); diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index a09d1ddf6..a2f99a526 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -4,7 +4,14 @@ import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; +import { + mkdtemp, + mkdir, + readFile, + realpath, + rm, + writeFile, +} from 'node:fs/promises'; import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; @@ -19,18 +26,18 @@ import { build as bundleSource, transform } from 'esbuild'; import { format } from 'oxfmt'; import { MicroVerticalReadinessSchema } from '../../packages/shared-contracts/src/microvertical-api-baseline.ts'; +import { hasValidGovernedHttpCompositionRoot } from '../generated-governed-http-boundary.mts'; +import { + hasGeneratedOperationGatewayContract, + hasGeneratedOperationPrincipalContract, +} from '../generated-module-api-boundary.mts'; import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation as microVerticalApiBaselineViolationForFile, } from '../microvertical-api-baseline-boundary.mts'; import type { MicroVerticalApiBaselineExpectation } from '../microvertical-api-baseline-boundary.mts'; -import { strictEffectRuntimeTopologyViolation } from '../ultramodern-api-boundary-rules.mts'; import { moduleFederationBridgeViolation } from '../module-federation-bridge-boundary.mts'; -import { hasValidGovernedHttpCompositionRoot } from '../generated-governed-http-boundary.mts'; -import { - hasGeneratedOperationGatewayContract, - hasGeneratedOperationPrincipalContract, -} from '../generated-module-api-boundary.mts'; +import { strictEffectRuntimeTopologyViolation } from '../ultramodern-api-boundary-rules.mts'; const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); const partyId = 'party-registry'; @@ -39,7 +46,8 @@ const partySharedApiPath = `${partyDirectory}/shared/api.ts`; const generatedFixtureId = 'inventory-stock'; const generatedApiPrefix = '/inventory-stock-api'; const generatedServiceModuleName = 'api/service'; -const generatedApiServiceModule = 'dist/esm-node/ultramodern-workspace/api/service.js'; +const generatedApiServiceModule = + 'dist/esm-node/ultramodern-workspace/api/service.js'; const generatedSharedApiImport = '../shared/api.ts'; const generatedSharedRpcImport = '../shared/rpc.ts'; const apiIndexFile = 'api/index.ts'; @@ -77,6 +85,7 @@ const routesManifestFile = 'routes-manifest.json'; const mfManifestFile = 'mf-manifest.json'; const generatedClientContractImport = '../../shared/api.ts'; const generatedSharedApiModule = 'api/shared'; +const generatedProofScope = 'generated-proof'; const generatedSharedContractsPackage = '@generated-proof/shared-contracts'; const effectClientPackage = '@modern-js/plugin-bff/effect-client'; const generatedBaselineTemplateEntry = 'src/index.ts'; @@ -93,9 +102,11 @@ const partyReadinessMetadataLine = const microVerticalApiBaselineViolation = ( stem: string, source: string, - expectation?: Partial, + expectation?: Partial ): string | undefined => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-microvertical-api-test-')); + const fixture = mkdtempSync( + path.join(os.tmpdir(), 'ontos-microvertical-api-test-') + ); const contractPath = path.join(fixture, 'api.ts'); try { writeFileSync(contractPath, source); @@ -119,9 +130,16 @@ const unexpectedTopologyImport = (specifier: string): never => { }; const generatorRoot = await realpath( - path.join(workspaceRoot, 'node_modules/@modern-js/ultramodern-create'), + path.join(workspaceRoot, 'node_modules/@modern-js/ultramodern-create') ); const require = createRequire(import.meta.url); +const generatorModulePathFor = + (moduleFormat: string) => + (name: string): string => + path.join( + generatorRoot, + `dist/${moduleFormat}/ultramodern-workspace/${name}.${moduleFormat === 'cjs' ? 'cjs' : 'js'}` + ); const AppIdSchema = Schema.String.pipe(Schema.brand('AppId')); const IdentitySchema = Schema.Struct({ @@ -169,7 +187,10 @@ interface ReleaseFramework { readonly distDirectory: string; readonly outputDirectory: string; }) => Promise; - readonly verifyBuildOutputReleaseEnvelope: (root: string, target: string) => Promise; + readonly verifyBuildOutputReleaseEnvelope: ( + root: string, + target: string + ) => Promise; readonly verifyNodeReleaseEnvelopeStaging: (input: { readonly outputDirectory: string; }) => Promise; @@ -191,7 +212,7 @@ type CreateSharedPackage = ( packageSource: { readonly modernPackageVersion: string; readonly strategy: 'install'; - }, + } ) => { readonly dependencies: Readonly>; readonly exports: Readonly>; @@ -212,11 +233,20 @@ type StrictEffectApiBoundaryRuleFactory = () => { readonly Program: (node: StrictEffectApiBoundaryNode) => void; }; }; -type CreateVerticalDescriptor = (appId: string, port: number) => WorkspaceAppFixture; +type CreateVerticalDescriptor = ( + appId: string, + port: number +) => WorkspaceAppFixture; type CreateLayout = (appId: typeof AppIdSchema.Type) => string; -type CreateAppModernConfig = (applicationRoot: string, app: WorkspaceAppFixture) => string; +type CreateAppModernConfig = ( + applicationRoot: string, + app: WorkspaceAppFixture +) => string; type CreateBackendModuleFederationConfig = (app: WorkspaceAppFixture) => string; -type CreateUltramodernBuildModule = (applicationRoot: string, app: WorkspaceAppFixture) => string; +type CreateUltramodernBuildModule = ( + applicationRoot: string, + app: WorkspaceAppFixture +) => string; interface ApiGeneratorFixture { readonly api?: { readonly consumedBy?: readonly string[]; @@ -227,9 +257,23 @@ interface ApiGeneratorFixture { readonly exposes?: Readonly>; readonly id: string; } -type CreateSharedApi = (app: ApiGeneratorFixture) => string; -type CreateApiClient = (app: WorkspaceAppFixture, contractImportPath: string) => string; -type CreateApiServiceEntry = (app: ApiGeneratorFixture, contractImportPath: string) => string; +interface ApiGeneratorOptions { + readonly scope: string; +} +type CreateSharedApi = ( + app: ApiGeneratorFixture, + options: ApiGeneratorOptions +) => string; +type CreateApiClient = ( + app: WorkspaceAppFixture, + contractImportPath: string, + options: ApiGeneratorOptions +) => string; +type CreateApiServiceEntry = ( + app: ApiGeneratorFixture, + contractImportPath: string, + options: ApiGeneratorOptions +) => string; interface GeneratedWorkspaceScriptArtifact { readonly content: string; readonly relativePath: string; @@ -248,10 +292,12 @@ type GeneratedReadinessEffect = EffectType; type GetGeneratedReadiness = (options?: { readonly baseUrl?: string | URL; }) => GeneratedReadinessEffect; -type RunGeneratedEffect = (effect: GeneratedReadinessEffect) => Promise; +type RunGeneratedEffect = ( + effect: GeneratedReadinessEffect +) => Promise; type ValidateCloudflareApp = ( app: ApiOnlyAppFixture, - applicationPublicUrl: string, + applicationPublicUrl: string ) => Promise; type ValidateModuleFederationTypes = (input: { readonly appDirs: readonly string[]; @@ -260,7 +306,7 @@ type ValidateModuleFederationTypes = (input: { type InspectModuleFederationConfigSource = ( source: string, appDirectory: string, - configFile: string, + configFile: string ) => typeof ModuleFederationInspectionSchema.Type; interface RspackPluginFixture { readonly apply: (...argumentsList: never[]) => void; @@ -290,7 +336,7 @@ interface RspackConfiguration { } type RspackFactory = (configuration: RspackConfiguration) => CompilerFixture; type DefinePluginConstructor = new ( - definitions: Readonly>, + definitions: Readonly> ) => RspackPluginFixture; type RspackModuleFixture = RspackFactory & { readonly DefinePlugin: DefinePluginConstructor; @@ -298,17 +344,25 @@ type RspackModuleFixture = RspackFactory & { }; interface CompilerStatsFixture { readonly hasErrors: () => boolean; - readonly toString: (options: { readonly all: boolean; readonly errors: boolean }) => string; + readonly toString: (options: { + readonly all: boolean; + readonly errors: boolean; + }) => string; } interface CompilerFixture { readonly close: (onComplete: (error?: Error | null) => void) => void; readonly run: ( - onComplete: (error: Error | null, stats?: CompilerStatsFixture | null) => void, + onComplete: ( + error: Error | null, + stats?: CompilerStatsFixture | null + ) => void ) => void; } const callable = void>() => - Schema.Opaque()(Schema.Unknown.pipe(Schema.refine(Predicate.isFunction))); + Schema.Opaque()( + Schema.Unknown.pipe(Schema.refine(Predicate.isFunction)) + ); const ReleaseEnvelopeSchema = Schema.Struct({ surfaces: Schema.Struct({ apiBackend: Schema.Array(Schema.String), @@ -319,7 +373,8 @@ const ReleaseEnvelopeSchema = Schema.Struct({ const ReleaseFrameworkModuleSchema = Schema.Struct({ emitFrameworkMicroVerticalReleaseEnvelope: callable(), - emitNodeStagedReleaseEnvelope: callable(), + emitNodeStagedReleaseEnvelope: + callable(), verifyBuildOutputReleaseEnvelope: callable(), verifyNodeReleaseEnvelopeStaging: @@ -331,7 +386,7 @@ const WorkspaceAppFixtureSchema = Schema.Struct({ prefix: Schema.String, protocol: Schema.optionalKey(Schema.String), stem: Schema.String, - }), + }) ), exposes: Schema.Record(Schema.String, Schema.String), id: AppIdSchema, @@ -349,14 +404,16 @@ const SharedApiGeneratorModuleSchema = Schema.Struct({ createSharedApi: callable(), }); const StrictEffectApiBoundaryRuleModuleSchema = Schema.Struct({ - createStrictEffectApiBoundariesRule: callable(), + createStrictEffectApiBoundariesRule: + callable(), }); const ComponentModuleSchema = Schema.Struct({ createLayout: callable(), }); const FederationConfigModuleSchema = Schema.Struct({ createAppModernConfig: callable(), - createBackendModuleFederationConfig: callable(), + createBackendModuleFederationConfig: + callable(), }); const BuildModuleGeneratorSchema = Schema.Struct({ createUltramodernBuildModule: callable(), @@ -371,7 +428,8 @@ const ApiServiceGeneratorSchema = Schema.Struct({ createApiServiceEntry: callable(), }); const WorkspaceScriptsGeneratorSchema = Schema.Struct({ - migratedWorkspaceScriptArtifacts: callable(), + migratedWorkspaceScriptArtifacts: + callable(), }); const GeneratedApiRuntimeModuleSchema = Schema.Struct({ default: Schema.Struct({ @@ -379,7 +437,9 @@ const GeneratedApiRuntimeModuleSchema = Schema.Struct({ }), }); const CloudflareEvidenceSchema = Schema.Struct({ - assertions: Schema.Array(Schema.Struct({ status: Schema.String, type: Schema.String })), + assertions: Schema.Array( + Schema.Struct({ status: Schema.String, type: Schema.String }) + ), }); const CloudflareProofModuleSchema = Schema.Struct({ validateApp: callable(), @@ -391,7 +451,8 @@ const ModuleFederationValidationResultSchema = Schema.Struct({ hostOnlyAppCount: Schema.Number, }); const ModuleFederationInspectionModuleSchema = Schema.Struct({ - inspectModuleFederationConfigSource: callable(), + inspectModuleFederationConfigSource: + callable(), }); const ModuleFederationInspectionSchema = Schema.Struct({ dts: Schema.Record(Schema.String, Schema.Json), @@ -417,7 +478,7 @@ const TopologySchema = Schema.Struct({ backendFederation: Schema.Struct({ exposes: Schema.Record( Schema.String, - Schema.Struct({ contract: Schema.String, openapi: Schema.String }), + Schema.Struct({ contract: Schema.String, openapi: Schema.String }) ), }), cloudflare: Schema.Struct({ @@ -430,7 +491,7 @@ const TopologySchema = Schema.Struct({ }), id: AppIdSchema, moduleFederation: Schema.Struct({ exposes: Schema.Array(Schema.String) }), - }), + }) ), }); const OverlaySchema = Schema.Struct({ @@ -442,22 +503,30 @@ const CloudflareReportSchema = Schema.Struct({ Schema.Struct({ appId: AppIdSchema, assertions: Schema.Array(Schema.Struct({ status: Schema.String })), - }), + }) ), status: Schema.String, }); -const loadReleaseFramework = async (modulePath: string): Promise => { +const loadReleaseFramework = async ( + modulePath: string +): Promise => { const source: unknown = await import(pathToFileURL(modulePath).href); - const framework = Schema.decodeUnknownSync(ReleaseFrameworkModuleSchema)(source); + const framework = Schema.decodeUnknownSync(ReleaseFrameworkModuleSchema)( + source + ); const emitFrameworkMicroVerticalReleaseEnvelope = framework.emitFrameworkMicroVerticalReleaseEnvelope.bind(source); - const emitNodeStagedReleaseEnvelope = framework.emitNodeStagedReleaseEnvelope.bind(source); - const verifyBuildOutputReleaseEnvelope = framework.verifyBuildOutputReleaseEnvelope.bind(source); - const verifyNodeReleaseEnvelopeStaging = framework.verifyNodeReleaseEnvelopeStaging.bind(source); + const emitNodeStagedReleaseEnvelope = + framework.emitNodeStagedReleaseEnvelope.bind(source); + const verifyBuildOutputReleaseEnvelope = + framework.verifyBuildOutputReleaseEnvelope.bind(source); + const verifyNodeReleaseEnvelopeStaging = + framework.verifyNodeReleaseEnvelopeStaging.bind(source); return { emitFrameworkMicroVerticalReleaseEnvelope: async (input) => { - const output: unknown = await emitFrameworkMicroVerticalReleaseEnvelope(input); + const output: unknown = + await emitFrameworkMicroVerticalReleaseEnvelope(input); return Schema.decodeUnknownSync(ReleaseEnvelopeSchema)(output); }, emitNodeStagedReleaseEnvelope: async (input) => { @@ -475,20 +544,26 @@ const loadReleaseFramework = async (modulePath: string): Promise>( schema: JsonSchema, - filePath: string, + filePath: string ): Promise => - Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(filePath, 'utf-8'))); + Schema.decodeUnknownSync(schema)( + JSON.parse(await readFile(filePath, 'utf-8')) + ); const writeJson = async ( root: string, logicalPath: string, - value: Value, + value: Value ): Promise => { await mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }); await writeFile(path.join(root, logicalPath), JSON.stringify(value)); }; -const writeText = async (root: string, logicalPath: string, value: string): Promise => { +const writeText = async ( + root: string, + logicalPath: string, + value: string +): Promise => { await mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }); await writeFile(path.join(root, logicalPath), value); }; @@ -498,7 +573,7 @@ const runNode = ( options: { readonly cwd?: string; readonly env?: Readonly>; - } = {}, + } = {} ): string => execFileSync(process.execPath, argumentsList, { cwd: options.cwd, @@ -510,32 +585,43 @@ const appToolsRequire = createRequire( await realpath( path.join( workspaceRoot, - 'verticals/party-registry/node_modules/@modern-js/app-tools/package.json', - ), - ), + 'verticals/party-registry/node_modules/@modern-js/app-tools/package.json' + ) + ) ); const releaseFrameworkRoot = path.resolve( path.dirname( - appToolsRequire.resolve('@modern-js/app-tools-extensions/release-envelope/framework-output'), + appToolsRequire.resolve( + '@modern-js/app-tools-extensions/release-envelope/framework-output' + ) ), - '../..', + '../..' ); const releaseFramework = await loadReleaseFramework( - path.join(releaseFrameworkRoot, 'esm-node/release-envelope/framework-output.mjs'), + path.join( + releaseFrameworkRoot, + 'esm-node/release-envelope/framework-output.mjs' + ) ); void test('MicroVertical templates use the shared strict Effect BFF assembly primitive', async () => { const apiServiceModule: unknown = await import( pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceModule, - ); + const apiServiceGenerator = Schema.decodeUnknownSync( + ApiServiceGeneratorModuleSchema + )(apiServiceModule); const packageModule: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/package-json.js')) - .href + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/package-json.js' + ) + ).href ); - const packageGenerator = Schema.decodeUnknownSync(PackageGeneratorModuleSchema)(packageModule); + const packageGenerator = Schema.decodeUnknownSync( + PackageGeneratorModuleSchema + )(packageModule); const source = apiServiceGenerator.createApiServiceEntry( { api: { @@ -546,38 +632,51 @@ void test('MicroVertical templates use the shared strict Effect BFF assembly pri id: generatedFixtureId, }, generatedSharedApiImport, + { scope: 'fixture' } ); assert.match( source, - /import \{ assembleEffectBffRuntime \} from '@fixture\/shared-contracts\/server\/effect-bff-runtime';/u, + /import \{ assembleEffectBffRuntime \} from '@fixture\/shared-contracts\/server\/effect-bff-runtime';/u ); assert.match(source, /const apiHandlersLive = Layer\.mergeAll\(/u); - assert.match(source, /assembleEffectBffRuntime\(\{[\s\S]*handlers: apiHandlersLive/u); + assert.match( + source, + /assembleEffectBffRuntime\(\{[\s\S]*handlers: apiHandlersLive/u + ); assert.doesNotMatch(source, /\bdefineEffectBff\b/u); const sharedContractsPackage = packageGenerator.createSharedPackage( 'fixture', 'shared-contracts', 'fixture contracts', - { modernPackageVersion: '3.8.2', strategy: 'install' }, + { modernPackageVersion: '3.8.2', strategy: 'install' } ); assert.equal( sharedContractsPackage.exports['./server/effect-bff-runtime'], - './src/effect-bff-runtime.ts', + './src/effect-bff-runtime.ts' + ); + assert.equal( + sharedContractsPackage.dependencies['@modern-js/plugin-bff'], + '3.8.2' ); - assert.equal(sharedContractsPackage.dependencies['@modern-js/plugin-bff'], '3.8.2'); - assert.equal(sharedContractsPackage.dependencies.effect, '4.0.0-beta.107'); + assert.equal(sharedContractsPackage.dependencies.effect, '4.0.0-rc.112'); assert.match( - await readFile(path.join(generatorRoot, 'templates/packages/effect-bff-runtime.ts'), 'utf-8'), - /export const assembleEffectBffRuntime/u, + await readFile( + path.join(generatorRoot, 'templates/packages/effect-bff-runtime.ts'), + 'utf-8' + ), + /export const assembleEffectBffRuntime/u ); assert.match( await readFile( - path.join(generatorRoot, 'templates/workspace-scripts/check-ultramodern-api-boundaries.mts'), - 'utf-8', + path.join( + generatorRoot, + 'templates/workspace-scripts/check-ultramodern-api-boundaries.mts' + ), + 'utf-8' ), - /strictEffectRuntimeTopologyViolation/u, + /strictEffectRuntimeTopologyViolation/u ); }); @@ -1044,12 +1143,15 @@ void test('static API validation proves the imported helper call topology', () = }; }; assert.equal( - strictEffectRuntimeTopologyViolation(importedHandlers, resolveImportedHandlers), - undefined, + strictEffectRuntimeTopologyViolation( + importedHandlers, + resolveImportedHandlers + ), + undefined ); const foreignGroupModule = groupModule.replace( `from '${generatedSharedApiImport}'`, - "from '../../foreign/shared/api.ts'", + "from '../../foreign/shared/api.ts'" ); const resolveForeignHandlers = (specifier: string) => { if (specifier === generatedSharedApiImport) { @@ -1083,8 +1185,11 @@ void test('static API validation proves the imported helper call topology', () = }; }; assert.match( - strictEffectRuntimeTopologyViolation(importedHandlers, resolveForeignHandlers) ?? '', - /explicitly composed Layer/u, + strictEffectRuntimeTopologyViolation( + importedHandlers, + resolveForeignHandlers + ) ?? '', + /explicitly composed Layer/u ); assert.equal( strictEffectRuntimeTopologyViolation(` @@ -1100,13 +1205,16 @@ void test('static API validation proves the imported helper call topology', () = const handlers = Layer.mergeAll(groupLayer); export default assemble({ api: fixtureApi, handlers: handlers }); `), - undefined, + undefined ); - for (const [index, source] of validAdversarialStrictRuntimeSources.entries()) { + for (const [ + index, + source, + ] of validAdversarialStrictRuntimeSources.entries()) { assert.equal( strictEffectRuntimeTopologyViolation(source), undefined, - `valid adversarial source ${index + 1}`, + `valid adversarial source ${index + 1}` ); } for (const source of adversarialStrictRuntimeSources) { @@ -1381,7 +1489,11 @@ void test('static API validation proves the imported helper call topology', () = /explicitly composed Layer/u, ], ] as const) { - assert.match(strictEffectRuntimeTopologyViolation(source) ?? '', expected, label); + assert.match( + strictEffectRuntimeTopologyViolation(source) ?? '', + expected, + label + ); } }); @@ -1389,7 +1501,7 @@ void test('static API validation proves the imported helper call topology', () = const strictBoundaryReports = ( module: typeof StrictEffectApiBoundaryRuleModuleSchema.Type, filename: string, - source: string, + source: string ): readonly string[] => { const messages: string[] = []; module @@ -1407,13 +1519,13 @@ const strictBoundaryReports = ( const reportsAssemblyViolation = (messages: readonly string[]): boolean => messages.some((message) => /server-only shared Effect BFF assembly helper|explicitly composed handler Layer|Generated API entries must export defineEffectBff|Generated API entries must implement handlers through HttpApiBuilder/u.test( - message, - ), + message + ) ); void test('published lint validators reject comment, string, and local strict-root spoofs', async (context) => { const codeToolsRoot = await realpath( - path.join(workspaceRoot, 'node_modules/@modern-js/code-tools'), + path.join(workspaceRoot, 'node_modules/@modern-js/code-tools') ); const formats = [ 'dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs', @@ -1423,9 +1535,9 @@ void test('published lint validators reject comment, string, and local strict-ro const apiServiceSource: unknown = await import( pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceSource, - ); + const apiServiceGenerator = Schema.decodeUnknownSync( + ApiServiceGeneratorModuleSchema + )(apiServiceSource); const generatedSource = apiServiceGenerator.createApiServiceEntry( { api: { @@ -1436,6 +1548,7 @@ void test('published lint validators reject comment, string, and local strict-ro id: generatedFixtureId, }, generatedSharedApiImport, + { scope: 'app' } ); const generatedRpcSource = apiServiceGenerator.createApiServiceEntry( { @@ -1448,6 +1561,7 @@ void test('published lint validators reject comment, string, and local strict-ro id: generatedFixtureId, }, generatedSharedRpcImport, + { scope: 'app' } ); const generatedRpcContractSource = ` import { RpcGroup } from 'effect/unstable/rpc'; @@ -1461,9 +1575,9 @@ void test('published lint validators reject comment, string, and local strict-ro resolveImport: unexpectedTopologyImport, source: generatedRpcContractSource, } - : unexpectedTopologyImport(specifier), + : unexpectedTopologyImport(specifier) ), - undefined, + undefined ); assert.match( strictEffectRuntimeTopologyViolation(generatedRpcSource, (specifier) => @@ -1471,22 +1585,30 @@ void test('published lint validators reject comment, string, and local strict-ro ? { id: 'inventory-stock/shared/rpc.ts', resolveImport: unexpectedTopologyImport, - source: 'export const inventoryStockRpcGroup = { toLayer: () => undefined };', + source: + 'export const inventoryStockRpcGroup = { toLayer: () => undefined };', } - : unexpectedTopologyImport(specifier), + : unexpectedTopologyImport(specifier) ) ?? '', - /server-only shared Effect BFF assembly helper/u, + /server-only shared Effect BFF assembly helper/u + ); + const lintRoot = await mkdtemp( + path.join(os.tmpdir(), 'ontos-strict-api-lint-') + ); + context.after( + async () => await rm(lintRoot, { force: true, recursive: true }) ); - const lintRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-strict-api-lint-')); - context.after(async () => await rm(lintRoot, { force: true, recursive: true })); const fixtureRoot = path.join(lintRoot, 'verticals/fixture'); const fixtureApiEntryPath = path.join(fixtureRoot, apiIndexFile); await mkdir(path.join(fixtureRoot, 'api'), { recursive: true }); await mkdir(path.join(fixtureRoot, 'shared'), { recursive: true }); - await writeFile(path.join(fixtureRoot, sharedApiFile), governedApiModuleSource); + await writeFile( + path.join(fixtureRoot, sharedApiFile), + governedApiModuleSource + ); await writeFile( path.join(fixtureRoot, 'shared/rpc.ts'), - 'export const fixtureRpcGroup = { toLayer: () => undefined };\n', + 'export const fixtureRpcGroup = { toLayer: () => undefined };\n' ); await writeFile( path.join(fixtureRoot, 'api/shadowed-group.ts'), @@ -1502,12 +1624,15 @@ void test('published lint validators reject comment, string, and local strict-ro 'fixture', (handlers) => handlers.handle('reachable', () => undefined), ); - `, + ` ); const foreignRoot = path.join(lintRoot, 'verticals/foreign'); await mkdir(path.join(foreignRoot, 'shared'), { recursive: true }); await mkdir(path.join(foreignRoot, 'api'), { recursive: true }); - await writeFile(path.join(foreignRoot, sharedApiFile), `${fixtureApiModuleSource}\n`); + await writeFile( + path.join(foreignRoot, sharedApiFile), + `${fixtureApiModuleSource}\n` + ); await writeFile( path.join(foreignRoot, 'api/group.ts'), ` @@ -1518,19 +1643,22 @@ void test('published lint validators reject comment, string, and local strict-ro 'fixture', (handlers) => handlers.handle('reachable', () => undefined), ); - `, + ` ); const generatedRoot = path.join(lintRoot, 'verticals/inventory-stock'); await mkdir(path.join(generatedRoot, 'shared'), { recursive: true }); await writeFile( path.join(generatedRoot, sharedApiFile), - 'export const inventoryStockApi = {};\n', + 'export const inventoryStockApi = {};\n' + ); + await writeFile( + path.join(generatedRoot, 'shared/rpc.ts'), + generatedRpcContractSource ); - await writeFile(path.join(generatedRoot, 'shared/rpc.ts'), generatedRpcContractSource); const invalidSources = [ ...adversarialStrictRuntimeSources, ...governedLayerAliasMutations.map(([before, after]) => - governedLayerAliasFixture.replace(before, after), + governedLayerAliasFixture.replace(before, after) ), ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; @@ -1687,16 +1815,20 @@ void test('published lint validators reject comment, string, and local strict-ro pathToFileURL(path.join(codeToolsRoot, moduleFormat)).href ); return { - module: Schema.decodeUnknownSync(StrictEffectApiBoundaryRuleModuleSchema)(imported), + module: Schema.decodeUnknownSync( + StrictEffectApiBoundaryRuleModuleSchema + )(imported), moduleFormat, }; - }), + }) ); for (const { module, moduleFormat } of modules) { for (const source of invalidSources) { assert.ok( - reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, source)), - `${moduleFormat} accepted a fake strict runtime root`, + reportsAssemblyViolation( + strictBoundaryReports(module, fixtureApiEntryPath, source) + ), + `${moduleFormat} accepted a fake strict runtime root` ); } const legacySource = ` @@ -1708,40 +1840,49 @@ void test('published lint validators reject comment, string, and local strict-ro export default defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `; assert.equal( - reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, legacySource)), + reportsAssemblyViolation( + strictBoundaryReports(module, fixtureApiEntryPath, legacySource) + ), true, - `${moduleFormat} accepted a legacy runtime root without the shared assembly helper`, + `${moduleFormat} accepted a legacy runtime root without the shared assembly helper` ); const generatedMessages = strictBoundaryReports( module, path.join(generatedRoot, apiIndexFile), - generatedSource, + generatedSource ); assert.equal( reportsAssemblyViolation(generatedMessages), false, - `${moduleFormat} rejected exact generated helper output: ${generatedMessages.join(' | ')}`, + `${moduleFormat} rejected exact generated helper output: ${generatedMessages.join(' | ')}` ); const generatedRpcMessages = strictBoundaryReports( module, path.join(generatedRoot, apiIndexFile), - generatedRpcSource, + generatedRpcSource ); assert.equal( generatedRpcMessages.some((message) => /server-only shared Effect BFF assembly helper|explicitly composed handler Layer|\.\.\/shared\/api\.ts/u.test( - message, - ), + message + ) ), false, - `${moduleFormat} rejected exact generated RPC output: ${generatedRpcMessages.join(' | ')}`, + `${moduleFormat} rejected exact generated RPC output: ${generatedRpcMessages.join(' | ')}` ); - for (const source of [...validAdversarialStrictRuntimeSources, governedLayerAliasFixture]) { - const messages = strictBoundaryReports(module, fixtureApiEntryPath, source); + for (const source of [ + ...validAdversarialStrictRuntimeSources, + governedLayerAliasFixture, + ]) { + const messages = strictBoundaryReports( + module, + fixtureApiEntryPath, + source + ); assert.equal( reportsAssemblyViolation(messages), false, - `${moduleFormat} rejected a valid strict runtime root: ${messages.join(' | ')}`, + `${moduleFormat} rejected a valid strict runtime root: ${messages.join(' | ')}` ); } } @@ -1751,16 +1892,20 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', const apiServiceSource: unknown = await import( pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceSource, - ); + const apiServiceGenerator = Schema.decodeUnknownSync( + ApiServiceGeneratorModuleSchema + )(apiServiceSource); const sharedApiSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js')) - .href - ); - const sharedApiGenerator = Schema.decodeUnknownSync(SharedApiGeneratorModuleSchema)( - sharedApiSource, + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/api/shared.js' + ) + ).href ); + const sharedApiGenerator = Schema.decodeUnknownSync( + SharedApiGeneratorModuleSchema + )(sharedApiSource); const descriptor = { api: { consumedBy: [], @@ -1770,15 +1915,23 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', id: generatedFixtureId, } as const; const fixture = await mkdtemp( - path.join(workspaceRoot, 'verticals/party-registry/.generated-runtime-'), + path.join(workspaceRoot, 'verticals/party-registry/.generated-runtime-') ); try { await writeText( fixture, apiIndexFile, - apiServiceGenerator.createApiServiceEntry(descriptor, generatedSharedApiImport), + apiServiceGenerator.createApiServiceEntry( + descriptor, + generatedSharedApiImport, + { scope: 'app' } + ) + ); + await writeText( + fixture, + sharedApiFile, + sharedApiGenerator.createSharedApi(descriptor, { scope: 'app' }) ); - await writeText(fixture, sharedApiFile, sharedApiGenerator.createSharedApi(descriptor)); await writeText( fixture, buildMarkerFile, @@ -1792,17 +1945,21 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', surface: 'api', unitId: 'vertical/inventory-stock', version: '0.1.0', - } as const;\n`, + } as const;\n` ); await writeJson(fixture, tsconfigFile, { compilerOptions: { composite: false, noEmit: true, types: ['node'] }, extends: '../../../tsconfig.base.json', include: ['api/**/*.ts', 'shared/**/*.ts'], }); - execFileSync(path.join(workspaceRoot, 'node_modules/.bin/tsc'), ['-p', tsconfigFile], { - cwd: fixture, - encoding: 'utf-8', - }); + execFileSync( + path.join(workspaceRoot, 'node_modules/.bin/tsc'), + ['-p', tsconfigFile], + { + cwd: fixture, + encoding: 'utf-8', + } + ); await writeText( fixture, 'runtime-proof.mjs', @@ -1818,7 +1975,7 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', if (body.status !== 'ready') throw new Error('generated readiness response was invalid'); } finally { await server.dispose(); - }\n`, + }\n` ); runNode([path.join(fixture, 'runtime-proof.mjs')], { cwd: fixture }); } finally { @@ -1833,7 +1990,10 @@ const releaseFixture = async (context: TestContext) => { }); const baseArtifact = await readJson( BuildArtifactSchema, - path.join(workspaceRoot, 'verticals/party-registry/shared/ultramodern-build.json'), + path.join( + workspaceRoot, + 'verticals/party-registry/shared/ultramodern-build.json' + ) ); const sourceRevision = 'a'.repeat(40); const artifact = { @@ -1852,8 +2012,10 @@ const releaseFixture = async (context: TestContext) => { }, remotes: [], }; - const putJson = async (logicalPath: string, value: Value): Promise => - await writeJson(root, logicalPath, value); + const putJson = async ( + logicalPath: string, + value: Value + ): Promise => await writeJson(root, logicalPath, value); const putText = async (logicalPath: string, value: string): Promise => await writeText(root, logicalPath, value); await putJson('ultramodern-build.json', artifact); @@ -1874,9 +2036,15 @@ const releaseFixture = async (context: TestContext) => { await putJson('route.json', { routes: [{ bundle: ssrBundlePath }] }); await putJson('package.json', { type: 'module' }); await Promise.all( - [compiledUiAssetPath, ssrBundlePath, apiBundlePath, 'index.js', 'backendRemoteEntry.cjs'].map( - async (file) => await putText(file, 'console.log("compiled fixture");'), - ), + [ + compiledUiAssetPath, + ssrBundlePath, + apiBundlePath, + 'index.js', + 'backendRemoteEntry.cjs', + ].map( + async (file) => await putText(file, 'console.log("compiled fixture");') + ) ); const emit = async () => await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ @@ -1896,14 +2064,15 @@ void test('empty MF producers retain complete build and Node staged release evid path.join( releaseFrameworkRoot, moduleFormat, - `release-envelope/framework-output.${extension}`, - ), + `release-envelope/framework-output.${extension}` + ) ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); + const envelope = + await framework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: fixture.root, + target: 'node', + }); assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); assert.deepEqual(envelope.surfaces.ssr, [ssrBundlePath]); assert.deepEqual(envelope.surfaces.apiBackend, [apiBundlePath]); @@ -1916,14 +2085,18 @@ void test('empty MF producers retain complete build and Node staged release evid await framework.verifyNodeReleaseEnvelopeStaging({ outputDirectory: fixture.root, }); - }), + }) ); const fixture = await releaseFixture(context); await fixture.emit(); await fixture.putText(compiledUiAssetPath, 'console.log("tampered");'); await assert.rejects( - async () => await releaseFramework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), - /digest|hash|size/iu, + async () => + await releaseFramework.verifyBuildOutputReleaseEnvelope( + fixture.root, + 'node' + ), + /digest|hash|size/iu ); }); @@ -1941,16 +2114,17 @@ void test('empty MF producers bind root-relative route assets when publicPath is path.join( releaseFrameworkRoot, moduleFormat, - `release-envelope/framework-output.${extension}`, - ), + `release-envelope/framework-output.${extension}` + ) ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); + const envelope = + await framework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: fixture.root, + target: 'node', + }); assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); - }), + }) ); }); @@ -1975,9 +2149,9 @@ void test('empty-producer fallback rejects undeclared, foreign, traversing, miss await assert.rejects( fixture.emit, /UI\/client manifest references no compiled execution module/u, - reference, + reference ); - }), + }) ); const baseline = await releaseFixture(context); const invalidManifests = [ @@ -2005,9 +2179,9 @@ void test('empty-producer fallback rejects undeclared, foreign, traversing, miss await fixture.putJson(mfManifestFile, manifest); await assert.rejects( fixture.emit, - /UI\/client manifest references no compiled execution module/u, + /UI\/client manifest references no compiled execution module/u ); - }), + }) ); const fixture = await releaseFixture(context); await rm(path.join(fixture.root, routesManifestFile)); @@ -2016,14 +2190,16 @@ void test('empty-producer fallback rejects undeclared, foreign, traversing, miss void test('empty MF producers cannot bypass backend, SSR, revision, or identity proof', async (context) => { await Promise.all( - [apiBundlePath, ssrBundlePath, 'backendRemoteEntry.cjs'].map(async (file) => { - const fixture = await releaseFixture(context); - await rm(path.join(fixture.root, file)); - await assert.rejects( - fixture.emit, - /compiled Node Effect API|SSR artifacts|emitted together/u, - ); - }), + [apiBundlePath, ssrBundlePath, 'backendRemoteEntry.cjs'].map( + async (file) => { + const fixture = await releaseFixture(context); + await rm(path.join(fixture.root, file)); + await assert.rejects( + fixture.emit, + /compiled Node Effect API|SSR artifacts|emitted together/u + ); + } + ) ); const fixture = await releaseFixture(context); await fixture.putJson('backend-mf-manifest.json', { @@ -2055,20 +2231,27 @@ const GlobalVarsSchema = Schema.Struct({ }); const evaluatePartyBuildGlobalVars = async (shellOrigin: string) => { - const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-party-config-')); + const temporaryRoot = await mkdtemp( + path.join(os.tmpdir(), 'ontos-party-config-') + ); try { const harnessPath = path.join(temporaryRoot, 'read-config.mjs'); const configSource = await readFile( path.join(workspaceRoot, 'verticals/party-registry/modern.config.ts'), - 'utf-8', + 'utf-8' ); const effectModuleUrl = pathToFileURL( - require.resolve('effect', { paths: [workspaceRoot] }), + require.resolve('effect', { paths: [workspaceRoot] }) ).href; const { code } = await transform(configSource, { define: { 'import.meta.url': JSON.stringify( - pathToFileURL(path.join(workspaceRoot, 'verticals/party-registry/modern.config.ts')).href, + pathToFileURL( + path.join( + workspaceRoot, + 'verticals/party-registry/modern.config.ts' + ) + ).href ), }, format: 'cjs', @@ -2103,10 +2286,12 @@ runInNewContext(${JSON.stringify(code)}, { require: specifier => specifier === 'effect' ? effect : specifier === 'node:url' ? nodeUrl : specifier === 'node:path' ? nodePath : framework, }); process.stdout.write(JSON.stringify(module.exports.default.source.globalVars)); -`, +` ); const output = runNode([harnessPath]); - return Schema.decodeUnknownSync(Schema.fromJsonString(GlobalVarsSchema))(output); + return Schema.decodeUnknownSync(Schema.fromJsonString(GlobalVarsSchema))( + output + ); } finally { await rm(temporaryRoot, { force: true, recursive: true }); } @@ -2125,24 +2310,41 @@ void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin with const source = await readFile(path.join(partyRoot, apiIndexFile), 'utf-8'); const reader = /(?declare const ULTRAMODERN_SHELL_ORIGIN[\s\S]+?const shellOrigin = readShellOrigin\(\);)/u.exec( - source, + source )?.groups?.reader; - assert.notEqual(reader, undefined, 'compile the actual API origin-reader boundary'); - const appToolsPath = require.resolve('@modern-js/app-tools/config', { paths: [partyRoot] }); - const rsbuildPath = require.resolve('@rsbuild/core', { paths: [appToolsPath] }); - const rspackModule: unknown = require(require.resolve('@rspack/core', { paths: [rsbuildPath] })); - const rspackFixture = Schema.decodeUnknownSync(RspackModuleFixtureSchema)(rspackModule); - const temporaryRoot = await mkdtemp(path.join(partyRoot, 'node_modules/.ontos-compiled-cors-')); + assert.notEqual( + reader, + undefined, + 'compile the actual API origin-reader boundary' + ); + const appToolsPath = require.resolve('@modern-js/app-tools/config', { + paths: [partyRoot], + }); + const rsbuildPath = require.resolve('@rsbuild/core', { + paths: [appToolsPath], + }); + const rspackModule: unknown = require( + require.resolve('@rspack/core', { paths: [rsbuildPath] }) + ); + const rspackFixture = Schema.decodeUnknownSync(RspackModuleFixtureSchema)( + rspackModule + ); + const temporaryRoot = await mkdtemp( + path.join(partyRoot, 'node_modules/.ontos-compiled-cors-') + ); try { const entry = path.join(temporaryRoot, 'reader.ts'); await writeFile( entry, - `import { Schema } from 'effect';\nimport { resolvePartyRegistryShellOrigin, partyRegistryCorsAllowedOrigins } from ${JSON.stringify(path.join(partyRoot, 'api/read-server-support.ts'))};\n${reader}\nexport const allowedOrigins = partyRegistryCorsAllowedOrigins(shellOrigin);\n`, + `import { Schema } from 'effect';\nimport { resolvePartyRegistryShellOrigin, partyRegistryCorsAllowedOrigins } from ${JSON.stringify(path.join(partyRoot, 'api/read-server-support.ts'))};\n${reader}\nexport const allowedOrigins = partyRegistryCorsAllowedOrigins(shellOrigin);\n` ); const definePlugin = new rspackFixture.DefinePlugin( Object.fromEntries( - Object.entries(globalVars).map(([key, value]) => [key, JSON.stringify(value)]), - ), + Object.entries(globalVars).map(([key, value]) => [ + key, + JSON.stringify(value), + ]) + ) ); const createCompiler = rspackFixture.rspack.bind(rspackModule); const compilerSource = createCompiler({ @@ -2190,8 +2392,11 @@ void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin with } finally { await closeCompiler(); } - const compiledReaderModule: unknown = require(path.join(temporaryRoot, 'reader.cjs')); - const compiledReader = Schema.decodeUnknownSync(CompiledReaderSchema)(compiledReaderModule); + const compiledReaderModule: unknown = require( + path.join(temporaryRoot, 'reader.cjs') + ); + const compiledReader = + Schema.decodeUnknownSync(CompiledReaderSchema)(compiledReaderModule); assert.deepEqual([...compiledReader.allowedOrigins], [shellOrigin]); } finally { await rm(temporaryRoot, { force: true, recursive: true }); @@ -2199,7 +2404,10 @@ void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin with }); const normalizedGeneratedSource = async (fileName: string, source: string) => { - const result = await format(fileName, source, { singleQuote: true, sortImports: true }); + const result = await format(fileName, source, { + singleQuote: true, + sortImports: true, + }); assert.deepEqual(result.errors, []); return result.code.replaceAll(/^\s*\n/gmu, ''); }; @@ -2214,7 +2422,7 @@ const scaffoldSemanticKinds = new Map([ // No application server, deployment, real plugin or environment file is loaded by this harness. const evaluateScaffoldSemantics = async ( source: string, - kind: ScaffoldSemanticKind, + kind: ScaffoldSemanticKind ): Promise => { const scratchRoot = path.join(workspaceRoot, '.scratch'); await mkdir(scratchRoot, { recursive: true }); @@ -2222,7 +2430,9 @@ const evaluateScaffoldSemantics = async ( try { const effectUrl = pathToFileURL(require.resolve('effect')).href; const { code } = await transform(source, { - define: { 'import.meta.url': JSON.stringify('file:///fixture/config.ts') }, + define: { + 'import.meta.url': JSON.stringify('file:///fixture/config.ts'), + }, format: 'cjs', jsxFactory: 'element', loader: kind === 'layout' ? 'tsx' : 'ts', @@ -2263,7 +2473,7 @@ let evidence = module.exports; if (kind === 'layout') evidence = evidence.default(); if (kind === 'backend') evidence = evidence.default; process.stdout.write(JSON.stringify(evidence)); -`, +` ); return runNode([harnessPath]); } finally { @@ -2275,14 +2485,16 @@ const evaluatedInfrastructureSource = async ( fileName: string, source: string, cloudflare: boolean, - injection: Readonly>, + injection: Readonly> ): Promise => { const partyRoot = path.join(workspaceRoot, partyDirectory); const result = await bundleSource({ bundle: true, define: { 'import.meta.resolve': '__resolve', - 'import.meta.url': JSON.stringify(pathToFileURL(path.join(partyRoot, fileName)).href), + 'import.meta.url': JSON.stringify( + pathToFileURL(path.join(partyRoot, fileName)).href + ), }, external: ['./src/routes/ultramodern-route-metadata'], format: 'cjs', @@ -2300,8 +2512,8 @@ const evaluatedInfrastructureSource = async ( const effectUrl = pathToFileURL(require.resolve('effect')).href; const buildIdentityUrl = pathToFileURL( createRequire(path.join(partyRoot, fileName)).resolve( - '@app/shared-contracts/ultramodern-build', - ), + '@app/shared-contracts/ultramodern-build' + ) ).href; return runNode([ '--input-type=module', @@ -2378,41 +2590,50 @@ process.stdout.write(JSON.stringify({ exported: module.exports, observations }, void test('all published scaffold formats retain Party infrastructure behavior and source parity', async () => { await Promise.all( ['esm', 'esm-node', 'cjs'].map(async (moduleFormat) => { - const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); + const generatorModulePath = generatorModulePathFor(moduleFormat); const descriptorPath = generatorModulePath('descriptors'); const descriptorSource: unknown = moduleFormat === 'cjs' ? require(descriptorPath) : await import(pathToFileURL(descriptorPath).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); + const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)( + descriptorSource + ); const componentPath = generatorModulePath('demo-components'); const componentSource: unknown = moduleFormat === 'cjs' ? require(componentPath) : await import(pathToFileURL(componentPath).href); - const componentModule = Schema.decodeUnknownSync(ComponentModuleSchema)(componentSource); + const componentModule = Schema.decodeUnknownSync(ComponentModuleSchema)( + componentSource + ); const federationPath = generatorModulePath('module-federation/config'); const federationSource: unknown = moduleFormat === 'cjs' ? require(federationPath) : await import(pathToFileURL(federationPath).href); - const federationModule = Schema.decodeUnknownSync(FederationConfigModuleSchema)( - federationSource, + const federationModule = Schema.decodeUnknownSync( + FederationConfigModuleSchema + )(federationSource); + const buildModulePath = generatorModulePath( + 'module-federation/reexport-module' ); - const buildModulePath = generatorModulePath('module-federation/reexport-module'); const buildModuleSource: unknown = moduleFormat === 'cjs' ? require(buildModulePath) : await import(pathToFileURL(buildModulePath).href); - const buildModule = Schema.decodeUnknownSync(BuildModuleGeneratorSchema)(buildModuleSource); + const buildModule = Schema.decodeUnknownSync(BuildModuleGeneratorSchema)( + buildModuleSource + ); const createVerticalDescriptor = descriptorModule.createVerticalDescriptor.bind(descriptorSource); const createLayout = componentModule.createLayout.bind(componentSource); - const createAppModernConfig = federationModule.createAppModernConfig.bind(federationSource); + const createAppModernConfig = + federationModule.createAppModernConfig.bind(federationSource); const createBackendModuleFederationConfig = - federationModule.createBackendModuleFederationConfig.bind(federationSource); + federationModule.createBackendModuleFederationConfig.bind( + federationSource + ); const createUltramodernBuildModule = buildModule.createUltramodernBuildModule.bind(buildModuleSource); const descriptor: unknown = createVerticalDescriptor(partyId, 4102); @@ -2420,21 +2641,23 @@ void test('all published scaffold formats retain Party infrastructure behavior a const app = { ...descriptor, exposes: {} }; const generated = { 'backend-federation.config.ts': Schema.decodeUnknownSync(Schema.String)( - createBackendModuleFederationConfig(app), + createBackendModuleFederationConfig(app) ), [buildMarkerFile]: Schema.decodeUnknownSync(Schema.String)( - createUltramodernBuildModule('app', app), + createUltramodernBuildModule('app', app) ), 'modern.config.ts': Schema.decodeUnknownSync(Schema.String)( - createAppModernConfig('app', app), + createAppModernConfig('app', app) + ), + 'src/routes/layout.tsx': Schema.decodeUnknownSync(Schema.String)( + createLayout(app.id) ), - 'src/routes/layout.tsx': Schema.decodeUnknownSync(Schema.String)(createLayout(app.id)), }; await Promise.all( Object.entries(generated).map(async ([fileName, source]) => { const actual = await readFile( path.join(workspaceRoot, partyDirectory, fileName), - 'utf-8', + 'utf-8' ); if (fileName === 'modern.config.ts' || fileName === buildMarkerFile) { const injections: Readonly>[] = [ @@ -2448,17 +2671,29 @@ void test('all published scaffold formats retain Party infrastructure behavior a [false, true].flatMap((cloudflare) => injections.map(async (injection) => { const [expected, evaluated] = await Promise.all([ - evaluatedInfrastructureSource(fileName, source, cloudflare, injection), - evaluatedInfrastructureSource(fileName, actual, cloudflare, injection), + evaluatedInfrastructureSource( + fileName, + source, + cloudflare, + injection + ), + evaluatedInfrastructureSource( + fileName, + actual, + cloudflare, + injection + ), ]); - const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json)); + const decode = Schema.decodeUnknownSync( + Schema.fromJsonString(Schema.Json) + ); assert.deepEqual( decode(expected), decode(evaluated), - `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior`, + `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior` ); - }), - ), + }) + ) ); return; } @@ -2467,20 +2702,18 @@ void test('all published scaffold formats retain Party infrastructure behavior a assert.equal( await evaluateScaffoldSemantics(source, kind), await evaluateScaffoldSemantics(actual, kind), - `${moduleFormat}: ${fileName} must preserve typed runtime, ownership and release gates`, + `${moduleFormat}: ${fileName} must preserve typed runtime, ownership and release gates` ); - }), + }) ); - }), + }) ); }); void test('all published scaffold formats generate the shared MicroVertical API baseline', async () => { await Promise.all( ['esm', 'esm-node', 'cjs'].map(async (moduleFormat) => { - const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); + const generatorModulePath = generatorModulePathFor(moduleFormat); const descriptorPath = generatorModulePath('descriptors'); const sharedApiPath = generatorModulePath(generatedSharedApiModule); const clientPath = generatorModulePath('api/client'); @@ -2494,31 +2727,78 @@ void test('all published scaffold formats generate the shared MicroVertical API ? require(sharedApiPath) : await import(pathToFileURL(sharedApiPath).href); const clientSource: unknown = - moduleFormat === 'cjs' ? require(clientPath) : await import(pathToFileURL(clientPath).href); + moduleFormat === 'cjs' + ? require(clientPath) + : await import(pathToFileURL(clientPath).href); const serviceSource: unknown = moduleFormat === 'cjs' ? require(servicePath) : await import(pathToFileURL(servicePath).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const clientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)(clientSource); - const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(serviceSource); - const descriptor: unknown = descriptorModule.createVerticalDescriptor(inventoryStockId, 4103); + const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)( + descriptorSource + ); + const sharedApiModule = Schema.decodeUnknownSync( + SharedApiGeneratorSchema + )(sharedApiSource); + const clientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)( + clientSource + ); + const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)( + serviceSource + ); + const descriptor: unknown = descriptorModule.createVerticalDescriptor( + inventoryStockId, + 4103 + ); Schema.asserts(WorkspaceAppFixtureSchema, descriptor); const app = { ...descriptor, exposes: {} }; - const contract = sharedApiModule.createSharedApi(app); - const client = clientModule.createApiClient(app, generatedClientContractImport); - const service = serviceModule.createApiServiceEntry(app, generatedSharedApiImport); + const contract = sharedApiModule.createSharedApi(app, { + scope: generatedProofScope, + }); + const client = clientModule.createApiClient( + app, + generatedClientContractImport, + { scope: generatedProofScope } + ); + const service = serviceModule.createApiServiceEntry( + app, + generatedSharedApiImport, + { scope: generatedProofScope } + ); assert.match(contract, /MicroVerticalBuildMarkerSchema/u, moduleFormat); assert.match(contract, /MicroVerticalReadinessSchema/u, moduleFormat); - assert.match(contract, /createMicroVerticalOperationContext/u, moduleFormat); - assert.match(contract, /@generated-proof\/shared-contracts/u, moduleFormat); - assert.doesNotMatch(contract, /export interface OperationContext/u, moduleFormat); - assert.match(client, /client\.foundation\.readiness\(\{\}\)/u, moduleFormat); - assert.doesNotMatch(client, /client\.inventoryStock\.readiness/u, moduleFormat); + assert.match( + contract, + /createMicroVerticalOperationContext/u, + moduleFormat + ); + assert.match( + contract, + /@generated-proof\/shared-contracts/u, + moduleFormat + ); + assert.doesNotMatch( + contract, + /export interface OperationContext/u, + moduleFormat + ); + assert.match( + client, + /client\.foundation\.readiness\(\{\}\)/u, + moduleFormat + ); + assert.doesNotMatch( + client, + /client\.inventoryStock\.readiness/u, + moduleFormat + ); assert.match(service, /microVerticalOperationAttributes/u, moduleFormat); - assert.match(service, /@generated-proof\/shared-contracts/u, moduleFormat); + assert.match( + service, + /@generated-proof\/shared-contracts/u, + moduleFormat + ); assert.doesNotMatch(service, /const operationAttributes/u, moduleFormat); const generatedBaselineExpectation = { additionalPaths: {}, @@ -2530,8 +2810,12 @@ void test('all published scaffold formats generate the shared MicroVertical API sharedContractsPackage: generatedSharedContractsPackage, } as const; assert.equal( - microVerticalApiBaselineViolation(inventoryStockId, contract, generatedBaselineExpectation), - undefined, + microVerticalApiBaselineViolation( + inventoryStockId, + contract, + generatedBaselineExpectation + ), + undefined ); const customStemApp = { @@ -2542,29 +2826,38 @@ void test('all published scaffold formats generate the shared MicroVertical API stem: warehouseItemsApiStem, }, }; - const customStemContract = sharedApiModule.createSharedApi(customStemApp); + const customStemContract = sharedApiModule.createSharedApi( + customStemApp, + { scope: generatedProofScope } + ); assert.equal( - microVerticalApiBaselineViolation(warehouseItemsApiStem, customStemContract, { - ...generatedBaselineExpectation, - apiPrefix: warehouseApiPrefix, - basePath: `/warehouse-api/${warehouseItemsApiStem}`, - readinessPath: `/warehouse-api/${warehouseItemsApiStem}/readiness`, - }), - undefined, + microVerticalApiBaselineViolation( + warehouseItemsApiStem, + customStemContract, + { + ...generatedBaselineExpectation, + apiPrefix: warehouseApiPrefix, + basePath: `/warehouse-api/${warehouseItemsApiStem}`, + readinessPath: `/warehouse-api/${warehouseItemsApiStem}/readiness`, + } + ), + undefined ); - const checkoutDescriptor: unknown = descriptorModule.createVerticalDescriptor( - checkoutId, - 4105, - ); + const checkoutDescriptor: unknown = + descriptorModule.createVerticalDescriptor(checkoutId, 4105); Schema.asserts(WorkspaceAppFixtureSchema, checkoutDescriptor); - const checkoutContract = sharedApiModule.createSharedApi({ - ...checkoutDescriptor, - exposes: {}, - }); + const checkoutContract = sharedApiModule.createSharedApi( + { + ...checkoutDescriptor, + exposes: {}, + }, + { scope: generatedProofScope } + ); const checkoutClient = clientModule.createApiClient( checkoutDescriptor, generatedClientContractImport, + { scope: generatedProofScope } ); assert.equal( microVerticalApiBaselineViolation(checkoutId, checkoutContract, { @@ -2575,11 +2868,19 @@ void test('all published scaffold formats generate the shared MicroVertical API sharedContractsPackage: generatedSharedContractsPackage, }), undefined, - `${moduleFormat} checkout cart operations must retain baseline validation`, + `${moduleFormat} checkout cart operations must retain baseline validation` ); - assert.match(checkoutClient, /client\.foundation\.readiness\(\{\}\)/u, moduleFormat); - assert.doesNotMatch(checkoutClient, /client\.checkout\.readiness/u, moduleFormat); - }), + assert.match( + checkoutClient, + /client\.foundation\.readiness\(\{\}\)/u, + moduleFormat + ); + assert.doesNotMatch( + checkoutClient, + /client\.checkout\.readiness/u, + moduleFormat + ); + }) ); }); @@ -2587,8 +2888,14 @@ void test('all published scaffold formats generate the shared MicroVertical API * Generator proofs compile real output, so each one gets its own scratch root inside the * shared-contracts package and drops it when the owning test finishes. */ -const makeProofRoot = async (context: TestContext, prefix: string): Promise => { - const scratchRoot = path.join(workspaceRoot, 'packages/shared-contracts/.scratch'); +const makeProofRoot = async ( + context: TestContext, + prefix: string +): Promise => { + const scratchRoot = path.join( + workspaceRoot, + 'packages/shared-contracts/.scratch' + ); await mkdir(scratchRoot, { recursive: true }); const proofRoot = await mkdtemp(path.join(scratchRoot, `${prefix}-`)); context.after(async (): Promise => { @@ -2598,10 +2905,13 @@ const makeProofRoot = async (context: TestContext, prefix: string): Promise { - const proofRoot = await makeProofRoot(context, 'generated-baseline-validator-proof'); + const proofRoot = await makeProofRoot( + context, + 'generated-baseline-validator-proof' + ); const expectedHelper = await readFile( path.join(workspaceRoot, 'scripts/microvertical-api-baseline-boundary.mts'), - 'utf-8', + 'utf-8' ); await Promise.all( @@ -2609,30 +2919,43 @@ void test('all published scaffold formats emit the executable AST baseline valid const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; const modulePath = path.join( generatorRoot, - `dist/${moduleFormat}/ultramodern-workspace/workspace-scripts.${extension}`, + `dist/${moduleFormat}/ultramodern-workspace/workspace-scripts.${extension}` ); const moduleSource: unknown = - moduleFormat === 'cjs' ? require(modulePath) : await import(pathToFileURL(modulePath).href); - const generator = Schema.decodeUnknownSync(WorkspaceScriptsGeneratorSchema)(moduleSource); + moduleFormat === 'cjs' + ? require(modulePath) + : await import(pathToFileURL(modulePath).href); + const generator = Schema.decodeUnknownSync( + WorkspaceScriptsGeneratorSchema + )(moduleSource); const artifacts = generator.migratedWorkspaceScriptArtifacts({ hasBackendSurface: true, shellOnly: false, }); const helper = artifacts.find( - ({ relativePath }) => relativePath === 'scripts/microvertical-api-baseline-boundary.mts', + ({ relativePath }) => + relativePath === 'scripts/microvertical-api-baseline-boundary.mts' + ); + const checker = artifacts.find( + ({ relativePath }) => relativePath === apiBoundaryCheckerPath ); - const checker = artifacts.find(({ relativePath }) => relativePath === apiBoundaryCheckerPath); assert.ok(helper, `${moduleFormat} must emit the AST baseline helper`); assert.ok(checker, `${moduleFormat} must emit the API checker`); assert.equal( helper.content, expectedHelper, - `${moduleFormat} must emit byte-exact baseline source`, + `${moduleFormat} must emit byte-exact baseline source` ); assert.equal( - await normalizedGeneratedSource('microvertical-api-baseline-boundary.mts', helper.content), - await normalizedGeneratedSource('microvertical-api-baseline-boundary.mts', expectedHelper), - `${moduleFormat} must emit the exact repository validator`, + await normalizedGeneratedSource( + 'microvertical-api-baseline-boundary.mts', + helper.content + ), + await normalizedGeneratedSource( + 'microvertical-api-baseline-boundary.mts', + expectedHelper + ), + `${moduleFormat} must emit the exact repository validator` ); const formatRoot = path.join(proofRoot, moduleFormat); await writeText(formatRoot, helper.relativePath, helper.content); @@ -2643,22 +2966,32 @@ void test('all published scaffold formats emit the executable AST baseline valid env: { ULTRAMODERN_WORKSPACE_ROOT: workspaceRoot }, }), /UltraModern API boundary check passed/u, - moduleFormat, + moduleFormat ); - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); + const generatorModulePath = generatorModulePathFor(moduleFormat); const descriptorSource: unknown = moduleFormat === 'cjs' ? require(generatorModulePath('descriptors')) - : await import(pathToFileURL(generatorModulePath('descriptors')).href); + : await import( + pathToFileURL(generatorModulePath('descriptors')).href + ); const sharedApiSource: unknown = moduleFormat === 'cjs' ? require(generatorModulePath(generatedSharedApiModule)) - : await import(pathToFileURL(generatorModulePath(generatedSharedApiModule)).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const checkoutDescriptor = descriptorModule.createVerticalDescriptor('shopping', 4105); + : await import( + pathToFileURL(generatorModulePath(generatedSharedApiModule)).href + ); + const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)( + descriptorSource + ); + const sharedApiModule = Schema.decodeUnknownSync( + SharedApiGeneratorSchema + )(sharedApiSource); + const checkoutDescriptor = descriptorModule.createVerticalDescriptor( + 'shopping', + 4105 + ); const checkoutStemDescriptor = { ...checkoutDescriptor, api: { prefix: checkoutApiPrefix, stem: checkoutId }, @@ -2669,22 +3002,30 @@ void test('all published scaffold formats emit the executable AST baseline valid moduleFormat === 'cjs' ? require(servicePath) : await import(pathToFileURL(servicePath).href); - const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(serviceSource); + const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)( + serviceSource + ); const checkoutWorkspace = path.join(formatRoot, 'checkout-workspace'); await writeText( checkoutWorkspace, 'verticals/shopping/shared/api.ts', - sharedApiModule.createSharedApi(checkoutStemDescriptor), + sharedApiModule.createSharedApi(checkoutStemDescriptor, { + scope: generatedProofScope, + }) ); await writeText( checkoutWorkspace, 'verticals/shopping/api/index.ts', - serviceModule.createApiServiceEntry(checkoutStemDescriptor, generatedSharedApiImport), + serviceModule.createApiServiceEntry( + checkoutStemDescriptor, + generatedSharedApiImport, + { scope: generatedProofScope } + ) ); await writeText( checkoutWorkspace, 'verticals/shopping/src/api/checkout-client.ts', - 'export const checkoutClient = true;\n', + 'export const checkoutClient = true;\n' ); await writeText( checkoutWorkspace, @@ -2695,7 +3036,7 @@ void test('all published scaffold formats emit the executable AST baseline valid effect: { entry: './api/index', strictEffectApproach: true }, }, }; -`, +` ); await writeJson(checkoutWorkspace, 'verticals/shopping/package.json', { exports: { @@ -2703,9 +3044,13 @@ void test('all published scaffold formats emit the executable AST baseline valid './api/client': './src/api/checkout-client.ts', }, }); - await writeJson(checkoutWorkspace, 'packages/shared-contracts/package.json', { - name: generatedSharedContractsPackage, - }); + await writeJson( + checkoutWorkspace, + 'packages/shared-contracts/package.json', + { + name: generatedSharedContractsPackage, + } + ); await writeJson(checkoutWorkspace, topologyReferencePath, { verticals: [ { @@ -2724,7 +3069,7 @@ void test('all published scaffold formats emit the executable AST baseline valid await writeText( checkoutWorkspace, 'apps/shell-super-app/src/api/vertical-clients.ts', - 'export const verticalClients = {};\n', + 'export const verticalClients = {};\n' ); const invalidApiSource = `import { Schema } from 'effect'; export const response = new Response('generated'); @@ -2733,19 +3078,19 @@ export const responseSchema = Schema.Unknown; await writeText( checkoutWorkspace, 'verticals/shopping/dist-cloudflare/api/index.js', - invalidApiSource, + invalidApiSource ); await writeText( checkoutWorkspace, 'apps/shell-super-app/dist-cloudflare/api/index.js', - invalidApiSource, + invalidApiSource ); assert.match( runNode([path.join(formatRoot, checker.relativePath)], { env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, }), /UltraModern API boundary check passed/u, - `${moduleFormat} checkout workspace`, + `${moduleFormat} checkout workspace` ); const authoredApiPath = 'verticals/shopping/api/invalid.ts'; await writeText(checkoutWorkspace, authoredApiPath, invalidApiSource); @@ -2755,51 +3100,79 @@ export const responseSchema = Schema.Unknown; { encoding: 'utf-8', env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, - }, + } ); assert.equal(authoredResult.status, 1, moduleFormat); assert.match( authoredResult.stderr, /verticals\/shopping\/api\/invalid\.ts: API modules must not hand-build Response objects/u, - moduleFormat, + moduleFormat ); assert.match( authoredResult.stderr, /verticals\/shopping\/api\/invalid\.ts: API modules must use concrete request, response and error schemas/u, - moduleFormat, + moduleFormat ); - assert.doesNotMatch(authoredResult.stderr, /dist-cloudflare/u, moduleFormat); - }), + assert.doesNotMatch( + authoredResult.stderr, + /dist-cloudflare/u, + moduleFormat + ); + }) ); }); void test('two generated MicroVertical root contracts execute invariant readiness endpoints', async (context) => { const proofRoot = await mkdtemp( - path.join(workspaceRoot, `verticals/${partyId}/.generated-api-baseline-`), + path.join(workspaceRoot, `verticals/${partyId}/.generated-api-baseline-`) ); context.after(async (): Promise => { await rm(proofRoot, { force: true, recursive: true }); }); const descriptorSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/descriptors.js')) - .href + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/descriptors.js' + ) + ).href ); const sharedApiSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js')) - .href + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/api/shared.js' + ) + ).href ); const apiServiceSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/service.js')) - .href + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/api/service.js' + ) + ).href ); const apiClientSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/client.js')) - .href + pathToFileURL( + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/api/client.js' + ) + ).href + ); + const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)( + descriptorSource + ); + const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)( + sharedApiSource + ); + const apiServiceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)( + apiServiceSource + ); + const apiClientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)( + apiClientSource ); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const apiServiceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(apiServiceSource); - const apiClientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)(apiClientSource); const fixtures = [ { id: inventoryStockId, @@ -2828,7 +3201,7 @@ void test('two generated MicroVertical root contracts execute invariant readines fixtures.map(async (fixture) => { const descriptor: unknown = descriptorModule.createVerticalDescriptor( fixture.id, - fixture.port, + fixture.port ); Schema.asserts(WorkspaceAppFixtureSchema, descriptor); assert.ok(descriptor.api); @@ -2837,12 +3210,16 @@ void test('two generated MicroVertical root contracts execute invariant readines api: { ...descriptor.api, prefix: fixture.prefix, stem: fixture.stem }, exposes: {}, }; - const contract = sharedApiModule.createSharedApi(generatedDescriptor); + const contract = sharedApiModule.createSharedApi(generatedDescriptor, { + scope: 'app', + }); const basePath = `${fixture.prefix}/${fixture.stem}`; assert.equal( microVerticalApiBaselineViolation(fixture.stem, contract, { additionalPaths: - fixture.stem === checkoutId ? { checkoutCartPath: `${basePath}/cart` } : {}, + fixture.stem === checkoutId + ? { checkoutCartPath: `${basePath}/cart` } + : {}, apiPrefix: fixture.prefix, basePath, effectClientPackage, @@ -2850,7 +3227,7 @@ void test('two generated MicroVertical root contracts execute invariant readines readinessPath: `${basePath}/readiness`, sharedContractsPackage: '@app/shared-contracts', }), - undefined, + undefined ); const ownerRoot = path.join(proofRoot, fixture.id); await writeText(ownerRoot, 'shared/api.ts', contract); @@ -2870,18 +3247,26 @@ void test('two generated MicroVertical root contracts execute invariant readines unitId: 'app/${fixture.id}', version: '0.1.0', } as const; -`, +` ); await writeText( ownerRoot, apiIndexFile, - apiServiceModule.createApiServiceEntry(generatedDescriptor, generatedSharedApiImport), + apiServiceModule.createApiServiceEntry( + generatedDescriptor, + generatedSharedApiImport, + { scope: 'app' } + ) ); const clientEntryPath = `src/api/${fixture.id}-client.ts`; await writeText( ownerRoot, clientEntryPath, - apiClientModule.createApiClient(generatedDescriptor, generatedClientContractImport), + apiClientModule.createApiClient( + generatedDescriptor, + generatedClientContractImport, + { scope: 'app' } + ) ); await writeJson(ownerRoot, 'package.json', { type: 'module' }); await writeJson(ownerRoot, tsconfigFile, { @@ -2898,11 +3283,14 @@ void test('two generated MicroVertical root contracts execute invariant readines }); runNode( [ - path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), + path.join( + workspaceRoot, + 'node_modules/@typescript/native-preview/bin/tsc' + ), '-p', ownerRoot, ], - { cwd: workspaceRoot }, + { cwd: workspaceRoot } ); const generatedModuleSource: unknown = await import( pathToFileURL(path.join(ownerRoot, apiIndexFile)).href @@ -2910,31 +3298,33 @@ void test('two generated MicroVertical root contracts execute invariant readines const generatedClientSource: unknown = await import( pathToFileURL(path.join(ownerRoot, clientEntryPath)).href ); - const generatedModule = Schema.decodeUnknownSync(GeneratedApiRuntimeModuleSchema)( - generatedModuleSource, - ); + const generatedModule = Schema.decodeUnknownSync( + GeneratedApiRuntimeModuleSchema + )(generatedModuleSource); const generatedClient = Schema.decodeUnknownSync( - Schema.Record(Schema.String, Schema.Unknown), + Schema.Record(Schema.String, Schema.Unknown) )(generatedClientSource); - const getReadiness = Schema.decodeUnknownSync(callable())( - generatedClient[fixture.readinessExport], - ); - const runEffectRequest = Schema.decodeUnknownSync(callable())( - generatedClient.runEffectRequest, - ); + const getReadiness = Schema.decodeUnknownSync( + callable() + )(generatedClient[fixture.readinessExport]); + const runEffectRequest = Schema.decodeUnknownSync( + callable() + )(generatedClient.runEffectRequest); return { fixture, generatedModule, getReadiness, runEffectRequest }; - }), + }) ); const handlers = new Map( generatedProofs.map(({ fixture, generatedModule }) => [ fixture.stem, generatedModule.default.createHandler(), - ]), + ]) ); const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { const request = new Request(input, init); - const stem = new URL(request.url).pathname.split('/').find((segment) => segment.length > 0); + const stem = new URL(request.url).pathname + .split('/') + .find((segment) => segment.length > 0); const handler = stem === undefined ? undefined : handlers.get(stem); return handler === undefined ? new Response(undefined, { status: 503 }) @@ -2943,26 +3333,34 @@ void test('two generated MicroVertical root contracts execute invariant readines let readinessValues: (typeof MicroVerticalReadinessSchema.Type)[]; try { readinessValues = await Promise.all( - generatedProofs.map(async ({ fixture, getReadiness, runEffectRequest }) => { - const handler = handlers.get(fixture.stem); - assert.ok(handler); - const directResponse = await handler.handler( - new Request(`http://localhost/${fixture.stem}/readiness`), - ); - assert.equal(directResponse.status, 200); - const directReadiness = Schema.decodeUnknownSync(MicroVerticalReadinessSchema)( - await directResponse.json(), - ); - const clientReadiness = Schema.decodeUnknownSync(MicroVerticalReadinessSchema)( - await runEffectRequest(getReadiness({ baseUrl: 'http://localhost' })), - ); - assert.deepEqual(clientReadiness, directReadiness); - return clientReadiness; - }), + generatedProofs.map( + async ({ fixture, getReadiness, runEffectRequest }) => { + const handler = handlers.get(fixture.stem); + assert.ok(handler); + const directResponse = await handler.handler( + new Request(`http://localhost/${fixture.stem}/readiness`) + ); + assert.equal(directResponse.status, 200); + const directReadiness = Schema.decodeUnknownSync( + MicroVerticalReadinessSchema + )(await directResponse.json()); + const clientReadiness = Schema.decodeUnknownSync( + MicroVerticalReadinessSchema + )( + await runEffectRequest( + getReadiness({ baseUrl: 'http://localhost' }) + ) + ); + assert.deepEqual(clientReadiness, directReadiness); + return clientReadiness; + } + ) ); } finally { globalThis.fetch = originalFetch; - await Promise.all([...handlers.values()].map(async (handler) => await handler.dispose())); + await Promise.all( + [...handlers.values()].map(async (handler) => await handler.dispose()) + ); } const [firstReadiness, secondReadiness] = readinessValues; @@ -2977,12 +3375,17 @@ void test('two generated MicroVertical root contracts execute invariant readines }); void test('generated shared-contracts baseline template is lint-clean and type-safe', async (context) => { - const proofRoot = await makeProofRoot(context, 'generated-baseline-template-proof'); + const proofRoot = await makeProofRoot( + context, + 'generated-baseline-template-proof' + ); const templateSource = await readFile( path.join(generatorRoot, 'templates/packages/shared-contracts-index.ts'), - 'utf-8', + 'utf-8' + ); + const baselineEnd = templateSource.indexOf( + 'export type UltramodernPublicSitemapChangeFrequency' ); - const baselineEnd = templateSource.indexOf('export type UltramodernPublicSitemapChangeFrequency'); assert.notEqual(baselineEnd, -1); const generatedSource = templateSource.slice(0, baselineEnd); await writeText(proofRoot, generatedBaselineTemplateEntry, generatedSource); @@ -2998,19 +3401,29 @@ void test('generated shared-contracts baseline template is lint-clean and type-s include: [generatedBaselineTemplateEntry], }); const generatedFile = path.join(proofRoot, generatedBaselineTemplateEntry); - runNode([path.join(workspaceRoot, 'node_modules/oxlint/bin/oxlint'), generatedFile], { - cwd: workspaceRoot, - }); runNode( - [path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), '-p', proofRoot], - { cwd: workspaceRoot }, + [path.join(workspaceRoot, 'node_modules/oxlint/bin/oxlint'), generatedFile], + { + cwd: workspaceRoot, + } + ); + runNode( + [ + path.join( + workspaceRoot, + 'node_modules/@typescript/native-preview/bin/tsc' + ), + '-p', + proofRoot, + ], + { cwd: workspaceRoot } ); }); void test('baseline imports require values even with comments after the type keyword', async () => { const contract = await readFile( path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), - 'utf-8', + 'utf-8' ); assert.equal(microVerticalApiBaselineViolation(partyId, contract), undefined); for (const declaration of [ @@ -3020,7 +3433,7 @@ void test('baseline imports require values even with comments after the type key ]) { const mutated = contract.replace( 'import {\n MicroVerticalBuildMarkerSchema,', - `${declaration}{\n MicroVerticalBuildMarkerSchema,`, + `${declaration}{\n MicroVerticalBuildMarkerSchema,` ); const violation = microVerticalApiBaselineViolation(partyId, mutated); if (declaration.startsWith('import type')) { @@ -3032,20 +3445,26 @@ void test('baseline imports require values even with comments after the type key }); void test('repository checker respects custom readiness prefixes and diagnoses missing topology', async (context) => { - const fixture = await mkdtemp(path.join(os.tmpdir(), 'ontos-baseline-topology-')); + const fixture = await mkdtemp( + path.join(os.tmpdir(), 'ontos-baseline-topology-') + ); context.after(async (): Promise => { await rm(fixture, { force: true, recursive: true }); }); const generatorModulePath = (name: string): string => path.join(generatorRoot, `dist/esm-node/ultramodern-workspace/${name}.js`); const descriptors = Schema.decodeUnknownSync(DescriptorModuleSchema)( - await import(pathToFileURL(generatorModulePath('descriptors')).href), + await import(pathToFileURL(generatorModulePath('descriptors')).href) ); const shared = Schema.decodeUnknownSync(SharedApiGeneratorSchema)( - await import(pathToFileURL(generatorModulePath(generatedSharedApiModule)).href), + await import( + pathToFileURL(generatorModulePath(generatedSharedApiModule)).href + ) ); const service = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)( - await import(pathToFileURL(generatorModulePath(generatedServiceModuleName)).href), + await import( + pathToFileURL(generatorModulePath(generatedServiceModuleName)).href + ) ); const app = { ...descriptors.createVerticalDescriptor(inventoryStockId, 4103), @@ -3054,25 +3473,27 @@ void test('repository checker respects custom readiness prefixes and diagnoses m }; const ownerPath = `verticals/${inventoryStockId}`; const readinessContract = shared - .createSharedApi(app) + .createSharedApi(app, { scope: 'app' }) .replace( /(?\.addHttpApi\(warehouseItemsFoundationApi\))[\s\S]*?(?=export const warehouseItemsOperationContexts)/u, - '$;\n\n', + '$;\n\n' ) .replace( /(?export const warehouseItemsOperationContexts = \{)[\s\S]*?(?= {2}readiness:)/u, - '$\n', + '$\n' ); await writeText(fixture, `${ownerPath}/shared/api.ts`, readinessContract); await writeText( fixture, `${ownerPath}/api/index.ts`, - service.createApiServiceEntry(app, generatedSharedApiImport), + service.createApiServiceEntry(app, generatedSharedApiImport, { + scope: 'app', + }) ); await writeText( fixture, `${ownerPath}/src/api/warehouse-client.ts`, - 'export const client = true;\n', + 'export const client = true;\n' ); await writeJson(fixture, `${ownerPath}/package.json`, { exports: {} }); const api = { @@ -3110,18 +3531,24 @@ void test('repository checker respects custom readiness prefixes and diagnoses m }, { expected: /topology must declare api\.bff\.prefix/u, - verticals: [{ ...vertical, api: { ...api, bff: { strictEffectApproach: true } } }], + verticals: [ + { ...vertical, api: { ...api, bff: { strictEffectApproach: true } } }, + ], }, ]; for (const entry of cases) { writeFileSync( path.join(fixture, topologyReferencePath), - JSON.stringify({ verticals: entry.verticals }), + JSON.stringify({ verticals: entry.verticals }) + ); + const result = spawnSync( + process.execPath, + [path.join(workspaceRoot, apiBoundaryCheckerPath)], + { + encoding: 'utf-8', + env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, + } ); - const result = spawnSync(process.execPath, [path.join(workspaceRoot, apiBoundaryCheckerPath)], { - encoding: 'utf-8', - env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, - }); assert.equal(result.status, 1); assert.match(result.stderr, entry.expected); assert.doesNotMatch(result.stderr, /exact owner and API path metadata/u); @@ -3131,7 +3558,7 @@ void test('repository checker respects custom readiness prefixes and diagnoses m void test('static validation rejects a MicroVertical root contract without readiness baseline', async () => { const contract = await readFile( path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), - 'utf-8', + 'utf-8' ); assert.equal(microVerticalApiBaselineViolation(partyId, contract), undefined); const readinessEndpointDecoy = @@ -3145,32 +3572,41 @@ void test('static validation rejects a MicroVertical root contract without readi for (const [label, mutated, expected] of [ [ 'renamed readiness endpoint', - contract.replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'"), + contract.replace( + "HttpApiEndpoint.get('readiness'", + "HttpApiEndpoint.get('health'" + ), /exact readiness endpoint/u, ], [ 'foreign readiness schema fields', - contract.replace('...MicroVerticalReadinessSchema.fields,', '...Schema.Unknown.fields,'), + contract.replace( + '...MicroVerticalReadinessSchema.fields,', + '...Schema.Unknown.fields,' + ), /shared readiness schema/u, ], [ 'readiness success schema commented out', contract.replace( 'success: partyRegistryReadinessSchema', - 'success: Schema.String /* success: partyRegistryReadinessSchema */', + 'success: Schema.String /* success: partyRegistryReadinessSchema */' ), /exact readiness endpoint/u, ], [ 'baseline primitives imported from a copied package', - contract.replace("from '@app/shared-contracts';", "from '@app/copied-contracts';"), + contract.replace( + "from '@app/shared-contracts';", + "from '@app/copied-contracts';" + ), /import exact baseline primitives from the shared contracts package/u, ], [ 'Effect API primitives imported from a foreign client', contract.replace( "from '@modern-js/plugin-bff/effect-client';", - "from '@evil/fake-effect-client';", + "from '@evil/fake-effect-client';" ), /import exact Effect API primitives from the framework client package/u, ], @@ -3180,17 +3616,20 @@ void test('static validation rejects a MicroVertical root contract without readi sharedBaselineImport, `const MicroVerticalBuildMarkerSchema = Schema.Struct({ copied: Schema.String }); const MicroVerticalReadinessSchema = Schema.Struct({ copied: Schema.String }); -const createMicroVerticalOperationContext = (value: Value): Value => value;`, +const createMicroVerticalOperationContext = (value: Value): Value => value;` ), /import exact baseline primitives from the shared contracts package/u, ], [ 'renamed readiness endpoint with a decoy API name', contract - .replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'") + .replace( + "HttpApiEndpoint.get('readiness'", + "HttpApiEndpoint.get('health'" + ) .replace( "HttpApi.make('PartyRegistryFoundationApi')", - `HttpApi.make("${readinessEndpointDecoy}")`, + `HttpApi.make("${readinessEndpointDecoy}")` ), /exact readiness endpoint/u, ], @@ -3204,7 +3643,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu contract.replace( foundationComposition, `${foundationComposition} - .pipe(() => HttpApi.make('DiscardedPartyRegistryApi'))`, + .pipe(() => HttpApi.make('DiscardedPartyRegistryApi'))` ), /explicitly compose its readiness foundation API/u, ], @@ -3214,7 +3653,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu ` ), );`, ` ), -).pipe(() => HttpApi.make('DiscardedPartyRegistryFoundationApi'));`, +).pipe(() => HttpApi.make('DiscardedPartyRegistryFoundationApi'));` ), /directly compose its exact readiness endpoint/u, ], @@ -3224,7 +3663,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu .replace(foundationComposition, '') .replace( "HttpApi.make('PartyRegistryApi')", - "HttpApi.make('.addHttpApi(partyRegistryFoundationApi)')", + "HttpApi.make('.addHttpApi(partyRegistryFoundationApi)')" ), /explicitly compose its readiness foundation API/u, ], @@ -3237,7 +3676,10 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu ], [ 'hand-forked build marker fields', - contract.replace('...MicroVerticalBuildMarkerSchema.fields,', 'build: Schema.String,'), + contract.replace( + '...MicroVerticalBuildMarkerSchema.fields,', + 'build: Schema.String,' + ), /shared build marker schema/u, ], [ @@ -3250,7 +3692,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu `export const partyRegistryReadinessSchema = ( MicroVerticalReadinessSchema, Schema.Struct({ marker: partyRegistryMarkerSchema, status: Schema.String }) -);`, +);` ), /consume the shared readiness schema/u, ], @@ -3261,7 +3703,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu ...MicroVerticalReadinessSchema.fields, marker: partyRegistryMarkerSchema, });`, - 'export const partyRegistryReadinessSchema = MicroVerticalReadinessSchema;', + 'export const partyRegistryReadinessSchema = MicroVerticalReadinessSchema;' ), /consume the shared readiness schema/u, ], @@ -3272,7 +3714,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu .addHttpApi(partyRegistryFoundationApi)`, `export const partyRegistryApi = HttpApi.make('PartyRegistryApi').pipe( (api) => (api.addHttpApi(partyRegistryFoundationApi), api), -)`, +)` ), /explicitly compose its readiness foundation API/u, ], @@ -3280,7 +3722,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'decoy API declaration shadowed by an uncomposed API', `${contract.replace( 'export const partyRegistryApi =', - 'export const partyRegistryApiDecoy =', + 'export const partyRegistryApiDecoy =' )}\nexport const partyRegistryApi = HttpApi.make('PartyRegistryApi');\n`, /explicitly compose its readiness foundation API/u, ], @@ -3288,7 +3730,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'decoy foundation API declaration without a readiness endpoint', `${contract.replace( 'export const partyRegistryFoundationApi =', - 'export const partyRegistryFoundationApiDecoy =', + 'export const partyRegistryFoundationApiDecoy =' )}\nexport const partyRegistryFoundationApi = HttpApi.make('PartyRegistryFoundationApi');\n`, /exact readiness endpoint/u, ], @@ -3296,7 +3738,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'decoy contract declaration without path metadata', `${contract.replace( 'export const partyRegistryApiContract =', - 'export const partyRegistryApiContractDecoy =', + 'export const partyRegistryApiContractDecoy =' )}\nexport const partyRegistryApiContract = { ownerId: 'party-registry' };\n`, /exact owner and API path metadata/u, ], @@ -3304,7 +3746,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'build marker overriding a shared field', contract.replace( '...MicroVerticalBuildMarkerSchema.fields,', - '...MicroVerticalBuildMarkerSchema.fields,\n build: Schema.Number,', + '...MicroVerticalBuildMarkerSchema.fields,\n build: Schema.Number,' ), /without overriding shared fields/u, ], @@ -3312,7 +3754,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'foreign AppId schema', contract.replace( "const AppIdSchema = Schema.String.pipe(Schema.brand('AppId'));", - 'const AppIdSchema = Schema.Number;', + 'const AppIdSchema = Schema.Number;' ), /shared build marker schema/u, ], @@ -3320,25 +3762,31 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'readiness schema overriding a shared field', contract.replace( '...MicroVerticalReadinessSchema.fields,', - '...MicroVerticalReadinessSchema.fields,\n status: Schema.String,', + '...MicroVerticalReadinessSchema.fields,\n status: Schema.String,' ), /without overriding shared fields/u, ], [ 'renamed foundation group', - contract.replace("HttpApiGroup.make('foundation')", "HttpApiGroup.make('not-foundation')"), + contract.replace( + "HttpApiGroup.make('foundation')", + "HttpApiGroup.make('not-foundation')" + ), /exact readiness endpoint and foundation identity/u, ], [ 'renamed root API', - contract.replace("HttpApi.make('PartyRegistryApi')", "HttpApi.make('WrongApi')"), + contract.replace( + "HttpApi.make('PartyRegistryApi')", + "HttpApi.make('WrongApi')" + ), /explicitly compose its readiness foundation API/u, ], [ 'renamed operation contexts', contract.replace( 'export const partyRegistryOperationContexts =', - 'export const renamedOperationContexts =', + 'export const renamedOperationContexts =' ), /construct every operation with the shared context constructor/u, ], @@ -3346,13 +3794,16 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu 'foreign operation id', contract.replace( "operationId: 'PartyRegistryApi:/reads/ares-lookup'", - "operationId: 'WrongApi:unrelated'", + "operationId: 'WrongApi:unrelated'" ), /construct every operation with the shared context constructor/u, ], [ 'foreign route path', - contract.replace("routePath: '/reads/ares-lookup'", "routePath: '/not-an-endpoint'"), + contract.replace( + "routePath: '/reads/ares-lookup'", + "routePath: '/not-an-endpoint'" + ), /construct every operation with the shared context constructor/u, ], [ @@ -3367,7 +3818,7 @@ const createMicroVerticalOperationContext = (value: Value): Value => valu method: 'GET', operationId: 'PartyRegistryApi:/party-registry/readiness', routePath: '/party-registry/readiness', - },`, + },` )} createMicroVerticalOperationContext({ method: 'GET', @@ -3384,7 +3835,10 @@ createMicroVerticalOperationContext({ ], [ 'missing basePath', - contract.replace(" basePath: '/party-registry-api/party-registry',\n", ''), + contract.replace( + " basePath: '/party-registry-api/party-registry',\n", + '' + ), /exact owner and API path metadata/u, ], [ @@ -3394,14 +3848,17 @@ createMicroVerticalOperationContext({ ], [ 'wrong apiPrefix', - contract.replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'"), + contract.replace( + "apiPrefix: '/party-registry-api'", + "apiPrefix: '/evil-api'" + ), /exact owner and API path metadata/u, ], [ 'wrong basePath', contract.replace( "basePath: '/party-registry-api/party-registry'", - "basePath: '/party-registry-api/evil'", + "basePath: '/party-registry-api/evil'" ), /exact owner and API path metadata/u, ], @@ -3414,7 +3871,7 @@ createMicroVerticalOperationContext({ 'wrong readinessPath', contract.replace( "readinessPath: '/party-registry-api/party-registry/readiness'", - "readinessPath: '/evil-prefix/party-registry/readiness'", + "readinessPath: '/evil-prefix/party-registry/readiness'" ), /exact owner and API path metadata/u, ], @@ -3424,11 +3881,11 @@ createMicroVerticalOperationContext({ .replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'") .replace( "basePath: '/party-registry-api/party-registry'", - "basePath: '/evil-api/party-registry'", + "basePath: '/evil-api/party-registry'" ) .replace( "readinessPath: '/party-registry-api/party-registry/readiness'", - "readinessPath: '/evil-api/party-registry/readiness'", + "readinessPath: '/evil-api/party-registry/readiness'" ), /exact owner and API path metadata/u, ], @@ -3436,7 +3893,7 @@ createMicroVerticalOperationContext({ 'forbidden credential metadata', contract.replace( partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n credential: 'secret',`, + `${partyReadinessMetadataLine}\n credential: 'secret',` ), /exact owner and API path metadata/u, ], @@ -3444,7 +3901,7 @@ createMicroVerticalOperationContext({ 'forbidden credential path metadata', contract.replace( partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n credentialPath: '/party-registry-api/party-registry/secret',`, + `${partyReadinessMetadataLine}\n credentialPath: '/party-registry-api/party-registry/secret',` ), /exact owner and API path metadata/u, ], @@ -3452,7 +3909,7 @@ createMicroVerticalOperationContext({ 'unknown path metadata', contract.replace( partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n unknownPath: '/party-registry-api/party-registry/unknown',`, + `${partyReadinessMetadataLine}\n unknownPath: '/party-registry-api/party-registry/unknown',` ), /exact owner and API path metadata/u, ], @@ -3460,12 +3917,16 @@ createMicroVerticalOperationContext({ 'spread metadata', contract.replace( 'export const partyRegistryApiContract = {', - 'const copiedMetadata = {};\nexport const partyRegistryApiContract = {\n ...copiedMetadata,', + 'const copiedMetadata = {};\nexport const partyRegistryApiContract = {\n ...copiedMetadata,' ), /exact owner and API path metadata/u, ], ] as const) { - assert.match(microVerticalApiBaselineViolation(partyId, mutated) ?? '', expected, label); + assert.match( + microVerticalApiBaselineViolation(partyId, mutated) ?? '', + expected, + label + ); } }); @@ -3478,55 +3939,82 @@ void test('MicroVertical baseline validation resolves an API stem independently path: 'verticals/inventory', }, ]), - warehouseItemsApiStem, + warehouseItemsApiStem ); }); void test('full-stack Party Registry keeps backend and Contacts component tests executable', async () => { const packageJson = await readJson( PackageJsonSchema, - path.join(workspaceRoot, 'verticals/party-registry/package.json'), + path.join(workspaceRoot, 'verticals/party-registry/package.json') + ); + assert.equal( + packageJson.scripts['test:component'], + 'rstest --config rstest.config.ts' + ); + assert.equal( + packageJson.scripts['test:unit'], + 'node --test tests/unit/*.test.ts' + ); + assert.equal( + packageJson.scripts['test:integration'], + 'node --test tests/integration/*.test.ts' ); - assert.equal(packageJson.scripts['test:component'], 'rstest --config rstest.config.ts'); - assert.equal(packageJson.scripts['test:unit'], 'node --test tests/unit/*.test.ts'); - assert.equal(packageJson.scripts['test:integration'], 'node --test tests/integration/*.test.ts'); assert.match( - await readFile(path.join(workspaceRoot, 'verticals/party-registry/rstest.config.ts'), 'utf-8'), - /tests\/components/u, + await readFile( + path.join(workspaceRoot, 'verticals/party-registry/rstest.config.ts'), + 'utf-8' + ), + /tests\/components/u ); }); const cloudflareProofModule: unknown = await import( pathToFileURL( - path.join(generatorRoot, 'templates/workspace-scripts/ultramodern-cloudflare-proof.mjs'), + path.join( + generatorRoot, + 'templates/workspace-scripts/ultramodern-cloudflare-proof.mjs' + ) ).href ); const cloudflareProof = Schema.decodeUnknownSync(CloudflareProofModuleSchema)( - cloudflareProofModule, + cloudflareProofModule +); +const validateCloudflareApp = cloudflareProof.validateApp.bind( + cloudflareProofModule ); -const validateCloudflareApp = cloudflareProof.validateApp.bind(cloudflareProofModule); const validateApp = async ( app: ApiOnlyAppFixture, - applicationPublicUrl: string, + applicationPublicUrl: string ): Promise => { - const output: unknown = await validateCloudflareApp(app, applicationPublicUrl); + const output: unknown = await validateCloudflareApp( + app, + applicationPublicUrl + ); return Schema.decodeUnknownSync(CloudflareEvidenceSchema)(output); }; const federationValidationModule: unknown = await import( pathToFileURL( - path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/mf-validation/validate.js'), + path.join( + generatorRoot, + 'dist/esm-node/ultramodern-workspace/mf-validation/validate.js' + ) ).href ); -const federationValidation = Schema.decodeUnknownSync(ModuleFederationValidationModuleSchema)( - federationValidationModule, -); +const federationValidation = Schema.decodeUnknownSync( + ModuleFederationValidationModuleSchema +)(federationValidationModule); const validateInstalledModuleFederationTypes = - federationValidation.validateModuleFederationTypes.bind(federationValidationModule); + federationValidation.validateModuleFederationTypes.bind( + federationValidationModule + ); const validateModuleFederationTypes = (input: { readonly appDirs: readonly string[]; readonly workspaceRoot: string; }): typeof ModuleFederationValidationResultSchema.Type => { const output: unknown = validateInstalledModuleFederationTypes(input); - return Schema.decodeUnknownSync(ModuleFederationValidationResultSchema)(output); + return Schema.decodeUnknownSync(ModuleFederationValidationResultSchema)( + output + ); }; const publicUrl = 'https://party.example.test'; @@ -3552,7 +4040,9 @@ const apiOnlyApp = (): ApiOnlyAppFixture => ({ deliveryUnit: { buildMarker, unitId: 'app/party-registry' }, deploy: { cloudflare: { - jsonSmokeChecks: [{ expect: { status: 'ready' }, id: 'api', route: apiSmokePath }], + jsonSmokeChecks: [ + { expect: { status: 'ready' }, id: 'api', route: apiSmokePath }, + ], routes: { apiReadiness: readinessPath, mfManifest: mfManifestPath, @@ -3594,7 +4084,12 @@ const mockPublicResponses = (context: TestContext, failedPath?: string) => { void test('API-only proof keeps manifest, readiness, service-binding and JSON proofs without invented pages/locales', async (context) => { const requested = mockPublicResponses(context); const evidence = await validateApp(apiOnlyApp(), publicUrl); - assert.deepEqual(requested, [mfManifestPath, readinessPath, '/binding', apiSmokePath]); + assert.deepEqual(requested, [ + mfManifestPath, + readinessPath, + '/binding', + apiSmokePath, + ]); for (const proof of [ 'mf-manifest', 'api-marker', @@ -3602,11 +4097,17 @@ void test('API-only proof keeps manifest, readiness, service-binding and JSON pr 'service-binding-api-marker', 'json-smoke-value', ]) { - assert.ok(evidence.assertions.some((entry) => entry.type === proof && entry.status === 'pass')); + assert.ok( + evidence.assertions.some( + (entry) => entry.type === proof && entry.status === 'pass' + ) + ); } assert.equal( - evidence.assertions.some((entry) => entry.type === 'ssr' || entry.type === 'i18n-marker'), - false, + evidence.assertions.some( + (entry) => entry.type === 'ssr' || entry.type === 'i18n-marker' + ), + false ); }); @@ -3629,7 +4130,10 @@ void test('full-stack declared SSR remains mandatory', async (context) => { locale: localePath, ssr: '/en', }); - await assert.rejects(validateApp(app, publicUrl), /SSR route returned HTTP 503/u); + await assert.rejects( + validateApp(app, publicUrl), + /SSR route returned HTTP 503/u + ); assert.deepEqual(requested, ['/en']); }); @@ -3637,7 +4141,10 @@ void test('declared namespace locale remains mandatory independently of SSR', as const requested = mockPublicResponses(context, localePath); const app = apiOnlyApp(); Object.assign(app.deploy.cloudflare.routes, { locale: localePath }); - await assert.rejects(validateApp(app, publicUrl), /locale JSON returned HTTP 503/u); + await assert.rejects( + validateApp(app, publicUrl), + /locale JSON returned HTTP 503/u + ); assert.deepEqual(requested, [mfManifestPath, localePath]); }); @@ -3648,7 +4155,7 @@ for (const field of ['ssr', 'locale']) { Object.assign(app.deploy.cloudflare.routes, { [field]: '' }); await assert.rejects( validateApp(app, publicUrl), - /declared .* route must be a root-relative path/u, + /declared .* route must be a root-relative path/u ); }); } @@ -3660,30 +4167,37 @@ for (const variant of ['cjs', 'esm', 'esm-node']) { pathToFileURL( path.join( generatorRoot, - `dist/${variant}/ultramodern-workspace/mf-validation/inspect.${extension}`, - ), + `dist/${variant}/ultramodern-workspace/mf-validation/inspect.${extension}` + ) ).href ); - const inspection = Schema.decodeUnknownSync(ModuleFederationInspectionModuleSchema)( - inspectionModule, - ); + const inspection = Schema.decodeUnknownSync( + ModuleFederationInspectionModuleSchema + )(inspectionModule); const inspectInstalledModuleFederationConfig = inspection.inspectModuleFederationConfigSource.bind(inspectionModule); - const inspect = (source: string): typeof ModuleFederationInspectionSchema.Type => { + const inspect = ( + source: string + ): typeof ModuleFederationInspectionSchema.Type => { const output: unknown = inspectInstalledModuleFederationConfig( source, 'verticals/api', - 'module-federation.config.ts', + 'module-federation.config.ts' ); return Schema.decodeUnknownSync(ModuleFederationInspectionSchema)(output); }; assert.deepEqual( - inspect('// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };').dts, - {}, + inspect( + '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };' + ).dts, + {} ); assert.throws( - () => inspect('export default { dts: false, exposes: { "./Page": "./page.tsx" } };'), - /DTS cannot be disabled for exposed app/u, + () => + inspect( + 'export default { dts: false, exposes: { "./Page": "./page.tsx" } };' + ), + /DTS cannot be disabled for exposed app/u ); }); } @@ -3703,68 +4217,93 @@ void test('MF proof accepts explicit API-only intent but keeps exposed-app archi }); await writeFile( configPath, - '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };', + '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };' ); assert.equal(validate().hostOnlyAppCount, 1); await writeFile(configPath, 'export default { dts: false, exposes: {} };'); - assert.throws(validate, /without an explicit host-only\/no-exposes declaration/u); + assert.throws( + validate, + /without an explicit host-only\/no-exposes declaration/u + ); await writeFile( configPath, - 'export default { dts: { tsConfigPath: "./tsconfig.mf-types.json", generateTypes: { compilerInstance: "effect-tsgo" } }, exposes: { "./Page": "./page.tsx" } };', + 'export default { dts: { tsConfigPath: "./tsconfig.mf-types.json", generateTypes: { compilerInstance: "effect-tsgo" } }, exposes: { "./Page": "./page.tsx" } };' ); assert.throws(validate, /Missing Module Federation DTS archive/u); }); void test('Party deployment declares no fake SSR/locale URL while retaining backend contracts', async () => { - const topology = await readJson(TopologySchema, path.join(workspaceRoot, topologyReferencePath)); + const topology = await readJson( + TopologySchema, + path.join(workspaceRoot, topologyReferencePath) + ); const party = topology.verticals.find((entry) => entry.id === partyId); assert.ok(party); assert.equal(party.cloudflare.routes.ssr, undefined); assert.equal(party.cloudflare.routes.locale, undefined); assert.equal(party.cloudflare.routes.mfManifest, mfManifestPath); assert.equal(party.cloudflare.routes.apiReadiness, readinessPath); - assert.equal(party.backendFederation.exposes['./effect-api'].contract, partySharedApiPath); + assert.equal( + party.backendFederation.exposes['./effect-api'].contract, + partySharedApiPath + ); assert.equal( party.backendFederation.exposes['./effect-api'].openapi, - '/party-registry-api/openapi.json', + '/party-registry-api/openapi.json' ); }); void test('Party Registry is the sole deployment owner for Contacts capabilities', async () => { - const topology = await readJson(TopologySchema, path.join(workspaceRoot, topologyReferencePath)); + const topology = await readJson( + TopologySchema, + path.join(workspaceRoot, topologyReferencePath) + ); const overlay = await readJson( OverlaySchema, - path.join(workspaceRoot, 'topology/local-overlays/development.json'), + path.join(workspaceRoot, 'topology/local-overlays/development.json') + ); + const zerops = await readFile( + path.join(workspaceRoot, 'zerops.yaml'), + 'utf-8' ); - const zerops = await readFile(path.join(workspaceRoot, 'zerops.yaml'), 'utf-8'); - const partySetup = zerops.split(` - setup: '${partyId}'`)[1]?.split(' - setup:')[0]; + const partySetup = zerops + .split(` - setup: '${partyId}'`)[1] + ?.split(' - setup:')[0]; assert.ok(partySetup); assert.equal(zerops.includes(" - setup: 'contacts'"), false); assert.equal( topology.verticals.some((entry) => entry.id === 'contacts'), - false, + false ); const party = topology.verticals.find((entry) => entry.id === partyId); assert.ok(party); assert.equal(overlay.ports[party.id], 4102); - assert.equal(overlay.apis[party.id], 'http://localhost:4102/party-registry-api'); + assert.equal( + overlay.apis[party.id], + 'http://localhost:4102/party-registry-api' + ); assert.ok(partySetup.includes('ULTRAMODERN_ZEROPS_SERVICE: party-registry')); assert.ok(party.moduleFederation.exposes.includes('./PageContacts')); }); void test('installed Cloudflare CLI preserves API-only routes when synthesizing the real Party contract', async (context) => { - const fixture = await mkdtemp(path.join(os.tmpdir(), 'ontos-api-only-proof-')); + const fixture = await mkdtemp( + path.join(os.tmpdir(), 'ontos-api-only-proof-') + ); context.after(async (): Promise => { await rm(fixture, { force: true, recursive: true }); }); await mkdir(path.join(fixture, '.modernjs')); await writeFile( path.join(fixture, '.modernjs/ultramodern.json'), - await readFile(path.join(workspaceRoot, '.modernjs/ultramodern.json')), + await readFile(path.join(workspaceRoot, '.modernjs/ultramodern.json')) ); const build = await readJson( BuildArtifactSchema, - path.join(workspaceRoot, 'verticals/party-registry/shared/ultramodern-build.json'), + path.join( + workspaceRoot, + 'verticals/party-registry/shared/ultramodern-build.json' + ) ); const requestedPath = path.join(fixture, 'requested-routes.txt'); const fetchMockPath = path.join(fixture, 'cloudflare-fetch-mock.mjs'); @@ -3799,14 +4338,17 @@ globalThis.fetch = async input => { } return Response.json({ error: 'No owner route or locale exists' }, { headers, status: 404 }); }; -`, +` ); const reportPath = path.join(fixture, 'proof.json'); runNode( [ '--import', pathToFileURL(fetchMockPath).href, - path.join(generatorRoot, 'templates/workspace-scripts/proof-cloudflare-version.mjs'), + path.join( + generatorRoot, + 'templates/workspace-scripts/proof-cloudflare-version.mjs' + ), '--app', partyId, '--require-public-urls', @@ -3818,7 +4360,7 @@ globalThis.fetch = async input => { ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY: publicUrl, ULTRAMODERN_WORKSPACE_ROOT: fixture, }, - }, + } ); const requestedSource = await readFile(requestedPath, 'utf-8'); const requested = requestedSource.trimEnd().split('\n'); @@ -3826,7 +4368,9 @@ globalThis.fetch = async input => { const report = await readJson(CloudflareReportSchema, reportPath); assert.equal(report.status, 'pass'); assert.equal(report.results[0].appId, 'party-registry'); - assert.ok(report.results[0].assertions.every((entry) => entry.status === 'pass')); + assert.ok( + report.results[0].assertions.every((entry) => entry.status === 'pass') + ); }); void test('proves generated Layer bindings and API aliases without accepting unused neighbors', () => { @@ -3839,35 +4383,50 @@ void test('proves generated Layer bindings and API aliases without accepting unu source: governedApiModuleSource, } : unexpectedTopologyImport(specifier); - assert.equal(strictEffectRuntimeTopologyViolation(source, resolveImport), undefined); + assert.equal( + strictEffectRuntimeTopologyViolation(source, resolveImport), + undefined + ); for (const [before, after] of governedLayerAliasMutations) { assert.ok(source.includes(before)); assert.notEqual( - strictEffectRuntimeTopologyViolation(source.replace(before, after), resolveImport), - undefined, + strictEffectRuntimeTopologyViolation( + source.replace(before, after), + resolveImport + ), + undefined ); } }); void test('accepts only the trusted final identity terminator in a governed API slot', async () => { const identityTerminator = '.pipe(identity)'; - const source = await readFile(path.join(workspaceRoot, partySharedApiPath), 'utf-8'); + const source = await readFile( + path.join(workspaceRoot, partySharedApiPath), + 'utf-8' + ); assert.ok(source.includes(identityTerminator)); assert.equal(microVerticalApiBaselineViolation(partyId, source), undefined); const mutations = [ source.replace( "import { Brand, identity } from 'effect';", - "import { Brand } from 'effect';\nimport { identity } from './counterfeit.ts';", + "import { Brand } from 'effect';\nimport { identity } from './counterfeit.ts';" ), source.replace(identityTerminator, '.pipe(unrelatedIdentity)'), - source.replace(identityTerminator, '.pipe(() => HttpApi.make("DiscardedApi"))'), - source.replace(identityTerminator, '.pipe(identity).addHttpApi(partyRegistryFoundationApi)'), + source.replace( + identityTerminator, + '.pipe(() => HttpApi.make("DiscardedApi"))' + ), + source.replace( + identityTerminator, + '.pipe(identity).addHttpApi(partyRegistryFoundationApi)' + ), ]; for (const mutated of mutations) { assert.notEqual(mutated, source); assert.match( microVerticalApiBaselineViolation(partyId, mutated) ?? '', - /explicitly compose its readiness foundation API/u, + /explicitly compose its readiness foundation API/u ); } }); @@ -3889,23 +4448,25 @@ void test('consumer migration preserves native tooling and governed safety', asy sourceName: Schema.String, targetName: Schema.String, version: Schema.Literal(releaseVersion), - }), + }) ), release: Schema.Struct({ version: Schema.Literal(releaseVersion) }), source: Schema.Struct({ - commit: Schema.Literal('d2c75828230edf92775feca796c0960af754508f'), + commit: Schema.Literal( + 'd2c75828230edf92775feca796c0960af754508f' + ), }), - }), - ), + }) + ) )(await source('.modernjs/release-cohort.json')); assert.equal( cohort.aliases['@modern-js/ultramodern-create'], - '@bleedingdev/modern-js-ultramodern-create', + '@bleedingdev/modern-js-ultramodern-create' ); assert.equal(cohort.aliases['@modern-js/create'], undefined); assert.equal( new Set(cohort.packages.map((entry) => entry.sourceName)).size, - cohort.packages.length, + cohort.packages.length ); for (const entry of cohort.packages) { assert.equal(cohort.aliases[entry.sourceName], entry.targetName); @@ -3916,14 +4477,19 @@ void test('consumer migration preserves native tooling and governed safety', asy 'minimumReleaseAgeStrict: true', 'minimumReleaseAgeIgnoreMissingTime: false', ]) { - assert.equal(workspace.split('\n').filter((candidate) => candidate === line).length, 1); + assert.equal( + workspace.split('\n').filter((candidate) => candidate === line) + .length, + 1 + ); } - const exclusions = /^minimumReleaseAgeExclude:\n(?(?:[ \t]+[^\n]*\n)*)/mu.exec( - workspace, - )?.groups?.entries; + const exclusions = + /^minimumReleaseAgeExclude:\n(?(?:[ \t]+[^\n]*\n)*)/mu.exec( + workspace + )?.groups?.entries; assert.ok(exclusions !== undefined && exclusions.length > 0); const allowed = new Set( - cohort.packages.map((entry) => `${entry.targetName}@${entry.version}`), + cohort.packages.map((entry) => `${entry.targetName}@${entry.version}`) ); const declared = exclusions .trim() @@ -3933,42 +4499,49 @@ void test('consumer migration preserves native tooling and governed safety', asy for (const entry of declared) { assert.ok( allowed.has(entry), - `Release-age exception must name an exact authenticated package: ${entry}`, + `Release-age exception must name an exact authenticated package: ${entry}` ); } - const validator = await source('scripts/validate-ultramodern-workspace.mts'); + const validator = await source( + 'scripts/validate-ultramodern-workspace.mts' + ); assert.match(validator, /authenticated release cohort projection/u); assert.ok(validator.includes(cohort.source.commit)); assert.doesNotMatch(validator, /['"]@modern-js\/create['"]/u); - }, + } ); await context.test( 'current generator handoff preserves arguments and nonzero failures', async () => { const scratchRoot = path.join(workspaceRoot, '.scratch'); await mkdir(scratchRoot, { recursive: true }); - const fixture = await mkdtemp(path.join(scratchRoot, 'consumer-migration-')); + const fixture = await mkdtemp( + path.join(scratchRoot, 'consumer-migration-') + ); try { const executable = path.join(fixture, 'generator.mjs'); await writeFile( executable, - `process.stdout.write(JSON.stringify({ args: process.argv.slice(2), root: process.env.ULTRAMODERN_WORKSPACE_ROOT })); process.exitCode = 37;`, + `process.stdout.write(JSON.stringify({ args: process.argv.slice(2), root: process.env.ULTRAMODERN_WORKSPACE_ROOT })); process.exitCode = 37;` ); const wrappers = [ ['migrate-strict-effect.mts', 'migrate-strict-effect'], ['ultramodern-typecheck.mts', 'typecheck'], ] as const; const wrapperSources = await Promise.all( - wrappers.map(async ([file]) => await source(`scripts/${file}`)), + wrappers.map(async ([file]) => await source(`scripts/${file}`)) + ); + const runner = await source('scripts/shared/ultramodern-command.mts'); + const commandFailure = await source( + 'scripts/ultramodern-command-failure.mts' ); + assert.match(runner, /'ultramodern-create'/u); + assert.doesNotMatch(runner, /['"]modern-js-create['"]/u); + assert.match(commandFailure, /Schema\.TaggedError/u); for (const [index, [file, command]] of wrappers.entries()) { const script = wrapperSources[index] ?? ''; assert.match(script, /runUltramodernScript/u); assert.doesNotMatch(script, /['"]modern-js-create['"]/u); - const runner = await source('scripts/shared/ultramodern-command.mts'); - assert.match(runner, /'ultramodern-create'/u); - assert.doesNotMatch(runner, /['"]modern-js-create['"]/u); - assert.match(await source('scripts/ultramodern-command-failure.mts'), /Schema\.TaggedError/u); assert.match(script, /Effect\.runPromiseExit/u); const result = spawnSync( process.execPath, @@ -3980,32 +4553,36 @@ void test('consumer migration preserves native tooling and governed safety', asy ULTRAMODERN_CREATE_BIN: executable, ULTRAMODERN_WORKSPACE_ROOT: fixture, }, - }, + } ); assert.equal(result.status, 37, result.stderr); assert.deepEqual(JSON.parse(result.stdout), { args: ['ultramodern', command, '--fixture-argument'], root: fixture, }); - const missing = spawnSync(process.execPath, [path.join(workspaceRoot, 'scripts', file)], { - cwd: fixture, - encoding: 'utf-8', - env: { - PATH: fixture, - ULTRAMODERN_CREATE_BIN: '', - ULTRAMODERN_WORKSPACE_ROOT: fixture, - }, - }); + const missing = spawnSync( + process.execPath, + [path.join(workspaceRoot, 'scripts', file)], + { + cwd: fixture, + encoding: 'utf-8', + env: { + PATH: fixture, + ULTRAMODERN_CREATE_BIN: '', + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + } + ); assert.equal(missing.status, 1); assert.match( missing.stdout + missing.stderr, - /Failed to launch ultramodern-create from PATH/u, + /Failed to launch ultramodern-create from PATH/u ); } } finally { await rm(fixture, { force: true, recursive: true }); } - }, + } ); await context.test( 'native route, isolated materialization and workerd adaptations survive', @@ -4015,7 +4592,9 @@ void test('consumer migration preserves native tooling and governed safety', asy 'materialize-zerops-runtime.mjs', 'proof-workerd-ssr.mts', ]; - const scripts = await Promise.all(files.map(async (file) => await source(`scripts/${file}`))); + const scripts = await Promise.all( + files.map(async (file) => await source(`scripts/${file}`)) + ); for (const [index, file] of files.entries()) { const script = scripts[index] ?? ''; assert.match(script, /Effect\.gen/u, file); @@ -4023,11 +4602,13 @@ void test('consumer migration preserves native tooling and governed safety', asy assert.doesNotMatch( script, /import\s*\{[^}]*spawnSync[^}]*\}\s*from\s*['"]node:child_process/u, - file, + file ); assert.doesNotMatch(script, /['"]modern-js-create['"]/u, file); } - const materializer = await source('scripts/materialize-zerops-runtime.mjs'); + const materializer = await source( + 'scripts/materialize-zerops-runtime.mjs' + ); assert.match(materializer, /Flag\.boolean\('worker'\)/u); assert.match(materializer, /appPackage\.name !== packageName/u); assert.match(materializer, /makeTempDirectoryScoped/u); @@ -4040,30 +4621,48 @@ void test('consumer migration preserves native tooling and governed safety', asy assert.match(proof, /check\.body \?\? null/u); assert.match(proof, /check\.expect \?\? null/u); assert.match(proof, /Exit\.isFailure\(exit\)/u); - }, + } ); await context.test( 'custom Party contracts remain accepted and forged auth remains rejected', async () => { - const principal = await source('verticals/party-registry/api/auth/action-principal.ts'); - const gateway = await source('verticals/party-registry/src/api/action-gateway.ts'); + const principal = await source( + 'verticals/party-registry/api/auth/action-principal.ts' + ); + const gateway = await source( + 'verticals/party-registry/src/api/action-gateway.ts' + ); const sharedApi = await source(partySharedApiPath); const handlerRoot = await source('verticals/party-registry/api/index.ts'); assert.equal(hasGeneratedOperationPrincipalContract(principal), true); - assert.equal(hasGeneratedOperationGatewayContract(gateway, partyId), true); - assert.equal(hasValidGovernedHttpCompositionRoot(sharedApi, handlerRoot), true); - assert.equal(microVerticalApiBaselineViolation(partyId, sharedApi), undefined); + assert.equal( + hasGeneratedOperationGatewayContract(gateway, partyId), + true + ); + assert.equal( + hasValidGovernedHttpCompositionRoot(sharedApi, handlerRoot), + true + ); + assert.equal( + microVerticalApiBaselineViolation(partyId, sharedApi), + undefined + ); for (const [before, after] of [ [ 'makeMicroverticalHttpPrincipalAuthentication(verifyOperationPrincipal)', 'makeMicroverticalHttpPrincipalAuthentication(forgedPrincipal)', ], - ["'@app/core-runtime/http/principal-authentication'", "'./counterfeit.ts'"], + [ + "'@app/core-runtime/http/principal-authentication'", + "'./counterfeit.ts'", + ], ]) { assert.ok(principal.includes(before)); assert.equal( - hasGeneratedOperationPrincipalContract(principal.replace(before, after)), - false, + hasGeneratedOperationPrincipalContract( + principal.replace(before, after) + ), + false ); } const audience = "ACTION_GATEWAY_AUDIENCE = 'party-registry'"; @@ -4071,9 +4670,9 @@ void test('consumer migration preserves native tooling and governed safety', asy assert.equal( hasGeneratedOperationGatewayContract( gateway.replace(audience, "ACTION_GATEWAY_AUDIENCE = 'other-owner'"), - partyId, + partyId ), - false, + false ); // Exercise the complete Core/Party server, client, permission and transport negative matrix. const governed = spawnSync( @@ -4087,40 +4686,44 @@ void test('consumer migration preserves native tooling and governed safety', asy cwd: workspaceRoot, encoding: 'utf-8', env: { PATH: path.dirname(process.execPath) }, - }, + } ); assert.equal(governed.status, 0, governed.stdout + governed.stderr); assert.match(governed.stdout, /governed servers bind/u); assert.match(governed.stdout, /rejects generated governed clients/u); - }, + } ); await context.test( 'manifest-aware bridge accepts TanStack without permitting disguised router capability', () => { const imported = "import { createModuleFederationConfig as createConfig } from '@module-federation/modern-js-v3';"; - const config = (body: string) => `${imported} export default createConfig(${body});`; + const config = (body: string) => + `${imported} export default createConfig(${body});`; const disabled = '{ bridge: { enableBridgeRouter: false } }'; const enabled = '{ bridge: { enableBridgeRouter: true } }'; - assert.equal(moduleFederationBridgeViolation(config(disabled), {}), undefined); + assert.equal( + moduleFederationBridgeViolation(config(disabled), {}), + undefined + ); assert.equal( moduleFederationBridgeViolation( `${imported} const config = createConfig(${disabled}); export default config;`, - {}, + {} ), - undefined, + undefined ); assert.equal( moduleFederationBridgeViolation(config(enabled), { dependencies: { 'react-router': '7.18.0' }, }), - undefined, + undefined ); assert.equal( moduleFederationBridgeViolation(config(enabled), { devDependencies: { 'react-router-dom': '7.18.0' }, }), - undefined, + undefined ); for (const candidate of [ config(enabled), @@ -4129,14 +4732,20 @@ void test('consumer migration preserves native tooling and governed safety', asy config('{ bridge: { enableBridgeRouter: Boolean(false) } }'), config('{ bridge: { enableBridgeRouter: false, ...override } }'), config('{ bridge: { enableBridgeRouter: false, [key]: true } }'), - config('{ bridge: { enableBridgeRouter: false, enableBridgeRouter: true } }'), + config( + '{ bridge: { enableBridgeRouter: false, enableBridgeRouter: true } }' + ), config('{ bridge: { enableBridgeRouter: false }, ...override }'), config(disabled).replace('import {', 'import type {'), `${imported} function decoy(createConfig) { return createConfig(${disabled}); } export default otherConfig;`, `function createConfig(value) { return value; } export default createConfig(${disabled});`, ]) { - assert.notEqual(moduleFederationBridgeViolation(candidate, {}), undefined, candidate); + assert.notEqual( + moduleFederationBridgeViolation(candidate, {}), + undefined, + candidate + ); } - }, + } ); }); diff --git a/app/scripts/tests/setup-agent-reference-repos.test.mts b/app/scripts/tests/setup-agent-reference-repos.test.mts new file mode 100644 index 000000000..cff0d9804 --- /dev/null +++ b/app/scripts/tests/setup-agent-reference-repos.test.mts @@ -0,0 +1,217 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const script = fileURLToPath( + new URL('../setup-agent-reference-repos.mts', import.meta.url) +); +const configPath = '.agents/agent-reference-repos.json'; +const gitCallsPath = 'git-calls.txt'; +const manifestPath = '.modernjs/agent-reference-repos.json'; +const repository = { + id: 'fixture', + name: 'Fixture reference', + path: 'repos/fixture', + readOnly: true, + ref: 'main', + url: 'https://example.invalid/fixture.git', +}; +const config = { + defaultEnabled: true, + installDir: 'repos', + repositories: [repository], + schemaVersion: 1, + strategy: 'git-subtree-squash', +}; + +const withFixture = (run: (root: string) => void): void => { + const root = mkdtempSync(path.join(tmpdir(), 'ontos-agent-reference-')); + try { + mkdirSync(path.join(root, '.agents')); + mkdirSync(path.join(root, 'bin')); + writeFileSync(path.join(root, configPath), JSON.stringify(config)); + // No real Git mutation or network access: record the exact native child-process contract. + writeFileSync( + path.join(root, 'bin/git'), + `#!/bin/sh +printf '%s\\n' "$*" >> git-calls.txt +case "$*" in + '--version') printf 'git version fixture\\n' ;; + 'subtree -h') printf 'usage: git subtree\\n'; exit 129 ;; + 'rev-parse --is-inside-work-tree') printf 'true\\n' ;; + 'rev-parse --verify HEAD') printf 'fixture-head\\n' ;; + 'status --porcelain') ;; + 'status --porcelain -- '* ) printf ' M manifest\\n' ;; + 'log '* ) printf 'fixture-subtree-commit\\n' ;; + 'ls-remote '* ) printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\\n' ;; + 'fetch '* | 'subtree add '* | 'add '* | 'commit '* ) ;; + *) printf 'Unexpected git invocation: %s\\n' "$*" >&2; exit 91 ;; +esac +`, + { mode: 0o755 } + ); + run(root); + } finally { + rmSync(root, { force: true, recursive: true }); + } +}; +const runSetup = ( + root: string, + args: readonly string[] = [], + env: Record = {} +) => { + const result = spawnSync(process.execPath, [script, ...args], { + cwd: root, + encoding: 'utf-8', + env: { PATH: path.join(root, 'bin'), ...env }, + timeout: 15_000, + }); + assert.ifError(result.error); + return result; +}; + +void test('optional reference setup warns, while check and required modes fail closed', () => { + withFixture((root) => { + rmSync(path.join(root, configPath)); + for (const [args, env, status] of [ + [[], {}, 0], + [['--check'], {}, 1], + [[], { ULTRAMODERN_AGENT_REPOS_REQUIRED: 'true' }, 1], + ] as const) { + const result = runSetup(root, args, env); + assert.equal(result.status, status, result.stdout + result.stderr); + assert.match( + result.stdout + result.stderr, + /Missing \.agents\/agent-reference-repos\.json/u + ); + assert.equal(existsSync(path.join(root, gitCallsPath)), false); + } + }); +}); + +void test('disabled reference setup never invokes Git or writes a manifest', () => { + withFixture((root) => { + const disabledEnvironments: readonly Record[] = [ + { ULTRAMODERN_SKIP_AGENT_REPOS: 'YES' }, + { ULTRAMODERN_AGENT_REPOS: 'OFF' }, + ]; + for (const env of disabledEnvironments) { + const result = runSetup(root, [], env); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout + result.stderr, /setup skipped/u); + } + writeFileSync( + path.join(root, configPath), + JSON.stringify({ ...config, defaultEnabled: false }) + ); + assert.equal(runSetup(root).status, 0); + assert.equal(existsSync(path.join(root, gitCallsPath)), false); + assert.equal(existsSync(path.join(root, manifestPath)), false); + }); +}); + +void test('reference setup rejects malformed configuration and unsafe paths before Git', () => { + withFixture((root) => { + writeFileSync(path.join(root, configPath), '{invalid'); + const malformed = runSetup(root, ['--check']); + assert.equal(malformed.status, 1); + assert.match( + malformed.stdout + malformed.stderr, + /Invalid reference repository configuration/u + ); + for (const unsafePath of [ + '../outside', + 'repos/../outside', + String.raw`repos\..\outside`, + '/repos/fixture', + 'repos/.', + 'repos/', + 'repos//.', + ]) { + writeFileSync( + path.join(root, configPath), + JSON.stringify({ + ...config, + repositories: [{ ...repository, path: unsafePath }], + }) + ); + const result = runSetup(root, ['--check']); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match( + result.stdout + result.stderr, + /Unsafe reference repository path/u + ); + } + assert.equal(existsSync(path.join(root, gitCallsPath)), false); + }); +}); + +void test('reference check requires subtree evidence and never mutates Git or the manifest', () => { + withFixture((root) => { + const missing = runSetup(root, ['--check']); + assert.equal(missing.status, 1, missing.stdout + missing.stderr); + assert.match(missing.stdout + missing.stderr, /repos\/fixture is missing/u); + mkdirSync(path.join(root, repository.path), { recursive: true }); + const present = runSetup(root, ['--check']); + assert.equal(present.status, 0, present.stdout + present.stderr); + const calls = readFileSync(path.join(root, gitCallsPath), 'utf-8'); + assert.match(calls, /log --grep git-subtree-dir: repos\/fixture/u); + assert.doesNotMatch(calls, /^(?:fetch|add|commit|init|subtree add)\b/mu); + assert.equal(existsSync(path.join(root, manifestPath)), false); + }); +}); + +void test('reference installation defaults check off and preserves commit hooks', () => { + withFixture((root) => { + const result = runSetup(root, [], { + ULTRAMODERN_AGENT_REPOS_REQUIRED: 'true', + }); + assert.equal(result.status, 0, result.stdout + result.stderr); + const manifest = readFileSync(path.join(root, manifestPath), 'utf-8'); + assert.match(manifest, /"status": "installed"/u); + assert.match(manifest, /"commit": "a{40}"/u); + assert.match(manifest, /"installedAt": "\d{4}-\d{2}-\d{2}T/u); + const calls = readFileSync(path.join(root, gitCallsPath), 'utf-8'); + assert.match( + calls, + /fetch --depth 1 https:\/\/example.invalid\/fixture.git main/u + ); + assert.match( + calls, + /subtree add --prefix repos\/fixture FETCH_HEAD --squash/u + ); + assert.match(calls, /commit -m Record agent reference repo manifest/u); + assert.doesNotMatch(calls, /--no-verify/u); + }); +}); + +void test('reference refresh refuses existing subtrees without fetching or overwriting', () => { + withFixture((root) => { + mkdirSync(path.join(root, repository.path), { recursive: true }); + const result = runSetup(root, [], { + ULTRAMODERN_AGENT_REPOS_REFRESH: 'true', + ULTRAMODERN_AGENT_REPOS_REQUIRED: 'true', + }); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match( + result.stdout + result.stderr, + /refresh for subtree references is intentionally manual/u + ); + assert.doesNotMatch( + readFileSync(path.join(root, gitCallsPath), 'utf-8'), + /^(?:fetch|subtree add)\b/mu + ); + assert.equal(existsSync(path.join(root, manifestPath)), false); + }); +}); diff --git a/app/scripts/tests/ultramodern-command.test.mts b/app/scripts/tests/ultramodern-command.test.mts index 2669ab9e5..f9c92a41e 100644 --- a/app/scripts/tests/ultramodern-command.test.mts +++ b/app/scripts/tests/ultramodern-command.test.mts @@ -7,11 +7,14 @@ import path from 'node:path'; import test from 'node:test'; import type { TestContext } from 'node:test'; import { fileURLToPath } from 'node:url'; + import { NodeServices } from '@effect/platform-node'; import { Effect, ManagedRuntime, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); +const createFilename = 'create.mjs'; +const routeGeneratorScript = 'generate-tanstack-routes'; const wrappers = [ ['assert-mf-types', 'mf-types'], ['generate-node-backend-federation', 'backend-federation-generate'], @@ -37,7 +40,7 @@ test.after(async () => { const invokeWrapper = async ( script: string, environment: Readonly>, - args: readonly string[] = [], + args: readonly string[] = [] ) => await wrapperRuntime.runPromise( Effect.gen(function* invokeWrapperEffect() { @@ -53,8 +56,8 @@ const invokeWrapper = async ( stderr: 'pipe', stdin: 'ignore', stdout: 'pipe', - }, - ), + } + ) ); return yield* Effect.all( { @@ -62,44 +65,140 @@ const invokeWrapper = async ( stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString), stdout: child.stdout.pipe(Stream.decodeText(), Stream.mkString), }, - { concurrency: 'unbounded' }, + { concurrency: 'unbounded' } ); - }).pipe(Effect.scoped), + }).pipe(Effect.scoped) ); for (const [script, command] of wrappers) { void test(`${script} forwards arguments, workspace and child exit status`, async (context) => { const fixture = fixtureDirectory(context); - const createBin = path.join(fixture, 'create.mjs'); + const createBin = path.join(fixture, createFilename); writeFileSync( createBin, - 'console.log(process.argv.slice(2).join("|")); console.log(process.env.ULTRAMODERN_WORKSPACE_ROOT); process.exitCode = 7;', + 'console.log(process.argv.slice(2).join("|")); console.log(process.env.ULTRAMODERN_WORKSPACE_ROOT); process.exitCode = 7;' ); const result = await invokeWrapper( script, - { ULTRAMODERN_CREATE_BIN: createBin, ULTRAMODERN_WORKSPACE_ROOT: fixture }, - ['--probe', 'argument with spaces'], + { + ULTRAMODERN_CREATE_BIN: createBin, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + ['--probe', 'argument with spaces'] ); assert.equal(result.status, 7, result.stderr); assert.equal( result.stdout, - `ultramodern|${command}|--probe|argument with spaces\n${fixture}\n`, + `ultramodern|${command}|--probe|argument with spaces\n${fixture}\n` ); }); } -void test('route generation continues compatibility generation after a nonzero framework exit', async (context) => { +void test('route generation fails closed on a nonzero framework exit', async (context) => { const fixture = fixtureDirectory(context); - const createBin = path.join(fixture, 'create.mjs'); + const createBin = path.join(fixture, createFilename); writeFileSync(createBin, 'process.exitCode = 7;'); mkdirSync(path.join(fixture, '.modernjs')); - writeFileSync(path.join(fixture, '.modernjs/ultramodern.json'), '{"topology":{"apps":[]}}'); - const result = await invokeWrapper('generate-tanstack-routes', { + writeFileSync( + path.join(fixture, '.modernjs/ultramodern.json'), + '{"topology":{"apps":[]}}' + ); + const result = await invokeWrapper(routeGeneratorScript, { + ULTRAMODERN_CREATE_BIN: createBin, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }); + assert.equal(result.status, 1, result.stderr); + assert.match( + result.stderr, + /Framework route-artifact generation failed: exit 7/u + ); +}); + +const routeFixture = (context: TestContext, scope = 'tenant') => { + const fixture = fixtureDirectory(context); + const ownerPath = 'verticals/inventory'; + mkdirSync(path.join(fixture, '.modernjs')); + mkdirSync(path.join(fixture, ownerPath, 'src/routes/items'), { + recursive: true, + }); + mkdirSync(path.join(fixture, 'bin')); + writeFileSync(path.join(fixture, 'bin/pnpm'), '#!/bin/sh\nexit 0\n', { + mode: 0o755, + }); + writeFileSync( + path.join(fixture, '.modernjs/ultramodern.json'), + JSON.stringify({ + topology: { apps: [{ id: 'inventory', path: ownerPath }] }, + }) + ); + writeFileSync( + path.join(fixture, ownerPath, 'package.json'), + JSON.stringify({ + modernjs: { ontosModule: { moduleId: 'inventory' } }, + }) + ); + const metadata = { + canonicalPath: '/items', + descriptionKey: 'items.description', + entrypoint: { + access: 'read', + authorization: { kind: 'public' }, + entrypointKey: 'inventory.items', + moduleKey: 'inventory', + role: 'page', + scope, + }, + id: 'items', + indexable: false, + localisedPaths: { cs: '/polozky', en: '/items' }, + namespace: 'inventory', + ownerAppId: 'inventory', + public: false, + titleKey: 'items.title', + }; + writeFileSync( + path.join(fixture, ownerPath, 'src/routes/items/route.meta.ts'), + `export const routeMeta = ${JSON.stringify(metadata)};\n` + ); + const createBin = path.join(fixture, createFilename); + writeFileSync( + createBin, + `import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +const manifest = readFileSync(path.join(process.env.ULTRAMODERN_WORKSPACE_ROOT, '${ownerPath}/src/routes/ultramodern-route-metadata.ts'), 'utf8'); +const urls = JSON.parse(manifest.split('export const ultramodernLocalisedUrls = ')[1].split(' as const;')[0]); +assert.deepEqual(urls, { '/items': { cs: '/polozky', en: '/items' } }); +console.log('framework observed canonical-only metadata'); +` + ); + return { createBin, fixture }; +}; + +void test('route metadata precedes framework generation and keeps canonical-only locale keys', async (context) => { + const { createBin, fixture } = routeFixture(context); + const result = await invokeWrapper(routeGeneratorScript, { + PATH: path.join(fixture, 'bin'), ULTRAMODERN_CREATE_BIN: createBin, ULTRAMODERN_WORKSPACE_ROOT: fixture, }); assert.equal(result.status, 0, result.stderr); - assert.match(result.stderr, /continuing with the repository compatibility manifest/u); + assert.match(result.stdout, /framework observed canonical-only metadata/u); +}); + +void test('route metadata with the wrong owner scope fails before framework launch', async (context) => { + const { createBin, fixture } = routeFixture(context, 'system'); + const result = await invokeWrapper(routeGeneratorScript, { + PATH: path.join(fixture, 'bin'), + ULTRAMODERN_CREATE_BIN: createBin, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }); + assert.equal(result.status, 1, result.stderr); + assert.match( + result.stderr, + /must declare one governed tenant page entrypoint owned by inventory/u + ); + assert.doesNotMatch(result.stdout, /framework observed/u); }); void test('missing PATH launcher reports a typed launch failure and exits one', async (context) => { @@ -110,6 +209,6 @@ void test('missing PATH launcher reports a typed launch failure and exits one', ULTRAMODERN_WORKSPACE_ROOT: fixture, }); assert.equal(result.status, 1); - assert.match(result.stderr, /Failed to launch modern-js-create from PATH/u); + assert.match(result.stderr, /Failed to launch ultramodern-create from PATH/u); assert.match(result.stderr, /UltraModern command "mf-types"/u); }); diff --git a/app/scripts/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index 20f263c38..61932d9fd 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -1,7 +1,3 @@ -import { - hasUltramodernDispatch, - hasUltramodernSkillsDispatch, -} from './shared/ultramodern-wrapper-source.mts'; import { ok as assertCondition } from 'node:assert'; import type { execFileSync as nodeExecFileSync } from 'node:child_process'; import crypto from 'node:crypto'; @@ -17,6 +13,7 @@ import type { import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; + import { NodeRuntime, NodeServices } from '@effect/platform-node'; import { Array as EffectArray, @@ -30,53 +27,64 @@ import { Schema, } from 'effect'; import type { Json } from 'effect/Schema'; + import compactConfigDocument from '../.modernjs/ultramodern.json' with { type: 'json' }; import shellPackageDocument from '../apps/shell-super-app/package.json' with { type: 'json' }; import rootPackageDocument from '../package.json' with { type: 'json' }; import developmentOverlayDocument from '../topology/local-overlays/development.json' with { type: 'json' }; import ownershipDocument from '../topology/ownership.json' with { type: 'json' }; import referenceTopologyDocument from '../topology/reference-topology.json' with { type: 'json' }; -import { tailwindPrefixForNamespace } from './scaffolding/tailwind-prefix.mts'; +import { checkOntosModuleContracts } from './check-ontos-module-contracts.mts'; +import { moduleFederationBridgeViolation } from './module-federation-bridge-boundary.mts'; import { assertPublishedCrossMicroVerticalContractUsage, assertPublishedOutboxContractSource, publishedOutboxContractExports, resolvePublishedContractModuleId, } from './published-outbox-contracts.mts'; -import { checkOntosModuleContracts } from './check-ontos-module-contracts.mts'; -import { moduleFederationBridgeViolation } from './module-federation-bridge-boundary.mts'; +import { tailwindPrefixForNamespace } from './scaffolding/tailwind-prefix.mts'; +import { + hasUltramodernDispatch, + hasUltramodernSkillsDispatch, +} from './shared/ultramodern-wrapper-source.mts'; const nodeRequire = createRequire(import.meta.url); const nodeFileSystemModule = ['node', 'fs'].join(':'); const nodeChildProcessModule = ['node', 'child_process'].join(':'); const NodeFileSystemModuleSchema = Schema.Struct({ existsSync: Schema.declare((input): input is typeof nodeExistsSync => - Predicate.isFunction(input), + Predicate.isFunction(input) ), mkdtempSync: Schema.declare((input): input is typeof nodeMkdtempSync => - Predicate.isFunction(input), + Predicate.isFunction(input) ), readdirSync: Schema.declare((input): input is typeof nodeReaddirSync => - Predicate.isFunction(input), + Predicate.isFunction(input) ), readFileSync: Schema.declare((input): input is typeof nodeReadFileSync => - Predicate.isFunction(input), + Predicate.isFunction(input) + ), + rmSync: Schema.declare((input): input is typeof nodeRmSync => + Predicate.isFunction(input) ), - rmSync: Schema.declare((input): input is typeof nodeRmSync => Predicate.isFunction(input)), writeFileSync: Schema.declare((input): input is typeof nodeWriteFileSync => - Predicate.isFunction(input), + Predicate.isFunction(input) ), }); const NodeChildProcessModuleSchema = Schema.Struct({ execFileSync: Schema.declare((input): input is typeof nodeExecFileSync => - Predicate.isFunction(input), + Predicate.isFunction(input) ), }); const fs = Result.getOrThrow( - Schema.decodeUnknownResult(NodeFileSystemModuleSchema)(nodeRequire(nodeFileSystemModule)), + Schema.decodeUnknownResult(NodeFileSystemModuleSchema)( + nodeRequire(nodeFileSystemModule) + ) ); const { execFileSync } = Result.getOrThrow( - Schema.decodeUnknownResult(NodeChildProcessModuleSchema)(nodeRequire(nodeChildProcessModule)), + Schema.decodeUnknownResult(NodeChildProcessModuleSchema)( + nodeRequire(nodeChildProcessModule) + ) ); const SHARED_VALIDATOR_STRING_001 = '../../tsconfig.base.json'; @@ -86,10 +94,12 @@ const SHARED_VALIDATOR_STRING_004 = './effect-api'; const SHARED_VALIDATOR_STRING_005 = './PageContacts'; const SHARED_VALIDATOR_STRING_006 = './src/routes/ultramodern-route-metadata'; const SHARED_VALIDATOR_STRING_007 = './tsconfig.mf-types.json'; -const SHARED_VALIDATOR_STRING_008 = '.codex/reports/cloudflare-version-proof/public-url-proof.json'; +const SHARED_VALIDATOR_STRING_008 = + '.codex/reports/cloudflare-version-proof/public-url-proof.json'; const SHARED_VALIDATOR_STRING_009 = '.modernjs/release-cohort.json'; const SHARED_VALIDATOR_STRING_010 = '.output/server/index.mjs'; -const SHARED_VALIDATOR_STRING_011 = '.output/server/modern-worker-manifest.json'; +const SHARED_VALIDATOR_STRING_011 = + '.output/server/modern-worker-manifest.json'; const SHARED_VALIDATOR_STRING_012 = '.output/server/route.json'; const SHARED_VALIDATOR_STRING_013 = '.output/worker/__modern_bff_effect.js'; const SHARED_VALIDATOR_STRING_014 = '.output/worker/index.js'; @@ -112,7 +122,8 @@ const SHARED_VALIDATOR_STRING_030 = '/__ultramodern-smoke-missing/nope'; const SHARED_VALIDATOR_STRING_031 = '/mf-manifest.json'; const SHARED_VALIDATOR_STRING_032 = '/party-registry-api'; const SHARED_VALIDATOR_STRING_033 = '/party-registry-api/openapi.json'; -const SHARED_VALIDATOR_STRING_034 = '/party-registry-api/party-registry/readiness'; +const SHARED_VALIDATOR_STRING_034 = + '/party-registry-api/party-registry/readiness'; const SHARED_VALIDATOR_STRING_035 = '#super-app-platform'; const SHARED_VALIDATOR_STRING_036 = '2026-06-02'; const SHARED_VALIDATOR_STRING_037 = '3.9.0-ultramodern.2'; @@ -127,13 +138,16 @@ const SHARED_VALIDATOR_STRING_045 = 'app-public-origin'; const SHARED_VALIDATOR_STRING_046 = 'app/party-registry'; const SHARED_VALIDATOR_STRING_047 = 'apps/shell-super-app'; const SHARED_VALIDATOR_STRING_048 = 'apps/shell-super-app/modern.config.ts'; -const SHARED_VALIDATOR_STRING_049 = 'apps/shell-super-app/module-federation.config.ts'; +const SHARED_VALIDATOR_STRING_049 = + 'apps/shell-super-app/module-federation.config.ts'; const SHARED_VALIDATOR_STRING_050 = 'apps/shell-super-app/package.json'; const SHARED_VALIDATOR_STRING_051 = 'apps/shell-super-app/src'; -const SHARED_VALIDATOR_STRING_052 = 'apps/shell-super-app/tsconfig.mf-types.json'; +const SHARED_VALIDATOR_STRING_052 = + 'apps/shell-super-app/tsconfig.mf-types.json'; const SHARED_VALIDATOR_STRING_053 = 'authorization:provision-current-actions'; const SHARED_VALIDATOR_STRING_054 = 'backend-mf-effect-v1'; -const SHARED_VALIDATOR_STRING_055 = 'camera=(), geolocation=(), microphone=(), payment=(), usb=()'; +const SHARED_VALIDATOR_STRING_055 = + 'camera=(), geolocation=(), microphone=(), payment=(), usb=()'; const SHARED_VALIDATOR_STRING_056 = 'cloudflare'; const SHARED_VALIDATOR_STRING_057 = 'cloudflare-ssr-mf-effect-v1'; const SHARED_VALIDATOR_STRING_058 = 'cloudflare-worker-snapshot'; @@ -144,14 +158,17 @@ const SHARED_VALIDATOR_STRING_062 = 'colocated-route-meta'; const SHARED_VALIDATOR_STRING_063 = 'commonjs-module'; const SHARED_VALIDATOR_STRING_064 = 'core-runtime'; const SHARED_VALIDATOR_STRING_065 = 'deliveryUnit'; -const SHARED_VALIDATOR_STRING_066 = 'docs/super-app-rfc-adr/wave2/blast-radius.md#shared-packages'; +const SHARED_VALIDATOR_STRING_066 = + 'docs/super-app-rfc-adr/wave2/blast-radius.md#shared-packages'; const SHARED_VALIDATOR_STRING_067 = 'docs/super-app-rfc-adr/wave2/reference-topology.md#shared-packages'; const SHARED_VALIDATOR_STRING_068 = 'effect-tsgo'; const SHARED_VALIDATOR_STRING_069 = 'framework-invariant'; const SHARED_VALIDATOR_STRING_070 = 'global_fetch_strictly_public'; -const SHARED_VALIDATOR_STRING_071 = 'http://localhost:4102/backend-mf-manifest.json'; -const SHARED_VALIDATOR_STRING_072 = 'http://localhost:4102/backendRemoteEntry.cjs'; +const SHARED_VALIDATOR_STRING_071 = + 'http://localhost:4102/backend-mf-manifest.json'; +const SHARED_VALIDATOR_STRING_072 = + 'http://localhost:4102/backendRemoteEntry.cjs'; const SHARED_VALIDATOR_STRING_073 = 'http://localhost:4102/mf-manifest.json'; const SHARED_VALIDATOR_STRING_074 = 'jsx-attribute'; const SHARED_VALIDATOR_STRING_075 = 'locales/**/*.json'; @@ -165,10 +182,12 @@ const SHARED_VALIDATOR_STRING_082 = 'node ./scripts/assert-mf-types.mts'; const SHARED_VALIDATOR_STRING_083 = 'node ./scripts/migrate-strict-effect.mts'; const SHARED_VALIDATOR_STRING_084 = 'node ./scripts/proof-cloudflare-version.mts --out .codex/reports/cloudflare-version-proof/public-url-proof.json'; -const SHARED_VALIDATOR_STRING_085 = 'node ./scripts/ultramodern-performance-readiness.mts'; +const SHARED_VALIDATOR_STRING_085 = + 'node ./scripts/ultramodern-performance-readiness.mts'; const SHARED_VALIDATOR_STRING_086 = 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json'; -const SHARED_VALIDATOR_STRING_087 = 'node ./scripts/validate-ultramodern-workspace.mts'; +const SHARED_VALIDATOR_STRING_087 = + 'node ./scripts/validate-ultramodern-workspace.mts'; const SHARED_VALIDATOR_STRING_088 = 'node-mf-runtime'; const SHARED_VALIDATOR_STRING_089 = 'nodejs_compat'; const SHARED_VALIDATOR_STRING_090 = 'noindex, nofollow'; @@ -178,7 +197,8 @@ const SHARED_VALIDATOR_STRING_093 = 'packages/core-runtime/package.json'; const SHARED_VALIDATOR_STRING_094 = 'packages/shared-contracts'; const SHARED_VALIDATOR_STRING_095 = 'packages/shared-contracts/package.json'; const SHARED_VALIDATOR_STRING_096 = 'packages/shared-design-tokens'; -const SHARED_VALIDATOR_STRING_097 = 'packages/shared-design-tokens/src/tokens.css'; +const SHARED_VALIDATOR_STRING_097 = + 'packages/shared-design-tokens/src/tokens.css'; const SHARED_VALIDATOR_STRING_098 = 'party-registry'; const SHARED_VALIDATOR_STRING_099 = 'partyRegistry'; const SHARED_VALIDATOR_STRING_100 = 'pd-super-app-platform'; @@ -195,19 +215,25 @@ const SHARED_VALIDATOR_STRING_109 = 'Report-only remains the generated final mode until public smoke proof records MF SSR script/style/connect compatibility for the deployed surface.'; const SHARED_VALIDATOR_STRING_110 = 'report-only-dogfood'; const SHARED_VALIDATOR_STRING_111 = 'restore generated ownership entries'; -const SHARED_VALIDATOR_STRING_112 = 'restore generated topology vertical entries'; +const SHARED_VALIDATOR_STRING_112 = + 'restore generated topology vertical entries'; const SHARED_VALIDATOR_STRING_113 = 'robots.txt'; const SHARED_VALIDATOR_STRING_114 = 'scripts/assert-mf-types.mts'; const SHARED_VALIDATOR_STRING_115 = 'scripts/bootstrap-agent-skills.mts'; -const SHARED_VALIDATOR_STRING_116 = 'scripts/generate-node-backend-federation.mts'; -const SHARED_VALIDATOR_STRING_117 = 'scripts/generate-public-surface-assets.mts'; +const SHARED_VALIDATOR_STRING_116 = + 'scripts/generate-node-backend-federation.mts'; +const SHARED_VALIDATOR_STRING_117 = + 'scripts/generate-public-surface-assets.mts'; const SHARED_VALIDATOR_STRING_118 = 'scripts/generate-tanstack-routes.mts'; const SHARED_VALIDATOR_STRING_119 = 'scripts/proof-cloudflare-version.mts'; const SHARED_VALIDATOR_STRING_120 = 'scripts/proof-node-backend-federation.mts'; -const SHARED_VALIDATOR_STRING_121 = 'scripts/ultramodern-performance-readiness.config.mjs'; -const SHARED_VALIDATOR_STRING_122 = 'scripts/ultramodern-performance-readiness.mts'; +const SHARED_VALIDATOR_STRING_121 = + 'scripts/ultramodern-performance-readiness.config.mjs'; +const SHARED_VALIDATOR_STRING_122 = + 'scripts/ultramodern-performance-readiness.mts'; const SHARED_VALIDATOR_STRING_123 = 'scripts/ultramodern-typecheck.mts'; -const SHARED_VALIDATOR_STRING_124 = 'scripts/validate-ultramodern-workspace.mts'; +const SHARED_VALIDATOR_STRING_124 = + 'scripts/validate-ultramodern-workspace.mts'; const SHARED_VALIDATOR_STRING_125 = 'scripts/verify-cloudflare-output.mts'; const SHARED_VALIDATOR_STRING_126 = 'service-binding'; const SHARED_VALIDATOR_STRING_127 = 'shared-contracts'; @@ -229,16 +255,20 @@ const SHARED_VALIDATOR_STRING_142 = 'traceparent'; const SHARED_VALIDATOR_STRING_143 = 'ULTRAMODERN_ASSET_PREFIX'; const SHARED_VALIDATOR_STRING_144 = 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json'; -const SHARED_VALIDATOR_STRING_145 = 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'; -const SHARED_VALIDATOR_STRING_146 = 'ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS=false'; +const SHARED_VALIDATOR_STRING_145 = + 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'; +const SHARED_VALIDATOR_STRING_146 = + 'ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS=false'; const SHARED_VALIDATOR_STRING_147 = 'ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY'; const SHARED_VALIDATOR_STRING_148 = 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP'; const SHARED_VALIDATOR_STRING_149 = 'ultramodern-shared-tokens'; const SHARED_VALIDATOR_STRING_150 = 'ultramodern-shell-base'; const SHARED_VALIDATOR_STRING_151 = 'ultramodernApiMarker'; const SHARED_VALIDATOR_STRING_152 = 'ultramodernUiMarker'; -const SHARED_VALIDATOR_STRING_153 = 'VERTICAL_PARTY_REGISTRY_BACKEND_MF_MANIFEST'; -const SHARED_VALIDATOR_STRING_154 = 'VERTICAL_PARTY_REGISTRY_DISPATCH_NAMESPACE'; +const SHARED_VALIDATOR_STRING_153 = + 'VERTICAL_PARTY_REGISTRY_BACKEND_MF_MANIFEST'; +const SHARED_VALIDATOR_STRING_154 = + 'VERTICAL_PARTY_REGISTRY_DISPATCH_NAMESPACE'; const SHARED_VALIDATOR_STRING_155 = 'VERTICAL_PARTY_REGISTRY_MF_MANIFEST'; const SHARED_VALIDATOR_STRING_156 = 'VERTICAL_PARTY_REGISTRY_WORKER'; const SHARED_VALIDATOR_STRING_157 = 'VERTICAL_PARTY_REGISTRY_WORKER_BINDING'; @@ -248,10 +278,12 @@ const SHARED_VALIDATOR_STRING_160 = 'verticalPartyRegistryBackend'; const SHARED_VALIDATOR_STRING_161 = 'verticals/party-registry'; const SHARED_VALIDATOR_STRING_162 = 'verticals/party-registry/api/index.ts'; const SHARED_VALIDATOR_STRING_163 = 'verticals/party-registry/modern.config.ts'; -const SHARED_VALIDATOR_STRING_164 = 'verticals/party-registry/module-federation.config.ts'; +const SHARED_VALIDATOR_STRING_164 = + 'verticals/party-registry/module-federation.config.ts'; const SHARED_VALIDATOR_STRING_165 = 'verticals/party-registry/package.json'; const SHARED_VALIDATOR_STRING_166 = 'verticals/party-registry/shared/api.ts'; -const SHARED_VALIDATOR_STRING_167 = 'verticals/party-registry/src/api/party-registry-client.ts'; +const SHARED_VALIDATOR_STRING_167 = + 'verticals/party-registry/src/api/party-registry-client.ts'; const SHARED_VALIDATOR_STRING_168 = 'web-and-api-same-build'; const SHARED_VALIDATOR_STRING_169 = 'workspace:*'; const SHARED_VALIDATOR_STRING_170 = 'ZEPHYR_PARTY_REGISTRY_APPLICATION_UID'; @@ -284,10 +316,13 @@ const ComparableJsonSchema: Schema.Codec = Schema.suspend(() => Schema.String, Schema.Array(ComparableJsonSchema), Schema.Record(Schema.String, ComparableJsonSchema), - ]), + ]) ); const ComparableJsonArraySchema = Schema.Array(ComparableJsonSchema); -const ComparableJsonObjectSchema = Schema.Record(Schema.String, ComparableJsonSchema); +const ComparableJsonObjectSchema = Schema.Record( + Schema.String, + ComparableJsonSchema +); const isComparableJsonArray = Schema.is(ComparableJsonArraySchema); const isComparableJsonObject = Schema.is(ComparableJsonObjectSchema); const MetadataDocumentSchema = Schema.Struct({ schemaVersion: Schema.Number }); @@ -425,7 +460,10 @@ const createVerticalExecutionSurfaces = () => ({ const createShellCloudflareContract = () => ({ assetsBinding: 'ASSETS', compatibilityDate: SHARED_VALIDATOR_STRING_036, - compatibilityFlags: [SHARED_VALIDATOR_STRING_089, SHARED_VALIDATOR_STRING_070], + compatibilityFlags: [ + SHARED_VALIDATOR_STRING_089, + SHARED_VALIDATOR_STRING_070, + ], evidence: { proofScript: SHARED_VALIDATOR_STRING_119, reportDefault: SHARED_VALIDATOR_STRING_008, @@ -445,7 +483,10 @@ const createShellCloudflareContract = () => ({ const createVerticalCloudflareContract = () => ({ assetsBinding: 'ASSETS', compatibilityDate: SHARED_VALIDATOR_STRING_036, - compatibilityFlags: [SHARED_VALIDATOR_STRING_089, SHARED_VALIDATOR_STRING_070], + compatibilityFlags: [ + SHARED_VALIDATOR_STRING_089, + SHARED_VALIDATOR_STRING_070, + ], evidence: { proofScript: SHARED_VALIDATOR_STRING_119, reportDefault: SHARED_VALIDATOR_STRING_008, @@ -524,7 +565,8 @@ const createVerticalBackendFederationContract = () => ({ invariant: SHARED_VALIDATOR_STRING_168, packageName: SHARED_VALIDATOR_STRING_018, ui: { - buildMarker: 'verticals/party-registry/src/routes/ultramodern-route-metadata.ts', + buildMarker: + 'verticals/party-registry/src/routes/ultramodern-route-metadata.ts', manifestEnv: SHARED_VALIDATOR_STRING_155, manifestUrl: SHARED_VALIDATOR_STRING_073, }, @@ -533,7 +575,8 @@ const createVerticalBackendFederationContract = () => ({ const workspaceValidationContractDefinition = { ciEvidenceScripts: { - 'action:test:integration': 'pnpm --filter @app/core-runtime action:test:integration', + 'action:test:integration': + 'pnpm --filter @app/core-runtime action:test:integration', 'deployment-impact:plan': 'node ./scripts/plan-deployment-impact.mts', 'quality:audit': 'node ./scripts/quality-audit.mts', 'quality:audit:gate': 'node ./scripts/quality-audit-gate.mts', @@ -545,7 +588,8 @@ const workspaceValidationContractDefinition = { 'test:integration': 'pnpm -r --if-present run test:integration', 'test:scripts': 'node --test scripts/tests/boundary-source-structure.test.mts scripts/local-environment-values.test.mts scripts/tests/audit-database-trust-boundaries.test.mts scripts/tests/authorization-rollout-contract.test.mts scripts/tests/check-authorization-readiness.test.mts scripts/tests/database-access-boundaries.test.mts scripts/tests/initialize-local-development.test.mts scripts/tests/locki-feature.test.mts scripts/tests/migrate-contacts-authorization.test.mts scripts/tests/module-entrypoint-boundaries.test.mts scripts/tests/plan-deployment-impact.test.mts scripts/tests/protected-entrypoint-inventory.test.mts scripts/tests/provision-current-action-authorization.test.mts scripts/tests/report-fail-closed-authorization-impact.test.mts scripts/tests/api-only-tooling.test.mts scripts/tests/generated-slot-entries.test.mts scripts/tests/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts scripts/tests/ultramodern-command.test.mts scripts/tests/code-tools-i18n.test.mts scripts/tests/dependency-declarations.test.mts', - 'test:unit': 'pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component', + 'test:unit': + 'pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component', }, cloudflareSecurity: createCloudflareSecurityContract(), cohort: { @@ -617,40 +661,48 @@ const workspaceValidationContractDefinition = { aliases: { '@modern-js/adapter-rstest': '@bleedingdev/modern-js-adapter-rstest', '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools', - '@modern-js/app-tools-extensions': '@bleedingdev/modern-js-app-tools-extensions', + '@modern-js/app-tools-extensions': + '@bleedingdev/modern-js-app-tools-extensions', '@modern-js/bff-core': '@bleedingdev/modern-js-bff-core', '@modern-js/bff-effect': '@bleedingdev/modern-js-bff-effect', '@modern-js/bff-runtime': '@bleedingdev/modern-js-bff-runtime', '@modern-js/builder': '@bleedingdev/modern-js-builder', '@modern-js/code-tools': '@bleedingdev/modern-js-code-tools', '@modern-js/create-request': '@bleedingdev/modern-js-create-request', - '@modern-js/i18n-runtime-extensions': '@bleedingdev/modern-js-i18n-runtime-extensions', + '@modern-js/i18n-runtime-extensions': + '@bleedingdev/modern-js-i18n-runtime-extensions', '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils', '@modern-js/image': '@bleedingdev/modern-js-image', '@modern-js/main-doc': '@bleedingdev/modern-js-main-doc', '@modern-js/plugin': '@bleedingdev/modern-js-plugin', '@modern-js/plugin-bff': '@bleedingdev/modern-js-plugin-bff', - '@modern-js/plugin-bff-extensions': '@bleedingdev/modern-js-plugin-bff-extensions', - '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader', + '@modern-js/plugin-bff-extensions': + '@bleedingdev/modern-js-plugin-bff-extensions', + '@modern-js/plugin-data-loader': + '@bleedingdev/modern-js-plugin-data-loader', '@modern-js/plugin-i18n': '@bleedingdev/modern-js-plugin-i18n', '@modern-js/plugin-polyfill': '@bleedingdev/modern-js-plugin-polyfill', '@modern-js/plugin-ssg': '@bleedingdev/modern-js-plugin-ssg', - '@modern-js/plugin-styled-components': '@bleedingdev/modern-js-plugin-styled-components', + '@modern-js/plugin-styled-components': + '@bleedingdev/modern-js-plugin-styled-components', '@modern-js/plugin-tanstack': '@bleedingdev/modern-js-plugin-tanstack', '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server', '@modern-js/render': '@bleedingdev/modern-js-render', '@modern-js/runtime': '@bleedingdev/modern-js-runtime', - '@modern-js/runtime-extensions': '@bleedingdev/modern-js-runtime-extensions', + '@modern-js/runtime-extensions': + '@bleedingdev/modern-js-runtime-extensions', '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils', '@modern-js/sandpack-react': '@bleedingdev/modern-js-sandpack-react', '@modern-js/server': '@bleedingdev/modern-js-server', '@modern-js/server-core': '@bleedingdev/modern-js-server-core', '@modern-js/server-runtime': '@bleedingdev/modern-js-server-runtime', - '@modern-js/server-runtime-extensions': '@bleedingdev/modern-js-server-runtime-extensions', + '@modern-js/server-runtime-extensions': + '@bleedingdev/modern-js-server-runtime-extensions', '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils', '@modern-js/tsconfig': '@bleedingdev/modern-js-tsconfig', '@modern-js/types': '@bleedingdev/modern-js-types', - '@modern-js/ultramodern-create': '@bleedingdev/modern-js-ultramodern-create', + '@modern-js/ultramodern-create': + '@bleedingdev/modern-js-ultramodern-create', '@modern-js/ultramodern-sandpack-profile': '@bleedingdev/modern-js-ultramodern-sandpack-profile', '@modern-js/utils': '@bleedingdev/modern-js-utils', @@ -939,7 +991,9 @@ const workspaceValidationContractDefinition = { runtimeFramework: 'effect', strictEffectApproach: true, }, - componentPaths: ['verticals/party-registry/src/federation/page-contacts.tsx'], + componentPaths: [ + 'verticals/party-registry/src/federation/page-contacts.tsx', + ], deliveryUnit: { appId: SHARED_VALIDATOR_STRING_098, buildMarker: SHARED_VALIDATOR_STRING_038, @@ -966,8 +1020,12 @@ const workspaceValidationContractDefinition = { packageName: SHARED_VALIDATOR_STRING_018, path: SHARED_VALIDATOR_STRING_161, port: 4102, - routeMetaPaths: ['verticals/party-registry/src/routes/[lang]/contacts/route.meta.ts'], - routePagePaths: ['verticals/party-registry/src/routes/[lang]/contacts/page.tsx'], + routeMetaPaths: [ + 'verticals/party-registry/src/routes/[lang]/contacts/route.meta.ts', + ], + routePagePaths: [ + 'verticals/party-registry/src/routes/[lang]/contacts/page.tsx', + ], stem: SHARED_VALIDATOR_STRING_098, surfaceProfile: 'full-stack', tailwindPrefix: 'partyregistry', @@ -1033,7 +1091,8 @@ const workspaceValidationContractDefinition = { ], patterns: [ { - diagnostic: 'Generated sources must not suppress Effect diagnostics.', + diagnostic: + 'Generated sources must not suppress Effect diagnostics.', expression: '@effect-diagnostics\\b', fixArea: 'remove the @effect-diagnostics suppression directive', flags: 'u', @@ -1078,7 +1137,8 @@ const workspaceValidationContractDefinition = { diagnostic: 'Generated Zephyr integration must not be gated or disabled through ULTRAMODERN_ZEPHYR.', expression: '\\bULTRAMODERN_ZEPHYR\\b', - fixArea: 'use the framework-owned Zephyr integration without a gate', + fixArea: + 'use the framework-owned Zephyr integration without a gate', flags: 'u', id: 'ultramodern-zephyr-environment-gate', }, @@ -1098,7 +1158,8 @@ const workspaceValidationContractDefinition = { ], patterns: [ { - diagnostic: 'Generated Module Federation must keep dynamic remote type hints enabled.', + diagnostic: + 'Generated Module Federation must keep dynamic remote type hints enabled.', expression: '\\bdisableDynamicRemoteTypeHints\\s*:\\s*true\\b', fixArea: 'remove disableDynamicRemoteTypeHints: true', flags: 'u', @@ -1129,13 +1190,16 @@ const workspaceValidationContractDefinition = { 'Generated shell routing must use native router navigation instead of window.location.', expression: '\\bwindow\\s*\\.\\s*location(?:\\s*\\.\\s*(?:assign|replace|reload)\\s*\\(|\\s*\\.\\s*href\\s*=|\\s*=)', - fixArea: 'replace manual window.location navigation with the router primitive', + fixArea: + 'replace manual window.location navigation with the router primitive', flags: 'u', id: 'window-location-navigation', }, { - diagnostic: 'Generated shell routing must not intercept anchor clicks synthetically.', - fixArea: 'use the router Link primitive without preventDefault interception', + diagnostic: + 'Generated shell routing must not intercept anchor clicks synthetically.', + fixArea: + 'use the router Link primitive without preventDefault interception', id: 'synthetic-anchor-click-interception', structuralMatcher: { attributeName: 'onClick', @@ -1159,7 +1223,8 @@ const workspaceValidationContractDefinition = { diagnostic: 'Generated shell routing must use native Module Federation loading primitives.', expression: '\\b(?:hydrateRoot|loadRemote|loadShare)\\s*\\(', - fixArea: 'remove the manual Module Federation hydration or loading wrapper', + fixArea: + 'remove the manual Module Federation hydration or loading wrapper', flags: 'u', id: 'manual-module-federation-loading-wrapper', }, @@ -1190,14 +1255,17 @@ const workspaceValidationContractDefinition = { diagnostic: 'Generated config must use the framework config environment API instead of direct process.env access.', expression: '\\bprocess\\s*\\.\\s*env\\b', - fixArea: 'replace direct process.env access with the framework config API', + fixArea: + 'replace direct process.env access with the framework config API', flags: 'u', id: 'direct-process-env-access', }, { - diagnostic: 'Generated config must not invoke node:child_process directly.', + diagnostic: + 'Generated config must not invoke node:child_process directly.', expression: '[\'"]node:child_process[\'"]', - fixArea: 'use the framework config API instead of node:child_process', + fixArea: + 'use the framework config API instead of node:child_process', flags: 'u', id: 'node-child-process-access', }, @@ -1213,7 +1281,11 @@ const workspaceValidationContractDefinition = { 'packageCohort', 'workspaceValidationContract', ], - forbiddenPackageSourceFields: ['generatedWorkspacePackages', 'metadata', 'modernPackages'], + forbiddenPackageSourceFields: [ + 'generatedWorkspacePackages', + 'metadata', + 'modernPackages', + ], forbiddenTopologyFields: ['effectServices', 'remotes'], retiredMetadataPaths: [ '.modernjs/ultramodern-generated-contract.json', @@ -1251,7 +1323,8 @@ const workspaceValidationContractDefinition = { packageScope: 'app', packageScripts: { 'action:test:unit': 'pnpm --filter @app/core-runtime action:test:unit', - 'agents:refs:check': 'node ./scripts/setup-agent-reference-repos.mts --check', + 'agents:refs:check': + 'node ./scripts/setup-agent-reference-repos.mts --check', 'agents:refs:install': 'node ./scripts/setup-agent-reference-repos.mts', 'api:check': 'node ./scripts/check-ultramodern-api-boundaries.mts', build: @@ -1262,11 +1335,14 @@ const workspaceValidationContractDefinition = { 'cloudflare-output:verify': 'node ./scripts/verify-cloudflare-output.mts', 'cloudflare:build': 'pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm mf:types && pnpm cloudflare-output:verify', - 'cloudflare:deploy': 'pnpm --filter "./apps/shell-super-app" run cloudflare:deploy', + 'cloudflare:deploy': + 'pnpm --filter "./apps/shell-super-app" run cloudflare:deploy', 'cloudflare:proof': SHARED_VALIDATOR_STRING_084, 'contract:check': SHARED_VALIDATOR_STRING_087, - 'database-access:check': 'node ./scripts/check-database-access-boundaries.mts', - 'db:bootstrap-runtime-role': 'node ./scripts/postgres/bootstrap-runtime-role.mts', + 'database-access:check': + 'node ./scripts/check-database-access-boundaries.mts', + 'db:bootstrap-runtime-role': + 'node ./scripts/postgres/bootstrap-runtime-role.mts', 'db:check': 'pnpm --filter @app/core-runtime db:check && pnpm --filter @app/shell-super-app db:check && pnpm --filter @app/party-registry db:check', 'db:generate': @@ -1286,24 +1362,33 @@ const workspaceValidationContractDefinition = { 'lint:fix': 'oxlint apps verticals packages --fix', 'mf:types': SHARED_VALIDATOR_STRING_082, 'migrate:strict-effect': SHARED_VALIDATOR_STRING_083, - 'module-entrypoints:check': 'node ./scripts/check-module-entrypoint-boundaries.mts', + 'module-entrypoints:check': + 'node ./scripts/check-module-entrypoint-boundaries.mts', 'outbox:test': 'pnpm --filter @app/core-runtime outbox:test:unit && pnpm --filter @app/core-runtime outbox:test:integration', 'performance:readiness': SHARED_VALIDATOR_STRING_085, - postinstall: "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt . '!repos/**'", + postinstall: + "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt . '!repos/**'", 'scaffold:action': 'node ./scripts/scaffolding/cli.mts action', 'scaffold:microvertical-action-boundary': 'node ./scripts/scaffolding/cli.mts microvertical-action-boundary', - 'scaffold:microvertical-page': 'node ./scripts/scaffolding/cli.mts microvertical-page', + 'scaffold:microvertical-page': + 'node ./scripts/scaffolding/cli.mts microvertical-page', 'scaffold:module-api': 'node ./scripts/scaffolding/cli.mts module-api', - 'scaffold:module-contract': 'node ./scripts/scaffolding/cli.mts module-contract', - 'scaffold:outbox-message': 'node ./scripts/scaffolding/cli.mts outbox-message', - 'scaffold:outbox-worker': 'node ./scripts/scaffolding/cli.mts outbox-worker', + 'scaffold:module-contract': + 'node ./scripts/scaffolding/cli.mts module-contract', + 'scaffold:outbox-message': + 'node ./scripts/scaffolding/cli.mts outbox-message', + 'scaffold:outbox-worker': + 'node ./scripts/scaffolding/cli.mts outbox-worker', 'scaffold:policy': 'node ./scripts/scaffolding/cli.mts policy', - 'scaffold:public-component': 'node ./scripts/scaffolding/cli.mts public-component', + 'scaffold:public-component': + 'node ./scripts/scaffolding/cli.mts public-component', 'scaffold:report': 'node ./scripts/scaffolding/cli.mts report', - 'scaffold:search-provider': 'node ./scripts/scaffolding/cli.mts search-provider', - 'scaffold:search-provider-access': 'node ./scripts/scaffolding/cli.mts search-provider-access', + 'scaffold:search-provider': + 'node ./scripts/scaffolding/cli.mts search-provider', + 'scaffold:search-provider-access': + 'node ./scripts/scaffolding/cli.mts search-provider-access', 'skills:check': 'node ./scripts/bootstrap-agent-skills.mts --check', 'skills:install': 'node ./scripts/bootstrap-agent-skills.mts', typecheck: SHARED_VALIDATOR_STRING_086, @@ -1314,7 +1399,10 @@ const workspaceValidationContractDefinition = { installDir: './.codex/skills', lockfile: './.codex/skills-lock.json', mode: 'repo-owned-default-on', - optOutEnv: ['ULTRAMODERN_SKIP_CODEX_SKILLS=1', 'ULTRAMODERN_CODEX_SKILLS=0'], + optOutEnv: [ + 'ULTRAMODERN_SKIP_CODEX_SKILLS=1', + 'ULTRAMODERN_CODEX_SKILLS=0', + ], selfContainedVendoring: true, target: 'codex', }, @@ -1347,7 +1435,10 @@ const workspaceValidationContractDefinition = { publicAssetExcludes: [], wrangler: { compatibility_date: SHARED_VALIDATOR_STRING_036, - compatibility_flags: [SHARED_VALIDATOR_STRING_089, SHARED_VALIDATOR_STRING_070], + compatibility_flags: [ + SHARED_VALIDATOR_STRING_089, + SHARED_VALIDATOR_STRING_070, + ], }, }, }, @@ -1414,7 +1505,8 @@ const workspaceValidationContractDefinition = { ], schemaVersion: 1, scripts: { - backendFederationGenerate: 'node ./scripts/generate-node-backend-federation.mts', + backendFederationGenerate: + 'node ./scripts/generate-node-backend-federation.mts', build: 'pnpm -r --filter "./verticals/*" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm mf:types && pnpm performance:readiness', check: @@ -1566,7 +1658,10 @@ const workspaceValidationContractDefinition = { }, { api: { - consumedBy: [SHARED_VALIDATOR_STRING_131, SHARED_VALIDATOR_STRING_098], + consumedBy: [ + SHARED_VALIDATOR_STRING_131, + SHARED_VALIDATOR_STRING_098, + ], prefix: SHARED_VALIDATOR_STRING_032, runtime: 'effect', serverEntry: SHARED_VALIDATOR_STRING_162, @@ -1617,7 +1712,8 @@ const workspaceValidationContractDefinition = { 'party-registry': SHARED_VALIDATOR_STRING_073, }, ontosModuleManifests: { - 'party-registry': 'http://localhost:4102/.well-known/ontos-module-manifest.json', + 'party-registry': + 'http://localhost:4102/.well-known/ontos-module-manifest.json', }, ports: { 'party-registry': 4102, @@ -1675,7 +1771,8 @@ const workspaceValidationContractDefinition = { { id: SHARED_VALIDATOR_STRING_131, ownership: { - adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app', + adrRef: + 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app', blastRadius: { references: [ 'docs/super-app-rfc-adr/wave2/blast-radius.md#shell', @@ -1728,7 +1825,9 @@ const workspaceValidationContractDefinition = { ownership: { adrRef: 'docs/super-app-rfc-adr/verticals.md#party-registry', blastRadius: { - references: ['docs/super-app-rfc-adr/blast-radius.md#party-registry'], + references: [ + 'docs/super-app-rfc-adr/blast-radius.md#party-registry', + ], tier: 'tier-2-vertical', }, pagerDuty: SHARED_VALIDATOR_STRING_100, @@ -1751,13 +1850,15 @@ const workspaceValidationContractDefinition = { schemaVersion: 1, sharedPackages: [ { - description: 'Server-only Core infrastructure and typed PostgreSQL ownership.', + description: + 'Server-only Core infrastructure and typed PostgreSQL ownership.', id: SHARED_VALIDATOR_STRING_064, package: SHARED_VALIDATOR_STRING_017, path: SHARED_VALIDATOR_STRING_092, }, { - description: 'Server-only audience-bound Shell gateway assertion verification.', + description: + 'Server-only audience-bound Shell gateway assertion verification.', id: SHARED_VALIDATOR_STRING_176, package: SHARED_VALIDATOR_STRING_175, path: SHARED_VALIDATOR_STRING_177, @@ -1769,7 +1870,8 @@ const workspaceValidationContractDefinition = { path: SHARED_VALIDATOR_STRING_094, }, { - description: 'Generated design tokens consumed by shell and verticals.', + description: + 'Generated design tokens consumed by shell and verticals.', id: SHARED_VALIDATOR_STRING_128, package: SHARED_VALIDATOR_STRING_020, path: SHARED_VALIDATOR_STRING_096, @@ -1820,7 +1922,8 @@ const workspaceValidationContractDefinition = { ssr: true, }, ownership: { - adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app', + adrRef: + 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app', blastRadius: { references: [ 'docs/super-app-rfc-adr/wave2/blast-radius.md#shell', @@ -1858,7 +1961,10 @@ const workspaceValidationContractDefinition = { export: SHARED_VALIDATOR_STRING_002, path: SHARED_VALIDATOR_STRING_167, }, - consumedBy: [SHARED_VALIDATOR_STRING_131, SHARED_VALIDATOR_STRING_098], + consumedBy: [ + SHARED_VALIDATOR_STRING_131, + SHARED_VALIDATOR_STRING_098, + ], contract: { export: './api', path: SHARED_VALIDATOR_STRING_166, @@ -1912,7 +2018,9 @@ const workspaceValidationContractDefinition = { ownership: { adrRef: 'docs/super-app-rfc-adr/verticals.md#party-registry', blastRadius: { - references: ['docs/super-app-rfc-adr/blast-radius.md#party-registry'], + references: [ + 'docs/super-app-rfc-adr/blast-radius.md#party-registry', + ], tier: 'tier-2-vertical', }, pagerDuty: SHARED_VALIDATOR_STRING_100, @@ -2019,7 +2127,9 @@ interface CompactConfig extends Omit< 'bridge' | 'packageSource' | 'topology' > { readonly bridge?: BridgeConfig; - readonly packageSource: CompactConfigDocument['packageSource'] & { readonly registry?: string }; + readonly packageSource: CompactConfigDocument['packageSource'] & { + readonly registry?: string; + }; readonly shells?: unknown; readonly topology: Omit & { readonly apps?: readonly CompactApp[]; @@ -2030,7 +2140,8 @@ type OverlayServerExecution = DevelopmentOverlay['serverExecution'][keyof DevelopmentOverlay['serverExecution']]; type Ownership = typeof ownershipDocument; type ReferenceTopologyDocument = typeof referenceTopologyDocument; -type ReferenceTopologyVerticalDocument = ReferenceTopologyDocument['verticals'][number]; +type ReferenceTopologyVerticalDocument = + ReferenceTopologyDocument['verticals'][number]; interface ReferenceTopologyVertical extends Omit< ReferenceTopologyVerticalDocument, 'api' | 'moduleFederation' @@ -2043,7 +2154,10 @@ interface ReferenceTopologyVertical extends Omit< readonly verticalRefs?: readonly string[]; }; } -interface ReferenceTopology extends Omit { +interface ReferenceTopology extends Omit< + ReferenceTopologyDocument, + 'verticals' +> { readonly verticals: readonly ReferenceTopologyVertical[]; } type RootPackage = typeof rootPackageDocument; @@ -2101,7 +2215,9 @@ const StringValuesSchema = Schema.Record(Schema.String, Schema.String); const PackageJsonSchema = Schema.Struct({ dependencies: Schema.optionalKey(StringValuesSchema), devDependencies: Schema.optionalKey(StringValuesSchema), - engines: Schema.optionalKey(Schema.Struct({ node: Schema.optionalKey(Schema.String) })), + engines: Schema.optionalKey( + Schema.Struct({ node: Schema.optionalKey(Schema.String) }) + ), exports: Schema.optionalKey(StringValuesSchema), modernjs: Schema.optionalKey( Schema.Struct({ @@ -2111,10 +2227,10 @@ const PackageJsonSchema = Schema.Struct({ Schema.Struct({ manifest: Schema.optionalKey(Schema.String), moduleId: Schema.optionalKey(ModuleIdSchema), - }), + }) ), role: Schema.optionalKey(Schema.String), - }), + }) ), name: Schema.optionalKey(Schema.String), optionalDependencies: Schema.optionalKey(StringValuesSchema), @@ -2156,12 +2272,14 @@ const TsConfigSchema = Schema.Struct({ rootDir: Schema.optionalKey(Schema.String), skipLibCheck: Schema.optionalKey(Schema.Boolean), tsBuildInfoFile: Schema.optionalKey(Schema.String), - }), + }) ), extends: Schema.optionalKey(Schema.String), files: Schema.optionalKey(Schema.Array(Schema.String)), include: Schema.optionalKey(Schema.Array(Schema.String)), - references: Schema.optionalKey(Schema.Array(Schema.Struct({ path: Schema.String }))), + references: Schema.optionalKey( + Schema.Array(Schema.Struct({ path: Schema.String })) + ), }); interface TsConfig { readonly compilerOptions?: { @@ -2185,10 +2303,14 @@ const BuildArtifactSchema = Schema.Struct({ }); type NodeFileTrace = ( files: readonly string[], - options: { readonly base: string; readonly log: boolean; readonly processCwd: string }, + options: { + readonly base: string; + readonly log: boolean; + readonly processCwd: string; + } ) => Promise; -const NodeFileTraceSchema = Schema.declare((input): input is NodeFileTrace => - Predicate.isFunction(input), +const NodeFileTraceSchema = Schema.declare( + (input): input is NodeFileTrace => Predicate.isFunction(input) ); const NftModuleSchema = Schema.Struct({ nodeFileTrace: NodeFileTraceSchema }); const LegacyTopologyFieldsSchema = Schema.Struct({ @@ -2253,7 +2375,8 @@ const shellPackage: ShellPackage = shellPackageDocument; const { packageScope } = workspaceValidationContract; const expectedNodeVersion = workspaceValidationContract.versions.node; const expectedEffectVersion = workspaceValidationContract.versions.effect; -const expectedModuleFederationVersion = workspaceValidationContract.versions.moduleFederation; +const expectedModuleFederationVersion = + workspaceValidationContract.versions.moduleFederation; const expectedCloudflareCompatibilityDate = workspaceValidationContract.versions.cloudflareCompatibilityDate; const { tailwindEnabled } = workspaceValidationContract; @@ -2261,7 +2384,9 @@ const { fullStackVerticals } = workspaceValidationContract; // Backend-federation and Zerops runtime surfaces only exist when the workspace // exposes API-bearing verticals. Shell-only workspaces skip their // materialization during migrate, so the contract must not require them. -const hasBackendSurfaces = fullStackVerticals.some((vertical) => vertical.emitsApi); +const hasBackendSurfaces = fullStackVerticals.some( + (vertical) => vertical.emitsApi +); // Every vertical (ui-only and horizontal-remote included) is a delivery unit // and deploys via Zerops; only the BACKEND proof/generation surfaces depend on // an API-bearing unit existing (split gating). @@ -2269,19 +2394,25 @@ const hasDeliveryUnits = fullStackVerticals.length > 0; const { shellNamespace } = workspaceValidationContract; const { oldRemotePaths } = workspaceValidationContract; const expectedBuildScript = workspaceValidationContract.scripts.build; -const expectedCloudflareBuildScript = workspaceValidationContract.scripts.cloudflareBuild; -const expectedCloudflareDeployScript = workspaceValidationContract.scripts.cloudflareDeploy; -const expectedCloudflareSecurity = workspaceValidationContract.cloudflareSecurity; +const expectedCloudflareBuildScript = + workspaceValidationContract.scripts.cloudflareBuild; +const expectedCloudflareDeployScript = + workspaceValidationContract.scripts.cloudflareDeploy; +const expectedCloudflareSecurity = + workspaceValidationContract.cloudflareSecurity; const { publicSurfaceManagedSourceAssetPaths } = workspaceValidationContract; const { shellRouteMetaPaths } = workspaceValidationContract; -const compactConfigPath = workspaceValidationContract.metadata.compactConfig.path; +const compactConfigPath = + workspaceValidationContract.metadata.compactConfig.path; const { retiredMetadataPaths } = workspaceValidationContract.legacy; const modernPackageCohort = workspaceValidationContract.cohort.modernPackages; const expectedPrimaryShellVerticalIds = - workspaceValidationContract.topology?.referenceTopology?.shell?.verticalRefs ?? - workspaceValidationContract.cohort.verticalIds; + workspaceValidationContract.topology?.referenceTopology?.shell + ?.verticalRefs ?? workspaceValidationContract.cohort.verticalIds; const expectedReleaseCohort = workspaceValidationContract.cohort.releaseCohort; -const expectedModernPackageSpecifier = (packageName: string): string | undefined => { +const expectedModernPackageSpecifier = ( + packageName: string +): string | undefined => { const { packageSource } = compactConfigDocument; if (packageSource.strategy === 'workspace') { return SHARED_VALIDATOR_STRING_169; @@ -2308,58 +2439,70 @@ const readText = (relativePath: string): string => fs.readFileSync(path.join(root, relativePath), 'utf-8'); const readJson = >( schema: DocumentSchema, - relativePath: string, + relativePath: string ): DocumentSchema['Type'] => Result.getOrThrow( - Schema.decodeUnknownResult(Schema.fromJsonString(schema))(readText(relativePath)), + Schema.decodeUnknownResult(Schema.fromJsonString(schema))( + readText(relativePath) + ) ); type Assert = (condition: boolean, message: string) => void; type AssertSelfCheck = ( condition: boolean, contract: string, message: string, - fixArea: string, + fixArea: string ) => void; type AssertObject = ( value: Value | null | undefined, contract: string, - fixArea: string, + fixArea: string ) => void; type AssertArray = ( value: readonly Value[] | undefined, contract: string, - fixArea: string, + fixArea: string ) => void; const assert: Assert = (condition, message) => { assertCondition(condition, message); }; const assertExists = (relativePath: string): void => { - assert(fs.existsSync(path.join(root, relativePath)), `Missing ${relativePath}`); + assert( + fs.existsSync(path.join(root, relativePath)), + `Missing ${relativePath}` + ); }; const assertNotExists = (relativePath: string): void => { - assert(!fs.existsSync(path.join(root, relativePath)), `Unexpected ${relativePath}`); + assert( + !fs.existsSync(path.join(root, relativePath)), + `Unexpected ${relativePath}` + ); }; const assertAnyOf = (relativePaths: readonly string[]): void => { assert( - relativePaths.some((relativePath) => fs.existsSync(path.join(root, relativePath))), - `Missing one of: ${relativePaths.join(', ')}`, + relativePaths.some((relativePath) => + fs.existsSync(path.join(root, relativePath)) + ), + `Missing one of: ${relativePaths.join(', ')}` ); }; const sortedCopy = ( values: readonly Value[], - compare: (left: Value, right: Value) => number, + compare: (left: Value, right: Value) => number ): Value[] => { const result: Value[] = []; for (const value of values ?? []) { - const insertAt = result.findIndex((existing) => compare(value, existing) < 0); + const insertAt = result.findIndex( + (existing) => compare(value, existing) < 0 + ); result.splice(insertAt === -1 ? result.length : insertAt, 0, value); } return result; }; const valueForKey = ( entries: readonly (readonly [string, Value])[], - key: string, + key: string ): Value | undefined => entries.find(([candidate]) => candidate === key)?.[1]; const canonicalizeJsonValue = (value: ComparableJson): ComparableJson => { if (isComparableJsonArray(value)) { @@ -2368,16 +2511,20 @@ const canonicalizeJsonValue = (value: ComparableJson): ComparableJson => { if (keyedEntries) { const ids = entries.map((entry) => entry.id); if (new Set(ids).size === ids.length) { - return sortedCopy(entries, (left, right) => left.id.localeCompare(right.id)); + return sortedCopy(entries, (left, right) => + left.id.localeCompare(right.id) + ); } } return entries; } if (isComparableJsonObject(value)) { return Object.fromEntries( - sortedCopy(Object.entries(value), ([left], [right]) => left.localeCompare(right)) + sortedCopy(Object.entries(value), ([left], [right]) => + left.localeCompare(right) + ) .filter(([, entry]) => entry !== undefined) - .map(([key, entry]) => [key, canonicalizeJsonValue(entry)]), + .map(([key, entry]) => [key, canonicalizeJsonValue(entry)]) ); } return value; @@ -2386,17 +2533,28 @@ const canonicalizeJson = (value: Value): ComparableJson | undefined => value === undefined ? undefined : canonicalizeJsonValue( - Result.getOrThrow(Schema.decodeUnknownResult(ComparableJsonSchema)(value)), + Result.getOrThrow( + Schema.decodeUnknownResult(ComparableJsonSchema)(value) + ) ); -const sameJson = (actual: Actual, expected: Expected): boolean => +const sameJson = ( + actual: Actual, + expected: Expected +): boolean => jsonEquivalent(canonicalizeJson(actual), canonicalizeJson(expected)); const formatJson = (value: Value): string => - value === undefined ? 'undefined' : Inspectable.toStringUnknown(canonicalizeJson(value), 0); + value === undefined + ? 'undefined' + : Inspectable.toStringUnknown(canonicalizeJson(value), 0); const quoteYamlString = (value: string | number): string => `'${String(value).replaceAll("'", "''")}'`; const quoteShellValue = (value: string | number): string => `'${String(value).replaceAll("'", shellSingleQuoteEscape)}'`; -const yamlListItemBlock = (source: string, key: string, value: string | number): string => { +const yamlListItemBlock = ( + source: string, + key: string, + value: string | number +): string => { const marker = ` - ${key}: ${quoteYamlString(value)}`; const start = source.indexOf(marker); if (start === -1) { @@ -2405,7 +2563,11 @@ const yamlListItemBlock = (source: string, key: string, value: string | number): const end = source.indexOf('\n - ', start + marker.length); return source.slice(start, end === -1 ? undefined : end); }; -const yamlMappingBlock = (source: string, key: string, indent: number): string => { +const yamlMappingBlock = ( + source: string, + key: string, + indent: number +): string => { const indentation = ' '.repeat(indent); const marker = `${indentation}${key}:`; const start = source.indexOf(marker); @@ -2415,25 +2577,35 @@ const yamlMappingBlock = (source: string, key: string, indent: number): string = const nextSibling = source .slice(start + marker.length) .search(new RegExp(`\\n${indentation}\\S`, 'u')); - const end = nextSibling === -1 ? undefined : start + marker.length + nextSibling; + const end = + nextSibling === -1 ? undefined : start + marker.length + nextSibling; return source.slice(start, end); }; -const selfCheckFailure = (contract: string, message: string, fixArea: string): string => +const selfCheckFailure = ( + contract: string, + message: string, + fixArea: string +): string => `MicroVertical contract self-check failed: ${contract}. ${message}. Fix area: ${fixArea}.`; -const assertSelfCheck: AssertSelfCheck = (condition, contract, message, fixArea) => { +const assertSelfCheck: AssertSelfCheck = ( + condition, + contract, + message, + fixArea +) => { assert(condition, selfCheckFailure(contract, message, fixArea)); }; const assertSameJson = ( actual: Actual, expected: Expected, contract: string, - fixArea: string, + fixArea: string ): void => { assertSelfCheck( sameJson(actual, expected), contract, `Expected ${formatJson(expected)}, found ${formatJson(actual)}`, - fixArea, + fixArea ); }; const assertObject: AssertObject = (value, contract, fixArea) => { @@ -2441,7 +2613,7 @@ const assertObject: AssertObject = (value, contract, fixArea) => { value !== null && value !== undefined && !Array.isArray(value), contract, `Expected JSON object, found ${formatJson(value)}`, - fixArea, + fixArea ); }; const assertArray: AssertArray = (value, contract, fixArea) => { @@ -2449,27 +2621,36 @@ const assertArray: AssertArray = (value, contract, fixArea) => { Array.isArray(value), contract, `Expected JSON array, found ${formatJson(value)}`, - fixArea, + fixArea ); }; -const assertUniqueStrings = (values: readonly string[] | undefined, contract: string): void => { +const assertUniqueStrings = ( + values: readonly string[] | undefined, + contract: string +): void => { assert(Array.isArray(values), `${contract} must be an array`); const seen = new Set(); for (const value of values ?? []) { - assert(isString(value) && value.length > 0, `${contract} must contain non-empty strings`); + assert( + isString(value) && value.length > 0, + `${contract} must contain non-empty strings` + ); assert(!seen.has(value), `Duplicate value "${value}" in ${contract}`); seen.add(value); } }; const assertUniqueIdEntries = ( entries: readonly IdentifierEntry[] | undefined, - contract: string, + contract: string ): void => { assert(Array.isArray(entries), `${contract} must be an array`); const seen = new Set(); for (const entry of entries ?? []) { const id = entry?.id; - assert(isString(id) && id.length > 0, `${contract} entries must have non-empty string ids`); + assert( + isString(id) && id.length > 0, + `${contract} entries must have non-empty string ids` + ); assert(!seen.has(id), `Duplicate id "${id}" in ${contract}`); seen.add(id); } @@ -2478,178 +2659,208 @@ const assertSameIdCohort = ( entries: readonly IdentifierEntry[] | undefined, expectedIds: readonly string[], contract: string, - fixArea: string, + fixArea: string ): void => { assertUniqueIdEntries(entries, contract); assertSameJson( - sortedCopy(entries?.map((entry) => entry.id) ?? [], (left, right) => left.localeCompare(right)), + sortedCopy(entries?.map((entry) => entry.id) ?? [], (left, right) => + left.localeCompare(right) + ), sortedCopy(expectedIds, (left, right) => left.localeCompare(right)), `${contract} cohort`, - fixArea, + fixArea ); }; const assertGeneratedSurfaceTarget = ( rule: GeneratedSurfaceRule, - target: GeneratedSurfaceTarget, + target: GeneratedSurfaceTarget ): void => { assert( target.kind === 'file' || target.kind === 'directory', - `generated surface policy ${rule.id} has an invalid target kind`, + `generated surface policy ${rule.id} has an invalid target kind` ); assert( isString(target.path) && target.path.length > 0, - `generated surface policy ${rule.id} target path is required`, + `generated surface policy ${rule.id} target path is required` ); if (target.kind === 'directory') { assertUniqueStrings( target.extensions, - `generated surface policy ${rule.id} directory extensions`, + `generated surface policy ${rule.id} directory extensions` ); assertUniqueStrings( target.excludePaths ?? [], - `generated surface policy ${rule.id} directory exclusions`, + `generated surface policy ${rule.id} directory exclusions` ); } }; const assertGeneratedSurfacePattern = ( rule: GeneratedSurfaceRule, - pattern: GeneratedSurfacePattern, + pattern: GeneratedSurfacePattern ): void => { if (pattern.structuralMatcher === undefined) { const { expression, flags } = pattern; assert( isString(expression) && expression.length > 0, - `generated surface policy ${rule.id}.${pattern.id} expression is required`, + `generated surface policy ${rule.id}.${pattern.id} expression is required` ); assert( flags === 'u', - `generated surface policy ${rule.id}.${pattern.id} must use deterministic Unicode matching`, + `generated surface policy ${rule.id}.${pattern.id} must use deterministic Unicode matching` ); if (expression !== undefined && flags !== undefined) { const compiledPattern = new RegExp(expression, flags); assert( compiledPattern.flags === flags, - `generated surface policy ${rule.id}.${pattern.id} flags are not preserved`, + `generated surface policy ${rule.id}.${pattern.id} flags are not preserved` ); } } else { assert( pattern.expression === undefined && pattern.flags === undefined, - `generated surface policy ${rule.id}.${pattern.id} must use exactly one matcher`, + `generated surface policy ${rule.id}.${pattern.id} must use exactly one matcher` ); assert( pattern.structuralMatcher.kind === SHARED_VALIDATOR_STRING_074, - `generated surface policy ${rule.id}.${pattern.id} has an unsupported structural matcher`, + `generated surface policy ${rule.id}.${pattern.id} has an unsupported structural matcher` ); assert( isString(pattern.structuralMatcher.elementName) && pattern.structuralMatcher.elementName.length > 0, - `generated surface policy ${rule.id}.${pattern.id} structural elementName is required`, + `generated surface policy ${rule.id}.${pattern.id} structural elementName is required` ); assert( isString(pattern.structuralMatcher.attributeName) && pattern.structuralMatcher.attributeName.length > 0, - `generated surface policy ${rule.id}.${pattern.id} structural attributeName is required`, + `generated surface policy ${rule.id}.${pattern.id} structural attributeName is required` ); } assert( isString(pattern.diagnostic) && pattern.diagnostic.length > 0, - `generated surface policy ${rule.id}.${pattern.id} diagnostic is required`, + `generated surface policy ${rule.id}.${pattern.id} diagnostic is required` ); assert( isString(pattern.fixArea) && pattern.fixArea.length > 0, - `generated surface policy ${rule.id}.${pattern.id} fixArea is required`, + `generated surface policy ${rule.id}.${pattern.id} fixArea is required` ); }; -const assertGeneratedSurfaceRules = (contract: WorkspaceValidationContract): void => { +const assertGeneratedSurfaceRules = ( + contract: WorkspaceValidationContract +): void => { const { generatedSurfacePolicy } = contract; assert( generatedSurfacePolicy.schemaVersion === 1, - `Unsupported generated surface policy schemaVersion ${formatJson(generatedSurfacePolicy.schemaVersion)}; expected 1`, + `Unsupported generated surface policy schemaVersion ${formatJson(generatedSurfacePolicy.schemaVersion)}; expected 1` ); assertUniqueIdEntries( generatedSurfacePolicy.rules, - 'workspace validation contract generated surface policy rules', + 'workspace validation contract generated surface policy rules' ); for (const rule of generatedSurfacePolicy.rules) { assertUniqueStrings( rule.paths.map((entry) => entry.path), - `generated surface policy ${rule.id} paths`, + `generated surface policy ${rule.id} paths` ); assert( Array.isArray(rule.paths) && rule.paths.length > 0, - `generated surface policy ${rule.id} must target generated paths`, + `generated surface policy ${rule.id} must target generated paths` ); for (const target of rule.paths) { assertGeneratedSurfaceTarget(rule, target); } - assertUniqueIdEntries(rule.patterns, `generated surface policy ${rule.id} patterns`); + assertUniqueIdEntries( + rule.patterns, + `generated surface policy ${rule.id} patterns` + ); for (const pattern of rule.patterns) { assertGeneratedSurfacePattern(rule, pattern); } } }; -const assertWorkspaceValidationContract = (contract: WorkspaceValidationContract): void => { - assert(!Array.isArray(contract), 'Workspace validation contract must be a JSON object'); +const assertWorkspaceValidationContract = ( + contract: WorkspaceValidationContract +): void => { + assert( + !Array.isArray(contract), + 'Workspace validation contract must be a JSON object' + ); assert( contract.schemaVersion === 1, - `Unsupported workspace validation contract schemaVersion ${formatJson(contract.schemaVersion)}; expected 1`, + `Unsupported workspace validation contract schemaVersion ${formatJson(contract.schemaVersion)}; expected 1` ); assert( contract.kind === 'modernjs.ultramodern-workspace-validation-contract', - `Unsupported workspace validation contract kind ${formatJson(contract.kind)}`, + `Unsupported workspace validation contract kind ${formatJson(contract.kind)}` ); const metadataEntries = Object.entries(contract.metadata); assert( - metadataEntries.length === (contract.cohort.releaseCohort === undefined ? 4 : 5), - 'Workspace validation contract must declare every structured metadata input', + metadataEntries.length === + (contract.cohort.releaseCohort === undefined ? 4 : 5), + 'Workspace validation contract must declare every structured metadata input' ); for (const [name, metadata] of metadataEntries) { assert( isString(metadata.path) && metadata.path.length > 0, - `Workspace validation contract metadata.${name}.path is required`, + `Workspace validation contract metadata.${name}.path is required` ); assert( metadata.schemaVersion === 1, - `Unsupported expected metadata schemaVersion ${formatJson(metadata.schemaVersion)} for ${name}`, + `Unsupported expected metadata schemaVersion ${formatJson(metadata.schemaVersion)} for ${name}` ); } assertUniqueStrings( contract.cohort.modernPackages, - 'workspace validation contract Modern package cohort', + 'workspace validation contract Modern package cohort' + ); + assertUniqueStrings( + contract.cohort.appIds, + 'workspace validation contract app cohort' ); - assertUniqueStrings(contract.cohort.appIds, 'workspace validation contract app cohort'); assertUniqueStrings( contract.cohort.backendAppIds, - 'workspace validation contract backend app cohort', + 'workspace validation contract backend app cohort' + ); + assertUniqueStrings( + contract.cohort.verticalIds, + 'workspace validation contract vertical cohort' ); - assertUniqueStrings(contract.cohort.verticalIds, 'workspace validation contract vertical cohort'); assertUniqueStrings( contract.cohort.sharedPackageIds, - 'workspace validation contract shared package cohort', + 'workspace validation contract shared package cohort' + ); + assertUniqueStrings( + contract.cohort.ownerIds, + 'workspace validation contract owner cohort' ); - assertUniqueStrings(contract.cohort.ownerIds, 'workspace validation contract owner cohort'); assertUniqueIdEntries( contract.cohort.packageManifests, - 'workspace validation contract package manifests', + 'workspace validation contract package manifests' ); assertUniqueStrings( contract.cohort.packageManifests.map((manifest) => manifest.path), - 'workspace validation contract package manifest paths', + 'workspace validation contract package manifest paths' ); assertGeneratedSurfaceRules(contract); }; -const generatedSurfacePolicyFiles = (target: GeneratedSurfaceTarget): string[] => { +const generatedSurfacePolicyFiles = ( + target: GeneratedSurfaceTarget +): string[] => { const absolutePath = path.join(root, target.path); if (target.kind === 'file') { - assert(fs.existsSync(absolutePath), `Missing generated surface policy file ${target.path}`); + assert( + fs.existsSync(absolutePath), + `Missing generated surface policy file ${target.path}` + ); return [target.path]; } - assert(fs.existsSync(absolutePath), `Missing generated surface policy directory ${target.path}`); + assert( + fs.existsSync(absolutePath), + `Missing generated surface policy directory ${target.path}` + ); const files: string[] = []; const queue = [absolutePath]; while (queue.length > 0) { @@ -2659,8 +2870,12 @@ const generatedSurfacePolicyFiles = (target: GeneratedSurfaceTarget): string[] = const absoluteEntryPath = path.join(current, entry.name); if (entry.isDirectory()) { queue.push(absoluteEntryPath); - } else if ((target.extensions ?? []).includes(path.extname(entry.name))) { - files.push(path.relative(root, absoluteEntryPath).split(path.sep).join('/')); + } else if ( + (target.extensions ?? []).includes(path.extname(entry.name)) + ) { + files.push( + path.relative(root, absoluteEntryPath).split(path.sep).join('/') + ); } } } @@ -2668,7 +2883,7 @@ const generatedSurfacePolicyFiles = (target: GeneratedSurfaceTarget): string[] = const excludedPaths = new Set(target.excludePaths); return sortedCopy( files.filter((file) => !excludedPaths.has(file)), - (left, right) => left.localeCompare(right), + (left, right) => left.localeCompare(right) ); }; const isJsxNameCharacter = (character: string | undefined): boolean => @@ -2700,7 +2915,11 @@ const skipSourceComment = (source: string, start: number): number => { }; const isSourceQuote = (character: string | undefined): boolean => character !== undefined && '\'"`'.includes(character); -const matchesJsxAttribute = (source: string, cursor: number, attributeName: string): boolean => { +const matchesJsxAttribute = ( + source: string, + cursor: number, + attributeName: string +): boolean => { if ( !source.startsWith(attributeName, cursor) || isJsxNameCharacter(source[cursor - 1]) || @@ -2718,12 +2937,14 @@ const skipJsxNonAttributeSource = (source: string, cursor: number): number => { if (isSourceQuote(source[cursor])) { return skipQuotedSource(source, cursor); } - return source[cursor] === '/' ? Math.max(cursor + 1, skipSourceComment(source, cursor)) : cursor; + return source[cursor] === '/' + ? Math.max(cursor + 1, skipSourceComment(source, cursor)) + : cursor; }; const findOpeningElementAttribute = ( source: string, start: number, - attributeName: string, + attributeName: string ): { readonly index: number } | null => { let cursor = start; let expressionDepth = 0; @@ -2752,14 +2973,18 @@ const findOpeningElementAttribute = ( }; const findJsxAttribute = ( source: string, - matcher: NonNullable, + matcher: NonNullable ): { readonly index: number } | null => { const opening = `<${matcher.elementName}`; let elementIndex = source.indexOf(opening); while (elementIndex !== -1) { const start = elementIndex + opening.length; if (!isJsxNameCharacter(source[start])) { - const match = findOpeningElementAttribute(source, start, matcher.attributeName); + const match = findOpeningElementAttribute( + source, + start, + matcher.attributeName + ); if (match !== null) { return match; } @@ -2770,7 +2995,7 @@ const findJsxAttribute = ( }; const findGeneratedSurfacePolicyMatch = ( source: string, - pattern: GeneratedSurfacePattern, + pattern: GeneratedSurfacePattern ): { readonly index: number } | null => { if (pattern.structuralMatcher?.kind === SHARED_VALIDATOR_STRING_074) { return findJsxAttribute(source, pattern.structuralMatcher); @@ -2782,27 +3007,28 @@ const findGeneratedSurfacePolicyMatch = ( const assertSingleShellDeclarations = (): void => { assert( workspaceValidationContract.cohort.additionalShellIds === undefined, - 'Single-shell workspace must not declare additionalShellIds', + 'Single-shell workspace must not declare additionalShellIds' ); assert( workspaceValidationContract.cohort?.additionalShellManifests === undefined, - 'Single-shell workspace must not declare additional-shell manifests', + 'Single-shell workspace must not declare additional-shell manifests' ); assert( workspaceValidationContract.additionalShells === undefined, - 'Single-shell workspace must not declare additional-shell records', + 'Single-shell workspace must not declare additional-shell records' ); for (const field of additionalShellCohortFields) { assert( workspaceValidationContract.cohort?.[field] === undefined, - `Single-shell workspace must not declare ${field}`, + `Single-shell workspace must not declare ${field}` ); } }; const assertGeneratedSurfacePolicy = () => { for (const rule of workspaceValidationContract.generatedSurfacePolicy.rules) { - const files = sortedCopy(rule.paths.flatMap(generatedSurfacePolicyFiles), (left, right) => - left.localeCompare(right), + const files = sortedCopy( + rule.paths.flatMap(generatedSurfacePolicyFiles), + (left, right) => left.localeCompare(right) ); for (const relativePath of files) { const source = readText(relativePath); @@ -2812,7 +3038,7 @@ const assertGeneratedSurfacePolicy = () => { match === null, `generated surface policy ${rule.id}.${pattern.id}`, `${pattern.diagnostic} Found forbidden source at ${relativePath}:${match?.index ?? 0}`, - pattern.fixArea, + pattern.fixArea ); } } @@ -2834,24 +3060,27 @@ const assertLegacyMetadataFields = (): void => { assertObject( ultramodernConfig.packageSource, `${compactConfigPath} packageSource`, - 'restore generated compact package-source metadata', + 'restore generated compact package-source metadata' ); - for (const field of workspaceValidationContract.legacy.forbiddenCompactConfigFields) { + for (const field of workspaceValidationContract.legacy + .forbiddenCompactConfigFields) { assert( !Object.hasOwn(ultramodernConfig, field), - `Stale legacy field ${compactConfigPath}.${field} is forbidden`, + `Stale legacy field ${compactConfigPath}.${field} is forbidden` ); } - for (const field of workspaceValidationContract.legacy.forbiddenPackageSourceFields) { + for (const field of workspaceValidationContract.legacy + .forbiddenPackageSourceFields) { assert( !Object.hasOwn(ultramodernConfig.packageSource, field), - `Stale legacy field ${compactConfigPath}.packageSource.${field} is forbidden`, + `Stale legacy field ${compactConfigPath}.packageSource.${field} is forbidden` ); } - for (const field of workspaceValidationContract.legacy.forbiddenTopologyFields) { + for (const field of workspaceValidationContract.legacy + .forbiddenTopologyFields) { assert( !Object.hasOwn(topology, field), - `Stale legacy field ${workspaceValidationContract.metadata.referenceTopology.path}.${field} is forbidden`, + `Stale legacy field ${workspaceValidationContract.metadata.referenceTopology.path}.${field} is forbidden` ); } }; @@ -2861,28 +3090,29 @@ const assertMetadataPackageManifests = (): void => { const packageJson = readJson(PackageJsonSchema, manifest.path); assert( packageJson.name === manifest.packageName, - `${manifest.path} package name must be ${manifest.packageName}`, + `${manifest.path} package name must be ${manifest.packageName}` ); if (manifest.role === 'shell' || manifest.role === 'vertical') { assert( packageJson.modernjs?.appId === manifest.id, - `${manifest.path} modernjs.appId must be ${manifest.id}`, + `${manifest.path} modernjs.appId must be ${manifest.id}` ); } } if (expectedReleaseCohort !== undefined) { - const releaseCohortContract = workspaceValidationContract.metadata.releaseCohort; + const releaseCohortContract = + workspaceValidationContract.metadata.releaseCohort; assertSelfCheck( releaseCohortContract?.path === SHARED_VALIDATOR_STRING_009, 'authenticated release cohort projection', 'Expected release-cohort metadata path is missing or invalid', - SHARED_VALIDATOR_STRING_009, + SHARED_VALIDATOR_STRING_009 ); assertSameJson( readJson(ComparableJsonSchema, releaseCohortContract.path), expectedReleaseCohort, 'authenticated release cohort projection', - releaseCohortContract.path, + releaseCohortContract.path ); } }; @@ -2907,26 +3137,29 @@ const assertStructuredWorkspaceMetadata = (): void => { ]; for (const entry of observedMetadata) { - assert(isMetadataDocument(entry.value), `${entry.contract.path} must contain a JSON object`); + assert( + isMetadataDocument(entry.value), + `${entry.contract.path} must contain a JSON object` + ); assert( Number.isInteger(entry.value.schemaVersion), - `${entry.contract.path} must declare an integer schemaVersion`, + `${entry.contract.path} must declare an integer schemaVersion` ); } const observedSchemaVersions = new Set( - observedMetadata.map((entry) => entry.value.schemaVersion), + observedMetadata.map((entry) => entry.value.schemaVersion) ); assert( observedSchemaVersions.size === 1, `Mixed workspace metadata schema versions: ${observedMetadata .map((entry) => `${entry.contract.path}=${entry.value.schemaVersion}`) - .join(', ')}`, + .join(', ')}` ); for (const entry of observedMetadata) { assert( entry.value.schemaVersion === entry.contract.schemaVersion, - `Unsupported workspace metadata schemaVersion ${entry.value.schemaVersion} at ${entry.contract.path}; expected ${entry.contract.schemaVersion}`, + `Unsupported workspace metadata schemaVersion ${entry.value.schemaVersion} at ${entry.contract.path}; expected ${entry.contract.schemaVersion}` ); } @@ -2935,45 +3168,45 @@ const assertStructuredWorkspaceMetadata = (): void => { ultramodernConfig.topology?.apps, workspaceValidationContract.cohort.appIds, `${compactConfigPath} topology.apps`, - 'restore the complete generated app cohort', + 'restore the complete generated app cohort' ); assertSameIdCohort( ultramodernConfig.moduleFederation?.apps, workspaceValidationContract.cohort.appIds, `${compactConfigPath} moduleFederation.apps`, - 'restore the complete generated Module Federation app cohort', + 'restore the complete generated Module Federation app cohort' ); assertSameIdCohort( ultramodernConfig.backendFederation?.apps, workspaceValidationContract.cohort.backendAppIds, `${compactConfigPath} backendFederation.apps`, - 'restore the complete generated backend app cohort', + 'restore the complete generated backend app cohort' ); assertSameIdCohort( topology.verticals, workspaceValidationContract.cohort.verticalIds, `${workspaceValidationContract.metadata.referenceTopology.path} verticals`, - 'restore the complete generated vertical cohort', + 'restore the complete generated vertical cohort' ); assertSameIdCohort( topology.sharedPackages, workspaceValidationContract.cohort.sharedPackageIds, `${workspaceValidationContract.metadata.referenceTopology.path} sharedPackages`, - 'restore the complete generated shared package cohort', + 'restore the complete generated shared package cohort' ); assertSameIdCohort( topology.shell?.moduleFederation?.remotes, workspaceValidationContract.topology.referenceTopology.shell.moduleFederation.remotes.map( - (remote) => remote.id, + (remote) => remote.id ), `${workspaceValidationContract.metadata.referenceTopology.path} shell.moduleFederation.remotes`, - 'restore the complete generated shell remote cohort', + 'restore the complete generated shell remote cohort' ); assertSameIdCohort( ownership.owners, workspaceValidationContract.cohort.ownerIds, `${workspaceValidationContract.metadata.ownership.path} owners`, - 'restore the complete generated ownership cohort', + 'restore the complete generated ownership cohort' ); assertMetadataPackageManifests(); @@ -2983,36 +3216,36 @@ const assertStructuredWorkspaceMetadataSemantics = (): void => { compactConfigPolicyView(ultramodernConfig), workspaceValidationContract.policy.compactConfig, `${compactConfigPath} policy`, - 'restore generated compact workspace policy metadata', + 'restore generated compact workspace policy metadata' ); assertSameJson( ultramodernConfig.topology, workspaceValidationContract.topology.compactConfig, `${compactConfigPath} topology`, - 'restore the complete generated compact topology cohort', + 'restore the complete generated compact topology cohort' ); assertSameJson( topology, workspaceValidationContract.topology.referenceTopology, workspaceValidationContract.metadata.referenceTopology.path, - 'restore the complete generated reference topology', + 'restore the complete generated reference topology' ); assertSameJson( ownership, workspaceValidationContract.topology.ownership, workspaceValidationContract.metadata.ownership.path, - 'restore the complete generated ownership topology', + 'restore the complete generated ownership topology' ); assertSameJson( overlay, workspaceValidationContract.topology.developmentOverlay, workspaceValidationContract.metadata.developmentOverlay.path, - 'restore the complete generated development topology', + 'restore the complete generated development topology' ); }; const findById = ( entries: readonly Entry[] | undefined, - id: string, + id: string ): Entry | undefined => entries?.find((entry) => entry.id === id); const generatedContractLabel = compactConfigPath; const toKebabCase = (value: string): string => @@ -3036,13 +3269,16 @@ const toCamelCase = (value: string): string => { }; const toEnvSegment = (value: string): string => toKebabCase(value).replaceAll('-', '_').toUpperCase(); -const packageNameFor = (scope: string, suffix: string): string => `@${scope}/${suffix}`; +const packageNameFor = (scope: string, suffix: string): string => + `@${scope}/${suffix}`; const normalizeRelativePath = (value: string | undefined): string => (value ?? '').replaceAll('\\', '/').replace(/^\.\/+/u, ''); const appNamespace = (app: NormalizedApp): string => app.kind === 'shell' ? 'shell' : (app.domain ?? app.id); const tailwindPrefixFor = (app: NormalizedApp): string => - app.kind === 'shell' ? 'shell' : tailwindPrefixForNamespace(app.domain ?? app.id); + app.kind === 'shell' + ? 'shell' + : tailwindPrefixForNamespace(app.domain ?? app.id); const buildMarkerFor = (app: NormalizedApp): string => crypto .createHash('sha256') @@ -3061,7 +3297,9 @@ const deliveryUnitBlock = (record: DeliveryUnit | undefined) => ({ version: record?.version, }); const expectedCompactAppFor = (id: string) => - workspaceValidationContract.topology.compactConfig?.apps?.find((entry) => entry?.id === id); + workspaceValidationContract.topology.compactConfig?.apps?.find( + (entry) => entry?.id === id + ); const expectedDeliveryUnitFor = (vertical: FullStackVertical): DeliveryUnit => { const expectedApp = expectedCompactAppFor(vertical.id); return ( @@ -3074,27 +3312,35 @@ const assertBuildFacadeExport = ( source: string, exportName: string, sourcePath: string, - contract: string, + contract: string ): void => { const escapedSourcePath = sourcePath.replaceAll('.', String.raw`\.`); - const exportPattern = new RegExp(`export const ${exportName} = ${escapedSourcePath};`, 'u'); + const exportPattern = new RegExp( + `export const ${exportName} = ${escapedSourcePath};`, + 'u' + ); assertSelfCheck( exportPattern.test(source), contract, `${exportName} must re-export the canonical ultramodern-build.json artifact rather than hand-forking fields`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); }; -const normalizedAppPath = (rawApp: CompactApp, kind: NormalizedApp['kind']): string => { +const normalizedAppPath = ( + rawApp: CompactApp, + kind: NormalizedApp['kind'] +): string => { if (isString(rawApp.path)) { return normalizeRelativePath(rawApp.path); } - return kind === 'shell' ? SHARED_VALIDATOR_STRING_047 : `verticals/${toKebabCase(rawApp.id)}`; + return kind === 'shell' + ? SHARED_VALIDATOR_STRING_047 + : `verticals/${toKebabCase(rawApp.id)}`; }; const normalizedAppDomain = ( rawDomain: string | undefined, kind: NormalizedApp['kind'], - packageSuffix: string, + packageSuffix: string ): string | undefined => { if (isString(rawDomain)) { return rawDomain; @@ -3104,14 +3350,16 @@ const normalizedAppDomain = ( const normalizedAppApi = ( rawApi: CompactApi | undefined, domain: string | undefined, - id: string, + id: string ): NormalizedApp['api'] => { if (rawApi === undefined) { return undefined; } return { consumedBy: - rawApi.consumedBy === undefined ? [SHARED_VALIDATOR_STRING_131, id] : [...rawApi.consumedBy], + rawApi.consumedBy === undefined + ? [SHARED_VALIDATOR_STRING_131, id] + : [...rawApi.consumedBy], prefix: isString(rawApi.prefix) ? rawApi.prefix : `/${domain ?? id}-api`, protocol: rawApi.protocol === 'rpc' ? 'rpc' : 'rest', stem: isString(rawApi.stem) ? rawApi.stem : (domain ?? id), @@ -3121,16 +3369,18 @@ const normalizedAppMfName = ( configuredName: string | undefined, domain: string | undefined, id: string, - kind: NormalizedApp['kind'], + kind: NormalizedApp['kind'] ): string => { if (isString(configuredName)) { return configuredName; } - return kind === 'shell' ? SHARED_VALIDATOR_STRING_133 : `vertical${toPascalCase(domain ?? id)}`; + return kind === 'shell' + ? SHARED_VALIDATOR_STRING_133 + : `vertical${toPascalCase(domain ?? id)}`; }; const normalizedAppPort = ( configuredPort: number | undefined, - kind: NormalizedApp['kind'], + kind: NormalizedApp['kind'] ): number => { if (isNumber(configuredPort)) { return configuredPort; @@ -3141,7 +3391,7 @@ const normalizedAppPortEnv = ( configuredPortEnv: string | undefined, domain: string | undefined, id: string, - kind: NormalizedApp['kind'], + kind: NormalizedApp['kind'] ): string => { if (isString(configuredPortEnv)) { return configuredPortEnv; @@ -3152,17 +3402,28 @@ const normalizedAppPortEnv = ( }; const normalizedApiExports = ( appPath: string, - api: NormalizedApp['api'], + api: NormalizedApp['api'] ): Pick => { - const packageExports = readJson(PackageJsonSchema, `${appPath}/package.json`).exports ?? {}; - const apiContractExport = packageExports['./api'] === undefined ? undefined : './api'; + const packageExports = + readJson(PackageJsonSchema, `${appPath}/package.json`).exports ?? {}; + const apiContractExport = + packageExports['./api'] === undefined ? undefined : './api'; const clientExport = - api?.protocol === 'rpc' ? SHARED_VALIDATOR_STRING_003 : SHARED_VALIDATOR_STRING_002; - const apiClientExport = packageExports[clientExport] === undefined ? undefined : clientExport; + api?.protocol === 'rpc' + ? SHARED_VALIDATOR_STRING_003 + : SHARED_VALIDATOR_STRING_002; + const apiClientExport = + packageExports[clientExport] === undefined ? undefined : clientExport; return { apiClientExport, apiContractExport }; }; const normalizeCompactApp = (rawApp: CompactApp): NormalizedApp => { - const { api: rawApi, domain: rawDomain, id, port: rawPort, portEnv: rawPortEnv } = rawApp; + const { + api: rawApi, + domain: rawDomain, + id, + port: rawPort, + portEnv: rawPortEnv, + } = rawApp; const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell'; const appPath = normalizedAppPath(rawApp, kind); const packageSuffix = isString(rawApp.packageSuffix) @@ -3173,7 +3434,10 @@ const normalizeCompactApp = (rawApp: CompactApp): NormalizedApp => { // Preserve the API protocol so the synthesized generated contract can branch // between REST and RPC surfaces. const api = normalizedAppApi(rawApi, domain, id); - const { apiClientExport, apiContractExport } = normalizedApiExports(appPath, api); + const { apiClientExport, apiContractExport } = normalizedApiExports( + appPath, + api + ); const mfName = normalizedAppMfName(moduleFederation.name, domain, id, kind); const port = normalizedAppPort(rawPort, kind); const portEnv = normalizedAppPortEnv(rawPortEnv, domain, id, kind); @@ -3185,7 +3449,10 @@ const normalizeCompactApp = (rawApp: CompactApp): NormalizedApp => { backendFederation: rawApp.backendFederation, deliveryUnit: rawApp.deliveryUnit, domain, - exposes: moduleFederation.exposes === undefined ? [] : [...moduleFederation.exposes], + exposes: + moduleFederation.exposes === undefined + ? [] + : [...moduleFederation.exposes], id, kind, mfName, @@ -3196,18 +3463,27 @@ const normalizeCompactApp = (rawApp: CompactApp): NormalizedApp => { port, portEnv, verticalRefs: - moduleFederation.verticalRefs === undefined ? [] : [...moduleFederation.verticalRefs], + moduleFederation.verticalRefs === undefined + ? [] + : [...moduleFederation.verticalRefs], }; }; const compactAppsFromConfig = (config: CompactConfig): NormalizedApp[] => - Array.isArray(config.topology?.apps) ? config.topology.apps.map(normalizeCompactApp) : []; + Array.isArray(config.topology?.apps) + ? config.topology.apps.map(normalizeCompactApp) + : []; const remoteDependencyAliasFor = (app: NormalizedApp): string => toCamelCase(app.domain ?? app.id.replace(/^remote-/u, '')); -const remoteContractsFor = (app: NormalizedApp, apps: readonly NormalizedApp[]) => +const remoteContractsFor = ( + app: NormalizedApp, + apps: readonly NormalizedApp[] +) => (app.verticalRefs ?? []) .flatMap((ref) => { const remote = apps.find((candidate) => candidate.id === ref); - return remote === undefined || remote.exposes.length === 0 ? [] : [remote]; + return remote === undefined || remote.exposes.length === 0 + ? [] + : [remote]; }) .map((remote) => ({ alias: remoteDependencyAliasFor(remote), @@ -3262,10 +3538,13 @@ const createLocalisedUrls = (app: NormalizedApp) => if (route.canonicalPath === '/') { return []; } - return [...new Set([route.canonicalPath, ...Object.values(route.localisedPaths)])].map( - (pathname) => [pathname, route.localisedPaths], - ); - }), + return [ + ...new Set([ + route.canonicalPath, + ...Object.values(route.localisedPaths), + ]), + ].map((pathname) => [pathname, route.localisedPaths]); + }) ); const createPublicSurface = (app: NormalizedApp) => { const publicRoutes = createPublicRoutes(app); @@ -3286,7 +3565,11 @@ const createPublicSurface = (app: NormalizedApp) => { contentSources: [], files: publicRoutes.length > 0 - ? [SHARED_VALIDATOR_STRING_113, SHARED_VALIDATOR_STRING_135, SHARED_VALIDATOR_STRING_134] + ? [ + SHARED_VALIDATOR_STRING_113, + SHARED_VALIDATOR_STRING_135, + SHARED_VALIDATOR_STRING_134, + ] : [SHARED_VALIDATOR_STRING_113], generatedManifest: SHARED_VALIDATOR_STRING_006, generator: SHARED_VALIDATOR_STRING_117, @@ -3352,8 +3635,12 @@ const createCloudflareRoutes = (app: NormalizedApp) => { const hasRenderedSurface = app.kind === 'shell' || app.exposes.length > 0; return { apiReadiness: - app.api?.protocol === 'rest' ? `${app.api.prefix}/${app.api.stem}/readiness` : undefined, - locale: hasRenderedSurface ? `/locales/en/${appNamespace(app)}.json` : undefined, + app.api?.protocol === 'rest' + ? `${app.api.prefix}/${app.api.stem}/readiness` + : undefined, + locale: hasRenderedSurface + ? `/locales/en/${appNamespace(app)}.json` + : undefined, mfManifest: SHARED_VALIDATOR_STRING_031, ssr: hasRenderedSurface ? '/en' : undefined, }; @@ -3361,7 +3648,10 @@ const createCloudflareRoutes = (app: NormalizedApp) => { const createCloudflareDeploy = (app: NormalizedApp) => ({ assetsBinding: 'ASSETS', compatibilityDate: expectedCloudflareCompatibilityDate, - compatibilityFlags: [SHARED_VALIDATOR_STRING_089, SHARED_VALIDATOR_STRING_070], + compatibilityFlags: [ + SHARED_VALIDATOR_STRING_089, + SHARED_VALIDATOR_STRING_070, + ], evidence: { proofScript: SHARED_VALIDATOR_STRING_119, reportDefault: SHARED_VALIDATOR_STRING_008, @@ -3439,7 +3729,10 @@ const createAppConfigContract = (app: NormalizedApp) => ({ output: { assetPrefix: { default: app.kind === 'shell' ? '/' : SHARED_VALIDATOR_STRING_045, - envFallbackOrder: [SHARED_VALIDATOR_STRING_080, SHARED_VALIDATOR_STRING_143], + envFallbackOrder: [ + SHARED_VALIDATOR_STRING_080, + SHARED_VALIDATOR_STRING_143, + ], }, disableTsChecker: false, }, @@ -3488,7 +3781,10 @@ const cssDedupe = () => ({ strategy: 'shared-token-package-plus-css-content-hash', }); const createStylingContract = (app: NormalizedApp) => { - const sharedTokenPackage = packageNameFor(packageScope, SHARED_VALIDATOR_STRING_128); + const sharedTokenPackage = packageNameFor( + packageScope, + SHARED_VALIDATOR_STRING_128 + ); const ownedLayers = app.kind === 'shell' ? [SHARED_VALIDATOR_STRING_150, 'ultramodern-shell-overlay'] @@ -3559,7 +3855,9 @@ const createApiContract = (app: NormalizedApp) => { client: app.apiClientExport, contract: app.apiContractExport, domainOperations: - app.apiContractExport === undefined ? undefined : createEffectDomainOperations(app), + app.apiContractExport === undefined + ? undefined + : createEffectDomainOperations(app), import: '@modern-js/plugin-bff/effect-edge', openapi: '/openapi.json', prefix: api.prefix, @@ -3571,7 +3869,10 @@ const createApiContract = (app: NormalizedApp) => { ...createEffectOperationContract(app), }; }; -const createAppFederationContract = (app: NormalizedApp, apps: readonly NormalizedApp[]) => ({ +const createAppFederationContract = ( + app: NormalizedApp, + apps: readonly NormalizedApp[] +) => ({ browserSafeExposesOnly: true, dts: app.kind === 'shell' || app.exposes.length > 0 @@ -3583,10 +3884,14 @@ const createAppFederationContract = (app: NormalizedApp, apps: readonly Normaliz : undefined, exposes: app.exposes, name: app.mfName, - remotes: app.verticalRefs.length > 0 ? remoteContractsFor(app, apps) : undefined, + remotes: + app.verticalRefs.length > 0 ? remoteContractsFor(app, apps) : undefined, verticalRefs: app.verticalRefs.length > 0 ? app.verticalRefs : undefined, }); -const createAppContract = (app: NormalizedApp, apps: readonly NormalizedApp[]) => ({ +const createAppContract = ( + app: NormalizedApp, + apps: readonly NormalizedApp[] +) => ({ api: createApiContract(app), config: createAppConfigContract(app), deploy: { @@ -3685,7 +3990,7 @@ const createPerformanceReadinessContract = () => ({ ].map((id) => ({ id })), }); const createModernPackageAliases = ( - packageSourceConfig: CompactConfig['packageSource'], + packageSourceConfig: CompactConfig['packageSource'] ): Readonly> | undefined => { if (!isString(packageSourceConfig?.aliasScope)) { return undefined; @@ -3698,28 +4003,31 @@ const createModernPackageAliases = ( modernPackageCohort.map((packageName) => [ packageName, `@${scope}/${prefix}${packageName.split('/').at(-1)}`, - ]), + ]) ); }; const createPackageSourceView = (config: CompactConfig) => { const source = config.packageSource; assert( isPackageSourceDocument(source), - `${compactConfigPath} packageSource must be a JSON object`, + `${compactConfigPath} packageSource must be a JSON object` ); assert( source.strategy === 'workspace' || source.strategy === 'install', - `${compactConfigPath} packageSource.strategy must be workspace or install`, + `${compactConfigPath} packageSource.strategy must be workspace or install` ); if (source.strategy === 'install') { assert( - isString(source.modernPackageVersion) && source.modernPackageVersion.length > 0, - `${compactConfigPath} install package source must declare modernPackageVersion`, + isString(source.modernPackageVersion) && + source.modernPackageVersion.length > 0, + `${compactConfigPath} install package source must declare modernPackageVersion` ); } const { strategy } = source; const specifier = - strategy === 'install' ? source.modernPackageVersion : SHARED_VALIDATOR_STRING_169; + strategy === 'install' + ? source.modernPackageVersion + : SHARED_VALIDATOR_STRING_169; const aliases = createModernPackageAliases(source); return { generatedWorkspacePackages: { @@ -3772,8 +4080,13 @@ const expectedBackendManifestEnv = (vertical: FullStackVertical): string => const expectedPublicUrlEnv = (vertical: FullStackVertical): string => `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(vertical.id)}`; const expectedCloudflareWorkerName = (vertical: FullStackVertical): string => - toKebabCase(`${packageScope}-${vertical.packageSuffix ?? vertical.id}`).slice(0, 63); -const backendFederationSubset = (backendFederation: CompactBackendFederation | undefined) => { + toKebabCase(`${packageScope}-${vertical.packageSuffix ?? vertical.id}`).slice( + 0, + 63 + ); +const backendFederationSubset = ( + backendFederation: CompactBackendFederation | undefined +) => { if (backendFederation === undefined) { return { cloudflare: {}, @@ -3782,7 +4095,8 @@ const backendFederationSubset = (backendFederation: CompactBackendFederation | u versionBoundary: {}, }; } - const effectApiExpose = backendFederation.exposes[SHARED_VALIDATOR_STRING_004]; + const effectApiExpose = + backendFederation.exposes[SHARED_VALIDATOR_STRING_004]; const { cloudflare, node } = backendFederation.executionSurfaces; return { cloudflare: { @@ -3836,7 +4150,9 @@ const expectedBackendFederationSubset = (vertical: FullStackVertical) => ({ contractVersion: SHARED_VALIDATOR_STRING_079, }, exposeReadiness: - vertical.apiProtocol === 'rpc' ? undefined : `${vertical.apiPrefix}/${vertical.stem}/readiness`, + vertical.apiProtocol === 'rpc' + ? undefined + : `${vertical.apiPrefix}/${vertical.stem}/readiness`, node: { containerEntry: expectedBackendContainerEntry(vertical), expose: SHARED_VALIDATOR_STRING_004, @@ -3855,7 +4171,9 @@ const expectedBackendFederationSubset = (vertical: FullStackVertical) => ({ uiManifestUrl: expectedManifestUrl(vertical), }, }); -const serverExecutionSubset = (serverExecution: OverlayServerExecution | undefined) => ({ +const serverExecutionSubset = ( + serverExecution: OverlayServerExecution | undefined +) => ({ apiBaseUrl: serverExecution?.apiBaseUrl, cloudflareKind: serverExecution?.cloudflare?.kind, cloudflareWorkerName: serverExecution?.cloudflare?.workerName, @@ -3874,7 +4192,7 @@ const expectedServerExecutionSubset = (vertical: FullStackVertical) => ({ versionBoundary: SHARED_VALIDATOR_STRING_168, }); const remoteContractSubset = ( - remote: Pick | undefined, + remote: Pick | undefined ) => ({ id: remote?.id, manifestUrl: remote?.manifestUrl, @@ -3888,8 +4206,12 @@ const expectedRemoteContractSubset = (vertical: FullStackVertical) => ({ const expectedRemoteSubsetsForRefs = (refs: readonly string[]) => refs .flatMap((ref) => { - const vertical = fullStackVerticals.find((candidate) => candidate.id === ref); - return vertical === undefined || vertical.exposes.length === 0 ? [] : [vertical]; + const vertical = fullStackVerticals.find( + (candidate) => candidate.id === ref + ); + return vertical === undefined || vertical.exposes.length === 0 + ? [] + : [vertical]; }) .map(expectedRemoteContractSubset); const requiredMicroVerticalPaths = (vertical: FullStackVertical): string[] => [ @@ -3910,7 +4232,9 @@ const requiredMicroVerticalPaths = (vertical: FullStackVertical): string[] => [ ...(vertical.emitsUi ? [ `${vertical.path}/module-federation.config.ts`, - ...(vertical.hasFederationEntry ? [`${vertical.path}/src/federation-entry.tsx`] : []), + ...(vertical.hasFederationEntry + ? [`${vertical.path}/src/federation-entry.tsx`] + : []), ...vertical.componentPaths, `${vertical.path}/src/routes/index.css`, `${vertical.path}/src/routes/layout.tsx`, @@ -3944,7 +4268,9 @@ const requiredMicroVerticalPaths = (vertical: FullStackVertical): string[] => [ const forbiddenMicroVerticalPaths = (vertical: FullStackVertical): string[] => [ // Structured data is owner-page-only: a unit that renders no owner page emits no // `application/ld+json`, so it must not ship the JSON-LD helper module either. - ...(vertical.hasOwnerPage ? [] : [`${vertical.path}/src/routes/ultramodern-jsonld.ts`]), + ...(vertical.hasOwnerPage + ? [] + : [`${vertical.path}/src/routes/ultramodern-jsonld.ts`]), ...(vertical.emitsUi ? [] : [ @@ -3987,7 +4313,7 @@ const assertRequiredVerticalFile = fs.existsSync(path.join(root, relativePath)), `required files for ${vertical.id}`, `Missing ${relativePath}`, - 'restore the generated MicroVertical files or rerun the MicroVertical generator', + 'restore the generated MicroVertical files or rerun the MicroVertical generator' ); }; const assertForbiddenVerticalFile = @@ -3997,62 +4323,71 @@ const assertForbiddenVerticalFile = !fs.existsSync(path.join(root, relativePath)), `forbidden files for ${vertical.id}`, `Unexpected ${relativePath} for a ${vertical.surfaceProfile} unit`, - `remove ${relativePath}; a ${vertical.surfaceProfile} unit does not emit this surface`, + `remove ${relativePath}; a ${vertical.surfaceProfile} unit does not emit this surface` ); }; const verticalExposes = (vertical: FullStackVertical): string[] => - Array.isArray(vertical.exposes) ? vertical.exposes : Object.keys(vertical.exposes ?? {}); -const regenerateMicroVerticalContractFix = 'regenerate the generated MicroVertical contract entry'; + Array.isArray(vertical.exposes) + ? vertical.exposes + : Object.keys(vertical.exposes ?? {}); +const regenerateMicroVerticalContractFix = + 'regenerate the generated MicroVertical contract entry'; const assertTopologyVerticalDeliveryUnitContract = ( vertical: FullStackVertical, - topologyEntry: ReferenceTopologyVertical, + topologyEntry: ReferenceTopologyVertical ): void => { if (vertical.deliveryUnit === undefined) { return; } const compactApp = findById(ultramodernConfig.topology?.apps, vertical.id); - const expectedDeliveryUnit = deliveryUnitBlock(expectedDeliveryUnitFor(vertical)); + const expectedDeliveryUnit = deliveryUnitBlock( + expectedDeliveryUnitFor(vertical) + ); assertSameJson( deliveryUnitBlock(compactApp?.deliveryUnit), expectedDeliveryUnit, `${generatedContractLabel} topology.apps.${vertical.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); if (vertical.emitsApi) { assertSameJson( deliveryUnitBlock(compactApp?.backendFederation?.deliveryUnit), expectedDeliveryUnit, `${generatedContractLabel} topology.apps.${vertical.id}.backendFederation.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); } assertSameJson( deliveryUnitBlock(topologyEntry.deliveryUnit), expectedDeliveryUnit, `topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); if (vertical.emitsApi) { assertSameJson( deliveryUnitBlock(topologyEntry.backendFederation?.deliveryUnit), expectedDeliveryUnit, `topology/reference-topology.json verticals.${vertical.id}.backendFederation.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSelfCheck( topologyEntry.backendFederation?.versionBoundary?.identityRoot === SHARED_VALIDATOR_STRING_065, `topology/reference-topology.json verticals.${vertical.id}.backendFederation.versionBoundary.identityRoot`, `Expected "deliveryUnit", found ${formatJson(topologyEntry.backendFederation?.versionBoundary?.identityRoot)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); } }; -const topologyVerticalFederationView = (topologyEntry: ReferenceTopologyVertical) => ({ +const topologyVerticalFederationView = ( + topologyEntry: ReferenceTopologyVertical +) => ({ exposes: topologyEntry.moduleFederation?.exposes ?? [], manifestUrl: topologyEntry.moduleFederation?.manifestUrl, name: topologyEntry.moduleFederation?.name, - remotes: (topologyEntry.moduleFederation?.remotes ?? []).map(remoteContractSubset), + remotes: (topologyEntry.moduleFederation?.remotes ?? []).map( + remoteContractSubset + ), verticalRefs: topologyEntry.moduleFederation?.verticalRefs ?? [], }); const assertTopologyVerticalContract = (vertical: FullStackVertical): void => { @@ -4061,7 +4396,7 @@ const assertTopologyVerticalContract = (vertical: FullStackVertical): void => { assertObject( topologyEntry, `topology/reference-topology.json verticals.${vertical.id}`, - SHARED_VALIDATOR_STRING_112, + SHARED_VALIDATOR_STRING_112 ); if (topologyEntry === undefined) { return; @@ -4098,24 +4433,26 @@ const assertTopologyVerticalContract = (vertical: FullStackVertical): void => { path: vertical.path, }, `topology/reference-topology.json verticals.${vertical.id}`, - SHARED_VALIDATOR_STRING_112, + SHARED_VALIDATOR_STRING_112 ); if (vertical.emitsApi) { assertSameJson( backendFederationSubset(topologyEntry.backendFederation), expectedBackendFederationSubset(vertical), `topology/reference-topology.json verticals.${vertical.id}.backendFederation`, - 'restore generated MicroVertical server execution contract', + 'restore generated MicroVertical server execution contract' ); } assertTopologyVerticalDeliveryUnitContract(vertical, topologyEntry); }; -const assertVerticalOwnershipAndOverlay = (vertical: FullStackVertical): void => { +const assertVerticalOwnershipAndOverlay = ( + vertical: FullStackVertical +): void => { const ownershipEntry = findById(ownership.owners, vertical.id); assertObject( ownershipEntry, `topology/ownership.json owners.${vertical.id}`, - SHARED_VALIDATOR_STRING_111, + SHARED_VALIDATOR_STRING_111 ); if (ownershipEntry === undefined) { return; @@ -4124,20 +4461,20 @@ const assertVerticalOwnershipAndOverlay = (vertical: FullStackVertical): void => { package: ownershipEntry.package, path: ownershipEntry.path }, { package: vertical.packageName, path: vertical.path }, `topology/ownership.json owners.${vertical.id}`, - SHARED_VALIDATOR_STRING_111, + SHARED_VALIDATOR_STRING_111 ); assertSameJson( valueForKey(Object.entries(overlay.ports), vertical.id), vertical.port, `topology/local-overlays/development.json ports.${vertical.id}`, - 'restore generated local development port overlay', + 'restore generated local development port overlay' ); if (vertical.emitsUi) { assertSameJson( valueForKey(Object.entries(overlay.manifests), vertical.id), expectedManifestUrl(vertical), `topology/local-overlays/development.json manifests.${vertical.id}`, - 'restore generated local Module Federation manifest overlay', + 'restore generated local Module Federation manifest overlay' ); } if (vertical.emitsApi) { @@ -4145,58 +4482,67 @@ const assertVerticalOwnershipAndOverlay = (vertical: FullStackVertical): void => valueForKey(Object.entries(overlay.apis), vertical.id), expectedApiUrl(vertical), `topology/local-overlays/development.json apis.${vertical.id}`, - 'restore generated local API overlay', + 'restore generated local API overlay' ); assertSameJson( serverExecutionSubset( - valueForKey(Object.entries(overlay.serverExecution ?? {}), vertical.id), + valueForKey(Object.entries(overlay.serverExecution ?? {}), vertical.id) ), expectedServerExecutionSubset(vertical), `topology/local-overlays/development.json serverExecution.${vertical.id}`, - 'restore generated local MicroVertical server execution overlay', + 'restore generated local MicroVertical server execution overlay' ); } }; const assertShellDependenciesForVertical = ( vertical: FullStackVertical, - expectedShellVerticalIds: readonly string[], + expectedShellVerticalIds: readonly string[] ): void => { - const composed = expectedShellVerticalIds.includes(vertical.id) && vertical.exposes.length > 0; + const composed = + expectedShellVerticalIds.includes(vertical.id) && + vertical.exposes.length > 0; if (vertical.emitsApi || composed) { assertSameJson( - valueForKey(Object.entries(shellPackage.dependencies ?? {}), vertical.packageName), + valueForKey( + Object.entries(shellPackage.dependencies ?? {}), + vertical.packageName + ), SHARED_VALIDATOR_STRING_169, `${SHARED_VALIDATOR_STRING_047}/package.json dependencies.${vertical.packageName}`, - 'restore shell dependency for the MicroVertical consumer', + 'restore shell dependency for the MicroVertical consumer' ); } if (composed) { assertSameJson( valueForKey( Object.entries(shellPackage[SHARED_VALIDATOR_STRING_173] ?? {}), - vertical.zephyrAlias, + vertical.zephyrAlias ), `${vertical.packageName}@workspace:*`, `${SHARED_VALIDATOR_STRING_047}/package.json zephyr:dependencies.${vertical.zephyrAlias}`, - 'restore shell Zephyr dependency metadata for the MicroVertical', + 'restore shell Zephyr dependency metadata for the MicroVertical' ); } }; -const generatedVerticalFederationView = (contractEntry: ReturnType) => ({ +const generatedVerticalFederationView = ( + contractEntry: ReturnType +) => ({ exposes: contractEntry.moduleFederation?.exposes ?? [], name: contractEntry.moduleFederation?.name, - remotes: (contractEntry.moduleFederation?.remotes ?? []).map(remoteContractSubset), + remotes: (contractEntry.moduleFederation?.remotes ?? []).map( + remoteContractSubset + ), verticalRefs: contractEntry.moduleFederation?.verticalRefs ?? [], }); const assertGeneratedVerticalContract = ( vertical: FullStackVertical, - generatedContract: ReturnType, + generatedContract: ReturnType ): void => { const contractEntry = findById(generatedContract.apps, vertical.id); assertObject( contractEntry, `${generatedContractLabel} apps.${vertical.id}`, - regenerateMicroVerticalContractFix, + regenerateMicroVerticalContractFix ); if (contractEntry === undefined) { return; @@ -4237,19 +4583,22 @@ const assertGeneratedVerticalContract = ( ssr: { mode: 'stream', moduleFederationAppSSR: true }, }, `${generatedContractLabel} apps.${vertical.id}`, - regenerateMicroVerticalContractFix, + regenerateMicroVerticalContractFix ); }; const assertGeneratedPrimaryShellContract = ( generatedContract: ReturnType, expectedShellVerticalIds: readonly string[], - expectedShellRemotes: ReturnType[], + expectedShellRemotes: ReturnType[] ): boolean => { - const shellContract = findById(generatedContract.apps, SHARED_VALIDATOR_STRING_131); + const shellContract = findById( + generatedContract.apps, + SHARED_VALIDATOR_STRING_131 + ); assertObject( shellContract, `${generatedContractLabel} apps.shell-super-app`, - 'regenerate the generated shell contract entry', + 'regenerate the generated shell contract entry' ); if (shellContract === undefined) { return false; @@ -4258,13 +4607,13 @@ const assertGeneratedPrimaryShellContract = ( shellContract.moduleFederation?.verticalRefs ?? [], expectedShellVerticalIds, `${generatedContractLabel} shell moduleFederation.verticalRefs`, - 'regenerate the generated shell Module Federation contract', + 'regenerate the generated shell Module Federation contract' ); assertSameJson( (shellContract.moduleFederation?.remotes ?? []).map(remoteContractSubset), expectedShellRemotes, `${generatedContractLabel} shell moduleFederation.remotes`, - 'regenerate the generated shell Module Federation contract', + 'regenerate the generated shell Module Federation contract' ); assertSameJson( shellContract.ssr, @@ -4273,101 +4622,109 @@ const assertGeneratedPrimaryShellContract = ( moduleFederationAppSSR: true, }, `${generatedContractLabel} shell SSR contract`, - 'restore generated streaming SSR Module Federation settings', + 'restore generated streaming SSR Module Federation settings' ); return true; }; const assertMicroVerticalContractGraph = ( - generatedContract: ReturnType, + generatedContract: ReturnType ): void => { const expectedVerticalIds = fullStackVerticals.map((vertical) => vertical.id); const expectedAppIds = [SHARED_VALIDATOR_STRING_131, ...expectedVerticalIds]; const expectedShellVerticalIds = expectedPrimaryShellVerticalIds; - const expectedShellRemotes = expectedShellVerticalIds.flatMap((verticalId) => { - const vertical = fullStackVerticals.find((candidate) => candidate.id === verticalId); - return vertical === undefined || vertical.exposes.length === 0 - ? [] - : [expectedRemoteContractSubset(vertical)]; - }); + const expectedShellRemotes = expectedShellVerticalIds.flatMap( + (verticalId) => { + const vertical = fullStackVerticals.find( + (candidate) => candidate.id === verticalId + ); + return vertical === undefined || vertical.exposes.length === 0 + ? [] + : [expectedRemoteContractSubset(vertical)]; + } + ); assertObject( topology.shell, 'topology/reference-topology.json shell', - 'restore generated topology shell metadata', + 'restore generated topology shell metadata' ); assertArray( topology.verticals, 'topology/reference-topology.json verticals', - SHARED_VALIDATOR_STRING_112, + SHARED_VALIDATOR_STRING_112 ); assertObject( topology.shell?.moduleFederation, 'topology/reference-topology.json shell.moduleFederation', - 'restore generated shell Module Federation metadata', + 'restore generated shell Module Federation metadata' ); assertArray( topology.shell?.moduleFederation?.remotes, 'topology/reference-topology.json shell.moduleFederation.remotes', - 'restore generated shell Module Federation remotes', + 'restore generated shell Module Federation remotes' + ); + assertArray( + ownership.owners, + 'topology/ownership.json owners', + SHARED_VALIDATOR_STRING_111 ); - assertArray(ownership.owners, 'topology/ownership.json owners', SHARED_VALIDATOR_STRING_111); assertObject( overlay.ports, 'topology/local-overlays/development.json ports', - 'restore generated local development port overlays', + 'restore generated local development port overlays' ); assertObject( overlay.manifests, 'topology/local-overlays/development.json manifests', - 'restore generated local Module Federation manifest overlays', + 'restore generated local Module Federation manifest overlays' ); assertObject( overlay.ontosModuleManifests, 'topology/local-overlays/development.json ontosModuleManifests', - 'restore generated OntOS module contract allowlist overlays', + 'restore generated OntOS module contract allowlist overlays' ); assertObject( overlay.apis, 'topology/local-overlays/development.json apis', - 'restore generated local API overlays', + 'restore generated local API overlays' ); assertArray( generatedContract.apps, `${generatedContractLabel} apps`, - 'regenerate the generated contract from the workspace topology', + 'regenerate the generated contract from the workspace topology' ); assertSameJson( topology.shell.verticalRefs ?? [], expectedShellVerticalIds, 'topology/reference-topology.json shell.verticalRefs', - 'restore generated topology shell references', + 'restore generated topology shell references' ); assertSameJson( topology.verticals.map((vertical) => vertical?.id), expectedVerticalIds, 'topology/reference-topology.json verticals', - SHARED_VALIDATOR_STRING_112, + SHARED_VALIDATOR_STRING_112 ); assertSameJson( topology.shell.moduleFederation.remotes.map(remoteContractSubset), expectedShellRemotes, 'topology/reference-topology.json shell.moduleFederation.remotes', - 'restore generated shell Module Federation remotes', + 'restore generated shell Module Federation remotes' ); assertSameJson( generatedContract.apps.map((app) => app?.id), expectedAppIds, `${generatedContractLabel} apps`, - 'regenerate the generated contract after topology changes', + 'regenerate the generated contract after topology changes' ); if ( !assertGeneratedPrimaryShellContract( generatedContract, expectedShellVerticalIds, - expectedShellRemotes, + expectedShellRemotes ) ) { return; @@ -4392,43 +4749,52 @@ const sharedPackagePaths = [ SHARED_VALIDATOR_STRING_094, SHARED_VALIDATOR_STRING_096, ]; -const workspacePackagePaths = [...infrastructurePackagePaths, ...sharedPackagePaths]; +const workspacePackagePaths = [ + ...infrastructurePackagePaths, + ...sharedPackagePaths, +]; const tsgoCacheKey = (packagePath: string): string => packagePath.replaceAll(/[^a-zA-Z0-9._-]+/gu, '__'); -const assertProjectReferenceEmitConfig = (tsConfig: TsConfig, packagePath: string): void => { +const assertProjectReferenceEmitConfig = ( + tsConfig: TsConfig, + packagePath: string +): void => { const compilerOptions = tsConfig.compilerOptions ?? {}; const relativeRoot = toPosixPath(path.relative(packagePath, '.')) ?? '.'; - assert(compilerOptions.composite === true, `${packagePath} must stay a composite TS-Go project`); + assert( + compilerOptions.composite === true, + `${packagePath} must stay a composite TS-Go project` + ); assert( compilerOptions.declaration === true, - `${packagePath} must emit declarations for TS-Go build mode`, + `${packagePath} must emit declarations for TS-Go build mode` ); assert( compilerOptions.declarationMap === false, - `${packagePath} must not emit declaration maps during checks`, + `${packagePath} must not emit declaration maps during checks` ); assert( compilerOptions.emitDeclarationOnly === true, - `${packagePath} must only emit declarations during checks`, + `${packagePath} must only emit declarations during checks` ); assert( compilerOptions.noEmit === false, - `${packagePath} must override root noEmit for TS-Go build mode`, + `${packagePath} must override root noEmit for TS-Go build mode` ); assert( compilerOptions.outDir === `${relativeRoot}/node_modules/.cache/tsgo/declarations/${tsgoCacheKey(packagePath)}`, - `${packagePath} must emit TS-Go declarations into the generated cache`, + `${packagePath} must emit TS-Go declarations into the generated cache` ); assert( compilerOptions.tsBuildInfoFile === `${relativeRoot}/node_modules/.cache/tsgo/${tsgoCacheKey(packagePath)}.tsbuildinfo`, - `${packagePath} must keep TS-Go build info in the generated cache`, + `${packagePath} must keep TS-Go build info in the generated cache` ); }; const expectedVerticalTypecheckIncludes = ( vertical: FullStackVertical, - verticalPackage: PackageJson, + verticalPackage: PackageJson ) => vertical.typecheckIncludes ?? [ 'src', @@ -4440,13 +4806,21 @@ const expectedVerticalTypecheckIncludes = ( ? [] : ['vertical.manifest.ts', 'vertical.registration.ts']), ]; -const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void => { - const verticalTsConfig = readJson(TsConfigSchema, `${vertical.path}/tsconfig.json`); +const assertVerticalTsConfigReferenceGraph = ( + vertical: FullStackVertical +): void => { + const verticalTsConfig = readJson( + TsConfigSchema, + `${vertical.path}/tsconfig.json` + ); const verticalMfTypesTsConfig = readJson( TsConfigSchema, - `${vertical.path}/tsconfig.mf-types.json`, + `${vertical.path}/tsconfig.mf-types.json` + ); + const verticalPackage = readJson( + PackageJsonSchema, + `${vertical.path}/package.json` ); - const verticalPackage = readJson(PackageJsonSchema, `${vertical.path}/package.json`); const sourceSpecifiers = ['api', 'shared', 'src'] .flatMap((sourceRoot) => { const sourceRootPath = `${vertical.path}/${sourceRoot}`; @@ -4462,7 +4836,7 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void const source = readText(sourcePath); return [ ...source.matchAll( - /\b(?:import|export)\s+(?:type\s+)?[^;'"`]+?\s+from\s*['"](?[^'"]+)['"]/gu, + /\b(?:import|export)\s+(?:type\s+)?[^;'"`]+?\s+from\s*['"](?[^'"]+)['"]/gu ), ...source.matchAll(/\bimport\s*['"](?[^'"]+)['"]/gu), ...source.matchAll(/\bimport\s*\(\s*['"](?[^'"]+)['"]/gu), @@ -4474,20 +4848,29 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void }); const topologyVerticalRefs = new Set(vertical.verticalRefs); const publishedContractRefs = fullStackVerticals.flatMap((candidate) => { - if (candidate.id === vertical.id || topologyVerticalRefs.has(candidate.id)) { + if ( + candidate.id === vertical.id || + topologyVerticalRefs.has(candidate.id) + ) { return []; } const importsCandidate = sourceSpecifiers.some( (specifier) => - specifier === candidate.packageName || specifier.startsWith(`${candidate.packageName}/`), + specifier === candidate.packageName || + specifier.startsWith(`${candidate.packageName}/`) ); const dependencyDeclared = - valueForKey(Object.entries(verticalPackage.dependencies ?? {}), candidate.packageName) === - SHARED_VALIDATOR_STRING_169; + valueForKey( + Object.entries(verticalPackage.dependencies ?? {}), + candidate.packageName + ) === SHARED_VALIDATOR_STRING_169; if (!importsCandidate && !dependencyDeclared) { return []; } - const candidatePackage = readJson(PackageJsonSchema, `${candidate.path}/package.json`); + const candidatePackage = readJson( + PackageJsonSchema, + `${candidate.path}/package.json` + ); const producerModuleId = resolvePublishedContractModuleId({ dependencyPackageJson: candidatePackage, dependencyPackageName: candidate.packageName, @@ -4501,20 +4884,23 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void dependencyPackageName: candidate.packageName, moduleSpecifiers: sourceSpecifiers, projectReferenceDeclared: (verticalTsConfig.references ?? []).some( - (reference) => reference.path === expectedReference.path, + (reference) => reference.path === expectedReference.path ), - readExportSource: (exportTarget) => readText(`${candidate.path}/${exportTarget.slice(2)}`), + readExportSource: (exportTarget) => + readText(`${candidate.path}/${exportTarget.slice(2)}`), }); for (const exportKey of publishedOutboxContractExports(candidatePackage)) { const exportTarget = candidatePackage.exports?.[exportKey]; assert( isString(exportTarget), - `${candidate.packageName}${exportKey.slice(1)} must resolve to one source file`, + `${candidate.packageName}${exportKey.slice(1)} must resolve to one source file` ); if (exportTarget === undefined) { continue; } - const contractSource = readText(`${candidate.path}/${exportTarget.slice(2)}`); + const contractSource = readText( + `${candidate.path}/${exportTarget.slice(2)}` + ); assertPublishedOutboxContractSource({ moduleId: producerModuleId, source: contractSource, @@ -4530,7 +4916,7 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void ...(vertical.verticalRefs ?? []) .flatMap((verticalRef) => { const referencedVertical = fullStackVerticals.find( - (candidate) => candidate.id === verticalRef, + (candidate) => candidate.id === verticalRef ); return referencedVertical === undefined ? [] : [referencedVertical]; }) @@ -4542,13 +4928,13 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void verticalTsConfig.references ?? [], expectedVerticalReferences, `${vertical.path}/tsconfig.json references`, - 'restore the generated MicroVertical project-reference graph', + 'restore the generated MicroVertical project-reference graph' ); assertSameJson( verticalTsConfig.include ?? [], expectedVerticalTypecheckIncludes(vertical, verticalPackage), `${vertical.path}/tsconfig.json include`, - 'restore the generated MicroVertical typecheck boundary', + 'restore the generated MicroVertical typecheck boundary' ); assertProjectReferenceEmitConfig(verticalTsConfig, vertical.path); assertSameJson( @@ -4562,7 +4948,7 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void ? [ SHARED_VALIDATOR_STRING_136, ...vertical.componentPaths.map((componentPath) => - componentPath.replace(`${vertical.path}/`, ''), + componentPath.replace(`${vertical.path}/`, '') ), ...(vertical.emitsApi ? [vertical.apiContractPath] : []), SHARED_VALIDATOR_STRING_137, @@ -4570,32 +4956,46 @@ const assertVerticalTsConfigReferenceGraph = (vertical: FullStackVertical): void : [SHARED_VALIDATOR_STRING_137], }, `${vertical.path}/tsconfig.mf-types.json`, - 'restore the generated MicroVertical Module Federation DTS boundary', + 'restore the generated MicroVertical Module Federation DTS boundary' ); }; const primaryShellTsConfigReferences = () => { const expectedShellReferences = [ SHARED_VALIDATOR_STRING_092, - ...sharedPackagePaths.filter((packagePath) => packagePath !== SHARED_VALIDATOR_STRING_177), + ...sharedPackagePaths.filter( + (packagePath) => packagePath !== SHARED_VALIDATOR_STRING_177 + ), ...(topology.shell?.verticalRefs ?? []) .flatMap((verticalRef) => { - const vertical = fullStackVerticals.find((candidate) => candidate.id === verticalRef); + const vertical = fullStackVerticals.find( + (candidate) => candidate.id === verticalRef + ); return vertical === undefined ? [] : [vertical]; }) // The shell only project-references verticals whose API client types it // imports; UI-only remotes are federated at runtime, not type-referenced. .filter((vertical) => vertical.emitsApi) .map((vertical) => vertical.path), - ].map((referencePath) => referenceFrom(SHARED_VALIDATOR_STRING_047, referencePath)); + ].map((referencePath) => + referenceFrom(SHARED_VALIDATOR_STRING_047, referencePath) + ); return expectedShellReferences; }; const assertTsConfigReferenceGraph = () => { const baseTsConfig = readJson(TsConfigSchema, 'tsconfig.base.json'); const rootTsConfig = readJson(TsConfigSchema, 'tsconfig.json'); - const shellTsConfig = readJson(TsConfigSchema, 'apps/shell-super-app/tsconfig.json'); - const shellMfTypesTsConfig = readJson(TsConfigSchema, SHARED_VALIDATOR_STRING_052); - const additionalShellPaths = (workspaceValidationContract.structuralShellPolicy?.shells ?? []) + const shellTsConfig = readJson( + TsConfigSchema, + 'apps/shell-super-app/tsconfig.json' + ); + const shellMfTypesTsConfig = readJson( + TsConfigSchema, + SHARED_VALIDATOR_STRING_052 + ); + const additionalShellPaths = ( + workspaceValidationContract.structuralShellPolicy?.shells ?? [] + ) .filter((shell) => shell.id !== SHARED_VALIDATOR_STRING_131) .map((shell) => shell.packageDir); const expectedRootReferences = [ @@ -4610,38 +5010,51 @@ const assertTsConfigReferenceGraph = () => { rootTsConfig.files, [], 'tsconfig.json files', - 'restore the generated root project-reference tsconfig', + 'restore the generated root project-reference tsconfig' ); assertSameJson( rootTsConfig.references ?? [], expectedRootReferences, 'tsconfig.json references', - 'restore the generated root project-reference graph', + 'restore the generated root project-reference graph' ); assertSameJson( EffectArray.sort( shellTsConfig.references ?? [], - Order.mapInput(Order.String, (reference: { readonly path: string }) => reference.path), + Order.mapInput( + Order.String, + (reference: { readonly path: string }) => reference.path + ) ), EffectArray.sort( expectedShellReferences, - Order.mapInput(Order.String, (reference: { readonly path: string }) => reference.path), + Order.mapInput( + Order.String, + (reference: { readonly path: string }) => reference.path + ) ), 'apps/shell-super-app/tsconfig.json references', - 'restore the generated shell project-reference graph', + 'restore the generated shell project-reference graph' ); assert( baseTsConfig.compilerOptions?.skipLibCheck !== true, - 'tsconfig.base.json must not use skipLibCheck', + 'tsconfig.base.json must not use skipLibCheck' ); assertSameJson( EffectArray.sort(shellTsConfig.include ?? [], Order.String), EffectArray.sort( - ['api', 'server', 'src', SHARED_VALIDATOR_STRING_075, SHARED_VALIDATOR_STRING_091, 'shared'], - Order.String, + [ + 'api', + 'server', + 'src', + SHARED_VALIDATOR_STRING_075, + SHARED_VALIDATOR_STRING_091, + 'shared', + ], + Order.String ), 'apps/shell-super-app/tsconfig.json include', - 'restore the generated shell typecheck boundary', + 'restore the generated shell typecheck boundary' ); assertProjectReferenceEmitConfig(shellTsConfig, SHARED_VALIDATOR_STRING_047); assertSameJson( @@ -4651,12 +5064,12 @@ const assertTsConfigReferenceGraph = () => { include: [SHARED_VALIDATOR_STRING_137], }, SHARED_VALIDATOR_STRING_052, - 'restore the generated shell Module Federation DTS boundary', + 'restore the generated shell Module Federation DTS boundary' ); for (const workspacePackagePath of workspacePackagePaths) { assertProjectReferenceEmitConfig( readJson(TsConfigSchema, `${workspacePackagePath}/tsconfig.json`), - workspacePackagePath, + workspacePackagePath ); } @@ -4673,7 +5086,16 @@ const packageJsonFiles = (startDir: string): string[] => { continue; } for (const entry of fs.readdirSync(current, { withFileTypes: true })) { - if (['.git', '.output', '.zerops', 'dist', 'node_modules', 'repos'].includes(entry.name)) { + if ( + [ + '.git', + '.output', + '.zerops', + 'dist', + 'node_modules', + 'repos', + ].includes(entry.name) + ) { continue; } const absolute = path.join(current, entry.name); @@ -4695,7 +5117,7 @@ const modernDependencyNames = (packageJson: PackageJson): string[] => [ packageJson.peerDependencies, ] .flatMap((section) => Object.keys(section ?? {})) - .filter((packageName) => packageName.startsWith('@modern-js/')), + .filter((packageName) => packageName.startsWith('@modern-js/')) ), ]; const packageDependencySections = [ @@ -4707,29 +5129,40 @@ const packageDependencySections = [ const observeModernPackageDependencies = ( packageJson: PackageJson, relativePath: string, - observedModernPackageNames: Set, + observedModernPackageNames: Set ): void => { - const modernPackageNameSet = new Set(workspaceValidationContract.cohort.modernPackages); - const standaloneModernTools = workspaceValidationContract.cohort.standaloneModernTools ?? {}; + const modernPackageNameSet = new Set( + workspaceValidationContract.cohort.modernPackages + ); + const standaloneModernTools = + workspaceValidationContract.cohort.standaloneModernTools ?? {}; for (const packageName of modernDependencyNames(packageJson)) { observedModernPackageNames.add(packageName); if (!modernPackageNameSet.has(packageName)) { - const expected = valueForKey(Object.entries(standaloneModernTools), packageName); + const expected = valueForKey( + Object.entries(standaloneModernTools), + packageName + ); assert( expected !== undefined, - `${relativePath} declares ${packageName} outside package source metadata`, + `${relativePath} declares ${packageName} outside package source metadata` ); const declared = packageDependencySections.map( - (section) => packageJson[section]?.[packageName], + (section) => packageJson[section]?.[packageName] ); assert( - declared.every((specifier) => specifier === undefined || specifier === expected), - `${relativePath} ${packageName} must match standalone package source metadata`, + declared.every( + (specifier) => specifier === undefined || specifier === expected + ), + `${relativePath} ${packageName} must match standalone package source metadata` ); } } }; -const assertModernPackageSpecifiers = (packageJson: PackageJson, relativePath: string): void => { +const assertModernPackageSpecifiers = ( + packageJson: PackageJson, + relativePath: string +): void => { const modernPackageNames = workspaceValidationContract.cohort.modernPackages; for (const section of packageDependencySections) { for (const packageName of modernPackageNames) { @@ -4737,7 +5170,7 @@ const assertModernPackageSpecifiers = (packageJson: PackageJson, relativePath: s if (actual !== undefined) { assert( actual === expectedModernPackageSpecifier(packageName), - `${relativePath} ${section}.${packageName} must match package source metadata`, + `${relativePath} ${section}.${packageName} must match package source metadata` ); } } @@ -4745,7 +5178,8 @@ const assertModernPackageSpecifiers = (packageJson: PackageJson, relativePath: s }; const assertModernPackageCohort = () => { const modernPackageNames = workspaceValidationContract.cohort.modernPackages; - const standaloneModernTools = workspaceValidationContract.cohort.standaloneModernTools ?? {}; + const standaloneModernTools = + workspaceValidationContract.cohort.standaloneModernTools ?? {}; const observedModernPackageNames = new Set(); const observedAppIds: string[] = []; // Additional shells (G28) are their own Delivery Units registered in the @@ -4755,10 +5189,13 @@ const assertModernPackageCohort = () => { const additionalShellAppIds = new Set( (workspaceValidationContract.structuralShellPolicy?.shells ?? []) .map((shell) => shell.id) - .filter((id) => id !== SHARED_VALIDATOR_STRING_131), + .filter((id) => id !== SHARED_VALIDATOR_STRING_131) ); for (const packageJsonPath of packageJsonFiles(root)) { - const relativePath = path.relative(root, packageJsonPath).split(path.sep).join('/'); + const relativePath = path + .relative(root, packageJsonPath) + .split(path.sep) + .join('/'); const packageJson = readJson(PackageJsonSchema, relativePath); if ( isString(packageJson.modernjs?.appId) && @@ -4766,20 +5203,24 @@ const assertModernPackageCohort = () => { ) { observedAppIds.push(packageJson.modernjs.appId); } - observeModernPackageDependencies(packageJson, relativePath, observedModernPackageNames); + observeModernPackageDependencies( + packageJson, + relativePath, + observedModernPackageNames + ); assertModernPackageSpecifiers(packageJson, relativePath); } for (const packageName of modernPackageNames) { assert( observedModernPackageNames.has(packageName), - `Modern package cohort is missing ${packageName}`, + `Modern package cohort is missing ${packageName}` ); } for (const packageName of Object.keys(standaloneModernTools)) { assert( observedModernPackageNames.has(packageName), - `Standalone Modern tool metadata is missing ${packageName}`, + `Standalone Modern tool metadata is missing ${packageName}` ); } assertUniqueStrings(observedAppIds, 'generated app package manifests'); @@ -4791,15 +5232,15 @@ const assertModernPackageCohort = () => { assertSameJson( sortedCopy(observedAppIds, (left, right) => left.localeCompare(right)), sortedCopy(workspaceValidationContract.cohort.appIds, (left, right) => - left.localeCompare(right), + left.localeCompare(right) ), 'generated app package manifest cohort', - 'restore every generated app package manifest', + 'restore every generated app package manifest' ); }; const assertPublicSurfaceAssets = ( appPath: string, - publicRoutes: ReturnType, + publicRoutes: ReturnType ): void => { for (const relativePath of publicSurfaceManagedSourceAssetPaths) { assertNotExists(`${appPath}/${relativePath}`); @@ -4808,77 +5249,80 @@ const assertPublicSurfaceAssets = ( }; const assertPublicSurfaceContract = ( appId: string, - publicSurface: ReturnType | undefined, + publicSurface: ReturnType | undefined ): void => { if (publicSurface === undefined) { - assert(false, `${appId} public surface artifacts must be build/deploy outputs`); + assert( + false, + `${appId} public surface artifacts must be build/deploy outputs` + ); return; } assert( publicSurface.artifactLifecycle === 'build-and-deploy-output', - `${appId} public surface artifacts must be build/deploy outputs`, + `${appId} public surface artifacts must be build/deploy outputs` ); assert( publicSurface.generator === SHARED_VALIDATOR_STRING_117, - `${appId} public surface generator script is incorrect`, + `${appId} public surface generator script is incorrect` ); assert( publicSurface.outputRoot === 'dist/public', - `${appId} public surface dist outputRoot is incorrect`, + `${appId} public surface dist outputRoot is incorrect` ); assert( publicSurface.cloudflareBuildOutputRoot === 'dist-cloudflare/public', - `${appId} public surface Cloudflare build outputRoot is incorrect`, + `${appId} public surface Cloudflare build outputRoot is incorrect` ); assert( !('cloudflareOutputRoot' in publicSurface), - `${appId} public surface must not target final .output directly`, + `${appId} public surface must not target final .output directly` ); assert( !('staticRoot' in publicSurface), - `${appId} public surface must not point at source config/public`, + `${appId} public surface must not point at source config/public` ); assert( publicSurface.files.includes(SHARED_VALIDATOR_STRING_113), - `${appId} public surface must always emit robots.txt`, + `${appId} public surface must always emit robots.txt` ); assert( publicSurface.contentExpansion.authoring === 'route-owned-esm-provider', - `${appId} public content expansion authoring is incorrect`, + `${appId} public content expansion authoring is incorrect` ); assert( publicSurface.contentExpansion.defaultProviderFile === 'route.sitemap.mjs', - `${appId} public content expansion provider file is incorrect`, + `${appId} public content expansion provider file is incorrect` ); assert( publicSurface.contentExpansion.draftPolicy === 'omit-draft-by-default', - `${appId} public content expansion draft policy is incorrect`, + `${appId} public content expansion draft policy is incorrect` ); assert( publicSurface.contentExpansion.indexablePolicy === 'omit-indexable-false', - `${appId} public content expansion indexable policy is incorrect`, + `${appId} public content expansion indexable policy is incorrect` ); assert( Array.isArray(publicSurface.contentSources), - `${appId} public content sources must be an array`, + `${appId} public content sources must be an array` ); if (publicSurface.publicRoutes.length === 0) { assert( !publicSurface.files.includes(SHARED_VALIDATOR_STRING_135), - `${appId} private public surface must omit sitemap.xml`, + `${appId} private public surface must omit sitemap.xml` ); assert( !publicSurface.files.includes(SHARED_VALIDATOR_STRING_134), - `${appId} private public surface must omit site.webmanifest`, + `${appId} private public surface must omit site.webmanifest` ); } else { assert( publicSurface.files.includes(SHARED_VALIDATOR_STRING_135), - `${appId} public surface must emit sitemap.xml when public routes exist`, + `${appId} public surface must emit sitemap.xml when public routes exist` ); assert( publicSurface.files.includes(SHARED_VALIDATOR_STRING_134), - `${appId} public surface must emit site.webmanifest when public routes exist`, + `${appId} public surface must emit site.webmanifest when public routes exist` ); } }; @@ -4886,7 +5330,7 @@ const assertPublicHeadContract = ( appId: string, publicHead: ReturnType | undefined, headModule: string, - hasOwnerPage = true, + hasOwnerPage = true ): void => { if (publicHead === undefined) { assert(false, `${appId} public head generator is incorrect`); @@ -4894,41 +5338,44 @@ const assertPublicHeadContract = ( } assert( publicHead.generator === './src/routes/ultramodern-route-head', - `${appId} public head generator is incorrect`, + `${appId} public head generator is incorrect` ); assert( publicHead.renderer === '@modern-js/runtime/head Helmet', - `${appId} public head renderer is incorrect`, + `${appId} public head renderer is incorrect` ); assert(publicHead.ssr, `${appId} public head must be SSR-rendered`); assert( publicHead.title.source === 'route.titleKey', - `${appId} public head title must come from route metadata`, + `${appId} public head title must come from route metadata` ); assert( publicHead.description.source === 'route.descriptionKey', - `${appId} public head description must come from route metadata`, + `${appId} public head description must come from route metadata` ); assert( publicHead.canonical.publicIndexableOnly, - `${appId} canonical links must be public/indexable only`, + `${appId} canonical links must be public/indexable only` + ); + assert( + publicHead.structuredData.optional, + `${appId} structured data must be optional` ); - assert(publicHead.structuredData.optional, `${appId} structured data must be optional`); assert( publicHead.structuredData.source === 'route.jsonLd', - `${appId} structured data must come from explicit route metadata`, + `${appId} structured data must come from explicit route metadata` ); assert( !publicHead.structuredData.inference, - `${appId} structured data inference must stay disabled`, + `${appId} structured data inference must stay disabled` ); assert( publicHead.structuredData.sanitizesHtmlOpenBracket, - `${appId} structured data must sanitize HTML open brackets`, + `${appId} structured data must sanitize HTML open brackets` ); assert( publicHead.privateRouteRobots === SHARED_VALIDATOR_STRING_090, - `${appId} private route robots policy is incorrect`, + `${appId} private route robots policy is incorrect` ); if (!hasOwnerPage) { for (const snippet of [ @@ -4938,19 +5385,27 @@ const assertPublicHeadContract = ( 'name="robots"', SHARED_VALIDATOR_STRING_090, ]) { - assert(headModule.includes(snippet), `${appId} private API head is missing ${snippet}`); + assert( + headModule.includes(snippet), + `${appId} private API head is missing ${snippet}` + ); } - for (const snippet of ['rel="canonical"', 'rel="alternate"', 'application/ld+json']) { + for (const snippet of [ + 'rel="canonical"', + 'rel="alternate"', + 'application/ld+json', + ]) { assert( !headModule.includes(snippet), - `${appId} must not publish ${snippet} without an owner-rendered route`, + `${appId} must not publish ${snippet} without an owner-rendered route` ); } return; } assert( - publicHead.structuredData.helperModule === './src/routes/ultramodern-jsonld', - `${appId} structured data helper module is incorrect`, + publicHead.structuredData.helperModule === + './src/routes/ultramodern-jsonld', + `${appId} structured data helper module is incorrect` ); for (const snippet of [ "from '@modern-js/runtime/head'", @@ -4966,71 +5421,84 @@ const assertPublicHeadContract = ( 'route?.jsonLd', "replaceAll('<', String.raw`\\u003c`)", ]) { - assert(headModule.includes(snippet), `${appId} route head module is missing ${snippet}`); + assert( + headModule.includes(snippet), + `${appId} route head module is missing ${snippet}` + ); } }; const assertCloudflareQualityGates = ( appId: string, - qualityGates: ReturnType | undefined, + qualityGates: ReturnType | undefined ): void => { if (qualityGates === undefined) { - assert(false, `${appId} quality gates must require sitemap for public routes`); + assert( + false, + `${appId} quality gates must require sitemap for public routes` + ); return; } assert( qualityGates.publicRoutes.requireSitemapWhenPresent, - `${appId} quality gates must require sitemap for public routes`, + `${appId} quality gates must require sitemap for public routes` ); assert( qualityGates.publicRoutes.requireRobotsSitemapConsistency, - `${appId} quality gates must require robots/sitemap consistency`, + `${appId} quality gates must require robots/sitemap consistency` ); assert( qualityGates.statusCodes.unknownRouteStatus === 404, - `${appId} quality gates must require 404 unknown routes`, + `${appId} quality gates must require 404 unknown routes` ); assert( qualityGates.indexing.previewNoindex, - `${appId} quality gates must require preview noindex`, + `${appId} quality gates must require preview noindex` ); assert( qualityGates.indexing.productionPublicRoutesIndexable, - `${appId} quality gates must require production public routes to be indexable`, + `${appId} quality gates must require production public routes to be indexable` ); assert( qualityGates.assets.cssPreloadRequired, - `${appId} quality gates must require CSS preload evidence`, + `${appId} quality gates must require CSS preload evidence` ); assert( !qualityGates.assets.sourcemapsPubliclyReferenced, - `${appId} quality gates must reject public sourcemap references`, + `${appId} quality gates must reject public sourcemap references` ); assert( isNumber(qualityGates.budgets.ssrHtmlMaxBytes), - `${appId} quality gates must define SSR HTML byte budget`, + `${appId} quality gates must define SSR HTML byte budget` ); assert( isNumber(qualityGates.budgets.mfManifestMaxBytes), - `${appId} quality gates must define MF manifest byte budget`, + `${appId} quality gates must define MF manifest byte budget` ); assert( qualityGates.csp.finalMode === SHARED_VALIDATOR_STRING_110, - `${appId} CSP final mode decision is missing`, + `${appId} CSP final mode decision is missing` ); }; const extractAssetPrefixExpression = (modernConfig: string): string => { - const match = /const\s+assetPrefix\s*=\s*(?[\s\S]*?);/u.exec(modernConfig); + const match = /const\s+assetPrefix\s*=\s*(?[\s\S]*?);/u.exec( + modernConfig + ); assert( isString(match?.groups?.expression) && match.groups.expression.length > 0, - 'modern.config.ts must assign assetPrefix', + 'modern.config.ts must assign assetPrefix' ); return match?.groups?.expression ?? ''; }; -const assertTargetIsolatedBuildArtifacts = (appId: string, modernConfig: string): void => { +const assertTargetIsolatedBuildArtifacts = ( + appId: string, + modernConfig: string +): void => { assert( - modernConfig.includes("const buildTarget = cloudflareDeployEnabled ? 'cloudflare' : 'web';") && + modernConfig.includes( + "const buildTarget = cloudflareDeployEnabled ? 'cloudflare' : 'web';" + ) && modernConfig.includes( - "const buildOutputRoot = cloudflareDeployEnabled ? 'dist-cloudflare' : 'dist';", + "const buildOutputRoot = cloudflareDeployEnabled ? 'dist-cloudflare' : 'dist';" ) && modernConfig.includes( sourceFragment( @@ -5039,8 +5507,8 @@ const assertTargetIsolatedBuildArtifacts = (appId: string, modernConfig: string) templatePlaceholderOpening, 'appId}-', templatePlaceholderOpening, - 'buildTarget}`;', - ), + 'buildTarget}`;' + ) ) && modernConfig.includes( sourceFragment( @@ -5048,34 +5516,40 @@ const assertTargetIsolatedBuildArtifacts = (appId: string, modernConfig: string) templatePlaceholderOpening, 'appId}-', templatePlaceholderOpening, - 'buildTarget}`;', - ), + 'buildTarget}`;' + ) ) && modernConfig.includes('root: buildOutputRoot,') && modernConfig.includes('tempDir: buildTempDirectory,') && modernConfig.includes('cacheDigest: [appId, buildTarget],') && modernConfig.includes('cacheDirectory: buildCacheDirectory,'), - `${appId} must isolate build output, Modern temp files, and Rspack cache by app and build target`, + `${appId} must isolate build output, Modern temp files, and Rspack cache by app and build target` ); }; -const assertCloudflareBuildSkipsDeployRebuild = (appId: string, packageJson: PackageJson): void => { - const cloudflareBuild = packageJson.scripts?.[SHARED_VALIDATOR_STRING_059] ?? ''; +const assertCloudflareBuildSkipsDeployRebuild = ( + appId: string, + packageJson: PackageJson +): void => { + const cloudflareBuild = + packageJson.scripts?.[SHARED_VALIDATOR_STRING_059] ?? ''; const buildCommand = 'MODERNJS_DEPLOY=cloudflare modern build'; const publicSurfaceCommand = `--app ${appId} --target cloudflare-dist`; const deployCommand = 'MODERNJS_DEPLOY=cloudflare modern deploy --skip-build'; assert( cloudflareBuild.includes(deployCommand), - `${appId} cloudflare:build must deploy with --skip-build after the explicit Cloudflare build`, + `${appId} cloudflare:build must deploy with --skip-build after the explicit Cloudflare build` ); assert( cloudflareBuild.includes(publicSurfaceCommand) && - cloudflareBuild.indexOf(buildCommand) < cloudflareBuild.indexOf(publicSurfaceCommand) && - cloudflareBuild.indexOf(publicSurfaceCommand) < cloudflareBuild.indexOf(deployCommand), - `${appId} cloudflare:build must generate public-surface assets in the explicit Cloudflare build output before deploy`, + cloudflareBuild.indexOf(buildCommand) < + cloudflareBuild.indexOf(publicSurfaceCommand) && + cloudflareBuild.indexOf(publicSurfaceCommand) < + cloudflareBuild.indexOf(deployCommand), + `${appId} cloudflare:build must generate public-surface assets in the explicit Cloudflare build output before deploy` ); assert( !/--target (?:dist|cloudflare)(?=\s|$)/u.test(cloudflareBuild), - `${appId} cloudflare:build must not target the Node dist or final Cloudflare output directly`, + `${appId} cloudflare:build must not target the Node dist or final Cloudflare output directly` ); }; const stripYamlInlineComment = (value: string): string => { @@ -5088,7 +5562,10 @@ const stripYamlInlineComment = (value: string): string => { } } else if (character === '"' || character === "'") { quote = character; - } else if (character === '#' && (index === 0 || /\s/u.test(value[index - 1]))) { + } else if ( + character === '#' && + (index === 0 || /\s/u.test(value[index - 1])) + ) { return value.slice(0, index).trimEnd(); } } @@ -5108,7 +5585,7 @@ const normalizeYamlScalar = (value: string): string => { const nextYamlBlockScalar = ( lines: readonly string[], startIndex: number, - parentIndent: number, + parentIndent: number ): string => { const nextLine = lines.slice(startIndex).find((line) => { const trimmed = line.trim(); @@ -5124,14 +5601,20 @@ const extractWorkflowNodeVersions = (workflowText: string): string[] => { const versions = []; const lines = workflowText.split(/\r?\n/u); for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - const match = /^(?\s*)node-version\s*:\s*(?.*)$/u.exec(lines[lineIndex]); + const match = /^(?\s*)node-version\s*:\s*(?.*)$/u.exec( + lines[lineIndex] + ); if (!match?.groups) { continue; } let value = normalizeYamlScalar(match.groups.value); if (value === '' || value === '|' || value === '>') { - value = nextYamlBlockScalar(lines, lineIndex + 1, match.groups.indent.length); + value = nextYamlBlockScalar( + lines, + lineIndex + 1, + match.groups.indent.length + ); } if (value !== '') { versions.push(value); @@ -5149,34 +5632,37 @@ const parseSemver = (version: string): Semver => { }; }; const compareSemver = (left: Semver, right: Semver): number => - left.major - right.major || left.minor - right.minor || left.patch - right.patch; + left.major - right.major || + left.minor - right.minor || + left.patch - right.patch; const activeNodeVersion = process.versions.node; const minimumPnpmVersion = { major: 11, minor: 0, patch: 0 }; const minimumNodeVersion = { major: 26, minor: 0, patch: 0 }; const currentNodeVersion = parseSemver(activeNodeVersion); const repositoryWorkflowPath = fs.existsSync( - path.join(root, '../.github/workflows/ultramodern-workspace-gates.yml'), + path.join(root, '../.github/workflows/ultramodern-workspace-gates.yml') ) ? '../.github/workflows/ultramodern-workspace-gates.yml' : '.github/workflows/ultramodern-workspace-gates.yml'; const assertActivePnpmVersion = (packageManagerUserAgent: string): void => { - const activePnpmVersion = /^pnpm\/(?\d+\.\d+\.\d+)/u.exec(packageManagerUserAgent) - ?.groups?.version; + const activePnpmVersion = /^pnpm\/(?\d+\.\d+\.\d+)/u.exec( + packageManagerUserAgent + )?.groups?.version; assert( isString(activePnpmVersion) && activePnpmVersion.length > 0, - 'Validator must run through the workspace pnpm command', + 'Validator must run through the workspace pnpm command' ); const currentPnpmVersion = parseSemver(activePnpmVersion ?? ''); assert( compareSemver(currentPnpmVersion, minimumPnpmVersion) >= 0, - `Generated workspace requires pnpm >=11; active pnpm is ${activePnpmVersion}. Run mise install, then rerun pnpm from the activated shell.`, + `Generated workspace requires pnpm >=11; active pnpm is ${activePnpmVersion}. Run mise install, then rerun pnpm from the activated shell.` ); }; assert( compareSemver(currentNodeVersion, minimumNodeVersion) >= 0, - `Generated workspace requires Node >=26; active Node is ${activeNodeVersion}. Run mise install, then rerun node from the activated shell.`, + `Generated workspace requires Node >=26; active Node is ${activeNodeVersion}. Run mise install, then rerun node from the activated shell.` ); const requiredPaths = [ @@ -5260,7 +5746,7 @@ if (tailwindEnabled) { 'apps/shell-super-app/tailwind.config.ts', ...fullStackVerticals .filter((vertical) => vertical.emitsUi) - .flatMap((vertical) => [`${vertical.path}/tailwind.config.ts`]), + .flatMap((vertical) => [`${vertical.path}/tailwind.config.ts`]) ); } @@ -5282,15 +5768,20 @@ for (const requiredPath of requiredPaths) { // Agent skills metadata may live under .agents/ (agents-standard layout) or // .codex/ (legacy scaffold default). assertAnyOf(['.agents/skills-lock.json', '.codex/skills-lock.json']); -assertAnyOf(['.agents/rstackjs-agent-skills-LICENSE', '.codex/rstackjs-agent-skills-LICENSE']); +assertAnyOf([ + '.agents/rstackjs-agent-skills-LICENSE', + '.codex/rstackjs-agent-skills-LICENSE', +]); const pnpmWorkspace = readText(SHARED_VALIDATOR_STRING_103); assert( pnpmWorkspace.includes('enableGlobalVirtualStore: false'), - 'pnpm-workspace.yaml must keep deployable dependency trees independent of the host global virtual store', + 'pnpm-workspace.yaml must keep deployable dependency trees independent of the host global virtual store' ); assert( - pnpmWorkspace.includes("'@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch"), - 'pnpm-workspace.yaml must patch the deployment tracer for transient filesystem markers', + pnpmWorkspace.includes( + "'@vercel/nft@0.29.2': patches/@vercel__nft@0.29.2.patch" + ), + 'pnpm-workspace.yaml must patch the deployment tracer for transient filesystem markers' ); // The published 3.9 cohort owns deploy-entry interop; no obsolete app-tools // patch is required. Release output is exercised by the runtime script proofs. @@ -5309,89 +5800,103 @@ assert( vercelNftPatch.includes('isBuildHostWildcardRoot(wildcardDirPath)') && vercelNftPatch.includes('if (isBuildHostSystemPath(path))') && vercelNftPatch.includes('const source = await this.readFile(path);') && - vercelNftPatch.includes("throw new Error('File ' + path + ' does not exist.')"), - 'The deployment tracer patch must reject build-host globs before enumeration, bound dependency expansion, exclude build-host system paths, ignore only missing pnpm markers, and reject other missing files', + vercelNftPatch.includes( + "throw new Error('File ' + path + ' does not exist.')" + ), + 'The deployment tracer patch must reject build-host globs before enumeration, bound dependency expansion, exclude build-host system paths, ignore only missing pnpm markers, and reject other missing files' ); -const traceDeploymentSystemGlobs = Effect.gen(function* traceDeploymentSystemGlobs() { - if (process.platform === 'win32') { - return; - } - const partyRegistryRequire = createRequire(path.join(root, SHARED_VALIDATOR_STRING_165)); - const appToolsRequire = createRequire(partyRegistryRequire.resolve(SHARED_VALIDATOR_STRING_022)); - const ndepeRequire = createRequire(appToolsRequire.resolve('ndepe')); - const { nodeFileTrace } = Result.getOrThrow( - Schema.decodeUnknownResult(NftModuleSchema)(ndepeRequire('@vercel/nft')), - ); - const tracerFixtureDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'ultramodern-system-glob-')); - const tracerFixturePath = path.join(tracerFixtureDirectory, 'entry.cjs'); - const tracerLogs: string[] = []; - const originalConsoleLog = console.log; +const traceDeploymentSystemGlobs = Effect.gen( + function* traceDeploymentSystemGlobs() { + if (process.platform === 'win32') { + return; + } + const partyRegistryRequire = createRequire( + path.join(root, SHARED_VALIDATOR_STRING_165) + ); + const appToolsRequire = createRequire( + partyRegistryRequire.resolve(SHARED_VALIDATOR_STRING_022) + ); + const ndepeRequire = createRequire(appToolsRequire.resolve('ndepe')); + const { nodeFileTrace } = Result.getOrThrow( + Schema.decodeUnknownResult(NftModuleSchema)(ndepeRequire('@vercel/nft')) + ); + const tracerFixtureDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'ultramodern-system-glob-') + ); + const tracerFixturePath = path.join(tracerFixtureDirectory, 'entry.cjs'); + const tracerLogs: string[] = []; + const originalConsoleLog = console.log; - fs.writeFileSync( - tracerFixturePath, - [ - 'const os = require("node:os");', - 'const path = require("node:path");', - 'require(path.join("/etc", process.env.ULTRAMODERN_DYNAMIC_SYSTEM_FILE));', - 'require(path.join(os.homedir(), process.env.ULTRAMODERN_DYNAMIC_HOME_FILE));', - '', - ].join('\n'), - ); - console.log = (...values: unknown[]) => { - tracerLogs.push(values.map(String).join(' ')); - }; - try { - yield* Effect.promise( - async () => - await nodeFileTrace([tracerFixturePath], { - base: '/', - log: true, - processCwd: tracerFixtureDirectory, - }), + fs.writeFileSync( + tracerFixturePath, + [ + 'const os = require("node:os");', + 'const path = require("node:path");', + 'require(path.join("/etc", process.env.ULTRAMODERN_DYNAMIC_SYSTEM_FILE));', + 'require(path.join(os.homedir(), process.env.ULTRAMODERN_DYNAMIC_HOME_FILE));', + '', + ].join('\n') + ); + console.log = (...values: unknown[]) => { + tracerLogs.push(values.map(String).join(' ')); + }; + try { + yield* Effect.promise( + async () => + await nodeFileTrace([tracerFixturePath], { + base: '/', + log: true, + processCwd: tracerFixtureDirectory, + }) + ); + } finally { + console.log = originalConsoleLog; + fs.rmSync(tracerFixtureDirectory, { force: true, recursive: true }); + } + assert( + !tracerLogs.some((line) => line.startsWith('Globbing /etc')), + 'The deployment tracer must reject build-host system globs before filesystem enumeration' + ); + assert( + !tracerLogs.some((line) => line.startsWith(`Globbing ${os.homedir()}`)), + 'The deployment tracer must reject build-host home globs before filesystem enumeration' ); - } finally { - console.log = originalConsoleLog; - fs.rmSync(tracerFixtureDirectory, { force: true, recursive: true }); } - assert( - !tracerLogs.some((line) => line.startsWith('Globbing /etc')), - 'The deployment tracer must reject build-host system globs before filesystem enumeration', - ); - assert( - !tracerLogs.some((line) => line.startsWith(`Globbing ${os.homedir()}`)), - 'The deployment tracer must reject build-host home globs before filesystem enumeration', - ); -}); +); assert( pnpmWorkspace.includes(`'@effect/opentelemetry': ${expectedEffectVersion}`), - 'pnpm-workspace.yaml must override @effect/opentelemetry to the generated Effect cohort', + 'pnpm-workspace.yaml must override @effect/opentelemetry to the generated Effect cohort' ); assert( pnpmWorkspace.includes(`effect: ${expectedEffectVersion}`), - 'pnpm-workspace.yaml must override effect to the generated Effect cohort', + 'pnpm-workspace.yaml must override effect to the generated Effect cohort' ); assert( pnpmWorkspace.includes( - `'@module-federation/modern-js-v3@${expectedModuleFederationVersion}': patches/@module-federation__modern-js-v3@${expectedModuleFederationVersion}.patch`, + `'@module-federation/modern-js-v3@${expectedModuleFederationVersion}': patches/@module-federation__modern-js-v3@${expectedModuleFederationVersion}.patch` ), - 'pnpm-workspace.yaml must patch the generated Module Federation Modern.js integration cohort', + 'pnpm-workspace.yaml must patch the generated Module Federation Modern.js integration cohort' ); assert( pnpmWorkspace.includes( - `'@module-federation/bridge-react@${expectedModuleFederationVersion}': patches/@module-federation__bridge-react@${expectedModuleFederationVersion}.patch`, + `'@module-federation/bridge-react@${expectedModuleFederationVersion}': patches/@module-federation__bridge-react@${expectedModuleFederationVersion}.patch` ), - 'pnpm-workspace.yaml must patch the generated Module Federation React bridge cohort', + 'pnpm-workspace.yaml must patch the generated Module Federation React bridge cohort' ); assertWorkspaceValidationContract(workspaceValidationContract); assertGeneratedSurfacePolicy(); for (const appPath of [ SHARED_VALIDATOR_STRING_047, - ...fullStackVerticals.filter((vertical) => vertical.emitsUi).map((vertical) => vertical.path), - ...expectedAdditionalShells.map((shell) => shell.path), + ...fullStackVerticals + .filter((vertical) => vertical.emitsUi) + .map((vertical) => vertical.path), + ...(workspaceValidationContract.structuralShellPolicy?.shells ?? []) + .filter((shell) => shell.id !== SHARED_VALIDATOR_STRING_131) + .map((shell) => shell.packageDir), ]) { const violation = moduleFederationBridgeViolation( readText(`${appPath}/module-federation.config.ts`), - readJson(PackageJsonSchema, `${appPath}/package.json`), + readJson(PackageJsonSchema, `${appPath}/package.json`) ); assert(violation === undefined, `${appPath}: ${violation}`); } @@ -5403,7 +5908,9 @@ for (const retiredMetadataPath of retiredMetadataPaths) { } assertStructuredWorkspaceMetadata(); const bridgeConfig = - ultramodernConfig?.bridge?.enabled === true ? ultramodernConfig.bridge : undefined; + ultramodernConfig?.bridge?.enabled === true + ? ultramodernConfig.bridge + : undefined; const packageSource = createPackageSourceView(ultramodernConfig); const generatedContract = readGeneratedContractView(ultramodernConfig); @@ -5412,53 +5919,63 @@ assertStructuredWorkspaceMetadataSemantics(); assertTsConfigReferenceGraph(); assert(rootPackage.private, 'Root package must be private'); -assert(isString(rootPackage.packageManager), 'Root must declare packageManager'); +assert( + isString(rootPackage.packageManager), + 'Root must declare packageManager' +); const packageManagerPnpmVersionMatch = /^pnpm@(?\d+\.\d+\.\d+)$/u.exec( - rootPackage.packageManager, + rootPackage.packageManager ); assert( packageManagerPnpmVersionMatch !== null, - 'Root packageManager must pin pnpm with a semver version', + 'Root packageManager must pin pnpm with a semver version' ); -const packageManagerPnpmVersion = packageManagerPnpmVersionMatch?.groups?.version ?? ''; +const packageManagerPnpmVersion = + packageManagerPnpmVersionMatch?.groups?.version ?? ''; assert( - compareSemver(parseSemver(packageManagerPnpmVersion), minimumPnpmVersion) >= 0, - 'Root packageManager must use pnpm >=11', + compareSemver(parseSemver(packageManagerPnpmVersion), minimumPnpmVersion) >= + 0, + 'Root packageManager must use pnpm >=11' ); assert(rootPackage.engines?.node === '>=26', 'Root must require Node >=26'); assert(rootPackage.engines?.pnpm === '>=11', 'Root must require pnpm >=11'); assert( generatedContract.node?.version === expectedNodeVersion, - 'Generated contract must record the Node toolchain version', + 'Generated contract must record the Node toolchain version' ); assert( generatedContract.node?.engineRange === '>=26', - 'Generated contract must record the Node engine range', + 'Generated contract must record the Node engine range' ); assert( readText('.mise.toml').includes(`node = "${expectedNodeVersion}"`), - 'mise must pin the generated Node version', + 'mise must pin the generated Node version' ); assert( readText('.mise.toml').includes(`pnpm = "${packageManagerPnpmVersion}"`), - 'mise must pin the generated pnpm version', + 'mise must pin the generated pnpm version' ); const workflowText = readText(repositoryWorkflowPath); const workflowNodeVersions = extractWorkflowNodeVersions(workflowText); -assert(workflowNodeVersions.length > 0, 'CI workflow must configure setup-node node-version'); assert( - workflowNodeVersions.every((nodeVersion) => nodeVersion === expectedNodeVersion), - `CI workflow must pin the generated Node version ${expectedNodeVersion}; found ${workflowNodeVersions.join(', ')}`, + workflowNodeVersions.length > 0, + 'CI workflow must configure setup-node node-version' +); +assert( + workflowNodeVersions.every( + (nodeVersion) => nodeVersion === expectedNodeVersion + ), + `CI workflow must pin the generated Node version ${expectedNodeVersion}; found ${workflowNodeVersions.join(', ')}` ); assert( !workflowText.includes('FORCE_JAVASCRIPT_ACTIONS_TO_NODE24'), - 'CI workflow must not carry the legacy Node 24 override', + 'CI workflow must not carry the legacy Node 24 override' ); assert( workflowText.includes('jdx/mise-action@') && workflowText.includes('mise exec -- pnpm install --frozen-lockfile') && !workflowText.includes('corepack'), - 'CI workflow must install and execute the repository-pinned pnpm toolchain through mise', + 'CI workflow must install and execute the repository-pinned pnpm toolchain through mise' ); const requiredWorkflowEvidence = [ ['Format', 'pnpm format:check'], @@ -5479,18 +5996,23 @@ const requiredWorkflowEvidence = [ ]; for (const [name, command] of requiredWorkflowEvidence) { assert( - workflowText.includes(`name: ${name}`) && workflowText.includes(`command: ${command}`), - `CI workflow is missing stable ${name} evidence using ${command}`, + workflowText.includes(`name: ${name}`) && + workflowText.includes(`command: ${command}`), + `CI workflow is missing stable ${name} evidence using ${command}` ); } for (const [jobId, jobName] of [ - ['service-integration', 'Database, Migration, RLS, Authorization, and Outbox Integration'], + [ + 'service-integration', + 'Database, Migration, RLS, Authorization, and Outbox Integration', + ], ['node-runtime', 'Node Backend Federation Artifact Proof'], ['cloudflare-runtime', 'Cloudflare Workerd Artifact Proof'], ]) { assert( - workflowText.includes(` ${jobId}:`) && workflowText.includes(`name: ${jobName}`), - `CI workflow is missing required job ${jobName}`, + workflowText.includes(` ${jobId}:`) && + workflowText.includes(`name: ${jobName}`), + `CI workflow is missing required job ${jobName}` ); } assert( @@ -5499,102 +6021,112 @@ assert( workflowText.includes('mise exec -- pnpm db:verify') && workflowText.includes('mise exec -- pnpm test:integration') && workflowText.includes('docker compose down --volumes --remove-orphans'), - 'CI service evidence must start fresh PostgreSQL and SpiceDB, apply and verify migrations, run complete integrations, and always remove volumes', + 'CI service evidence must start fresh PostgreSQL and SpiceDB, apply and verify migrations, run complete integrations, and always remove volumes' ); assert( - workflowText.includes('name: Apply and verify Core, Auth, and Contacts migrations') && + workflowText.includes( + 'name: Apply and verify Core, Auth, and Contacts migrations' + ) && workflowText.includes( - 'name: Run database, RLS, Action, authorization, identity, Outbox, module-state, Shell, and Contacts integration tests', + 'name: Run database, RLS, Action, authorization, identity, Outbox, module-state, Shell, and Contacts integration tests' ), - 'CI workflow must keep database/migration/RLS and authorization/Outbox integration evidence clearly named', + 'CI workflow must keep database/migration/RLS and authorization/Outbox integration evidence clearly named' ); assert( workflowText.includes('mise exec -- pnpm build') && workflowText.includes('mise exec -- pnpm node:proof') && workflowText.includes('mise exec -- pnpm cloudflare:build') && - workflowText.includes('MODERN_PUBLIC_SITE_URL: https://shell-super-app.invalid') && workflowText.includes( - 'ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY: https://party-registry.invalid', + 'MODERN_PUBLIC_SITE_URL: https://shell-super-app.invalid' ) && workflowText.includes( - 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP: https://shell-super-app.invalid', + 'ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY: https://party-registry.invalid' + ) && + workflowText.includes( + 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP: https://shell-super-app.invalid' ), - 'CI workflow must separately prove Node and Cloudflare/workerd runtime artifacts with explicit local proof URLs', + 'CI workflow must separately prove Node and Cloudflare/workerd runtime artifacts with explicit local proof URLs' ); assert( workflowText.includes( - 'DATABASE_URL: postgresql://ontos_proof:ontos_proof@localhost:5432/ontos_proof', + 'DATABASE_URL: postgresql://ontos_proof:ontos_proof@localhost:5432/ontos_proof' ), - 'CI Node artifact proof must provide a non-secret database URL so the readiness API layer can initialize without a service connection', + 'CI Node artifact proof must provide a non-secret database URL so the readiness API layer can initialize without a service connection' ); assert( rootPackage.scripts?.[SHARED_VALIDATOR_STRING_059]?.includes( - 'ULTRAMODERN_MF_TYPES_ARCHIVE=dist-cloudflare/@mf-types.zip pnpm mf:types', + 'ULTRAMODERN_MF_TYPES_ARCHIVE=dist-cloudflare/@mf-types.zip pnpm mf:types' ), - 'Cloudflare builds must validate the Module Federation DTS archive from the Cloudflare output directory', + 'Cloudflare builds must validate the Module Federation DTS archive from the Cloudflare output directory' ); assert( workflowText.includes('mise exec -- pnpm deployment-impact:plan') && workflowText.includes('mise exec -- pnpm authorization:inventory:check') && workflowText.includes('--authorization-environment stage') && workflowText.includes('## Reviewed deployment impact plan'), - 'Stage deployment must derive the exact-build authorization inventory, enforce the stage authorization gate, and summarize the topology-driven deployment impact planner', + 'Stage deployment must derive the exact-build authorization inventory, enforce the stage authorization gate, and summarize the topology-driven deployment impact planner' ); assert( workflowText.includes( - 'needs: [workspace-gate, service-integration, node-runtime, cloudflare-runtime]', + 'needs: [workspace-gate, service-integration, node-runtime, cloudflare-runtime]' ), - 'Stage deployment must depend on every fast, service-backed, Node, and Cloudflare required job', + 'Stage deployment must depend on every fast, service-backed, Node, and Cloudflare required job' ); assert( !/(?:verticals\/(?:crm|projects)|outputs\.(?:crm|projects)|ZEROPS_(?:CRM|PROJECTS)_SERVICE_ID|--setup\s+(?:crm|projects))/iu.test( - workflowText, + workflowText ) && !workflowText.includes('case "$path"'), - 'Stage deployment workflow must not contain stale CRM/Projects or hand-written changed-path branches', + 'Stage deployment workflow must not contain stale CRM/Projects or hand-written changed-path branches' ); const zeropsDeploymentSource = readText(SHARED_VALIDATOR_STRING_174); for (const vertical of topology.verticals ?? []) { const verticalId = vertical.id; - assert(isString(verticalId), 'Topology vertical deployment identity must be a string'); + assert( + isString(verticalId), + 'Topology vertical deployment identity must be a string' + ); const serviceEnvironment = `ZEROPS_${verticalId .replaceAll(/[^A-Za-z0-9]+/gu, '_') .replaceAll(/^_+|_+$/gu, '') .toUpperCase()}_SERVICE_ID`; assert( zeropsDeploymentSource.includes(`setup: ${quoteYamlString(verticalId)}`), - `Topology delivery unit ${verticalId} has no matching Zerops setup`, + `Topology delivery unit ${verticalId} has no matching Zerops setup` ); assert( workflowText.includes(serviceEnvironment), - `Topology delivery unit ${verticalId} has no matching workflow service variable ${serviceEnvironment}`, + `Topology delivery unit ${verticalId} has no matching workflow service variable ${serviceEnvironment}` ); } const shellDeploymentSetup = (topology.shell?.id ?? '').replaceAll('-', ''); assert( - zeropsDeploymentSource.includes(`setup: ${quoteYamlString(shellDeploymentSetup)}`) && - workflowText.includes('ZEROPS_SHELL_SERVICE_ID'), - 'Topology Shell delivery unit must match the current Zerops setup and workflow service-variable convention', + zeropsDeploymentSource.includes( + `setup: ${quoteYamlString(shellDeploymentSetup)}` + ) && workflowText.includes('ZEROPS_SHELL_SERVICE_ID'), + 'Topology Shell delivery unit must match the current Zerops setup and workflow service-variable convention' ); assert( rootPackage.modernjs?.preset === SHARED_VALIDATOR_STRING_104, - 'Root must declare presetUltramodern', + 'Root must declare presetUltramodern' ); assert( - rootPackage.modernjs?.packageSource?.config === './.modernjs/ultramodern.json', - 'Root must point at compact UltraModern config', + rootPackage.modernjs?.packageSource?.config === + './.modernjs/ultramodern.json', + 'Root must point at compact UltraModern config' ); assert( rootPackage.modernjs?.packageSource?.strategy === packageSource.strategy, - 'Root package source strategy must match metadata', + 'Root package source strategy must match metadata' ); assert( - packageSource.strategy === 'workspace' || packageSource.strategy === 'install', - 'Package source strategy must be workspace or install', + packageSource.strategy === 'workspace' || + packageSource.strategy === 'install', + 'Package source strategy must be workspace or install' ); assert( packageSource.strategy === 'install' || packageSource.modernPackages?.specifier === SHARED_VALIDATOR_STRING_169, - 'Workspace package source must be explicitly backed by workspace:*', + 'Workspace package source must be explicitly backed by workspace:*' ); assertModernPackageCohort(); const isIdentifierChar = (character: string): boolean => @@ -5619,12 +6151,17 @@ interface SourceScanner { result: string; state: SourceScannerStateValue; } -const emitSourceCodeCharacter = (scanner: SourceScanner, character: string): void => { +const emitSourceCodeCharacter = ( + scanner: SourceScanner, + character: string +): void => { scanner.result += character; if (/\s/u.test(character)) { return; } - scanner.currentWord = isIdentifierChar(character) ? scanner.currentWord + character : ''; + scanner.currentWord = isIdentifierChar(character) + ? scanner.currentWord + character + : ''; scanner.lastSignificant = character; }; const sourceRegexCanFollow = (scanner: SourceScanner): boolean => { @@ -5639,7 +6176,10 @@ const sourceRegexCanFollow = (scanner: SourceScanner): boolean => { scanner.regexPrecedingKeywords.has(scanner.currentWord) ); }; -const scanSourceSlash = (scanner: SourceScanner, next: string): number | undefined => { +const scanSourceSlash = ( + scanner: SourceScanner, + next: string +): number | undefined => { if (next === '/') { scanner.state = 'line'; return 1; @@ -5657,7 +6197,10 @@ const scanSourceSlash = (scanner: SourceScanner, next: string): number | undefin } return undefined; }; -const scanSourceInterpolation = (scanner: SourceScanner, character: string): boolean => { +const scanSourceInterpolation = ( + scanner: SourceScanner, + character: string +): boolean => { if ( character === '}' && scanner.interpolations.length > 0 && @@ -5683,7 +6226,7 @@ const scanSourceInterpolation = (scanner: SourceScanner, character: string): boo const scanSourceCodeCharacter = ( scanner: SourceScanner, character: string, - next: string, + next: string ): number => { if (character === '/') { const consumed = scanSourceSlash(scanner, next); @@ -5706,7 +6249,10 @@ const scanSourceCodeCharacter = ( } return 0; }; -const scanSourceLineComment = (scanner: SourceScanner, character: string): number => { +const scanSourceLineComment = ( + scanner: SourceScanner, + character: string +): number => { if (character === '\n') { scanner.state = 'code'; scanner.result += character; @@ -5716,7 +6262,7 @@ const scanSourceLineComment = (scanner: SourceScanner, character: string): numbe const scanSourceBlockComment = ( scanner: SourceScanner, character: string, - next: string, + next: string ): number => { if (character === '*' && next === '/') { scanner.state = 'code'; @@ -5727,7 +6273,11 @@ const scanSourceBlockComment = ( } return 0; }; -const scanSourceRegex = (scanner: SourceScanner, character: string, next: string): number => { +const scanSourceRegex = ( + scanner: SourceScanner, + character: string, + next: string +): number => { if (character === '\\') { scanner.result += character + next; return 1; @@ -5746,7 +6296,11 @@ const scanSourceRegex = (scanner: SourceScanner, character: string, next: string scanner.result += character; return 0; }; -const scanSourceTemplate = (scanner: SourceScanner, character: string, next: string): number => { +const scanSourceTemplate = ( + scanner: SourceScanner, + character: string, + next: string +): number => { if (character === '\\') { scanner.result += character + next; return 1; @@ -5772,7 +6326,7 @@ const scanSourceTemplate = (scanner: SourceScanner, character: string, next: str const scanSourceQuotedString = ( scanner: SourceScanner, character: string, - next: string, + next: string ): number => { if (character === '\\') { scanner.result += character + next; @@ -5789,7 +6343,11 @@ const scanSourceQuotedString = ( scanner.result += character; return 0; }; -type SourceCharacterScanner = (scanner: SourceScanner, character: string, next: string) => number; +type SourceCharacterScanner = ( + scanner: SourceScanner, + character: string, + next: string +) => number; const sourceCharacterScanners = { block: scanSourceBlockComment, code: scanSourceCodeCharacter, @@ -5802,7 +6360,7 @@ const sourceCharacterScanners = { const scanSourceCharacter: SourceCharacterScanner = ( scanner: SourceScanner, character: string, - next: string, + next: string ) => sourceCharacterScanners[scanner.state](scanner, character, next); const stripSourceComments = (code: string): string => { const scanner: SourceScanner = { @@ -5889,7 +6447,7 @@ const runtimeModuleSpecifiers = (source: string): string[] => { } }; for (const match of source.matchAll( - /\b(?:import|export)\s+(?[^;'"`]+?)\s+from\s*['"](?[^'"]+)['"]/gu, + /\b(?:import|export)\s+(?[^;'"`]+?)\s+from\s*['"](?[^'"]+)['"]/gu )) { const clause = match.groups?.clause; const specifier = match.groups?.specifier; @@ -5908,7 +6466,7 @@ const runtimeModuleSpecifiers = (source: string): string[] => { }; const remoteImplementationFor = ( specifier: string, - remotes: readonly CompositionRemote[], + remotes: readonly CompositionRemote[] ): CompositionRemote | undefined => remotes.find((remote) => { const packageSubpath = specifier.startsWith(`${remote.packageName}/`) @@ -5923,7 +6481,8 @@ const remoteImplementationFor = ( const normalizedSpecifier = specifier.replaceAll('\\', '/'); return normalizedSpecifier.includes(`${remote.directory}/`); }); -type StructuralShellPolicy = typeof workspaceValidationContractDefinition.structuralShellPolicy; +type StructuralShellPolicy = + typeof workspaceValidationContractDefinition.structuralShellPolicy; type FederatedCompositionSourcePolicy = typeof workspaceValidationContractDefinition.federatedCompositionSourcePolicy; const assertThinShellPolicy = (policy: StructuralShellPolicy): void => { @@ -5934,8 +6493,8 @@ const assertThinShellPolicy = (policy: StructuralShellPolicy): void => { selfCheckFailure( `structural thin-shell ${forbidden.id}`, `${forbidden.diagnostic} Forbidden artifact ${shell.packageDir}/${forbidden.path} exists`, - `remove forbidden thin-shell artifact ${shell.packageDir}/${forbidden.path}`, - ), + `remove forbidden thin-shell artifact ${shell.packageDir}/${forbidden.path}` + ) ); } const srcAbsolute = path.join(root, shell.srcDir); @@ -5944,14 +6503,16 @@ const assertThinShellPolicy = (policy: StructuralShellPolicy): void => { const source = stripSourceComments(fs.readFileSync(file, 'utf-8')); const relative = path.relative(root, file).split(path.sep).join('/'); for (const pattern of policy.forbiddenImportPatterns) { - const match = new RegExp(pattern.expression, pattern.flags).exec(source); + const match = new RegExp(pattern.expression, pattern.flags).exec( + source + ); assert( match === null, selfCheckFailure( `structural thin-shell ${pattern.id}`, `${pattern.diagnostic} Found forbidden import at ${relative}:${match?.index ?? 0}`, - 'consume only published surfaces from the thin shell', - ), + 'consume only published surfaces from the thin shell' + ) ); } } @@ -5961,7 +6522,7 @@ const assertThinShellPolicy = (policy: StructuralShellPolicy): void => { const assertFederatedCompositionFile = ( file: string, host: FederatedCompositionSourcePolicy['hosts'][number], - policy: FederatedCompositionSourcePolicy, + policy: FederatedCompositionSourcePolicy ): void => { const source = stripSourceComments(fs.readFileSync(file, 'utf-8')); const relative = path.relative(root, file).split(path.sep).join('/'); @@ -5972,8 +6533,8 @@ const assertFederatedCompositionFile = ( selfCheckFailure( `federated composition ${pattern.id}`, `${pattern.diagnostic} Found forbidden source at ${relative}:${match?.index ?? 0}`, - 'compose remote rendering through framework Module Federation primitives', - ), + 'compose remote rendering through framework Module Federation primitives' + ) ); } // Declaration files cannot execute. Ambient federation declarations may @@ -5987,13 +6548,15 @@ const assertFederatedCompositionFile = ( selfCheckFailure( 'federated composition remote-runtime-package-import', `Host ${host.id} imports remote render implementation ${specifier} from ${remote?.id} at ${relative}`, - 'use import type for contracts or compose the implementation through Module Federation', - ), + 'use import type for contracts or compose the implementation through Module Federation' + ) ); } } }; -const assertFederatedCompositionSourcePolicy = (policy: FederatedCompositionSourcePolicy): void => { +const assertFederatedCompositionSourcePolicy = ( + policy: FederatedCompositionSourcePolicy +): void => { for (const host of policy.hosts) { const srcAbsolute = path.join(root, host.srcDir); if (!fs.existsSync(srcAbsolute)) { @@ -6009,7 +6572,8 @@ const assertStructuralShellPolicy = (): void => { if (policy !== undefined) { assertThinShellPolicy(policy); } - const compositionPolicy = workspaceValidationContract.federatedCompositionSourcePolicy; + const compositionPolicy = + workspaceValidationContract.federatedCompositionSourcePolicy; if (compositionPolicy !== undefined) { assertFederatedCompositionSourcePolicy(compositionPolicy); } @@ -6018,7 +6582,7 @@ assertStructuralShellPolicy(); const assertConfiguredDevelopmentPorts = (): void => { const primaryShellConfig = findById( ultramodernConfig.topology?.apps, - SHARED_VALIDATOR_STRING_131, + SHARED_VALIDATOR_STRING_131 ); const overlayPorts = overlay.ports ?? {}; const configuredPorts = [ @@ -6033,11 +6597,14 @@ const assertConfiguredDevelopmentPorts = (): void => { assert(false, `Configured development port for ${id} must be finite`); continue; } - assert(Number.isFinite(port), `Configured development port for ${id} must be finite`); + assert( + Number.isFinite(port), + `Configured development port for ${id} must be finite` + ); const previous = portsByValue.get(port); assert( previous === undefined, - `Duplicate configured development port ${port} for ${previous} and ${id}`, + `Duplicate configured development port ${port} for ${previous} and ${id}` ); portsByValue.set(port, id); } @@ -6045,30 +6612,32 @@ const assertConfiguredDevelopmentPorts = (): void => { assertConfiguredDevelopmentPorts(); assert( ultramodernConfig.shells === undefined, - 'Single-shell workspace must not declare config.shells', + 'Single-shell workspace must not declare config.shells' ); assert( rootPackage.devDependencies?.[SHARED_VALIDATOR_STRING_024] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_024), - 'Root must depend on @modern-js/create through package source metadata', + 'Root must depend on @modern-js/create through package source metadata' ); assert( rootPackage.devDependencies?.[SHARED_VALIDATOR_STRING_023] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_023), - 'Root must depend on @modern-js/code-tools through package source metadata', + 'Root must depend on @modern-js/code-tools through package source metadata' ); assert( rootPackage.devDependencies?.[SHARED_VALIDATOR_STRING_025] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_025), - 'Root must depend on @modern-js/plugin-bff for Node backend federation proof', + 'Root must depend on @modern-js/plugin-bff for Node backend federation proof' ); if (packageSource.strategy === 'install') { const installSpecifier = packageSource.modernPackages?.specifier; assert( isString(installSpecifier) && - /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(installSpecifier) && + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test( + installSpecifier + ) && installSpecifier.includes('ultramodern'), - 'Install package source must use a semver UltraModern published cohort', + 'Install package source must use a semver UltraModern published cohort' ); const modernAliases = packageSource.modernPackages?.aliases ?? {}; if (Object.keys(modernAliases).length > 0) { @@ -6083,50 +6652,54 @@ if (packageSource.strategy === 'install') { ]) { assert( /^@[^/]+\/.+/u.test(modernAliases[modernPackageName] ?? ''), - `Install package source alias for ${modernPackageName} must be a scoped npm package`, + `Install package source alias for ${modernPackageName} must be a scoped npm package` ); } } } assert( - packageSource.generatedWorkspacePackages?.specifier === SHARED_VALIDATOR_STRING_169, - 'Generated workspace packages must keep workspace:* links', + packageSource.generatedWorkspacePackages?.specifier === + SHARED_VALIDATOR_STRING_169, + 'Generated workspace packages must keep workspace:* links' ); assert( rootPackage.scripts?.build === expectedBuildScript, - 'Root build script must build verticals before shell', + 'Root build script must build verticals before shell' ); assert( - rootPackage.scripts?.[SHARED_VALIDATOR_STRING_059] === expectedCloudflareBuildScript, - 'Root cloudflare:build script is incorrect', + rootPackage.scripts?.[SHARED_VALIDATOR_STRING_059] === + expectedCloudflareBuildScript, + 'Root cloudflare:build script is incorrect' ); assert( !('ultramodern:check' in (rootPackage.scripts ?? {})), - 'Root must not expose ultramodern:check', + 'Root must not expose ultramodern:check' ); if (bridgeConfig === undefined) { assert( rootPackage.scripts?.typecheck === SHARED_VALIDATOR_STRING_086, - 'Root typecheck must run TS-Go across the root project reference graph', + 'Root typecheck must run TS-Go across the root project reference graph' ); } else { assert( rootPackage.scripts?.typecheck === 'pnpm -r --filter "./apps/*" --filter "./verticals/*" --filter "./packages/*" run typecheck', - 'Bridge root typecheck must check generated package boundaries without building parent implementation sources', + 'Bridge root typecheck must check generated package boundaries without building parent implementation sources' ); assert( Array.isArray(bridgeConfig.workspacePackages), - 'Bridge config must record workspace package patterns', + 'Bridge config must record workspace package patterns' ); for (const workspacePackage of bridgeConfig.workspacePackages) { assert( rootPackage.workspaces?.includes(workspacePackage.pattern), - `Root workspaces must include bridge package pattern ${workspacePackage.pattern}`, + `Root workspaces must include bridge package pattern ${workspacePackage.pattern}` ); assert( - readText(SHARED_VALIDATOR_STRING_103).includes(` - ${workspacePackage.pattern}`), - `pnpm-workspace.yaml must include bridge package pattern ${workspacePackage.pattern}`, + readText(SHARED_VALIDATOR_STRING_103).includes( + ` - ${workspacePackage.pattern}` + ), + `pnpm-workspace.yaml must include bridge package pattern ${workspacePackage.pattern}` ); } for (const gate of bridgeConfig.gates ?? []) { @@ -6134,146 +6707,190 @@ if (bridgeConfig === undefined) { isString(gate.cwd) && gate.cwd.length > 0 ? `cd ${gate.cwd} && ` : ''; const expectedGateScript = `${workingDirectoryPrefix}${gate.command}`; assert( - valueForKey(Object.entries(rootPackage.scripts ?? {}), `bridge:${gate.name}`) === - expectedGateScript, - `Bridge gate script bridge:${gate.name} is incorrect`, + valueForKey( + Object.entries(rootPackage.scripts ?? {}), + `bridge:${gate.name}` + ) === expectedGateScript, + `Bridge gate script bridge:${gate.name} is incorrect` ); } assert( - (valueForKey(Object.entries(rootPackage.scripts ?? {}), 'bridge:check') ?? '').length > 0, - 'Bridge workspaces must expose bridge:check', + ( + valueForKey(Object.entries(rootPackage.scripts ?? {}), 'bridge:check') ?? + '' + ).length > 0, + 'Bridge workspaces must expose bridge:check' ); } assert( rootPackage.scripts?.['contract:check'] === SHARED_VALIDATOR_STRING_087, - 'Root must expose contract:check', + 'Root must expose contract:check' ); for (const [scriptName, expectedCommand] of Object.entries( - workspaceValidationContract.ciEvidenceScripts, + workspaceValidationContract.ciEvidenceScripts )) { assert( - valueForKey(Object.entries(rootPackage.scripts ?? {}), scriptName) === expectedCommand, - `Root CI evidence command ${scriptName} is missing or incorrect`, + valueForKey(Object.entries(rootPackage.scripts ?? {}), scriptName) === + expectedCommand, + `Root CI evidence command ${scriptName} is missing or incorrect` ); } -const coreRuntimePackage = readJson(PackageJsonSchema, SHARED_VALIDATOR_STRING_093); +const coreRuntimePackage = readJson( + PackageJsonSchema, + SHARED_VALIDATOR_STRING_093 +); assert( - coreRuntimePackage.scripts?.['test:unit'] === 'node --test tests/unit/*.test.ts', - 'Core runtime must expose its complete unit test surface', + coreRuntimePackage.scripts?.['test:unit'] === + 'node --test tests/unit/*.test.ts', + 'Core runtime must expose its complete unit test surface' ); assert( - coreRuntimePackage.scripts?.['test:integration'] === 'node --test tests/integration/*.test.ts', - 'Core runtime must expose its complete service-backed integration test surface', + coreRuntimePackage.scripts?.['test:integration'] === + 'node --test tests/integration/*.test.ts', + 'Core runtime must expose its complete service-backed integration test surface' ); assert( rootPackage.scripts?.['module-entrypoints:check'] === 'node ./scripts/check-module-entrypoint-boundaries.mts', - 'Root must expose module-entrypoints:check', + 'Root must expose module-entrypoints:check' ); assert( rootPackage.scripts?.['scaffold:microvertical-action-boundary'] === 'node ./scripts/scaffolding/cli.mts microvertical-action-boundary', - 'Root must expose the Codesmith MicroVertical Action-boundary command', + 'Root must expose the Codesmith MicroVertical Action-boundary command' ); assert( rootPackage.scripts?.['scaffold:outbox-worker'] === 'node ./scripts/scaffolding/cli.mts outbox-worker', - 'Root must expose the Codesmith Outbox Worker command', + 'Root must expose the Codesmith Outbox Worker command' ); assert( shellPackage.dependencies?.jose === '6.2.5', - 'Shell must own the exact EdDSA signing dependency', + 'Shell must own the exact EdDSA signing dependency' +); +const sharedContractsPackage = readJson( + PackageJsonSchema, + SHARED_VALIDATOR_STRING_095 ); -const sharedContractsPackage = readJson(PackageJsonSchema, SHARED_VALIDATOR_STRING_095); assert( sharedContractsPackage.dependencies?.effect === expectedEffectVersion && sharedContractsPackage.dependencies?.[SHARED_VALIDATOR_STRING_017] === SHARED_VALIDATOR_STRING_169, - 'Shared gateway contracts must use the generated Effect cohort and canonical Core context', + 'Shared gateway contracts must use the generated Effect cohort and canonical Core context' ); const gatewayPrincipalVerifierPackage = readJson( PackageJsonSchema, - `${SHARED_VALIDATOR_STRING_177}/package.json`, + `${SHARED_VALIDATOR_STRING_177}/package.json` ); const gatewayPrincipalVerifierSource = readText( - 'packages/gateway-principal-verifier/src/server.ts', + 'packages/gateway-principal-verifier/src/server.ts' ); assert( sameJson(gatewayPrincipalVerifierPackage.exports, { './server': './src/server.ts', }) && gatewayPrincipalVerifierPackage.dependencies?.jose === '6.2.5' && - gatewayPrincipalVerifierPackage.dependencies?.[SHARED_VALIDATOR_STRING_017] === - SHARED_VALIDATOR_STRING_169 && - gatewayPrincipalVerifierPackage.dependencies?.[SHARED_VALIDATOR_STRING_019] === - SHARED_VALIDATOR_STRING_169 && + gatewayPrincipalVerifierPackage.dependencies?.[ + SHARED_VALIDATOR_STRING_017 + ] === SHARED_VALIDATOR_STRING_169 && + gatewayPrincipalVerifierPackage.dependencies?.[ + SHARED_VALIDATOR_STRING_019 + ] === SHARED_VALIDATOR_STRING_169 && gatewayPrincipalVerifierSource.includes('bindGatewayPrincipalVerifier') && gatewayPrincipalVerifierSource.includes("algorithms: ['EdDSA']") && gatewayPrincipalVerifierSource.includes('decodeGatewayContextClaims') && gatewayPrincipalVerifierSource.includes('TrustedPrincipalContextSchema'), - 'Gateway principal verification must remain a server-only shared package entrypoint with the complete algorithm', + 'Gateway principal verification must remain a server-only shared package entrypoint with the complete algorithm' +); +const gatewayContractSource = readText( + 'packages/shared-contracts/src/gateway-context.ts' +); +const problemDetailsContractSource = readText( + 'packages/shared-contracts/src/problem-details.ts' ); -const gatewayContractSource = readText('packages/shared-contracts/src/gateway-context.ts'); -const problemDetailsContractSource = readText('packages/shared-contracts/src/problem-details.ts'); assert( gatewayContractSource.includes('GATEWAY_ASSERTION_VERSION = 1') && gatewayContractSource.includes('GATEWAY_ASSERTION_TTL_SECONDS = 300') && - gatewayContractSource.includes('GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS = 30') && - gatewayContractSource.includes("HttpApiEndpoint.post('issueGatewayContext'") && + gatewayContractSource.includes( + 'GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS = 30' + ) && + gatewayContractSource.includes( + "HttpApiEndpoint.post('issueGatewayContext'" + ) && gatewayContractSource.includes("alg: Schema.Literal('EdDSA')") && - gatewayContractSource.includes("from '@app/core-runtime/actions/principal-context'") && + gatewayContractSource.includes( + "from '@app/core-runtime/actions/principal-context'" + ) && gatewayContractSource.includes('makeProblemDetailsSchema') && - problemDetailsContractSource.includes("contentType: 'application/problem+json'"), - 'Shared contracts must retain the versioned generic EdDSA gateway assertion protocol', + problemDetailsContractSource.includes( + "contentType: 'application/problem+json'" + ), + 'Shared contracts must retain the versioned generic EdDSA gateway assertion protocol' ); const installedVerticalSource = readText( - 'apps/shell-super-app/api/verticals/installed-verticals.ts', + 'apps/shell-super-app/api/verticals/installed-verticals.ts' ); const shellModernConfigSource = readText(SHARED_VALIDATOR_STRING_048); assert( shellModernConfigSource.includes( - "new URL('../../topology/reference-topology.json', import.meta.url)", + "new URL('../../topology/reference-topology.json', import.meta.url)" ) && - shellModernConfigSource.includes("readFileSync(referenceTopologyPath, 'utf-8')") && shellModernConfigSource.includes( - 'Object.assign(globalThis, {\n ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: referenceTopology', + "readFileSync(referenceTopologyPath, 'utf-8')" + ) && + shellModernConfigSource.includes( + 'Object.assign(globalThis, {\n ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: referenceTopology' + ) && + shellModernConfigSource.includes( + 'ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: referenceTopology' ) && - shellModernConfigSource.includes('ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: referenceTopology') && installedVerticalSource.includes( - 'deriveInstalledVerticalIds(ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY)', + 'deriveInstalledVerticalIds(ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY)' ) && installedVerticalSource.includes("kind: Schema.Literal('vertical')"), - 'Shell installed verticals must derive exclusively from authoritative topology verticals', + 'Shell installed verticals must derive exclusively from authoritative topology verticals' ); assert( - !fs.existsSync(path.join(root, 'packages/shared-contracts/src/gateway-topology.generated.ts')) && - !installedVerticalSource.includes('ultramodernGatewayAudienceTopology'), - 'Shell installed verticals must not introduce a second topology registry', + !fs.existsSync( + path.join( + root, + 'packages/shared-contracts/src/gateway-topology.generated.ts' + ) + ) && !installedVerticalSource.includes('ultramodernGatewayAudienceTopology'), + 'Shell installed verticals must not introduce a second topology registry' ); assert( - rootPackage.scripts?.['api:check'] === 'node ./scripts/check-ultramodern-api-boundaries.mts', - 'Root must expose api:check', + rootPackage.scripts?.['api:check'] === + 'node ./scripts/check-ultramodern-api-boundaries.mts', + 'Root must expose api:check' ); assert( rootPackage.scripts?.['i18n:boundaries'] === 'node ./scripts/check-ultramodern-i18n-boundaries.mts', - 'Root must expose i18n:boundaries', + 'Root must expose i18n:boundaries' ); assert( - rootPackage.scripts?.['performance:readiness'] === SHARED_VALIDATOR_STRING_085, - 'Root must expose default-on performance readiness diagnostics', + rootPackage.scripts?.['performance:readiness'] === + SHARED_VALIDATOR_STRING_085, + 'Root must expose default-on performance readiness diagnostics' ); const actionAuthorizationProvisioningCommand = 'node ./scripts/provision-current-action-authorization.mts'; assert( - rootPackage.scripts?.[SHARED_VALIDATOR_STRING_053] === actionAuthorizationProvisioningCommand, - 'Root must expose the explicit current-Action authorization provisioning command', + rootPackage.scripts?.[SHARED_VALIDATOR_STRING_053] === + actionAuthorizationProvisioningCommand, + 'Root must expose the explicit current-Action authorization provisioning command' ); assert( - rootPackage.scripts?.['local:initialize'] === 'node ./scripts/initialize-local-development.mts' && - !readText('scripts/initialize-local-development.mts').includes(SHARED_VALIDATOR_STRING_106) && - !readText('scripts/initialize-local-development.mts').includes(SHARED_VALIDATOR_STRING_053), - 'Ordinary local initialization must not provision Action authorization', + rootPackage.scripts?.['local:initialize'] === + 'node ./scripts/initialize-local-development.mts' && + !readText('scripts/initialize-local-development.mts').includes( + SHARED_VALIDATOR_STRING_106 + ) && + !readText('scripts/initialize-local-development.mts').includes( + SHARED_VALIDATOR_STRING_053 + ), + 'Ordinary local initialization must not provision Action authorization' ); for (const startupPath of [ 'scripts/locki-feature.sh', @@ -6283,7 +6900,7 @@ for (const startupPath of [ assert( !readText(startupPath).includes(SHARED_VALIDATOR_STRING_106) && !readText(startupPath).includes(SHARED_VALIDATOR_STRING_053), - `${startupPath} must not provision Action authorization automatically`, + `${startupPath} must not provision Action authorization automatically` ); } for (const automaticScript of [ @@ -6292,54 +6909,62 @@ for (const automaticScript of [ SHARED_VALIDATOR_STRING_059, SHARED_VALIDATOR_STRING_060, ]) { - const automaticCommand = valueForKey(Object.entries(rootPackage.scripts ?? {}), automaticScript); + const automaticCommand = valueForKey( + Object.entries(rootPackage.scripts ?? {}), + automaticScript + ); assert( automaticCommand?.includes(SHARED_VALIDATOR_STRING_053) !== true && automaticCommand?.includes(SHARED_VALIDATOR_STRING_106) !== true, - `${automaticScript} must not invoke Action authorization provisioning`, + `${automaticScript} must not invoke Action authorization provisioning` ); } if (hasBackendSurfaces) { assert( rootPackage.scripts?.['node:backend-federation:generate'] === 'node ./scripts/generate-node-backend-federation.mts', - 'Root must expose local Node backend federation artifact generation', + 'Root must expose local Node backend federation artifact generation' ); assert( - rootPackage.scripts?.['node:proof'] === 'node ./scripts/proof-node-backend-federation.mts', - 'Root must expose read-only node:proof for already-built backend federation Effect modules', + rootPackage.scripts?.['node:proof'] === + 'node ./scripts/proof-node-backend-federation.mts', + 'Root must expose read-only node:proof for already-built backend federation Effect modules' ); } else { assert( rootPackage.scripts?.['node:backend-federation:generate'] === undefined, - 'Root must not expose backend federation generation without an API surface', + 'Root must not expose backend federation generation without an API surface' ); assert( rootPackage.scripts?.['node:proof'] === undefined, - 'Root must not expose node:proof without an API surface', + 'Root must not expose node:proof without an API surface' ); } if (hasDeliveryUnits) { assert( - rootPackage.scripts?.['zerops:materialize'] === 'node ./scripts/materialize-zerops-runtime.mjs', - 'Root must expose Zerops runtime materialization script', + rootPackage.scripts?.['zerops:materialize'] === + 'node ./scripts/materialize-zerops-runtime.mjs', + 'Root must expose Zerops runtime materialization script' ); assert( - rootPackage.scripts?.['cloudflare:ssr-proof'] === 'node ./scripts/proof-workerd-ssr.mts', - 'Root must expose workerd distributed SSR composition proof', + rootPackage.scripts?.['cloudflare:ssr-proof'] === + 'node ./scripts/proof-workerd-ssr.mts', + 'Root must expose workerd distributed SSR composition proof' ); assert( - rootPackage.scripts?.[SHARED_VALIDATOR_STRING_059]?.endsWith('&& pnpm cloudflare:ssr-proof'), - 'Root Cloudflare build must finish with workerd distributed SSR composition proof', + rootPackage.scripts?.[SHARED_VALIDATOR_STRING_059]?.endsWith( + '&& pnpm cloudflare:ssr-proof' + ), + 'Root Cloudflare build must finish with workerd distributed SSR composition proof' ); } else { assert( rootPackage.scripts?.['zerops:materialize'] === undefined, - 'Root must not expose Zerops materialization in a shell-only workspace', + 'Root must not expose Zerops materialization in a shell-only workspace' ); assert( rootPackage.scripts?.['cloudflare:ssr-proof'] === undefined, - 'Root must not expose workerd SSR proof in a shell-only workspace', + 'Root must not expose workerd SSR proof in a shell-only workspace' ); } assertNotExists('scripts/generate-node-backend-federation.mjs'); @@ -6353,17 +6978,21 @@ assert( rootPackage.scripts.check.endsWith( bridgeConfig ? '&& pnpm performance:readiness && pnpm bridge:check && pnpm quality:check' - : '&& pnpm performance:readiness && pnpm quality:check', + : '&& pnpm performance:readiness && pnpm quality:check' ), - 'Root check must remain static while running default-on performance readiness diagnostics and bridge gates when configured', + 'Root check must remain static while running default-on performance readiness diagnostics and bridge gates when configured' ); if (hasDeliveryUnits) { const zeropsYaml = readText(SHARED_VALIDATOR_STRING_174); const zeropsMigrator = readText('scripts/run-zerops-migrator.mjs'); const zeropsSpiceDbStart = readText('scripts/run-zerops-spicedb.sh'); - const workerStartCommand = 'node --experimental-strip-types ./src/worker-host/main.ts'; + const workerStartCommand = + 'node --experimental-strip-types ./src/worker-host/main.ts'; const workerDeliveryCount = fullStackVerticals.filter((vertical) => { - const packageJson = readJson(PackageJsonSchema, `${vertical.path}/package.json`); + const packageJson = readJson( + PackageJsonSchema, + `${vertical.path}/package.json` + ); const workerHostPath = `${vertical.path}/src/worker-host/main.ts`; const hasWorkerStart = packageJson.scripts?.['worker:start'] !== undefined; const hasWorkerHost = fs.existsSync(path.join(root, workerHostPath)); @@ -6374,41 +7003,50 @@ if (hasDeliveryUnits) { hasWorkerHost && moduleId !== undefined && readText(workerHostPath).startsWith( - `// @generated by scaffold:outbox-worker worker-host\n// @ontos-outbox-worker-host-owner ${moduleId}\n`, + `// @generated by scaffold:outbox-worker worker-host\n// @ontos-outbox-worker-host-owner ${moduleId}\n` ), - `${vertical.id} worker delivery requires the exact generated worker:start host capability`, + `${vertical.id} worker delivery requires the exact generated worker:start host capability` ); return true; } return false; }).length; - assert(zeropsYaml.includes('zerops:'), 'Zerops manifest must include zerops services'); + assert( + zeropsYaml.includes('zerops:'), + 'Zerops manifest must include zerops services' + ); assert( !zeropsYaml.includes(SHARED_VALIDATOR_STRING_106) && !zeropsYaml.includes(SHARED_VALIDATOR_STRING_053), - 'Zerops startup and deployment must not provision Action authorization automatically', + 'Zerops startup and deployment must not provision Action authorization automatically' ); assert( - zeropsYaml.includes(`setup: ${quoteYamlString(SHARED_VALIDATOR_STRING_132)}`), - 'Zerops manifest must include shell service', + zeropsYaml.includes( + `setup: ${quoteYamlString(SHARED_VALIDATOR_STRING_132)}` + ), + 'Zerops manifest must include shell service' + ); + const shellSetup = yamlListItemBlock( + zeropsYaml, + 'setup', + SHARED_VALIDATOR_STRING_132 ); - const shellSetup = yamlListItemBlock(zeropsYaml, 'setup', SHARED_VALIDATOR_STRING_132); const shellBuild = yamlMappingBlock(shellSetup, 'build', 4); const shellBuildEnvironment = yamlMappingBlock(shellBuild, 'envVariables', 6); assert( shellBuildEnvironment.includes('ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: stage'), - 'Zerops shell builds must compile stage module discovery from project deployment URLs', + 'Zerops shell builds must compile stage module discovery from project deployment URLs' ); assert( zeropsYaml.includes(`base: ${quoteYamlString('alpine@3.23')}`), - 'Zerops manifest must use the provisioned Alpine runtime version', + 'Zerops manifest must use the provisioned Alpine runtime version' ); assert( zeropsYaml.includes(`base: ${quoteYamlString('nodejs@24')}`) && zeropsYaml.includes( - `initCommands:\n - ZEROPS_NODE_ROOT=/var/www sh app/scripts/install-zerops-node.sh`, + `initCommands:\n - ZEROPS_NODE_ROOT=/var/www sh app/scripts/install-zerops-node.sh` ), - 'Zerops Node services must install pinned Node during container initialization without a custom runtime image', + 'Zerops Node services must install pinned Node during container initialization without a custom runtime image' ); assert( zeropsYaml.includes( @@ -6423,65 +7061,74 @@ if (hasDeliveryUnits) { templatePlaceholderOpening, 'db_port}/', templatePlaceholderOpening, - 'db_dbName}', - ), + 'db_dbName}' + ) ) && !zeropsYaml.includes( - sourceFragment('DATABASE_ADMIN_URL: ', templatePlaceholderOpening, 'db_connectionString}'), + sourceFragment( + 'DATABASE_ADMIN_URL: ', + templatePlaceholderOpening, + 'db_connectionString}' + ) ), - 'Zerops migrator must use the PostgreSQL administrative identity for role and database bootstrap', + 'Zerops migrator must use the PostgreSQL administrative identity for role and database bootstrap' ); assert( zeropsYaml.includes('deployFiles:'), - 'Zerops manifest must deploy package-pruned runtime directories', + 'Zerops manifest must deploy package-pruned runtime directories' ); const localVirtualStoreInstall = 'PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm install --frozen-lockfile --force --config.enable-global-virtual-store=false --virtual-store-dir=node_modules/.pnpm'; assert( zeropsYaml.split(localVirtualStoreInstall).length - 1 === fullStackVerticals.length + 2 + workerDeliveryCount, - 'Every Zerops Node build must install dependencies into a project-local virtual store', + 'Every Zerops Node build must install dependencies into a project-local virtual store' ); - const cleanWorkspaceDependencies = 'node scripts/reset-workspace-dependencies.mjs'; + const cleanWorkspaceDependencies = + 'node scripts/reset-workspace-dependencies.mjs'; assert( zeropsYaml.split(cleanWorkspaceDependencies).length - 1 === fullStackVerticals.length + 2 + workerDeliveryCount, - 'Every Zerops Node build must remove cached dependency links before installing', + 'Every Zerops Node build must remove cached dependency links before installing' ); const expectedZeropsPnpmCommands = 1 + 3 * (fullStackVerticals.length + 1) + 2 * workerDeliveryCount; assert( - zeropsYaml.split('--config.enable-global-virtual-store=false').length - 1 === + zeropsYaml.split('--config.enable-global-virtual-store=false').length - + 1 === expectedZeropsPnpmCommands, - 'Every Zerops pnpm command must override higher-priority host global-virtual-store configuration', + 'Every Zerops pnpm command must override higher-priority host global-virtual-store configuration' ); assert( - zeropsYaml.split('PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false').length - 1 === + zeropsYaml.split('PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false').length - + 1 === expectedZeropsPnpmCommands, - 'Every Zerops pnpm command must propagate local virtual-store configuration to child processes', + 'Every Zerops pnpm command must propagate local virtual-store configuration to child processes' ); assert( zeropsYaml.split('NODE_OPTIONS=--max-old-space-size=4096').length - 1 === fullStackVerticals.length + 1, - 'Every Modern.js Zerops deployment build must reserve enough Node.js heap for dependency tracing', + 'Every Modern.js Zerops deployment build must reserve enough Node.js heap for dependency tracing' ); assert( zeropsYaml.includes( - `start: sh -c ${quoteYamlString('cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve')}`, + `start: sh -c ${quoteYamlString('cd app/.zerops/runtime/shell-super-app && PATH="/var/www/.local/node-26.7.0/bin:$PATH" exec npm run serve')}` ), - 'Zerops shell service must start from materialized runtime package', + 'Zerops shell service must start from materialized runtime package' ); const allDeclaredPortsPresent = [ workspaceValidationContract.topology.compactConfig.apps.find( - (app) => app.id === SHARED_VALIDATOR_STRING_131, + (app) => app.id === SHARED_VALIDATOR_STRING_131 ), ...fullStackVerticals, - ].every((app) => app !== undefined && zeropsYaml.includes(`PORT: '${app.port}'`)); + ].every( + (app) => app !== undefined && zeropsYaml.includes(`PORT: '${app.port}'`) + ); assert( allDeclaredPortsPresent && zeropsYaml.includes('SHELL_SUPER_APP_PORT:') && zeropsYaml.includes('ULTRAMODERN_ZEROPS_SERVICE:'), - 'Zerops manifest must expose service identity and bind Modern.js runtimes to their declared ports', + 'Zerops manifest must expose service identity and bind Modern.js runtimes to their declared ports' ); assert( zeropsYaml.includes(`setup: ${quoteYamlString('migrator')}`) && @@ -6489,28 +7136,28 @@ if (hasDeliveryUnits) { zeropsYaml.includes(`start: sh app/scripts/run-zerops-spicedb.sh`) && zeropsSpiceDbStart.includes('authzed/spicedb:v1.56.0') && zeropsSpiceDbStart.includes('--network=host'), - 'Zerops manifest must include the pinned remote migration and SpiceDB services', + 'Zerops manifest must include the pinned remote migration and SpiceDB services' ); const spiceDbSetup = yamlListItemBlock(zeropsYaml, 'setup', 'spicedb'); assert( spiceDbSetup.includes('temporaryShutdown: true'), - 'Zerops SpiceDB deploys must avoid overlapping database connection pools', + 'Zerops SpiceDB deploys must avoid overlapping database connection pools' ); assert( !zeropsMigrator.includes("run('pnpm'") && zeropsMigrator.includes("'node_modules', '.bin', 'drizzle-kit'"), - 'Zerops migrator must execute the relocated dependency tree without invoking pnpm runtime verification', + 'Zerops migrator must execute the relocated dependency tree without invoking pnpm runtime verification' ); for (const vertical of fullStackVerticals) { assert( zeropsYaml.includes(`setup: ${quoteYamlString(vertical.id)}`), - `${vertical.id} must have a Zerops service`, + `${vertical.id} must have a Zerops service` ); assert( zeropsYaml.includes( - `PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app ${quoteShellValue(vertical.id)} --package ${quoteShellValue(vertical.packageName)} --package-dir ${quoteShellValue(vertical.path)}`, + `PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false PATH="$HOME/.local/node-26.7.0/bin:$PATH" pnpm --config.enable-global-virtual-store=false run zerops:materialize -- --app ${quoteShellValue(vertical.id)} --package ${quoteShellValue(vertical.packageName)} --package-dir ${quoteShellValue(vertical.path)}` ), - `${vertical.id} Zerops service must materialize its runtime package`, + `${vertical.id} Zerops service must materialize its runtime package` ); // Only REST-backed (or headless-less UI) units expose an HTTP readiness // probe; the RPC surface has no REST readiness endpoint (G7a). @@ -6520,7 +7167,7 @@ if (hasDeliveryUnits) { : '/'; assert( zeropsYaml.includes(`path: ${quoteYamlString(zeropsReadinessPath)}`), - `${vertical.id} BFF readiness path must use generated API prefix`, + `${vertical.id} BFF readiness path must use generated API prefix` ); } } @@ -6529,122 +7176,148 @@ if (hasDeliveryUnits) { SHARED_VALIDATOR_STRING_047, ...fullStackVerticals.map((vertical) => vertical.path), ]) { - const deliveryUnitPackage = readJson(PackageJsonSchema, `${deliveryUnitPath}/package.json`); + const deliveryUnitPackage = readJson( + PackageJsonSchema, + `${deliveryUnitPath}/package.json` + ); assert( deliveryUnitPackage.scripts?.build?.includes( - 'MODERNJS_DEPLOY=node modern deploy --skip-build', + 'MODERNJS_DEPLOY=node modern deploy --skip-build' ) ?? false, - `${deliveryUnitPath} build must produce the Modern.js Node output before Zerops materialization`, + `${deliveryUnitPath} build must produce the Modern.js Node output before Zerops materialization` ); } assert( - (zeropsMaterializer.includes("const appOutputDir = path.join(appRoot, '.output')") || - zeropsMaterializer.includes("const appOutputDir = pathService.join(appRoot, '.output')")) && + (zeropsMaterializer.includes( + "const appOutputDir = path.join(appRoot, '.output')" + ) || + zeropsMaterializer.includes( + "const appOutputDir = pathService.join(appRoot, '.output')" + )) && zeropsMaterializer.includes('before runtime materialization'), - 'Zerops materializer must require the Node output produced by the package build', + 'Zerops materializer must require the Node output produced by the package build' ); assert( zeropsMaterializer.includes('normalizeRuntimePackageDependencies'), - 'Zerops materializer must normalize generated Modern package aliases before installing dependencies', + 'Zerops materializer must normalize generated Modern package aliases before installing dependencies' ); assert( zeropsMaterializer.includes('removeIncompatiblePlatformDependencies') && zeropsMaterializer.includes('isCurrentPlatformSupported') && zeropsMaterializer.includes('process.platform') && zeropsMaterializer.includes('process.arch'), - 'Zerops materializer must exclude traced dependencies that declare an incompatible runtime OS or CPU', + 'Zerops materializer must exclude traced dependencies that declare an incompatible runtime OS or CPU' ); assert( zeropsMaterializer.includes('officialPackageName'), - 'Zerops materializer must add official Modern.js npm aliases for generated runtime imports', + 'Zerops materializer must add official Modern.js npm aliases for generated runtime imports' ); assert( zeropsMaterializer.includes('installRuntimeDependencies'), - 'Zerops materializer must install dependencies outside the workspace copy runtime node_modules', + 'Zerops materializer must install dependencies outside the workspace copy runtime node_modules' ); assert( zeropsMaterializer.includes('copyWorkspacePackage'), - 'Zerops materializer must preserve generated local workspace package dependencies', + 'Zerops materializer must preserve generated local workspace package dependencies' ); assert( zeropsMaterializer.includes('makeWorkspacePackageRuntimeSafe'), - 'Zerops materializer must rewrite copied workspace packages to runtime-safe JavaScript exports', + 'Zerops materializer must rewrite copied workspace packages to runtime-safe JavaScript exports' ); assert( - zeropsMaterializer.includes("serve: runtimePackage.scripts?.serve ?? 'node index.js'"), - 'Zerops materializer must preserve runtime serve script fallback', + zeropsMaterializer.includes( + "serve: runtimePackage.scripts?.serve ?? 'node index.js'" + ), + 'Zerops materializer must preserve runtime serve script fallback' ); assert( /'install',\s*'--omit=dev'/u.test(zeropsMaterializer), - 'Zerops materializer must omit dev dependencies from runtime installs', + 'Zerops materializer must omit dev dependencies from runtime installs' ); assert( zeropsMaterializer.includes("'--legacy-peer-deps'"), - 'Zerops materializer must tolerate generated-workspace peer dependency ranges in npm runtime install', + 'Zerops materializer must tolerate generated-workspace peer dependency ranges in npm runtime install' ); } const performanceReadinessConfig = readText(SHARED_VALIDATOR_STRING_121); const assertToolWrapper = (scriptPath: string, command: string): void => { const source = readText(scriptPath); assert( - hasUltramodernDispatch(source, command, readText('scripts/shared/ultramodern-command.mts')) || + hasUltramodernDispatch( + source, + command, + readText('scripts/shared/ultramodern-command.mts') + ) || (command === 'skills' && - hasUltramodernSkillsDispatch(source, readText('scripts/shared/ultramodern-launch.mts'))), - `${scriptPath} must delegate ${command} through the override-aware UltraModern runner`, + hasUltramodernSkillsDispatch( + source, + readText('scripts/shared/ultramodern-launch.mts') + )), + `${scriptPath} must delegate ${command} through the override-aware UltraModern runner` ); }; assert( - performanceReadinessConfig.includes('UltramodernPerformanceReadinessDiagnosticsConfig'), - 'Performance readiness config must carry the typed opt-out surface', + performanceReadinessConfig.includes( + 'UltramodernPerformanceReadinessDiagnosticsConfig' + ), + 'Performance readiness config must carry the typed opt-out surface' ); assert( performanceReadinessConfig.includes('enabled: true'), - 'Performance readiness diagnostics must be default-on', + 'Performance readiness diagnostics must be default-on' ); assert( performanceReadinessConfig.includes("failOn: 'framework-invariant'"), - 'Performance readiness diagnostics must only fail framework invariants by default', + 'Performance readiness diagnostics must only fail framework invariants by default' ); assertToolWrapper(SHARED_VALIDATOR_STRING_122, 'performance-readiness'); -const i18nBoundaryScript = readText('scripts/check-ultramodern-i18n-boundaries.mts'); +const i18nBoundaryScript = readText( + 'scripts/check-ultramodern-i18n-boundaries.mts' +); assertToolWrapper(SHARED_VALIDATOR_STRING_123, 'typecheck'); assert( i18nBoundaryScript.includes("from '@modern-js/code-tools'") && i18nBoundaryScript.includes('runWorkspaceSourceCheck'), - 'Root i18n boundary script must call @modern-js/code-tools', + 'Root i18n boundary script must call @modern-js/code-tools' ); assert( rootPackage.scripts?.['mf:types'] === SHARED_VALIDATOR_STRING_082, - 'Root must expose mf:types', + 'Root must expose mf:types' ); assert( - rootPackage.scripts?.[SHARED_VALIDATOR_STRING_060] === expectedCloudflareDeployScript, - 'Root must expose cloudflare:deploy', + rootPackage.scripts?.[SHARED_VALIDATOR_STRING_060] === + expectedCloudflareDeployScript, + 'Root must expose cloudflare:deploy' ); assert( - rootPackage.scripts?.[SHARED_VALIDATOR_STRING_061] === SHARED_VALIDATOR_STRING_084, - 'Root must expose cloudflare:proof', + rootPackage.scripts?.[SHARED_VALIDATOR_STRING_061] === + SHARED_VALIDATOR_STRING_084, + 'Root must expose cloudflare:proof' ); assert( - rootPackage.scripts?.['migrate:strict-effect'] === SHARED_VALIDATOR_STRING_083, - 'Root must expose migrate:strict-effect', + rootPackage.scripts?.['migrate:strict-effect'] === + SHARED_VALIDATOR_STRING_083, + 'Root must expose migrate:strict-effect' ); assert( - rootPackage.scripts?.['skills:install'] === 'node ./scripts/bootstrap-agent-skills.mts', - 'Root must expose skills:install', + rootPackage.scripts?.['skills:install'] === + 'node ./scripts/bootstrap-agent-skills.mts', + 'Root must expose skills:install' ); assert( - rootPackage.scripts?.['skills:check'] === 'node ./scripts/bootstrap-agent-skills.mts --check', - 'Root must expose skills:check', + rootPackage.scripts?.['skills:check'] === + 'node ./scripts/bootstrap-agent-skills.mts --check', + 'Root must expose skills:check' ); assert( rootPackage.scripts?.postinstall === "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt . '!repos/**'", - 'Root postinstall must run the default-on Codex skills bootstrap, format installed skills, and leave reference repository installs explicit', + 'Root postinstall must run the default-on Codex skills bootstrap, format installed skills, and leave reference repository installs explicit' ); assert( - rootPackage.scripts?.['agents:refs:install'] === 'node ./scripts/setup-agent-reference-repos.mts', - 'Root must expose agents:refs:install as the explicit reference repo installer', + rootPackage.scripts?.['agents:refs:install'] === + 'node ./scripts/setup-agent-reference-repos.mts', + 'Root must expose agents:refs:install as the explicit reference repo installer' ); const agentSkillsBootstrap = readText(SHARED_VALIDATOR_STRING_115); assertToolWrapper(SHARED_VALIDATOR_STRING_114, 'mf-types'); @@ -6659,23 +7332,28 @@ assertToolWrapper(SHARED_VALIDATOR_STRING_125, 'cloudflare-output-verify'); assertToolWrapper('scripts/migrate-strict-effect.mts', 'migrate-strict-effect'); assertToolWrapper(SHARED_VALIDATOR_STRING_115, 'skills'); assert( - !agentSkillsBootstrap.includes("run('brew'") && !agentSkillsBootstrap.includes('runShell('), - 'Agent skills bootstrap must never invoke system package managers', + !agentSkillsBootstrap.includes("run('brew'") && + !agentSkillsBootstrap.includes('runShell('), + 'Agent skills bootstrap must never invoke system package managers' +); +const agentReferenceRepoSetup = readText( + 'scripts/setup-agent-reference-repos.mts' ); -const agentReferenceRepoSetup = readText('scripts/setup-agent-reference-repos.mts'); assert( agentReferenceRepoSetup.includes("['commit', '--no-verify', '-m', message]"), - 'Agent reference repo installer commits must skip hooks during postinstall', + 'Agent reference repo installer commits must skip hooks during postinstall' ); assert( - agentReferenceRepoSetup.includes("commitInstallerChanges('Initialize UltraModern workspace')"), - 'Initial agent reference repo commit must use the installer commit helper', + agentReferenceRepoSetup.includes( + "commitInstallerChanges('Initialize UltraModern workspace')" + ), + 'Initial agent reference repo commit must use the installer commit helper' ); assert( agentReferenceRepoSetup.includes( - "commitInstallerChanges('Record agent reference repo manifest')", + "commitInstallerChanges('Record agent reference repo manifest')" ), - 'Agent reference repo manifest commit must use the installer commit helper', + 'Agent reference repo manifest commit must use the installer commit helper' ); const expectedAppIds = [ @@ -6689,49 +7367,54 @@ const expectedCloudflareCompatibilityFlags = [ assert( sameJson( generatedContract.apps?.map((app) => app.id), - expectedAppIds, + expectedAppIds ), - 'Generated contract must contain shell plus the full-stack verticals', + 'Generated contract must contain shell plus the full-stack verticals' ); assert( - generatedContract.cssFederation?.sharedDesignTokens?.owner?.id === SHARED_VALIDATOR_STRING_128, - 'CSS federation must declare shared design token ownership', + generatedContract.cssFederation?.sharedDesignTokens?.owner?.id === + SHARED_VALIDATOR_STRING_128, + 'CSS federation must declare shared design token ownership' ); assert( - generatedContract.cssFederation?.sharedDesignTokens?.role === SHARED_VALIDATOR_STRING_128, - 'CSS federation must mark shared-design-tokens as token owner', + generatedContract.cssFederation?.sharedDesignTokens?.role === + SHARED_VALIDATOR_STRING_128, + 'CSS federation must mark shared-design-tokens as token owner' ); assert( generatedContract.cssFederation?.sharedDesignTokens?.rootSelector === ':root', - 'Shared design tokens must declare their root selector', + 'Shared design tokens must declare their root selector' ); assert( generatedContract.cssFederation?.sharedDesignTokens?.classPrefix === '--um-', - 'Shared design tokens must declare their CSS custom property prefix', + 'Shared design tokens must declare their CSS custom property prefix' ); assert( generatedContract.cssFederation?.sharedDesignTokens?.layers?.owned?.includes( - SHARED_VALIDATOR_STRING_149, + SHARED_VALIDATOR_STRING_149 ), - 'Shared design tokens must own the shared token CSS layer', + 'Shared design tokens must own the shared token CSS layer' ); assert( generatedContract.cssFederation?.sharedDesignTokens?.entrypoints?.css?.includes( - SHARED_VALIDATOR_STRING_097, + SHARED_VALIDATOR_STRING_097 ), - 'Shared design tokens must declare their CSS entrypoint', + 'Shared design tokens must declare their CSS entrypoint' ); assert( - generatedContract.cssFederation?.sharedDesignTokens?.assets?.exports?.includes('./tokens.css'), - 'Shared design tokens must export their CSS asset', + generatedContract.cssFederation?.sharedDesignTokens?.assets?.exports?.includes( + './tokens.css' + ), + 'Shared design tokens must export their CSS asset' ); assert( - !generatedContract.cssFederation?.sharedDesignTokens?.dedupe?.duplicateBaseStylesAllowed, - 'Shared design token CSS must be deduplicated', + !generatedContract.cssFederation?.sharedDesignTokens?.dedupe + ?.duplicateBaseStylesAllowed, + 'Shared design token CSS must be deduplicated' ); assert( generatedContract.cssFederation?.sharedDesignTokens?.ssr?.firstPaintRequired, - 'Shared design token CSS must be required for SSR first paint', + 'Shared design token CSS must be required for SSR first paint' ); const expectedPerformanceReadinessSignals = [ 'bfcache', @@ -6743,266 +7426,304 @@ const expectedPerformanceReadinessSignals = [ ]; assert( generatedContract.performanceReadiness?.default === 'enabled', - 'Performance readiness diagnostics must be default-on in the generated contract', + 'Performance readiness diagnostics must be default-on in the generated contract' ); assert( generatedContract.performanceReadiness?.mode === 'diagnostic', - 'Performance readiness must remain diagnostic-only', + 'Performance readiness must remain diagnostic-only' ); assert( - generatedContract.performanceReadiness?.report?.script === SHARED_VALIDATOR_STRING_122, - 'Performance readiness contract must point at the generated script', + generatedContract.performanceReadiness?.report?.script === + SHARED_VALIDATOR_STRING_122, + 'Performance readiness contract must point at the generated script' ); assert( generatedContract.performanceReadiness?.report?.deterministic, - 'Performance readiness reports must be deterministic', + 'Performance readiness reports must be deterministic' ); assert( - generatedContract.performanceReadiness?.optOut?.env === SHARED_VALIDATOR_STRING_146, - 'Performance readiness env opt-out is incorrect', + generatedContract.performanceReadiness?.optOut?.env === + SHARED_VALIDATOR_STRING_146, + 'Performance readiness env opt-out is incorrect' ); assert( sameJson( generatedContract.performanceReadiness?.signals?.map((signal) => signal.id), - expectedPerformanceReadinessSignals, + expectedPerformanceReadinessSignals ), - 'Performance readiness signal ids are incorrect', + 'Performance readiness signal ids are incorrect' ); const shellModernConfig = readText(SHARED_VALIDATOR_STRING_048); const shellModuleFederationConfig = readText(SHARED_VALIDATOR_STRING_049); -const shellModernAppEnv = readText('apps/shell-super-app/src/modern-app-env.d.ts'); +const shellModernAppEnv = readText( + 'apps/shell-super-app/src/modern-app-env.d.ts' +); const gitignore = readText('.gitignore'); -const shellRouteHead = readText('apps/shell-super-app/src/routes/ultramodern-route-head.tsx'); +const shellRouteHead = readText( + 'apps/shell-super-app/src/routes/ultramodern-route-head.tsx' +); const shellRouteMetadata = readText( - 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', + 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts' ); assert( /^\.mf\/$/mu.test(gitignore), - 'Generated .gitignore must ignore root Module Federation diagnostics', + 'Generated .gitignore must ignore root Module Federation diagnostics' ); assert( /^\*\*\/\.mf\/$/mu.test(gitignore), - 'Generated .gitignore must ignore per-app Module Federation diagnostics', + 'Generated .gitignore must ignore per-app Module Federation diagnostics' ); assert( /^dist-cloudflare\/$/mu.test(gitignore), - 'Generated .gitignore must ignore Cloudflare build output', + 'Generated .gitignore must ignore Cloudflare build output' ); assert( /^\.output\/$/mu.test(gitignore), - 'Generated .gitignore must ignore root final deployment output', + 'Generated .gitignore must ignore root final deployment output' ); assert( /^\*\*\/\.output\/$/mu.test(gitignore), - 'Generated .gitignore must ignore per-app final deployment output', + 'Generated .gitignore must ignore per-app final deployment output' ); assert( /^\*\*\/src\/modern-tanstack\/$/mu.test(gitignore), - 'Generated .gitignore must ignore framework-owned TanStack router output', + 'Generated .gitignore must ignore framework-owned TanStack router output' ); assert( /^\*\*\/\.tsgo\.\*\.resolved\.json$/mu.test(gitignore), - 'Generated .gitignore must ignore transient TS-Go resolution output', + 'Generated .gitignore must ignore transient TS-Go resolution output' ); assert( - shellModernAppEnv.includes('/// '), - 'Shell app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file', + shellModernAppEnv.includes( + '/// ' + ), + 'Shell app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file' ); assert( /declare const ULTRAMODERN_SITE_URL: string;/u.test(shellModernAppEnv), - 'Shell app env must keep generated globals explicit in ambient scope', + 'Shell app env must keep generated globals explicit in ambient scope' ); assert( !shellModernAppEnv.includes("declare module '*.svg'"), - 'Shell app env must not redeclare framework-owned svg asset modules', + 'Shell app env must not redeclare framework-owned svg asset modules' ); assert( !shellModernAppEnv.includes("declare module '*.css'"), - 'Shell app env must not redeclare framework-owned css asset modules', + 'Shell app env must not redeclare framework-owned css asset modules' ); assert( shellRouteMetadata.includes('@generated by @modern-js/ultramodern-create'), - 'Shell route metadata compatibility manifest must be marked generated', + 'Shell route metadata compatibility manifest must be marked generated' ); assert( shellRouteMetadata.includes( - 'Author route metadata in colocated src/routes/**/route.meta.ts files.', + 'Author route metadata in colocated src/routes/**/route.meta.ts files.' ), - 'Shell route metadata manifest must advertise colocated authoring', + 'Shell route metadata manifest must advertise colocated authoring' ); const expectedZephyrDependencies = Object.fromEntries( expectedPrimaryShellVerticalIds.flatMap((verticalId) => { - const vertical = fullStackVerticals.find((candidate) => candidate.id === verticalId); - assert(vertical !== undefined, `Missing primary-shell vertical ${verticalId}`); + const vertical = fullStackVerticals.find( + (candidate) => candidate.id === verticalId + ); + assert( + vertical !== undefined, + `Missing primary-shell vertical ${verticalId}` + ); if (vertical === undefined) { return []; } return vertical.exposes.length === 0 ? [] : [[vertical.zephyrAlias, `${vertical.packageName}@workspace:*`]]; - }), + }) ); assert( - sameJson(shellPackage[SHARED_VALIDATOR_STRING_173], expectedZephyrDependencies), - 'Shell Zephyr dependencies must reference every primary-shell vertical package', + sameJson( + shellPackage[SHARED_VALIDATOR_STRING_173], + expectedZephyrDependencies + ), + 'Shell Zephyr dependencies must reference every primary-shell vertical package' ); assert( shellPackage.devDependencies?.[SHARED_VALIDATOR_STRING_022] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_022), - 'Shell app-tools dependency must match package source metadata', + 'Shell app-tools dependency must match package source metadata' ); assert( shellPackage.dependencies?.[SHARED_VALIDATOR_STRING_025] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_025), - 'Shell plugin-bff dependency must match package source metadata', + 'Shell plugin-bff dependency must match package source metadata' ); assert( shellPackage.dependencies?.[SHARED_VALIDATOR_STRING_027] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_027), - 'Shell plugin-i18n dependency must match package source metadata', + 'Shell plugin-i18n dependency must match package source metadata' ); assert( shellPackage.dependencies?.[SHARED_VALIDATOR_STRING_028] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_028), - 'Shell plugin-tanstack dependency must match package source metadata', + 'Shell plugin-tanstack dependency must match package source metadata' ); assert( shellPackage.dependencies?.[SHARED_VALIDATOR_STRING_029] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_029), - 'Shell runtime dependency must match package source metadata', + 'Shell runtime dependency must match package source metadata' ); assert( - shellPackage.scripts?.[SHARED_VALIDATOR_STRING_060] === SHARED_VALIDATOR_STRING_144, - 'Shell must expose cloudflare:deploy', + shellPackage.scripts?.[SHARED_VALIDATOR_STRING_060] === + SHARED_VALIDATOR_STRING_144, + 'Shell must expose cloudflare:deploy' +); +assertTargetIsolatedBuildArtifacts( + SHARED_VALIDATOR_STRING_131, + shellModernConfig +); +assertCloudflareBuildSkipsDeployRebuild( + SHARED_VALIDATOR_STRING_131, + shellPackage +); +const shellContract = generatedContract.apps?.find( + (app) => app.id === SHARED_VALIDATOR_STRING_131 ); -assertTargetIsolatedBuildArtifacts(SHARED_VALIDATOR_STRING_131, shellModernConfig); -assertCloudflareBuildSkipsDeployRebuild(SHARED_VALIDATOR_STRING_131, shellPackage); -const shellContract = generatedContract.apps?.find((app) => app.id === SHARED_VALIDATOR_STRING_131); assert( - shellContract?.deploy?.cloudflare?.workerName === expectedWorkerName(SHARED_VALIDATOR_STRING_131), - 'Shell Cloudflare workerName is incorrect', + shellContract?.deploy?.cloudflare?.workerName === + expectedWorkerName(SHARED_VALIDATOR_STRING_131), + 'Shell Cloudflare workerName is incorrect' ); assert( - shellContract?.deploy?.cloudflare?.publicUrlEnv === SHARED_VALIDATOR_STRING_148, - 'Shell Cloudflare public URL env is incorrect', + shellContract?.deploy?.cloudflare?.publicUrlEnv === + SHARED_VALIDATOR_STRING_148, + 'Shell Cloudflare public URL env is incorrect' ); assert( - shellContract?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, - 'Shell Cloudflare compatibilityDate is incorrect', + shellContract?.deploy?.cloudflare?.compatibilityDate === + expectedCloudflareCompatibilityDate, + 'Shell Cloudflare compatibilityDate is incorrect' ); assert( sameJson( shellContract?.deploy?.cloudflare?.compatibilityFlags, - expectedCloudflareCompatibilityFlags, + expectedCloudflareCompatibilityFlags ), - 'Shell Cloudflare compatibility flags are incorrect', + 'Shell Cloudflare compatibility flags are incorrect' ); assert( - sameJson(shellContract?.deploy?.cloudflare?.security, expectedCloudflareSecurity), - 'Shell Cloudflare security contract is incorrect', + sameJson( + shellContract?.deploy?.cloudflare?.security, + expectedCloudflareSecurity + ), + 'Shell Cloudflare security contract is incorrect' ); assertCloudflareQualityGates( SHARED_VALIDATOR_STRING_131, - shellContract?.deploy?.cloudflare?.qualityGates, + shellContract?.deploy?.cloudflare?.qualityGates ); assert( - shellContract?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, - 'Shell worker compatibilityDate is incorrect', + shellContract?.deploy?.worker?.compatibilityDate === + expectedCloudflareCompatibilityDate, + 'Shell worker compatibilityDate is incorrect' ); assert( - shellContract?.deploy?.worker?.name === expectedWorkerName(SHARED_VALIDATOR_STRING_131), - 'Shell worker name is incorrect', + shellContract?.deploy?.worker?.name === + expectedWorkerName(SHARED_VALIDATOR_STRING_131), + 'Shell worker name is incorrect' ); assert( shellModernConfig.includes( - `const cloudflareWorkerName = '${expectedWorkerName(SHARED_VALIDATOR_STRING_131)}'`, + `const cloudflareWorkerName = '${expectedWorkerName(SHARED_VALIDATOR_STRING_131)}'` ), - 'Shell modern.config.ts must define the Cloudflare worker name', + 'Shell modern.config.ts must define the Cloudflare worker name' ); assert( shellModernConfig.includes('name: cloudflareWorkerName'), - 'Shell modern.config.ts must wire deploy.worker.name', + 'Shell modern.config.ts must wire deploy.worker.name' ); assert( shellModernConfig.includes('const assetPrefix ='), - 'Shell modern.config.ts must derive a dedicated asset prefix', + 'Shell modern.config.ts must derive a dedicated asset prefix' ); assert( shellModernConfig.includes( - "const configuredUltramodernAssetPrefix = envValue('ULTRAMODERN_ASSET_PREFIX')", + "const configuredUltramodernAssetPrefix = envValue('ULTRAMODERN_ASSET_PREFIX')" ), - 'Shell asset prefix must support ULTRAMODERN_ASSET_PREFIX', + 'Shell asset prefix must support ULTRAMODERN_ASSET_PREFIX' ); assert( - shellModernConfig.includes("const configuredModernAssetPrefix = envValue('MODERN_ASSET_PREFIX')"), - 'Shell asset prefix must support MODERN_ASSET_PREFIX', + shellModernConfig.includes( + "const configuredModernAssetPrefix = envValue('MODERN_ASSET_PREFIX')" + ), + 'Shell asset prefix must support MODERN_ASSET_PREFIX' ); assert( shellModernConfig.includes("const defaultAssetPrefix = '/'"), - 'Shell asset prefix must default to origin-relative assets', + 'Shell asset prefix must default to origin-relative assets' ); -const shellAssetPrefixExpression = extractAssetPrefixExpression(shellModernConfig); +const shellAssetPrefixExpression = + extractAssetPrefixExpression(shellModernConfig); assert( shellAssetPrefixExpression.includes( - 'configuredModernAssetPrefix || configuredUltramodernAssetPrefix || defaultAssetPrefix', + 'configuredModernAssetPrefix || configuredUltramodernAssetPrefix || defaultAssetPrefix' ), - 'Shell asset prefix fallback order is incorrect', + 'Shell asset prefix fallback order is incorrect' ); assert( !shellAssetPrefixExpression.includes('configuredSiteUrl') && !shellAssetPrefixExpression.includes(SHARED_VALIDATOR_STRING_081), - 'Shell asset prefix must not fall back to MODERN_PUBLIC_SITE_URL', + 'Shell asset prefix must not fall back to MODERN_PUBLIC_SITE_URL' ); assert( !shellAssetPrefixExpression.includes('configuredCloudflareUrl') && !shellAssetPrefixExpression.includes(SHARED_VALIDATOR_STRING_148), - 'Shell asset prefix must not fall back to the per-app public URL', + 'Shell asset prefix must not fall back to the per-app public URL' ); assert( !shellAssetPrefixExpression.includes('inferredCloudflareUrl') && !shellAssetPrefixExpression.includes(SHARED_VALIDATOR_STRING_145), - 'Shell asset prefix must not infer workers.dev URLs', + 'Shell asset prefix must not infer workers.dev URLs' ); assert( shellModernConfig.includes("assetPrefix: '/'"), - 'Shell modern.config.ts must keep dev assets origin-relative', + 'Shell modern.config.ts must keep dev assets origin-relative' ); assert( shellModernConfig.includes('assetPrefix,'), - 'Shell modern.config.ts must wire output.assetPrefix to the derived asset prefix', + 'Shell modern.config.ts must wire output.assetPrefix to the derived asset prefix' ); assert( shellContract?.config?.dev?.assetPrefix === '/', - 'Shell dev asset prefix must stay origin-relative', + 'Shell dev asset prefix must stay origin-relative' ); assert( shellContract?.config?.output?.assetPrefix?.default === '/', - 'Shell asset prefix must default to origin-relative paths', + 'Shell asset prefix must default to origin-relative paths' ); assert( sameJson(shellContract?.config?.output?.assetPrefix?.envFallbackOrder, [ SHARED_VALIDATOR_STRING_080, SHARED_VALIDATOR_STRING_143, ]), - 'Shell asset prefix env fallback order is incorrect', + 'Shell asset prefix env fallback order is incorrect' ); assert( !(shellContract?.config?.output?.disableTsChecker ?? false), - 'Shell must keep the framework TypeScript checker enabled', + 'Shell must keep the framework TypeScript checker enabled' ); assert( - shellContract?.config?.performance?.readinessDiagnostics?.default === 'enabled', - 'Shell performance readiness diagnostics must be default-on', + shellContract?.config?.performance?.readinessDiagnostics?.default === + 'enabled', + 'Shell performance readiness diagnostics must be default-on' ); assert( - shellContract?.config?.performance?.readinessDiagnostics?.failOn === SHARED_VALIDATOR_STRING_069, - 'Shell performance readiness diagnostics must only fail framework invariants by default', + shellContract?.config?.performance?.readinessDiagnostics?.failOn === + SHARED_VALIDATOR_STRING_069, + 'Shell performance readiness diagnostics must only fail framework invariants by default' ); assert( shellContract?.config?.performance?.readinessDiagnostics?.optOut?.env === SHARED_VALIDATOR_STRING_146, - 'Shell performance readiness env opt-out is incorrect', + 'Shell performance readiness env opt-out is incorrect' ); assert( sameJson(shellContract?.config?.source?.siteUrl?.envFallbackOrder, [ @@ -7011,159 +7732,195 @@ assert( SHARED_VALIDATOR_STRING_145, SHARED_VALIDATOR_STRING_130, ]), - 'Shell site URL env fallback order is incorrect', + 'Shell site URL env fallback order is incorrect' ); assert( - shellContract?.config?.rspack?.output?.uniqueName === SHARED_VALIDATOR_STRING_133, - 'Shell Rspack uniqueName is incorrect', + shellContract?.config?.rspack?.output?.uniqueName === + SHARED_VALIDATOR_STRING_133, + 'Shell Rspack uniqueName is incorrect' ); assert( shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(SHARED_VALIDATOR_STRING_133), - 'Shell Rspack chunkLoadingGlobal is incorrect', + 'Shell Rspack chunkLoadingGlobal is incorrect' ); assert( - shellContract?.moduleFederation?.dts?.compilerInstance === SHARED_VALIDATOR_STRING_068, - 'Shell must keep mandatory DTS compiler', + shellContract?.moduleFederation?.dts?.compilerInstance === + SHARED_VALIDATOR_STRING_068, + 'Shell must keep mandatory DTS compiler' ); assert( - shellContract?.moduleFederation?.dts?.tsConfigPath === SHARED_VALIDATOR_STRING_007, - 'Shell must keep dedicated Module Federation DTS tsconfig', + shellContract?.moduleFederation?.dts?.tsConfigPath === + SHARED_VALIDATOR_STRING_007, + 'Shell must keep dedicated Module Federation DTS tsconfig' ); assert( - shellModuleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), - 'Shell Module Federation config must use the dedicated DTS tsconfig', + shellModuleFederationConfig.includes( + "tsConfigPath: './tsconfig.mf-types.json'" + ), + 'Shell Module Federation config must use the dedicated DTS tsconfig' ); assert( - topology.shell?.cloudflare?.workerName === expectedWorkerName(SHARED_VALIDATOR_STRING_131), - 'Shell topology Cloudflare workerName is incorrect', + topology.shell?.cloudflare?.workerName === + expectedWorkerName(SHARED_VALIDATOR_STRING_131), + 'Shell topology Cloudflare workerName is incorrect' ); assert( shellContract?.styling?.federation?.owner?.id === SHARED_VALIDATOR_STRING_131, - 'Shell CSS federation owner is missing', + 'Shell CSS federation owner is missing' ); assert( shellContract?.styling?.federation?.role === 'shell-base-overlay', - 'Shell must own base and overlay CSS', + 'Shell must own base and overlay CSS' ); assert( - shellContract?.styling?.federation?.rootSelector === '[data-app-id="shell-super-app"]', - 'Shell CSS root selector is incorrect', + shellContract?.styling?.federation?.rootSelector === + '[data-app-id="shell-super-app"]', + 'Shell CSS root selector is incorrect' ); assert( shellContract?.styling?.federation?.classPrefix === 'shell:', - 'Shell CSS class prefix is incorrect', + 'Shell CSS class prefix is incorrect' ); assert( - shellContract?.styling?.federation?.layers?.owned?.includes(SHARED_VALIDATOR_STRING_150) ?? false, - 'Shell must own the base CSS layer', + shellContract?.styling?.federation?.layers?.owned?.includes( + SHARED_VALIDATOR_STRING_150 + ) ?? false, + 'Shell must own the base CSS layer' ); assert( - shellContract?.styling?.federation?.layers?.owned?.includes('ultramodern-shell-overlay') ?? false, - 'Shell must own the overlay CSS layer', + shellContract?.styling?.federation?.layers?.owned?.includes( + 'ultramodern-shell-overlay' + ) ?? false, + 'Shell must own the overlay CSS layer' ); assert( - shellContract?.styling?.federation?.entrypoints?.css?.includes(SHARED_VALIDATOR_STRING_138) ?? - false, - 'Shell CSS entrypoint is missing', + shellContract?.styling?.federation?.entrypoints?.css?.includes( + SHARED_VALIDATOR_STRING_138 + ) ?? false, + 'Shell CSS entrypoint is missing' ); assert( shellContract?.styling?.federation?.assets?.shared?.some((asset) => - asset.endsWith('/shared-design-tokens/tokens.css'), + asset.endsWith('/shared-design-tokens/tokens.css') ) ?? false, - 'Shell must import the shared design token CSS asset', + 'Shell must import the shared design token CSS asset' ); assert( - !(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed ?? false), - 'Shell CSS contract must forbid duplicated base styles', + !( + shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed ?? + false + ), + 'Shell CSS contract must forbid duplicated base styles' ); assert( shellContract?.styling?.federation?.ssr?.firstPaintRequired ?? false, - 'Shell CSS must be required for SSR first paint', + 'Shell CSS must be required for SSR first paint' +); +assert( + shellContract?.routes?.privateByDefault ?? false, + 'Shell routes must be private by default' ); -assert(shellContract?.routes?.privateByDefault ?? false, 'Shell routes must be private by default'); assert( shellContract?.routes?.metadataAuthoring === SHARED_VALIDATOR_STRING_062, - 'Shell route metadata authoring mode is incorrect', + 'Shell route metadata authoring mode is incorrect' ); assert( shellContract?.routes?.generatedManifest ?? false, - 'Shell route metadata manifest must be generated', + 'Shell route metadata manifest must be generated' ); assert( shellContract?.routes?.publicnessDefault === SHARED_VALIDATOR_STRING_105, - 'Shell route publicness default is incorrect', + 'Shell route publicness default is incorrect' ); assert( sameJson(shellContract?.routes?.publicRoutes ?? [], []), - 'Shell must not expose generated public routes by default', + 'Shell must not expose generated public routes by default' ); assertPublicHeadContract( SHARED_VALIDATOR_STRING_131, shellContract?.routes?.publicHead, - shellRouteHead, + shellRouteHead +); +assertPublicSurfaceContract( + SHARED_VALIDATOR_STRING_131, + shellContract?.routes?.publicSurface ); -assertPublicSurfaceContract(SHARED_VALIDATOR_STRING_131, shellContract?.routes?.publicSurface); assert( (shellContract?.routes?.owned ?? []).every( (route) => !route.public && !route.indexable && route.publicSurface === SHARED_VALIDATOR_STRING_105 && - isString(route.descriptionKey), + isString(route.descriptionKey) ), - 'Shell owned routes must be non-indexable private app screens by default and include description keys', + 'Shell owned routes must be non-indexable private app screens by default and include description keys' +); +assertPublicSurfaceAssets( + SHARED_VALIDATOR_STRING_047, + shellContract?.routes?.publicRoutes ?? [] ); -assertPublicSurfaceAssets(SHARED_VALIDATOR_STRING_047, shellContract?.routes?.publicRoutes ?? []); assert( - topology.shell?.verticalRefs?.join(',') === expectedPrimaryShellVerticalIds.join(','), - 'Topology shell verticalRefs must match generated verticals', + topology.shell?.verticalRefs?.join(',') === + expectedPrimaryShellVerticalIds.join(','), + 'Topology shell verticalRefs must match generated verticals' ); assert( topology.verticals?.length === fullStackVerticals.length, - 'Topology must contain only generated verticals', + 'Topology must contain only generated verticals' ); const legacyTopologyFields = Result.getOrThrow( - Schema.decodeUnknownResult(LegacyTopologyFieldsSchema)(topology), + Schema.decodeUnknownResult(LegacyTopologyFieldsSchema)(topology) ); assert( legacyTopologyFields.remotes === undefined, - 'Topology must not expose legacy remotes; use verticals', + 'Topology must not expose legacy remotes; use verticals' ); assert( legacyTopologyFields.effectServices === undefined, - 'Default APIs must be vertical-owned, not effectServices', + 'Default APIs must be vertical-owned, not effectServices' ); for (const vertical of fullStackVerticals) { - const packageJson = readJson(PackageJsonSchema, `${vertical.path}/package.json`); + const packageJson = readJson( + PackageJsonSchema, + `${vertical.path}/package.json` + ); const actionPrincipalPath = `${vertical.path}/api/auth/action-principal.ts`; const actionGatewayPath = `${vertical.path}/src/api/action-gateway.ts`; - const hasActionPrincipal = fs.existsSync(path.join(root, actionPrincipalPath)); + const hasActionPrincipal = fs.existsSync( + path.join(root, actionPrincipalPath) + ); const hasActionGateway = fs.existsSync(path.join(root, actionGatewayPath)); assert( hasActionPrincipal === hasActionGateway, - `${vertical.id} generated Action identity boundary must contain both server and client adapters`, + `${vertical.id} generated Action identity boundary must contain both server and client adapters` ); if (hasActionPrincipal) { for (const boundaryPath of [actionPrincipalPath, actionGatewayPath]) { const boundary = readText(boundaryPath); assert( - boundary.includes('@generated by OntOS Codesmith MicroVertical Action Boundary v1') && + boundary.includes( + '@generated by OntOS Codesmith MicroVertical Action Boundary v1' + ) && boundary.includes(`@ontos-action-boundary-owner ${vertical.id}`) && boundary.includes(`@ontos-action-boundary-audience ${vertical.id}`) && boundary.includes(`ACTION_GATEWAY_AUDIENCE = '${vertical.id}'`), - `${boundaryPath} generated Action identity metadata is invalid`, + `${boundaryPath} generated Action identity metadata is invalid` ); } const actionPrincipal = readText(actionPrincipalPath); assert( - actionPrincipal.includes("from '@app/gateway-principal-verifier/server'") && - actionPrincipal.includes('bindGatewayPrincipalVerifier(ACTION_GATEWAY_AUDIENCE)') && + actionPrincipal.includes( + "from '@app/gateway-principal-verifier/server'" + ) && + actionPrincipal.includes( + 'bindGatewayPrincipalVerifier(ACTION_GATEWAY_AUDIENCE)' + ) && !/(?:createLocalJWKSet|decodeProtectedHeader|jwtVerify|PublicVerificationKeySchema)/u.test( - actionPrincipal, + actionPrincipal ), - `${actionPrincipalPath} must be a thin audience-bound shared verifier adapter`, + `${actionPrincipalPath} must be a thin audience-bound shared verifier adapter` ); for (const [dependency, version] of Object.entries({ '@app/core-runtime': SHARED_VALIDATOR_STRING_169, @@ -7173,12 +7930,12 @@ for (const vertical of fullStackVerticals) { })) { assert( packageJson.dependencies?.[dependency] === version, - `${vertical.id} Action identity boundary dependency ${dependency} must equal ${version}`, + `${vertical.id} Action identity boundary dependency ${dependency} must equal ${version}` ); } assert( packageJson.dependencies?.jose === undefined, - `${vertical.id} must not own the shared verifier's JOSE runtime dependency`, + `${vertical.id} must not own the shared verifier's JOSE runtime dependency` ); } const modernConfig = readText(`${vertical.path}/modern.config.ts`); @@ -7194,139 +7951,152 @@ for (const vertical of fullStackVerticals) { const routeMetadata = vertical.emitsUi ? readText(`${vertical.path}/src/routes/ultramodern-route-metadata.ts`) : ''; - const ultramodernBuildSource = readText(`${vertical.path}/shared/ultramodern-build.ts`); + const ultramodernBuildSource = readText( + `${vertical.path}/shared/ultramodern-build.ts` + ); const ultramodernBuildArtifact = readJson( BuildArtifactSchema, - `${vertical.path}/shared/ultramodern-build.json`, + `${vertical.path}/shared/ultramodern-build.json` ); if (vertical.deliveryUnit !== undefined) { - const expectedDeliveryUnit = deliveryUnitBlock(expectedDeliveryUnitFor(vertical)); + const expectedDeliveryUnit = deliveryUnitBlock( + expectedDeliveryUnitFor(vertical) + ); const buildLabel = `${vertical.path}/shared/ultramodern-build.json deliveryUnit`; assertSelfCheck( ultramodernBuildSource.includes('export const ultramodernDeliveryUnit'), buildLabel, 'Missing ultramodernDeliveryUnit export', - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); const buildIdentity = ultramodernBuildArtifact.deliveryUnit ?? {}; assertSelfCheck( buildIdentity.buildMarker === expectedDeliveryUnit.buildMarker, buildLabel, `Expected build "${expectedDeliveryUnit.buildMarker}", found ${formatJson(buildIdentity.buildMarker)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSelfCheck( buildIdentity.unitId === expectedDeliveryUnit.unitId, buildLabel, `Expected unitId "${expectedDeliveryUnit.unitId}", found ${formatJson(buildIdentity.unitId)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSelfCheck( buildIdentity.packageName === expectedDeliveryUnit.packageName, buildLabel, `Expected packageName "${expectedDeliveryUnit.packageName}", found ${formatJson(buildIdentity.packageName)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSelfCheck( buildIdentity.version === expectedDeliveryUnit.version, buildLabel, `Expected version "${expectedDeliveryUnit.version}", found ${formatJson(buildIdentity.version)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertBuildFacadeExport( ultramodernBuildSource, SHARED_VALIDATOR_STRING_152, 'ultramodernBuildArtifact.surfaces.ui', - `${vertical.path}/shared/ultramodern-build.ts ultramodernUiMarker`, + `${vertical.path}/shared/ultramodern-build.ts ultramodernUiMarker` ); assertBuildFacadeExport( ultramodernBuildSource, SHARED_VALIDATOR_STRING_151, 'ultramodernBuildArtifact.surfaces.api', - `${vertical.path}/shared/ultramodern-build.ts ultramodernApiMarker`, + `${vertical.path}/shared/ultramodern-build.ts ultramodernApiMarker` ); } assert( - modernAppEnv.includes('/// '), - `${vertical.id} app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file`, + modernAppEnv.includes( + '/// ' + ), + `${vertical.id} app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file` ); assert( /declare const ULTRAMODERN_SITE_URL: string;/u.test(modernAppEnv), - `${vertical.id} app env must keep generated globals explicit in ambient scope`, + `${vertical.id} app env must keep generated globals explicit in ambient scope` ); assert( !modernAppEnv.includes("declare module '*.svg'"), - `${vertical.id} app env must not redeclare framework-owned svg asset modules`, + `${vertical.id} app env must not redeclare framework-owned svg asset modules` ); assert( !modernAppEnv.includes("declare module '*.css'"), - `${vertical.id} app env must not redeclare framework-owned css asset modules`, + `${vertical.id} app env must not redeclare framework-owned css asset modules` ); if (vertical.emitsUi) { assert( routeMetadata.includes('@generated by @modern-js/ultramodern-create'), - `${vertical.id} route metadata compatibility manifest must be marked generated`, + `${vertical.id} route metadata compatibility manifest must be marked generated` ); assert( routeMetadata.includes( - 'Author route metadata in colocated src/routes/**/route.meta.ts files.', + 'Author route metadata in colocated src/routes/**/route.meta.ts files.' ), - `${vertical.id} route metadata manifest must advertise colocated authoring`, + `${vertical.id} route metadata manifest must advertise colocated authoring` ); } - assert(packageJson.name === vertical.packageName, `${vertical.id} package name is incorrect`); assert( - packageJson.scripts?.[SHARED_VALIDATOR_STRING_060] === SHARED_VALIDATOR_STRING_144, - `${vertical.id} must expose cloudflare:deploy`, + packageJson.name === vertical.packageName, + `${vertical.id} package name is incorrect` + ); + assert( + packageJson.scripts?.[SHARED_VALIDATOR_STRING_060] === + SHARED_VALIDATOR_STRING_144, + `${vertical.id} must expose cloudflare:deploy` ); assertTargetIsolatedBuildArtifacts(vertical.id, modernConfig); assertCloudflareBuildSkipsDeployRebuild(vertical.id, packageJson); assert( - packageJson.scripts?.[SHARED_VALIDATOR_STRING_061]?.includes(`--app ${vertical.id}`) ?? false, - `${vertical.id} must expose cloudflare:proof`, + packageJson.scripts?.[SHARED_VALIDATOR_STRING_061]?.includes( + `--app ${vertical.id}` + ) ?? false, + `${vertical.id} must expose cloudflare:proof` ); assert( packageJson.devDependencies?.[SHARED_VALIDATOR_STRING_022] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_022), - `${vertical.id} app-tools dependency must match package source metadata`, + `${vertical.id} app-tools dependency must match package source metadata` ); if (vertical.emitsApi) { assert( packageJson.dependencies?.[SHARED_VALIDATOR_STRING_025] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_025), - `${vertical.id} plugin-bff dependency must match package source metadata`, + `${vertical.id} plugin-bff dependency must match package source metadata` ); } assert( packageJson.dependencies?.[SHARED_VALIDATOR_STRING_027] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_027), - `${vertical.id} plugin-i18n dependency must match package source metadata`, + `${vertical.id} plugin-i18n dependency must match package source metadata` ); assert( packageJson.dependencies?.[SHARED_VALIDATOR_STRING_028] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_028), - `${vertical.id} plugin-tanstack dependency must match package source metadata`, + `${vertical.id} plugin-tanstack dependency must match package source metadata` ); assert( packageJson.dependencies?.[SHARED_VALIDATOR_STRING_029] === expectedModernPackageSpecifier(SHARED_VALIDATOR_STRING_029), - `${vertical.id} runtime dependency must match package source metadata`, + `${vertical.id} runtime dependency must match package source metadata` ); if (vertical.emitsApi) { if (vertical.apiContractExport === undefined) { assert( packageJson.exports?.['./api'] === undefined && packageJson.exports?.[SHARED_VALIDATOR_STRING_002] === undefined, - `${vertical.id} private deployment API must not be package-exported`, + `${vertical.id} private deployment API must not be package-exported` ); } else { assert( - packageJson.exports?.[vertical.apiClientExport] === `./${vertical.apiClientPath}`, - `${vertical.id} must export its API client`, + packageJson.exports?.[vertical.apiClientExport] === + `./${vertical.apiClientPath}`, + `${vertical.id} must export its API client` ); assert( packageJson.exports?.['./api'] === `./${vertical.apiContractPath}`, - `${vertical.id} must export its API contract`, + `${vertical.id} must export its API contract` ); } // API protocol exclusivity (G7a): an RPC unit ships only the RPC contract @@ -7334,170 +8104,208 @@ for (const vertical of fullStackVerticals) { // `${stem}-client`. Neither may carry the other protocol's surface. if (vertical.apiProtocol === 'rpc') { assert( - !fs.existsSync(path.join(root, `${vertical.path}/src/api/${vertical.stem}-client.ts`)), - `${vertical.id} RPC unit must not emit the REST API client`, + !fs.existsSync( + path.join(root, `${vertical.path}/src/api/${vertical.stem}-client.ts`) + ), + `${vertical.id} RPC unit must not emit the REST API client` ); assert( !fs.existsSync(path.join(root, `${vertical.path}/shared/api.ts`)), - `${vertical.id} RPC unit must not emit the REST API contract`, + `${vertical.id} RPC unit must not emit the REST API contract` ); } else { assert( - !fs.existsSync(path.join(root, `${vertical.path}/src/api/${vertical.stem}-rpc-client.ts`)), - `${vertical.id} REST unit must not emit the RPC API client`, + !fs.existsSync( + path.join( + root, + `${vertical.path}/src/api/${vertical.stem}-rpc-client.ts` + ) + ), + `${vertical.id} REST unit must not emit the RPC API client` ); assert( !fs.existsSync(path.join(root, `${vertical.path}/shared/rpc.ts`)), - `${vertical.id} REST unit must not emit the RPC API contract`, + `${vertical.id} REST unit must not emit the RPC API contract` ); } } const expectedVerticalZephyrDependencies = Object.fromEntries( fullStackVerticals - .filter((candidate) => new Set(vertical.verticalRefs).has(candidate.id)) - .map((candidate) => [candidate.zephyrAlias, `${candidate.packageName}@workspace:*`]), + .filter((candidate) => + new Set(vertical.verticalRefs).has(candidate.id) + ) + .map((candidate) => [ + candidate.zephyrAlias, + `${candidate.packageName}@workspace:*`, + ]) ); assert( - sameJson(packageJson[SHARED_VALIDATOR_STRING_173], expectedVerticalZephyrDependencies), - `${vertical.id} Zephyr dependencies must match declared vertical refs`, + sameJson( + packageJson[SHARED_VALIDATOR_STRING_173], + expectedVerticalZephyrDependencies + ), + `${vertical.id} Zephyr dependencies must match declared vertical refs` ); - const contractEntry = generatedContract.apps?.find((app) => app.id === vertical.id); + const contractEntry = generatedContract.apps?.find( + (app) => app.id === vertical.id + ); assert( contractEntry?.path === vertical.path, - `${vertical.id} generated contract path is incorrect`, + `${vertical.id} generated contract path is incorrect` + ); + assert( + contractEntry?.kind === 'vertical', + `${vertical.id} generated contract kind is incorrect` ); - assert(contractEntry?.kind === 'vertical', `${vertical.id} generated contract kind is incorrect`); assert( - contractEntry?.deploy?.cloudflare?.workerName === expectedWorkerName(vertical.id), - `${vertical.id} Cloudflare workerName is incorrect`, + contractEntry?.deploy?.cloudflare?.workerName === + expectedWorkerName(vertical.id), + `${vertical.id} Cloudflare workerName is incorrect` ); assert( contractEntry?.deploy?.cloudflare?.publicUrlEnv === `ULTRAMODERN_PUBLIC_URL_${vertical.id.replaceAll('-', '_').toUpperCase()}`, - `${vertical.id} Cloudflare public URL env is incorrect`, + `${vertical.id} Cloudflare public URL env is incorrect` ); assert( - contractEntry?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, - `${vertical.id} Cloudflare compatibilityDate is incorrect`, + contractEntry?.deploy?.cloudflare?.compatibilityDate === + expectedCloudflareCompatibilityDate, + `${vertical.id} Cloudflare compatibilityDate is incorrect` ); assert( sameJson( contractEntry?.deploy?.cloudflare?.compatibilityFlags, - expectedCloudflareCompatibilityFlags, + expectedCloudflareCompatibilityFlags ), - `${vertical.id} Cloudflare compatibility flags are incorrect`, + `${vertical.id} Cloudflare compatibility flags are incorrect` ); assert( - sameJson(contractEntry?.deploy?.cloudflare?.security, expectedCloudflareSecurity), - `${vertical.id} Cloudflare security contract is incorrect`, + sameJson( + contractEntry?.deploy?.cloudflare?.security, + expectedCloudflareSecurity + ), + `${vertical.id} Cloudflare security contract is incorrect` + ); + assertCloudflareQualityGates( + vertical.id, + contractEntry?.deploy?.cloudflare?.qualityGates ); - assertCloudflareQualityGates(vertical.id, contractEntry?.deploy?.cloudflare?.qualityGates); assert( - contractEntry?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, - `${vertical.id} worker compatibilityDate is incorrect`, + contractEntry?.deploy?.worker?.compatibilityDate === + expectedCloudflareCompatibilityDate, + `${vertical.id} worker compatibilityDate is incorrect` ); assert( contractEntry?.deploy?.worker?.name === expectedWorkerName(vertical.id), - `${vertical.id} worker name is incorrect`, + `${vertical.id} worker name is incorrect` ); assert( - modernConfig.includes(`const cloudflareWorkerName = '${expectedWorkerName(vertical.id)}'`), - `${vertical.id} modern.config.ts must define the Cloudflare worker name`, + modernConfig.includes( + `const cloudflareWorkerName = '${expectedWorkerName(vertical.id)}'` + ), + `${vertical.id} modern.config.ts must define the Cloudflare worker name` ); assert( modernConfig.includes('name: cloudflareWorkerName'), - `${vertical.id} modern.config.ts must wire deploy.worker.name`, + `${vertical.id} modern.config.ts must wire deploy.worker.name` ); assert( modernConfig.includes('const assetPrefix ='), - `${vertical.id} modern.config.ts must derive a dedicated asset prefix`, + `${vertical.id} modern.config.ts must derive a dedicated asset prefix` ); assert( modernConfig.includes( - "const configuredUltramodernAssetPrefix = envValue('ULTRAMODERN_ASSET_PREFIX')", + "const configuredUltramodernAssetPrefix = envValue('ULTRAMODERN_ASSET_PREFIX')" ), - `${vertical.id} asset prefix must support ULTRAMODERN_ASSET_PREFIX`, + `${vertical.id} asset prefix must support ULTRAMODERN_ASSET_PREFIX` ); assert( - modernConfig.includes("const configuredModernAssetPrefix = envValue('MODERN_ASSET_PREFIX')"), - `${vertical.id} asset prefix must support MODERN_ASSET_PREFIX`, + modernConfig.includes( + "const configuredModernAssetPrefix = envValue('MODERN_ASSET_PREFIX')" + ), + `${vertical.id} asset prefix must support MODERN_ASSET_PREFIX` ); assert( modernConfig.includes('const defaultRemoteAssetPrefix'), - `${vertical.id} asset prefix must derive the remote asset origin`, + `${vertical.id} asset prefix must derive the remote asset origin` ); assert( - modernConfig.includes('const defaultAssetPrefix = defaultRemoteAssetPrefix'), - `${vertical.id} asset prefix must default to its own remote origin`, + modernConfig.includes( + 'const defaultAssetPrefix = defaultRemoteAssetPrefix' + ), + `${vertical.id} asset prefix must default to its own remote origin` ); - const verticalAssetPrefixExpression = extractAssetPrefixExpression(modernConfig); + const verticalAssetPrefixExpression = + extractAssetPrefixExpression(modernConfig); assert( verticalAssetPrefixExpression.includes( - 'configuredModernAssetPrefix || configuredUltramodernAssetPrefix || defaultAssetPrefix', + 'configuredModernAssetPrefix || configuredUltramodernAssetPrefix || defaultAssetPrefix' ) || verticalAssetPrefixExpression.includes( - 'configuredModernAssetPrefix ?? configuredUltramodernAssetPrefix ?? defaultAssetPrefix', + 'configuredModernAssetPrefix ?? configuredUltramodernAssetPrefix ?? defaultAssetPrefix' ), - `${vertical.id} asset prefix fallback order is incorrect`, + `${vertical.id} asset prefix fallback order is incorrect` ); assert( !verticalAssetPrefixExpression.includes('configuredSiteUrl') && !verticalAssetPrefixExpression.includes(SHARED_VALIDATOR_STRING_081), - `${vertical.id} asset prefix must not fall back to MODERN_PUBLIC_SITE_URL`, + `${vertical.id} asset prefix must not fall back to MODERN_PUBLIC_SITE_URL` ); assert( modernConfig.includes( - `envValue('ULTRAMODERN_PUBLIC_URL_${vertical.id.replaceAll('-', '_').toUpperCase()}')`, + `envValue('ULTRAMODERN_PUBLIC_URL_${vertical.id.replaceAll('-', '_').toUpperCase()}')` ), - `${vertical.id} asset prefix must read its per-app public URL`, + `${vertical.id} asset prefix must read its per-app public URL` ); assert( modernConfig.includes('inferredCloudflareUrl'), - `${vertical.id} asset prefix must support workers.dev origin inference`, + `${vertical.id} asset prefix must support workers.dev origin inference` ); assert( /dev:\s*\{[\s\S]*?\/\/ Remote dev manifests must publish an absolute publicPath[\s\S]*?assetPrefix,/u.test( - modernConfig, + modernConfig ), - `${vertical.id} modern.config.ts must publish dev assets from its own remote origin`, + `${vertical.id} modern.config.ts must publish dev assets from its own remote origin` ); assert( modernConfig.includes('assetPrefix,'), - `${vertical.id} modern.config.ts must wire output.assetPrefix to the derived asset prefix`, + `${vertical.id} modern.config.ts must wire output.assetPrefix to the derived asset prefix` ); assert( contractEntry?.config?.dev?.assetPrefix === SHARED_VALIDATOR_STRING_045, - `${vertical.id} dev asset prefix must default to its app public origin`, + `${vertical.id} dev asset prefix must default to its app public origin` ); assert( - contractEntry?.config?.output?.assetPrefix?.default === SHARED_VALIDATOR_STRING_045, - `${vertical.id} asset prefix must default to its app public origin`, + contractEntry?.config?.output?.assetPrefix?.default === + SHARED_VALIDATOR_STRING_045, + `${vertical.id} asset prefix must default to its app public origin` ); assert( sameJson(contractEntry?.config?.output?.assetPrefix?.envFallbackOrder, [ SHARED_VALIDATOR_STRING_080, SHARED_VALIDATOR_STRING_143, ]), - `${vertical.id} asset prefix env fallback order is incorrect`, + `${vertical.id} asset prefix env fallback order is incorrect` ); assert( !(contractEntry?.config?.output?.disableTsChecker ?? false), - `${vertical.id} must keep the framework TypeScript checker enabled`, + `${vertical.id} must keep the framework TypeScript checker enabled` ); assert( - contractEntry?.config?.performance?.readinessDiagnostics?.default === 'enabled', - `${vertical.id} performance readiness diagnostics must be default-on`, + contractEntry?.config?.performance?.readinessDiagnostics?.default === + 'enabled', + `${vertical.id} performance readiness diagnostics must be default-on` ); assert( contractEntry?.config?.performance?.readinessDiagnostics?.failOn === SHARED_VALIDATOR_STRING_069, - `${vertical.id} performance readiness diagnostics must only fail framework invariants by default`, + `${vertical.id} performance readiness diagnostics must only fail framework invariants by default` ); assert( contractEntry?.config?.performance?.readinessDiagnostics?.optOut?.config === SHARED_VALIDATOR_STRING_121, - `${vertical.id} performance readiness opt-out config is incorrect`, + `${vertical.id} performance readiness opt-out config is incorrect` ); if (vertical.emitsApi) { // Per policy.ts:76 the Cloudflare proof advertises a REST readiness route @@ -7505,158 +8313,177 @@ for (const vertical of fullStackVerticals) { if (vertical.apiProtocol === 'rpc') { assert( contractEntry?.deploy?.cloudflare?.routes?.apiReadiness === undefined, - `${vertical.id} rpc unit must not carry a REST Cloudflare readiness route`, + `${vertical.id} rpc unit must not carry a REST Cloudflare readiness route` ); } else { assert( contractEntry?.deploy?.cloudflare?.routes?.apiReadiness === `${vertical.apiPrefix}/${vertical.stem}/readiness`, - `${vertical.id} Cloudflare proof readiness route is incorrect`, + `${vertical.id} Cloudflare proof readiness route is incorrect` ); } } assert( contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, - `${vertical.id} Rspack uniqueName is incorrect`, + `${vertical.id} Rspack uniqueName is incorrect` ); assert( contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), - `${vertical.id} Rspack chunkLoadingGlobal is incorrect`, + `${vertical.id} Rspack chunkLoadingGlobal is incorrect` ); assert( contractEntry?.moduleFederation?.name === vertical.mfName, - `${vertical.id} MF name is incorrect`, + `${vertical.id} MF name is incorrect` ); assert( sameJson(contractEntry?.moduleFederation?.exposes, vertical.exposes), - `${vertical.id} MF exposes are incorrect`, + `${vertical.id} MF exposes are incorrect` ); // The browser Module Federation DTS surface and its config file only exist // for UI-emitting units; a headless api-only unit federates no browser types. if (vertical.emitsUi && vertical.exposes.length > 0) { assert( - contractEntry?.moduleFederation?.dts?.compilerInstance === SHARED_VALIDATOR_STRING_068, - `${vertical.id} must keep mandatory DTS compiler`, + contractEntry?.moduleFederation?.dts?.compilerInstance === + SHARED_VALIDATOR_STRING_068, + `${vertical.id} must keep mandatory DTS compiler` ); assert( - contractEntry?.moduleFederation?.dts?.tsConfigPath === SHARED_VALIDATOR_STRING_007, - `${vertical.id} must keep dedicated Module Federation DTS tsconfig`, + contractEntry?.moduleFederation?.dts?.tsConfigPath === + SHARED_VALIDATOR_STRING_007, + `${vertical.id} must keep dedicated Module Federation DTS tsconfig` ); assert( - moduleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), - `${vertical.id} Module Federation config must use the dedicated DTS tsconfig`, + moduleFederationConfig.includes( + "tsConfigPath: './tsconfig.mf-types.json'" + ), + `${vertical.id} Module Federation config must use the dedicated DTS tsconfig` ); } assert( - sameJson(contractEntry?.moduleFederation?.verticalRefs ?? [], vertical.verticalRefs), - `${vertical.id} MF verticalRefs are incorrect`, + sameJson( + contractEntry?.moduleFederation?.verticalRefs ?? [], + vertical.verticalRefs + ), + `${vertical.id} MF verticalRefs are incorrect` ); assert( sameJson( - (contractEntry?.moduleFederation?.remotes ?? []).map((remote) => remote.id), - vertical.verticalRefs, + (contractEntry?.moduleFederation?.remotes ?? []).map( + (remote) => remote.id + ), + vertical.verticalRefs ), - `${vertical.id} MF consumed verticals are incorrect`, + `${vertical.id} MF consumed verticals are incorrect` ); // API contract surface is only present for API-bearing units. if (vertical.emitsApi) { assert( contractEntry?.api?.prefix === vertical.apiPrefix, - `${vertical.id} API prefix is incorrect`, + `${vertical.id} API prefix is incorrect` + ); + assert( + contractEntry?.api?.group === vertical.group, + `${vertical.id} API group is incorrect` + ); + assert( + contractEntry?.api?.runtime === 'effect', + `${vertical.id} API runtime must be Effect` ); - assert(contractEntry?.api?.group === vertical.group, `${vertical.id} API group is incorrect`); - assert(contractEntry?.api?.runtime === 'effect', `${vertical.id} API runtime must be Effect`); assert( contractEntry?.api?.strictEffectApproach ?? false, - `${vertical.id} strictEffectApproach must be enabled`, + `${vertical.id} strictEffectApproach must be enabled` ); assert( contractEntry?.api?.contract === vertical.apiContractExport, - `${vertical.id} API contract export is incorrect`, + `${vertical.id} API contract export is incorrect` ); assert( contractEntry?.api?.client === vertical.apiClientExport, - `${vertical.id} API client export is incorrect`, + `${vertical.id} API client export is incorrect` ); if (vertical.apiProtocol === 'rpc') { // An `rpc` unit records the `/rpc` route/serialization; it must not carry // REST readiness or domain-operation semantics. assert( contractEntry?.api?.protocol === 'rpc', - `${vertical.id} generated contract API protocol must be rpc`, + `${vertical.id} generated contract API protocol must be rpc` ); assert( contractEntry?.api?.rpc?.path === '/rpc', - `${vertical.id} generated contract RPC route path is incorrect`, + `${vertical.id} generated contract RPC route path is incorrect` ); assert( contractEntry?.api?.rpcPath === `${vertical.apiPrefix}/rpc`, - `${vertical.id} generated contract RPC path is incorrect`, + `${vertical.id} generated contract RPC path is incorrect` ); } else { const restApi = - contractEntry?.api && !('rpc' in contractEntry.api) ? contractEntry.api : undefined; + contractEntry?.api && !('rpc' in contractEntry.api) + ? contractEntry.api + : undefined; assert( restApi?.readiness?.endpoint === `/${vertical.stem}/readiness`, - `${vertical.id} readiness endpoint is incorrect`, + `${vertical.id} readiness endpoint is incorrect` ); assert( restApi?.operations?.readiness?.path === `/${vertical.stem}/readiness`, - `${vertical.id} readiness operation is missing`, + `${vertical.id} readiness operation is missing` ); assert( - restApi?.requestContext?.propagatedHeaders?.includes(SHARED_VALIDATOR_STRING_142) ?? false, - `${vertical.id} trace context propagation is missing`, + restApi?.requestContext?.propagatedHeaders?.includes( + SHARED_VALIDATOR_STRING_142 + ) ?? false, + `${vertical.id} trace context propagation is missing` ); assert( vertical.apiContractExport === undefined ? restApi?.domainOperations === undefined : Object.keys(restApi?.domainOperations ?? {}).length >= 3, - `${vertical.id} domain operations do not match its declared package API surface`, + `${vertical.id} domain operations do not match its declared package API surface` ); } } assert( (contractEntry?.i18n?.languages?.includes('en') ?? false) && (contractEntry?.i18n?.languages?.includes('cs') ?? false), - `${vertical.id} must declare i18n languages`, + `${vertical.id} must declare i18n languages` ); assert( contractEntry?.i18n?.namespace === vertical.namespace, - `${vertical.id} i18n namespace is incorrect`, + `${vertical.id} i18n namespace is incorrect` ); assert( sameJson(contractEntry?.i18n?.localisedUrls, vertical.localisedUrls), - `${vertical.id} localisedUrls must come from route metadata`, + `${vertical.id} localisedUrls must come from route metadata` ); assert( contractEntry?.routes?.source === 'route-owned', - `${vertical.id} routes must be route-owned`, + `${vertical.id} routes must be route-owned` ); assert( contractEntry?.routes?.metadataAuthoring === SHARED_VALIDATOR_STRING_062, - `${vertical.id} route metadata authoring mode is incorrect`, + `${vertical.id} route metadata authoring mode is incorrect` ); assert( contractEntry?.routes?.generatedManifest ?? false, - `${vertical.id} route metadata manifest must be generated`, + `${vertical.id} route metadata manifest must be generated` ); assert( contractEntry?.routes?.metadataExport === SHARED_VALIDATOR_STRING_006, - `${vertical.id} route metadata export is incorrect`, + `${vertical.id} route metadata export is incorrect` ); assert( contractEntry?.routes?.privateByDefault ?? false, - `${vertical.id} routes must be private by default`, + `${vertical.id} routes must be private by default` ); assert( contractEntry?.routes?.publicnessDefault === SHARED_VALIDATOR_STRING_105, - `${vertical.id} route publicness default is incorrect`, + `${vertical.id} route publicness default is incorrect` ); assert( (contractEntry?.routes?.publicRoutes ?? []).length === 0, - `${vertical.id} must not expose generated public routes by default`, + `${vertical.id} must not expose generated public routes by default` ); // Public head/surface and owned browser routes only exist for UI-emitting // units; a headless api-only unit renders no route head or public surface. @@ -7665,20 +8492,26 @@ for (const vertical of fullStackVerticals) { vertical.id, contractEntry?.routes?.publicHead, routeHead, - vertical.hasOwnerPage, + vertical.hasOwnerPage + ); + assertPublicSurfaceContract( + vertical.id, + contractEntry?.routes?.publicSurface ); - assertPublicSurfaceContract(vertical.id, contractEntry?.routes?.publicSurface); assert( (contractEntry?.routes?.owned ?? []).every( (route) => !route.public && !route.indexable && route.publicSurface === SHARED_VALIDATOR_STRING_105 && - isString(route.descriptionKey), + isString(route.descriptionKey) ), - `${vertical.id} owned routes must be non-indexable private app screens by default and include description keys`, + `${vertical.id} owned routes must be non-indexable private app screens by default and include description keys` + ); + assertPublicSurfaceAssets( + vertical.path, + contractEntry?.routes?.publicRoutes ?? [] ); - assertPublicSurfaceAssets(vertical.path, contractEntry?.routes?.publicRoutes ?? []); } // CSS federation (a federated browser stylesheet keyed off the federation // entry) only applies to UI-emitting units; a headless api-only unit owns no @@ -7686,173 +8519,192 @@ for (const vertical of fullStackVerticals) { if (vertical.emitsUi) { assert( contractEntry?.styling?.federation?.owner?.id === vertical.id, - `${vertical.id} CSS federation owner is missing`, + `${vertical.id} CSS federation owner is missing` ); assert( contractEntry?.styling?.federation?.role === 'vertical-css', - `${vertical.id} must own only vertical CSS`, + `${vertical.id} must own only vertical CSS` ); assert( - contractEntry?.styling?.federation?.rootSelector === `[data-app-id="${vertical.id}"]`, - `${vertical.id} CSS root selector is incorrect`, + contractEntry?.styling?.federation?.rootSelector === + `[data-app-id="${vertical.id}"]`, + `${vertical.id} CSS root selector is incorrect` ); assert( - contractEntry?.styling?.federation?.classPrefix === `${vertical.tailwindPrefix}:`, - `${vertical.id} CSS class prefix is incorrect`, + contractEntry?.styling?.federation?.classPrefix === + `${vertical.tailwindPrefix}:`, + `${vertical.id} CSS class prefix is incorrect` ); assert( contractEntry?.styling?.federation?.layers?.owned?.includes( - `ultramodern-vertical-${vertical.domain}`, + `ultramodern-vertical-${vertical.domain}` ) ?? false, - `${vertical.id} vertical CSS layer is missing`, + `${vertical.id} vertical CSS layer is missing` ); assert( !( - contractEntry?.styling?.federation?.layers?.owned?.includes(SHARED_VALIDATOR_STRING_150) ?? - false + contractEntry?.styling?.federation?.layers?.owned?.includes( + SHARED_VALIDATOR_STRING_150 + ) ?? false ), - `${vertical.id} must not own shell base CSS`, + `${vertical.id} must not own shell base CSS` ); assert( contractEntry?.styling?.federation?.entrypoints?.federationEntry === (vertical.hasFederationEntry ? SHARED_VALIDATOR_STRING_136 : undefined), - `${vertical.id} CSS federation entry must match its exposed browser surface`, + `${vertical.id} CSS federation entry must match its exposed browser surface` ); assert( contractEntry?.styling?.federation?.assets?.shared?.some((asset) => - asset.endsWith('/shared-design-tokens/tokens.css'), + asset.endsWith('/shared-design-tokens/tokens.css') ) ?? false, - `${vertical.id} must import shared design token CSS`, + `${vertical.id} must import shared design token CSS` ); assert( - contractEntry?.styling?.federation?.dedupe?.runtimeLoad === 'once-per-content-hash', - `${vertical.id} CSS dedupe strategy is incorrect`, + contractEntry?.styling?.federation?.dedupe?.runtimeLoad === + 'once-per-content-hash', + `${vertical.id} CSS dedupe strategy is incorrect` ); assert( - contractEntry?.styling?.federation?.ssr?.verticalCss === 'federated-manifest-owned-css', - `${vertical.id} SSR CSS loading contract is incorrect`, + contractEntry?.styling?.federation?.ssr?.verticalCss === + 'federated-manifest-owned-css', + `${vertical.id} SSR CSS loading contract is incorrect` ); } const topologyEntry = topology.verticals?.find( - (verticalEntry) => verticalEntry.id === vertical.id, + (verticalEntry) => verticalEntry.id === vertical.id + ); + assert( + topologyEntry?.kind === 'vertical', + `${vertical.id} topology kind is incorrect` ); - assert(topologyEntry?.kind === 'vertical', `${vertical.id} topology kind is incorrect`); assert( topologyEntry?.package === vertical.packageName, - `${vertical.id} topology package is incorrect`, + `${vertical.id} topology package is incorrect` ); assert( topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), - `${vertical.id} topology Cloudflare workerName is incorrect`, + `${vertical.id} topology Cloudflare workerName is incorrect` ); assert( topologyEntry?.moduleFederation?.name === vertical.mfName, - `${vertical.id} topology MF name is incorrect`, + `${vertical.id} topology MF name is incorrect` ); assert( sameJson(topologyEntry?.moduleFederation?.exposes, vertical.exposes), - `${vertical.id} topology exposes are incorrect`, + `${vertical.id} topology exposes are incorrect` ); assert( - sameJson(topologyEntry?.moduleFederation?.verticalRefs ?? [], vertical.verticalRefs), - `${vertical.id} topology verticalRefs are incorrect`, + sameJson( + topologyEntry?.moduleFederation?.verticalRefs ?? [], + vertical.verticalRefs + ), + `${vertical.id} topology verticalRefs are incorrect` ); // API/BFF topology metadata only exists for API-bearing units; and the REST // readiness/domain-operation surface is absent for the RPC protocol (G7a). if (vertical.emitsApi) { assert( topologyEntry?.api?.bff?.prefix === vertical.apiPrefix, - `${vertical.id} topology API prefix is incorrect`, + `${vertical.id} topology API prefix is incorrect` ); assert( topologyEntry?.api?.bff?.strictEffectApproach ?? false, - `${vertical.id} topology strictEffectApproach is incorrect`, + `${vertical.id} topology strictEffectApproach is incorrect` ); assert( topologyEntry?.api?.serverEntry === `${vertical.path}/api/index.ts`, - `${vertical.id} topology server entry is incorrect`, + `${vertical.id} topology server entry is incorrect` ); if (vertical.apiProtocol !== 'rpc') { assert( - topologyEntry?.api?.readiness?.endpoint === `/${vertical.stem}/readiness`, - `${vertical.id} topology readiness endpoint is incorrect`, + topologyEntry?.api?.readiness?.endpoint === + `/${vertical.stem}/readiness`, + `${vertical.id} topology readiness endpoint is incorrect` ); assert( topologyEntry?.api?.domainOperations === undefined || Object.keys(topologyEntry.api.domainOperations).length >= 3, - `${vertical.id} topology domain operations do not match its declared package API surface`, + `${vertical.id} topology domain operations do not match its declared package API surface` ); } } if (vertical.deliveryUnit !== undefined) { - const expectedDeliveryUnit = deliveryUnitBlock(expectedDeliveryUnitFor(vertical)); + const expectedDeliveryUnit = deliveryUnitBlock( + expectedDeliveryUnitFor(vertical) + ); const compactAppEntry = ultramodernConfig.topology?.apps?.find( - (entry) => entry?.id === vertical.id, + (entry) => entry?.id === vertical.id ); // The backend-federation delivery-unit mirror only exists for API-bearing // units; a UI-only vertical carries just the app-level delivery unit. if (vertical.emitsApi) { const compactBackendDeliveryUnit = deliveryUnitBlock( - compactAppEntry?.backendFederation?.deliveryUnit, + compactAppEntry?.backendFederation?.deliveryUnit ); assertSameJson( deliveryUnitBlock(compactAppEntry?.deliveryUnit), compactBackendDeliveryUnit, `${compactConfigPath} topology.apps.${vertical.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSameJson( compactBackendDeliveryUnit, expectedDeliveryUnit, `${compactConfigPath} topology.apps.${vertical.id}.backendFederation.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSelfCheck( compactAppEntry?.backendFederation?.versionBoundary?.identityRoot === SHARED_VALIDATOR_STRING_065, `${compactConfigPath} topology.apps.${vertical.id}.backendFederation.versionBoundary.identityRoot`, `Expected "deliveryUnit", found ${formatJson(compactAppEntry?.backendFederation?.versionBoundary?.identityRoot)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); } assertSameJson( deliveryUnitBlock(compactAppEntry?.deliveryUnit), expectedDeliveryUnit, `${compactConfigPath} topology.apps.${vertical.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); assertSameJson( deliveryUnitBlock(compactAppEntry?.deliveryUnit), deliveryUnitBlock(topologyEntry?.deliveryUnit), `${compactConfigPath} vs topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); } assert( - ownership.owners?.some((owner) => owner.id === vertical.id && owner.path === vertical.path), - `${vertical.id} ownership entry is missing`, + ownership.owners?.some( + (owner) => owner.id === vertical.id && owner.path === vertical.path + ), + `${vertical.id} ownership entry is missing` ); assert( (valueForKey(Object.entries(overlay.ports ?? {}), vertical.id) ?? 0) !== 0, - `${vertical.id} development port is missing`, + `${vertical.id} development port is missing` ); if (vertical.emitsUi) { assert( - valueForKey(Object.entries(overlay.manifests ?? {}), vertical.id)?.includes( - SHARED_VALIDATOR_STRING_031, - ) ?? false, - `${vertical.id} development manifest is missing`, + valueForKey( + Object.entries(overlay.manifests ?? {}), + vertical.id + )?.includes(SHARED_VALIDATOR_STRING_031) ?? false, + `${vertical.id} development manifest is missing` ); } if (vertical.emitsApi) { assert( valueForKey(Object.entries(overlay.apis ?? {}), vertical.id)?.endsWith( - vertical.apiProtocol === 'rpc' ? `${vertical.apiPrefix}/rpc` : vertical.apiPrefix, + vertical.apiProtocol === 'rpc' + ? `${vertical.apiPrefix}/rpc` + : vertical.apiPrefix ) ?? false, - `${vertical.id} development API URL is missing`, + `${vertical.id} development API URL is missing` ); } } @@ -7862,7 +8714,8 @@ for (const vertical of fullStackVerticals) { // delivery unit and must carry one consistent identity record across the // compact config, the reference topology, and its generated build artifact // (ADR-0019: one delivery-unit record, one build marker). -for (const expectedApp of workspaceValidationContract.topology.compactConfig?.apps ?? []) { +for (const expectedApp of workspaceValidationContract.topology.compactConfig + ?.apps ?? []) { const unitLabel = `delivery-unit identity for ${expectedApp.id}`; const expectedDeliveryUnit = deliveryUnitBlock(expectedApp.deliveryUnit); assertSelfCheck( @@ -7872,54 +8725,56 @@ for (const expectedApp of workspaceValidationContract.topology.compactConfig?.ap expectedDeliveryUnit.buildMarker.length > 0, unitLabel, `Every unit kind must declare a delivery-unit record; found ${formatJson(expectedApp.deliveryUnit)}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); const compactAppEntry = ultramodernConfig.topology?.apps?.find( - (entry) => entry?.id === expectedApp.id, + (entry) => entry?.id === expectedApp.id ); assertSameJson( compactAppEntry?.deploy?.cloudflare, expectedApp.deploy?.cloudflare, `${compactConfigPath} topology.apps.${expectedApp.id}.deploy.cloudflare`, - 'regenerate the app Cloudflare deployment contract; do not add local proof-only smoke checks', + 'regenerate the app Cloudflare deployment contract; do not add local proof-only smoke checks' ); assertSameJson( deliveryUnitBlock(compactAppEntry?.deliveryUnit), expectedDeliveryUnit, `${compactConfigPath} topology.apps.${expectedApp.id}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); const topologyUnitEntry = expectedApp.kind === 'shell' ? topology.shell : topology.verticals?.find((entry) => entry?.id === expectedApp.id); - const topologyUnitLabel = expectedApp.kind === 'shell' ? 'shell' : `verticals.${expectedApp.id}`; + const topologyUnitLabel = + expectedApp.kind === 'shell' ? 'shell' : `verticals.${expectedApp.id}`; assertSameJson( deliveryUnitBlock(topologyUnitEntry?.deliveryUnit), expectedDeliveryUnit, `topology/reference-topology.json ${topologyUnitLabel}.deliveryUnit`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); const appPath = expectedApp.path; const buildArtifactPath = `${appPath}/shared/ultramodern-build.json`; assertExists(buildArtifactPath); - const buildIdentity = readJson(BuildArtifactSchema, buildArtifactPath).deliveryUnit ?? {}; + const buildIdentity = + readJson(BuildArtifactSchema, buildArtifactPath).deliveryUnit ?? {}; assertSelfCheck( buildIdentity.unitId === expectedDeliveryUnit.unitId && buildIdentity.buildMarker === expectedDeliveryUnit.buildMarker, `${buildArtifactPath} deliveryUnit`, `Expected ${formatJson({ buildMarker: expectedDeliveryUnit.buildMarker, unitId: expectedDeliveryUnit.unitId })}, found ${formatJson({ buildMarker: buildIdentity.buildMarker, unitId: buildIdentity.unitId })}`, - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); const buildModuleSource = readText(`${appPath}/shared/ultramodern-build.ts`); assertSelfCheck( buildModuleSource.includes('export const ultramodernDeliveryUnit'), `${appPath}/shared/ultramodern-build.ts`, 'Missing ultramodernDeliveryUnit export', - deliveryUnitIdentityFixArea, + deliveryUnitIdentityFixArea ); } @@ -7939,15 +8794,22 @@ const legacyIdentityAllowlist = new Set([ 'verticals/party-registry/tests/unit/engagement-schema-contract.test.ts', ]); const legacyIdentityToken = /(?:^|[^A-Za-z])(?:crm|CRM|Crm)/u; -for (const legacyIdentityProbe of ['crm', 'CRM', 'Crm', 'crmClient', 'CrmApi', 'CRM_SERVICE']) { +for (const legacyIdentityProbe of [ + 'crm', + 'CRM', + 'Crm', + 'crmClient', + 'CrmApi', + 'CRM_SERVICE', +]) { assert( legacyIdentityToken.test(legacyIdentityProbe), - `Legacy identity guard does not reject ${legacyIdentityProbe}`, + `Legacy identity guard does not reject ${legacyIdentityProbe}` ); } assert( !legacyIdentityToken.test('scrm'), - 'Legacy identity guard must not match the CRM letters inside another word', + 'Legacy identity guard must not match the CRM letters inside another word' ); const staleNameScanPaths = [ '.', @@ -7968,7 +8830,7 @@ const trackedAndUntrackedFiles = [ { cwd: root, encoding: 'utf-8', - }, + } ), ] .join('\n') @@ -7993,7 +8855,7 @@ const legacyIdentityViolations = trackedAndUntrackedFiles.filter((filePath) => { }); assert( legacyIdentityViolations.length === 0, - `Active application and current documentation surfaces contain legacy CRM identity tokens: ${legacyIdentityViolations.join(', ')}`, + `Active application and current documentation surfaces contain legacy CRM identity tokens: ${legacyIdentityViolations.join(', ')}` ); const program = Effect.gen(function* validateUltramodernWorkspace() { @@ -8004,5 +8866,7 @@ const program = Effect.gen(function* validateUltramodernWorkspace() { console.log('UltraModern workspace scaffold validated'); }); -const executableLayer = Layer.effectDiscard(program).pipe(Layer.provide(NodeServices.layer)); +const executableLayer = Layer.effectDiscard(program).pipe( + Layer.provide(NodeServices.layer) +); NodeRuntime.runMain(Effect.scoped(Layer.build(executableLayer))); diff --git a/app/verticals/party-registry/modern.config.ts b/app/verticals/party-registry/modern.config.ts index 3cc10263a..90b90269d 100644 --- a/app/verticals/party-registry/modern.config.ts +++ b/app/verticals/party-registry/modern.config.ts @@ -217,10 +217,6 @@ export default defineConfig( cacheDigest: [appId, buildTarget], cacheDirectory: buildCacheDirectory, }, - rsdoctor: { - disableClientServer: true, - enabled: getBuildBoolean('ULTRAMODERN_RSDOCTOR'), - }, }, plugins: [ appTools(), diff --git a/app/verticals/party-registry/src/worker-host/layer.ts b/app/verticals/party-registry/src/worker-host/layer.ts index 54475cf3f..1244728c1 100644 --- a/app/verticals/party-registry/src/worker-host/layer.ts +++ b/app/verticals/party-registry/src/worker-host/layer.ts @@ -1,22 +1,23 @@ -// @generated by scaffold:outbox-worker worker-host -// @ontos-outbox-worker-host-owner party.registry -import { Layer } from 'effect'; import { CoreSearchIngestionLive, CoreSearchProjectionStoreLive, CoreSearchWorkerSnapshotLive, } from '@app/core-runtime'; -import { OutboxWorkerInfrastructureLive } from '@app/core-runtime/outbox/worker'; import type { CoreSearchIngestion, - CoreSearchProjectionStoreService, + CoreSearchProjectionStore, CoreSearchWorkerSnapshot, OutboxRuntime, } from '@app/core-runtime'; -import { PartySearchProjectorLive } from '../services/party-search-projection.service.ts'; -import type { PartySearchProjector } from '../services/party-search-projection.service.ts'; +import { OutboxWorkerInfrastructureLive } from '@app/core-runtime/outbox/worker'; +// @generated by scaffold:outbox-worker worker-host +// @ontos-outbox-worker-host-owner party.registry +import { Layer } from 'effect'; + import { PartySearchProjectionSourceLive } from '../services/party-search-projection-source.service.ts'; import type { PartySearchProjectionSource } from '../services/party-search-projection-source.service.ts'; +import { PartySearchProjectorLive } from '../services/party-search-projection.service.ts'; +import type { PartySearchProjector } from '../services/party-search-projection.service.ts'; export { CorePersistenceLive as outboxWorkerCorePersistenceLive, @@ -34,12 +35,22 @@ type OutboxWorkerHandlerLayers = Readonly<{ projector: Layer.Layer< PartySearchProjector, never, - CoreSearchIngestion | CoreSearchProjectionStoreService | PartySearchProjectionSource + | CoreSearchIngestion + | CoreSearchProjectionStore + | PartySearchProjectionSource + >; + projectionSource: Layer.Layer< + PartySearchProjectionSource, + never, + CoreSearchWorkerSnapshot + >; + searchIngestion: Layer.Layer< + CoreSearchIngestion, + never, + CoreSearchProjectionStore >; - projectionSource: Layer.Layer; - searchIngestion: Layer.Layer; searchProjectionStore: Layer.Layer< - CoreSearchProjectionStoreService, + CoreSearchProjectionStore, never, Layer.Services >; @@ -51,19 +62,23 @@ type OutboxWorkerHandlerLayers = Readonly<{ }>; /** Private canonical reads run post-commit; only sanitized projections cross into Core Search. */ -export const outboxWorkerHandlerLayers: OutboxWorkerHandlerLayers = Object.freeze({ - projector: PartySearchProjectorLive, - projectionSource: PartySearchProjectionSourceLive, - searchIngestion: CoreSearchIngestionLive, - searchProjectionStore: CoreSearchProjectionStoreLive, - searchWorkerSnapshot: CoreSearchWorkerSnapshotLive, -}); +export const outboxWorkerHandlerLayers: OutboxWorkerHandlerLayers = + Object.freeze({ + projector: PartySearchProjectorLive, + projectionSource: PartySearchProjectionSourceLive, + searchIngestion: CoreSearchIngestionLive, + searchProjectionStore: CoreSearchProjectionStoreLive, + searchWorkerSnapshot: CoreSearchWorkerSnapshotLive, + }); export const outboxWorkerLayer: Layer.Layer< OutboxRuntime | PartySearchProjector, never, | Layer.Services | CoreSearchIngestion - | CoreSearchProjectionStoreService + | CoreSearchProjectionStore | PartySearchProjectionSource -> = Layer.merge(outboxWorkerInfrastructureLayer, outboxWorkerHandlerLayers.projector); +> = Layer.merge( + outboxWorkerInfrastructureLayer, + outboxWorkerHandlerLayers.projector +); diff --git a/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts b/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts index baed0f539..6f58c99d9 100644 --- a/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts +++ b/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts @@ -1,17 +1,26 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import test from 'node:test'; -import { DateTime, Effect, Option, Schema } from 'effect'; + import { + CoreSearchProjectionStore, makeCoreSearchIngestion, createCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, } from '@app/core-runtime'; -import type { OutboxMessage, OutboxWorkerHandlerContext } from '@app/core-runtime'; -import { bindActionTestServices, makeActionTestHarness } from '@app/core-runtime/testing/actions'; -import { updatePartyOfficialIdentifierAction } from '../../src/actions/update-party-official-identifier.action.ts'; +import type { + OutboxMessage, + OutboxWorkerHandlerContext, +} from '@app/core-runtime'; +import { + bindActionTestServices, + makeActionTestHarness, +} from '@app/core-runtime/testing/actions'; +import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { DateTime, Effect, Option, Schema } from 'effect'; + import { createPartyAction } from '../../src/actions/create-party.action.ts'; import { resolveDuplicateCandidateMatchAction } from '../../src/actions/resolve-duplicate-candidate-match.action.ts'; +import { updatePartyOfficialIdentifierAction } from '../../src/actions/update-party-official-identifier.action.ts'; import { makePartySearchProjector, PartySearchProjector, @@ -107,13 +116,16 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { }), }, makeCoreSearchIngestion(store), - store, + store ); const replaceIdentifiers = (values: readonly PartySearchSourceValue[]) => Effect.sync(() => { canonical = { ...canonical, - parties: canonical.parties.map((party) => ({ ...party, identifiers: values })), + parties: canonical.parties.map((party) => ({ + ...party, + identifiers: values, + })), projectionVersion: '2', }; }); @@ -121,20 +133,23 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { Effect.gen(function* deliverIdentifierMessage() { if (message.topic === 'party.registry.official-identifier-added.v1') { const { descriptor } = projectOfficialIdentifierAddedToSearchWorker; - const payload = yield* Schema.decodeUnknownEffect(descriptor.payloadSchema)( - message.payloadJson, - ); + const payload = yield* Schema.decodeUnknownEffect( + descriptor.payloadSchema + )(message.payloadJson); yield* handleProjectOfficialIdentifierAddedToSearch(payload, { ...baseContext, topic: message.topic, workerKey: descriptor.workerKey, }).pipe(Effect.provideService(PartySearchProjector, projector)); } else { - assert.equal(message.topic, 'party.registry.official-identifier-updated.v1'); - const { descriptor } = projectOfficialIdentifierUpdatedToSearchWorker; - const payload = yield* Schema.decodeUnknownEffect(descriptor.payloadSchema)( - message.payloadJson, + assert.equal( + message.topic, + 'party.registry.official-identifier-updated.v1' ); + const { descriptor } = projectOfficialIdentifierUpdatedToSearchWorker; + const payload = yield* Schema.decodeUnknownEffect( + descriptor.payloadSchema + )(message.payloadJson); yield* handleProjectOfficialIdentifierUpdatedToSearch(payload, { ...baseContext, topic: message.topic, @@ -145,14 +160,17 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { return { deliver, query: () => - createCoreSearchQueryRuntime(store).search({ - effectiveAt: '2026-09-03T00:00:00.000Z', - includeArchived: false, - moduleId: 'party.registry', - query: identifier.value, - resourceType: 'party.registry.party', - tenantId, - }), + Effect.gen(function* queryIdentifiers() { + const search = yield* createCoreSearchQueryRuntime; + return yield* search.search({ + effectiveAt: '2026-09-03T00:00:00.000Z', + includeArchived: false, + moduleId: 'party.registry', + query: identifier.value, + resourceType: 'party.registry.party', + tenantId, + }); + }).pipe(Effect.provideService(CoreSearchProjectionStore, store)), replaceIdentifiers, seed: projector.project(baseContext, { partyId: partyRef.resourceId }), }; @@ -160,30 +178,39 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { const assertIdentifierOutbox = ( harness: ReturnType, - eventType: string, + eventType: string ) => { const [commit] = harness.snapshot().committed; assert.ok(commit); assert.equal(commit.evidence.outboxMessages.length, 1); const [outbox] = commit.evidence.outboxMessages; assert.ok(outbox); - assert.equal(commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType, eventType); - assert.deepEqual(outbox.message.payloadJson, { officialIdentifierRef, partyRef }); + assert.equal( + commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType, + eventType + ); + assert.deepEqual(outbox.message.payloadJson, { + officialIdentifierRef, + partyRef, + }); return outbox; }; const assertAttachedIdentifierDelivery = ( harness: ReturnType, - search: ReturnType, + search: ReturnType ) => Effect.gen(function* verifyCommittedIdentifierDelivery() { - const outbox = assertIdentifierOutbox(harness, 'party.registry.official-identifier-added.v1'); + const outbox = assertIdentifierOutbox( + harness, + 'party.registry.official-identifier-added.v1' + ); assert.deepEqual(yield* search.query(), []); yield* search.deliver(outbox.message); const hits = yield* search.query(); assert.deepEqual( hits.map((hit) => hit.ref), - [partyRef], + [partyRef] ); yield* search.deliver(outbox.message); assert.deepEqual(yield* search.query(), hits); @@ -205,7 +232,7 @@ test('CreateParty MATCHED_EXISTING publishes an attached identifier and indexes decisionRef, outcome: 'MATCHED_EXISTING' as const, partyRef, - }), + }) ), }), ], @@ -217,7 +244,11 @@ test('CreateParty MATCHED_EXISTING publishes an attached identifier and indexes displayName: 'Acme', evidenceRefs: ['evidence:confirmed-tax-registration'], officialIdentifiers: [ - { identifierType: 'CZ_DIC', value: identifier.value, verification: 'VERIFIED' }, + { + identifierType: 'CZ_DIC', + value: identifier.value, + verification: 'VERIFIED', + }, ], partyType: 'ORGANIZATION', provenance: { method: 'DOCUMENT_REVIEW', source: 'USER_ASSERTION' }, @@ -226,11 +257,18 @@ test('CreateParty MATCHED_EXISTING publishes an attached identifier and indexes }, principal, registration: createPartyAction, - transport: { correlationId: 'identifier-sync', idempotencyKey: 'match-identifier-1' }, + transport: { + correlationId: 'identifier-sync', + idempotencyKey: 'match-identifier-1', + }, + }); + assert.deepEqual(result, { + decisionRef, + outcome: 'MATCHED_EXISTING', + partyRef, }); - assert.deepEqual(result, { decisionRef, outcome: 'MATCHED_EXISTING', partyRef }); yield* assertAttachedIdentifierDelivery(harness, search); - }), + }) )); test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it after delivery only', () => @@ -251,7 +289,7 @@ test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it af lifecycleState: 'RESOLVED' as const, outcome: 'MATCH_EXISTING' as const, partyRef, - }), + }) ), }), ], @@ -279,7 +317,7 @@ test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it af partyRef, }); yield* assertAttachedIdentifierDelivery(harness, search); - }), + }) )); test('END_VALIDITY refreshes search only after its committed identifier message and remains replay-safe', () => @@ -301,19 +339,23 @@ test('END_VALIDITY refreshes search only after its committed identifier message services: [ bindActionTestServices(updatePartyOfficialIdentifierAction, { update: () => - search.replaceIdentifiers([{ ...identifier, state: 'ENDED', validTo }]).pipe( - Effect.as({ - after, - before, - result: { - officialIdentifierRef, - partyRef, - state: 'ENDED', - validTo: Option.some(DateTime.makeUnsafe(validTo)), - verification: 'VERIFIED', - }, - }), - ), + search + .replaceIdentifiers([ + { ...identifier, state: 'ENDED', validTo }, + ]) + .pipe( + Effect.as({ + after, + before, + result: { + officialIdentifierRef, + partyRef, + state: 'ENDED', + validTo: Option.some(DateTime.makeUnsafe(validTo)), + verification: 'VERIFIED', + }, + }) + ), }), ], tenantPermission: 'allowed', @@ -327,16 +369,19 @@ test('END_VALIDITY refreshes search only after its committed identifier message }, principal, registration: updatePartyOfficialIdentifierAction, - transport: { correlationId: 'identifier-sync', idempotencyKey: 'end-identifier-1' }, + transport: { + correlationId: 'identifier-sync', + idempotencyKey: 'end-identifier-1', + }, }); const outbox = assertIdentifierOutbox( harness, - 'party.registry.official-identifier-updated.v1', + 'party.registry.official-identifier-updated.v1' ); assert.equal((yield* search.query()).length, 1); yield* search.deliver(outbox.message); assert.deepEqual(yield* search.query(), []); yield* search.deliver(outbox.message); assert.deepEqual(yield* search.query(), []); - }), + }) )); diff --git a/app/verticals/party-registry/tests/unit/search-projector.test.ts b/app/verticals/party-registry/tests/unit/search-projector.test.ts index da8018fa6..119ddc6c2 100644 --- a/app/verticals/party-registry/tests/unit/search-projector.test.ts +++ b/app/verticals/party-registry/tests/unit/search-projector.test.ts @@ -1,21 +1,27 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import test from 'node:test'; -import { Effect, Exit, Match } from 'effect'; + import { + CoreSearchProjectionStore, createCoreSearchQueryRuntime, makeCoreSearchIngestion, makeInMemoryCoreSearchProjectionStore, } from '@app/core-runtime'; -import type { CoreSearchProjectionDocument, OutboxWorkerHandlerContext } from '@app/core-runtime'; +import type { + CoreSearchProjectionDocument, + OutboxWorkerHandlerContext, +} from '@app/core-runtime'; +import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { Effect, Exit, Match } from 'effect'; + +import { PartySearchProjectionUnavailable } from '../../shared/domain/search-projection-error.ts'; +import { normalizeCounterpartySearchHits } from '../../shared/domain/search-semantics.ts'; +import { makePartySearchProjectionGateway } from '../../src/search/parties.provider.ts'; import { buildPartySearchDocuments, makePartySearchProjector, } from '../../src/services/party-search-projection.service.ts'; import type { PartySearchSourceSnapshot } from '../../src/services/party-search-projection.service.ts'; -import { PartySearchProjectionUnavailable } from '../../shared/domain/search-projection-error.ts'; -import { makePartySearchProjectionGateway } from '../../src/search/parties.provider.ts'; -import { normalizeCounterpartySearchHits } from '../../shared/domain/search-semantics.ts'; const tenantId = '10000000-0000-4000-8000-000000000001'; const partyRef = { @@ -73,13 +79,15 @@ test('post-commit projection makes only active permission-safe identity evidence Effect.gen(function* testScenario() { const documents = yield* buildPartySearchDocuments(snapshot); const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); yield* Effect.forEach((document: CoreSearchProjectionDocument) => store.apply({ document, kind: 'upsert', - }), + }) )(documents); - const search = createCoreSearchQueryRuntime(store); const query = (value: string) => search.search({ effectiveAt: '2026-09-03T00:00:00.000Z', @@ -96,9 +104,15 @@ test('post-commit projection makes only active permission-safe identity evidence assert.deepEqual(yield* query('private@example.test'), []); assert.deepEqual(yield* query('+420123456789'), []); assert.deepEqual(publicHits, [ - { archived: false, facets: [], metadata: [], ref: partyRef, title: 'ACME' }, + { + archived: false, + facets: [], + metadata: [], + ref: partyRef, + title: 'ACME', + }, ]); - }), + }) )); test('aliases collapse to canonical identity and only alias-only evidence labels the match', () => runEffectTestPromise( @@ -106,6 +120,9 @@ test('aliases collapse to canonical identity and only alias-only evidence labels const [party] = snapshot.parties; assert.ok(party); const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); const documents = yield* buildPartySearchDocuments({ ...snapshot, parties: [ @@ -128,10 +145,10 @@ test('aliases collapse to canonical identity and only alias-only evidence labels store.apply({ document, kind: 'upsert', - }), + }) )(documents); const query = (value: string) => - createCoreSearchQueryRuntime(store).search({ + search.search({ includeArchived: false, moduleId: 'party.registry', query: value, @@ -144,7 +161,7 @@ test('aliases collapse to canonical identity and only alias-only evidence labels assert.equal(alias[0]?.matchedRef?.resourceId, 'absorbed'); const canonicalHits = yield* query('ACME'); assert.equal(canonicalHits[0]?.matchedRef, undefined); - }), + }) )); const context: OutboxWorkerHandlerContext = { attemptNumber: 1, @@ -162,20 +179,23 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); let current = snapshot; const projector = makePartySearchProjector( { load: () => Effect.succeed(current), }, makeCoreSearchIngestion(store), - store, + store ); const deliver = () => projector.project(context, { partyId: partyRef.resourceId, }); const query = (includeArchived = false) => - createCoreSearchQueryRuntime(store).search({ + search.search({ includeArchived, moduleId: 'party.registry', query: 'ACME', @@ -215,7 +235,7 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and current = snapshot; yield* deliver(); assert.deepEqual(yield* query(true), []); - }), + }) )); test('future-ended contact disappears at its period boundary without another lifecycle message', () => runEffectTestPromise( @@ -223,6 +243,9 @@ test('future-ended contact disappears at its period boundary without another lif const [party] = snapshot.parties; assert.ok(party); const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); const documents = yield* buildPartySearchDocuments({ ...snapshot, parties: [ @@ -245,10 +268,10 @@ test('future-ended contact disappears at its period boundary without another lif store.apply({ document, kind: 'upsert', - }), + }) )(documents); const query = (effectiveAt: string) => - createCoreSearchQueryRuntime(store).search({ + search.search({ effectiveAt, includeArchived: false, moduleId: 'party.registry', @@ -259,7 +282,7 @@ test('future-ended contact disappears at its period boundary without another lif const currentHits = yield* query('2026-09-03T00:00:00.000Z'); assert.equal(currentHits.length, 1); assert.deepEqual(yield* query('2026-09-04T00:00:00.000Z'), []); - }), + }) )); test('Counterparty identity survives aliases, current-role expiry and canonical-party collisions', () => runEffectTestPromise( @@ -272,6 +295,9 @@ test('Counterparty identity survives aliases, current-role expiry and canonical- resourceId: 'absorbed', }; const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); const documents = yield* buildPartySearchDocuments({ ...snapshot, counterparties: ['cp-1', 'cp-2'].map((resourceId) => ({ @@ -310,9 +336,9 @@ test('Counterparty identity survives aliases, current-role expiry and canonical- store.apply({ document, kind: 'upsert', - }), + }) )(documents); - const gateway = makePartySearchProjectionGateway(createCoreSearchQueryRuntime(store)); + const gateway = makePartySearchProjectionGateway(search); const input = { effectiveAt: '2026-09-03T00:00:00.000Z', includeArchived: false, @@ -325,31 +351,37 @@ test('Counterparty identity survives aliases, current-role expiry and canonical- assert.equal(hits.length, 2); assert.deepEqual( hits.map((hit) => hit.counterpartyRef.resourceId), - ['cp-1', 'cp-2'], + ['cp-1', 'cp-2'] ); const normalized = normalizeCounterpartySearchHits(input, hits); const normalizedItems = Match.value(normalized).pipe( Match.tag('SearchResults', ({ items }) => items), Match.tag('SearchProjectionViolation', ({ reason }) => - assert.fail(`Expected normalized search results: ${reason}`), + assert.fail(`Expected normalized search results: ${reason}`) ), - Match.exhaustive, + Match.exhaustive + ); + assert.equal( + normalizedItems[0]?.collision?.kind, + 'CANONICAL_PARTY_COUNTERPARTY_COLLISION' ); - assert.equal(normalizedItems[0]?.collision?.kind, 'CANONICAL_PARTY_COUNTERPARTY_COLLISION'); assert.equal(normalizedItems[0]?.party.matchedViaAlias, true); assert.deepEqual( yield* gateway.searchCounterparties({ ...input, effectiveAt: '2026-10-01T00:00:00.000Z', }), - [], + [] ); - }), + }) )); test('shared public contact returns multiple Parties without uniqueness or matching authority', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); const [party] = snapshot.parties; assert.ok(party); const documents = yield* buildPartySearchDocuments({ @@ -369,9 +401,9 @@ test('shared public contact returns multiple Parties without uniqueness or match store.apply({ document, kind: 'upsert', - }), + }) )(documents); - const hits = yield* createCoreSearchQueryRuntime(store).search({ + const hits = yield* search.search({ includeArchived: false, moduleId: 'party.registry', query: 'public@example.test', @@ -380,21 +412,24 @@ test('shared public contact returns multiple Parties without uniqueness or match }); assert.deepEqual( hits.map((hit) => hit.ref.resourceId), - ['party-1', 'party-2'], + ['party-1', 'party-2'] ); - }), + }) )); test('rebuild reconciles omitted documents and preserves tombstones against stale lifecycle delivery', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); let current = snapshot; const projector = makePartySearchProjector( { load: () => Effect.succeed(current), }, makeCoreSearchIngestion(store), - store, + store ); yield* projector.project(context, { partyId: 'party-1', @@ -415,21 +450,24 @@ test('rebuild reconciles omitted documents and preserves tombstones against stal partyId: 'party-1', }); assert.deepEqual( - yield* createCoreSearchQueryRuntime(store).search({ + yield* search.search({ includeArchived: true, moduleId: 'party.registry', query: 'ACME', resourceType: 'party.registry.party', tenantId, }), - [], + [] ); - }), + }) )); test('source failure is sanitized and leaves previously searchable state intact for retry', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); let fail = false; const projector = makePartySearchProjector( { @@ -439,12 +477,12 @@ test('source failure is sanitized and leaves previously searchable state intact new PartySearchProjectionUnavailable({ code: 'party_search_projection_unavailable', reason: 'Projection temporarily unavailable', - }), + }) ) : Effect.succeed(snapshot), }, makeCoreSearchIngestion(store), - store, + store ); yield* projector.project(context, { partyId: 'party-1', @@ -453,9 +491,9 @@ test('source failure is sanitized and leaves previously searchable state intact const failure = yield* Effect.exit( projector.project(context, { partyId: 'party-1', - }), + }) ); - const priorHits = yield* createCoreSearchQueryRuntime(store).search({ + const priorHits = yield* search.search({ includeArchived: false, moduleId: 'party.registry', query: 'ACME', @@ -464,7 +502,7 @@ test('source failure is sanitized and leaves previously searchable state intact }); assert.equal(failure._tag, 'Failure'); assert.equal(priorHits.length, 1); - }), + }) )); test('zero-length cancelled periods are never searchable and do not poison projection delivery', () => runEffectTestPromise( @@ -508,20 +546,25 @@ test('zero-length cancelled periods are never searchable and do not poison proje ], }, ], - }), + }) ); assert.ok(Exit.isSuccess(result)); assert.deepEqual( - result.value[0]?.temporalSearchableText?.filter((entry) => entry.value === 'cancelled'), - [], + result.value[0]?.temporalSearchableText?.filter( + (entry) => entry.value === 'cancelled' + ), + [] ); assert.deepEqual(result.value[1]?.temporalFacets, []); - }), + }) )); test('projection generation is independent of an out-of-order business event sequence', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); const projector = makePartySearchProjector( { load: () => @@ -531,7 +574,7 @@ test('projection generation is independent of an out-of-order business event seq }), }, makeCoreSearchIngestion(store), - store, + store ); yield* projector.project( { @@ -540,9 +583,9 @@ test('projection generation is independent of an out-of-order business event seq }, { partyId: 'party-1', - }, + } ); - const hits = yield* createCoreSearchQueryRuntime(store).search({ + const hits = yield* search.search({ includeArchived: false, moduleId: 'party.registry', query: 'ACME', @@ -550,7 +593,7 @@ test('projection generation is independent of an out-of-order business event seq tenantId, }); assert.equal(hits.length, 1); - }), + }) )); test('correction and identifier/contact changes replace obsolete evidence instead of accumulating history', () => runEffectTestPromise( @@ -558,13 +601,16 @@ test('correction and identifier/contact changes replace obsolete evidence instea const [party] = snapshot.parties; assert.ok(party); const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); let current = snapshot; const projector = makePartySearchProjector( { load: () => Effect.succeed(current), }, makeCoreSearchIngestion(store), - store, + store ); yield* projector.project(context, { partyId: 'party-1', @@ -591,7 +637,7 @@ test('correction and identifier/contact changes replace obsolete evidence instea partyId: 'party-1', }); const query = (value: string) => - createCoreSearchQueryRuntime(store).search({ + search.search({ includeArchived: false, moduleId: 'party.registry', query: value, @@ -603,12 +649,15 @@ test('correction and identifier/contact changes replace obsolete evidence instea assert.deepEqual(yield* query('public@example.test'), []); const corrected = yield* query('Corrected Company'); assert.equal(corrected.length, 1); - }), + }) )); test('a complete empty rebuild also rejects delayed evidence for a never-before-indexed Party', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); + const search = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, store) + ); let current: PartySearchSourceSnapshot = { ...snapshot, parties: [], @@ -619,7 +668,7 @@ test('a complete empty rebuild also rejects delayed evidence for a never-before- load: () => Effect.succeed(current), }, makeCoreSearchIngestion(store), - store, + store ); yield* projector.project(context, { rebuild: true, @@ -628,7 +677,7 @@ test('a complete empty rebuild also rejects delayed evidence for a never-before- yield* projector.project(context, { partyId: 'party-1', }); - const hits = yield* createCoreSearchQueryRuntime(store).search({ + const hits = yield* search.search({ includeArchived: true, moduleId: 'party.registry', query: 'ACME', @@ -636,5 +685,5 @@ test('a complete empty rebuild also rejects delayed evidence for a never-before- tenantId, }); assert.deepEqual(hits, []); - }), + }) )); From 2c236921658049b545834d1ef0b0fa108079ee8f Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 18:28:23 +0200 Subject: [PATCH 06/11] fix(lint): preserve precise scopes after reflow Keep existing schema-factory and SDK callback exceptions attached to their exact statements. Use the native object predicate in the Drizzle test seam. Formatting, full lint and focused tests pass together. Co-Authored-By: Claude Code --- .../tests/unit/permission-client.test.ts | 75 ++- .../shared-contracts/src/gateway-context.ts | 173 ++++--- .../unit/identity-persistence.service.test.ts | 427 +++++++++++------- 3 files changed, 432 insertions(+), 243 deletions(-) diff --git a/app/packages/core-runtime/tests/unit/permission-client.test.ts b/app/packages/core-runtime/tests/unit/permission-client.test.ts index ea820d5c9..698778f3f 100644 --- a/app/packages/core-runtime/tests/unit/permission-client.test.ts +++ b/app/packages/core-runtime/tests/unit/permission-client.test.ts @@ -1,8 +1,10 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import test from 'node:test'; + +import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { v1 } from '@authzed/authzed-node'; import { Cause, Effect, flow, Schema } from 'effect'; + import { SpiceDbPermissionClientError, createSpiceDbPermissionClient, @@ -26,9 +28,12 @@ test( ( _request: v1.CheckPermissionRequest, // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback: (error: null, response: v1.CheckPermissionResponse) => void, + callback: ( + error: null, + response: v1.CheckPermissionResponse + ) => void // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - ) => callback(null, v1.CheckPermissionResponse.create({})), + ) => callback(null, v1.CheckPermissionResponse.create({})) ); const bulk = context.mock.method( v1.PermissionsServiceClient.prototype, @@ -36,11 +41,17 @@ test( ( _request: v1.CheckBulkPermissionsRequest, // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback: (error: null, response: v1.CheckBulkPermissionsResponse) => void, + callback: ( + error: null, + response: v1.CheckBulkPermissionsResponse + ) => void // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - ) => callback(null, v1.CheckBulkPermissionsResponse.create({})), + ) => callback(null, v1.CheckBulkPermissionsResponse.create({})) + ); + const client = createSpiceDbPermissionClient( + configuration, + SPICEDB_CHECK_TIMEOUT_MS ); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); context.after(() => client.close()); const request = v1.CheckPermissionRequest.create({}); const bulkRequest = v1.CheckBulkPermissionsRequest.create({}); @@ -57,8 +68,8 @@ test( assert.equal(check.mock.calls[0]?.arguments[0], request); assert.equal(bulk.mock.calls[0]?.arguments[0], bulkRequest); }), - runEffectTestPromise, - ), + runEffectTestPromise + ) ); test( @@ -70,22 +81,31 @@ test( context.mock.method( v1.PermissionsServiceClient.prototype, 'checkPermission', - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - (_request: v1.CheckPermissionRequest, callback: (error: Error) => void) => + ( + _request: v1.CheckPermissionRequest, + // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. + callback: (error: Error) => void + ) => // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback(cause), + callback(cause) + ); + const client = createSpiceDbPermissionClient( + configuration, + SPICEDB_CHECK_TIMEOUT_MS ); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); context.after(() => client.close()); const failure = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})), + client.checkPermission(v1.CheckPermissionRequest.create({})) ); assert.ok(Schema.is(SpiceDbPermissionClientError)(failure)); assert.equal(failure.reason.includes(cause.message), false); - assert.equal(Object.getOwnPropertyDescriptor(failure, 'cause')?.value, cause); + assert.equal( + Object.getOwnPropertyDescriptor(failure, 'cause')?.value, + cause + ); }), - runEffectTestPromise, - ), + runEffectTestPromise + ) ); test( @@ -93,15 +113,26 @@ test( flow( (context) => Effect.gen(function* checksPermissionDeadline() { - context.mock.method(v1.PermissionsServiceClient.prototype, 'checkPermission', () => {}); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); + context.mock.method( + v1.PermissionsServiceClient.prototype, + 'checkPermission', + () => {} + ); + const client = createSpiceDbPermissionClient( + configuration, + SPICEDB_CHECK_TIMEOUT_MS + ); context.after(() => client.close()); const failure = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})), + client.checkPermission(v1.CheckPermissionRequest.create({})) ); assert.ok(Schema.is(SpiceDbPermissionClientError)(failure)); - assert.ok(Cause.isTimeoutError(Object.getOwnPropertyDescriptor(failure, 'cause')?.value)); + assert.ok( + Cause.isTimeoutError( + Object.getOwnPropertyDescriptor(failure, 'cause')?.value + ) + ); }), - runEffectTestPromise, - ), + runEffectTestPromise + ) ); diff --git a/app/packages/shared-contracts/src/gateway-context.ts b/app/packages/shared-contracts/src/gateway-context.ts index 3d5276f7a..be598d8e6 100644 --- a/app/packages/shared-contracts/src/gateway-context.ts +++ b/app/packages/shared-contracts/src/gateway-context.ts @@ -1,5 +1,5 @@ -// eslint-disable-next-line anti-slop-effect/no-service-constructor-imports -- These pure helpers construct contract schemas, not Effect services. -import { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from './problem-details.ts'; +import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; +import type { TrustedPrincipalContext } from '@app/core-runtime/actions/principal-context'; import { Effect, HttpApi, @@ -9,11 +9,16 @@ import { makeEffectHttpApiClient, } from '@modern-js/plugin-bff/effect-client'; import type { HttpClientError } from '@modern-js/plugin-bff/effect-client'; -import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; -import type { TrustedPrincipalContext } from '@app/core-runtime/actions/principal-context'; import { Context } from 'effect'; import { HttpClient, HttpClientRequest } from 'effect/unstable/http'; +/* oxlint-disable anti-slop-effect/no-service-constructor-imports -- These pure helpers construct contract schemas, not Effect services. */ +import { + makeProblemDetailsSchema, + makeRetryableProblemDetailsSchema, +} from './problem-details.ts'; +/* oxlint-enable anti-slop-effect/no-service-constructor-imports */ + export const GATEWAY_ASSERTION_VERSION = 1 as const; export const GATEWAY_ASSERTION_TTL_SECONDS = 300 as const; export const GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS = 30 as const; @@ -21,16 +26,20 @@ export const GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS = 30 as const; const nonEmptyString = Schema.String.check(Schema.isMinLength(1)); const uuid = Schema.String.check(Schema.isUUID()); const LegalEntityIdSchema = uuid.pipe(Schema.brand('LegalEntityId')); -const epochSeconds = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)); +const epochSeconds = Schema.Finite.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(0) +); export const GatewayAudienceSchema = nonEmptyString.check( Schema.makeFilter((value) => /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(value) ? undefined - : 'audience must be a stable topology app ID', - ), + : 'audience must be a stable topology app ID' + ) ); -export const GatewayTrustedPrincipalContextSchema = TrustedPrincipalContextSchema; +export const GatewayTrustedPrincipalContextSchema = + TrustedPrincipalContextSchema; export type GatewayTrustedPrincipalContext = TrustedPrincipalContext; @@ -60,24 +69,35 @@ export const GatewayContextClaimsSchema = Schema.Struct({ issues.push({ issue: 'exp must be greater than iat', path: ['exp'] }); } if (claims.exp - claims.iat !== GATEWAY_ASSERTION_TTL_SECONDS) { - issues.push({ issue: 'exp must be exactly 300 seconds after iat', path: ['exp'] }); + issues.push({ + issue: 'exp must be exactly 300 seconds after iat', + path: ['exp'], + }); } if (claims.sub !== claims.principal.principalId) { - issues.push({ issue: 'sub must equal principal.principalId', path: ['sub'] }); + issues.push({ + issue: 'sub must equal principal.principalId', + path: ['sub'], + }); } return issues; - }), + }) ); -export type GatewayContextClaims = Schema.Schema.Type; +export type GatewayContextClaims = Schema.Schema.Type< + typeof GatewayContextClaimsSchema +>; -export const decodeGatewayContextClaims = Schema.decodeUnknownEffect(GatewayContextClaimsSchema, { - onExcessProperty: 'error', -}); +export const decodeGatewayContextClaims = Schema.decodeUnknownEffect( + GatewayContextClaimsSchema, + { + onExcessProperty: 'error', + } +); export const decodeGatewayContextProtectedHeader = Schema.decodeUnknownEffect( GatewayContextProtectedHeaderSchema, - { onExcessProperty: 'error' }, + { onExcessProperty: 'error' } ); export const GatewayContextRequestSchema = Schema.Struct({ @@ -90,31 +110,35 @@ export const GatewayContextResponseSchema = Schema.Struct({ expiresAt: epochSeconds, token: nonEmptyString, }); -export type GatewayContextResponse = Schema.Schema.Type; +export type GatewayContextResponse = Schema.Schema.Type< + typeof GatewayContextResponseSchema +>; -export const GatewayAuthenticationRequiredProblemSchema = makeProblemDetailsSchema( - 'GatewayAuthenticationRequiredProblem', - 401, -); +export const GatewayAuthenticationRequiredProblemSchema = + makeProblemDetailsSchema('GatewayAuthenticationRequiredProblem', 401); export const GatewayAudienceInvalidProblemSchema = makeProblemDetailsSchema( 'GatewayAudienceInvalidProblem', - 400, + 400 ); -export const GatewayUnavailableProblemSchema = makeRetryableProblemDetailsSchema( - 'GatewayUnavailableProblem', - 503, -); +export const GatewayUnavailableProblemSchema = + makeRetryableProblemDetailsSchema('GatewayUnavailableProblem', 503); -export const GatewayInternalProblemSchema = makeProblemDetailsSchema('GatewayInternalProblem', 500); -const GatewayForbiddenProblemSchema = makeProblemDetailsSchema('GatewayForbiddenProblem', 403); +export const GatewayInternalProblemSchema = makeProblemDetailsSchema( + 'GatewayInternalProblem', + 500 +); +const GatewayForbiddenProblemSchema = makeProblemDetailsSchema( + 'GatewayForbiddenProblem', + 403 +); export const GatewayRateLimitedProblemSchema = makeProblemDetailsSchema( 'GatewayRateLimitedProblem', 429, { retryAfterSeconds: Schema.Finite, - }, + } ); export type GatewayAuthenticationRequiredProblem = Schema.Schema.Type< @@ -123,10 +147,18 @@ export type GatewayAuthenticationRequiredProblem = Schema.Schema.Type< export type GatewayAudienceInvalidProblem = Schema.Schema.Type< typeof GatewayAudienceInvalidProblemSchema >; -export type GatewayUnavailableProblem = Schema.Schema.Type; -export type GatewayInternalProblem = Schema.Schema.Type; -type GatewayForbiddenProblem = Schema.Schema.Type; -type GatewayRateLimitedProblem = Schema.Schema.Type; +export type GatewayUnavailableProblem = Schema.Schema.Type< + typeof GatewayUnavailableProblemSchema +>; +export type GatewayInternalProblem = Schema.Schema.Type< + typeof GatewayInternalProblemSchema +>; +type GatewayForbiddenProblem = Schema.Schema.Type< + typeof GatewayForbiddenProblemSchema +>; +type GatewayRateLimitedProblem = Schema.Schema.Type< + typeof GatewayRateLimitedProblemSchema +>; export type GatewayContextProblem = | GatewayAuthenticationRequiredProblem @@ -152,29 +184,36 @@ export const GatewayContextApiGroup = HttpApiGroup.make('gatewayContext') ], payload: GatewayContextRequestSchema, success: GatewayContextResponseSchema, - }), + }) ) .add( - HttpApiEndpoint.post('issueApiKeyGatewayContext', '/auth/api-key/gateway-context', { - error: [ - GatewayAuthenticationRequiredProblemSchema, - GatewayAudienceInvalidProblemSchema, - GatewayForbiddenProblemSchema, - GatewayRateLimitedProblemSchema, - GatewayUnavailableProblemSchema, - GatewayInternalProblemSchema, - ], - headers: ApiKeyGatewayHeadersSchema, - payload: GatewayContextRequestSchema, - success: GatewayContextResponseSchema, - }), + HttpApiEndpoint.post( + 'issueApiKeyGatewayContext', + '/auth/api-key/gateway-context', + { + error: [ + GatewayAuthenticationRequiredProblemSchema, + GatewayAudienceInvalidProblemSchema, + GatewayForbiddenProblemSchema, + GatewayRateLimitedProblemSchema, + GatewayUnavailableProblemSchema, + GatewayInternalProblemSchema, + ], + headers: ApiKeyGatewayHeadersSchema, + payload: GatewayContextRequestSchema, + success: GatewayContextResponseSchema, + } + ) ); -export const GatewayContextApi = HttpApi.make('shellGatewayContextApi').add(GatewayContextApiGroup); +export const GatewayContextApi = HttpApi.make('shellGatewayContextApi').add( + GatewayContextApiGroup +); export const shellGatewayContextContract = { apiPrefix: '/shell-super-app-api', - issueApiKeyGatewayContextPath: '/shell-super-app-api/auth/api-key/gateway-context', + issueApiKeyGatewayContextPath: + '/shell-super-app-api/auth/api-key/gateway-context', issueGatewayContextPath: '/shell-super-app-api/auth/gateway-context', ownerId: 'shell-super-app', } as const; @@ -208,12 +247,16 @@ export type GatewayContextClientError = | HttpClientError.HttpClientError | Schema.SchemaError; -export type GatewayContextClientEffect = Effect.Effect; +export type GatewayContextClientEffect = Effect.Effect< + Success, + GatewayContextClientError +>; -const GatewayContextRequestOptions = Context.Reference( - 'GatewayContextRequestOptions', - { defaultValue: () => ({}) }, -); +const GatewayContextRequestOptions = + Context.Reference( + 'GatewayContextRequestOptions', + { defaultValue: () => ({}) } + ); const gatewayContextClient = makeEffectHttpApiClient(GatewayContextApi, { transformClient: HttpClient.mapRequestEffect((request) => @@ -221,28 +264,32 @@ const gatewayContextClient = makeEffectHttpApiClient(GatewayContextApi, { Effect.map((options) => { let nextRequest = HttpClientRequest.prependUrl( request, - (options.baseUrl ?? shellGatewayContextContract.apiPrefix).toString(), + (options.baseUrl ?? shellGatewayContextContract.apiPrefix).toString() ); if (options.cookie !== undefined) { - nextRequest = HttpClientRequest.setHeader(nextRequest, 'cookie', options.cookie); + nextRequest = HttpClientRequest.setHeader( + nextRequest, + 'cookie', + options.cookie + ); } return nextRequest; - }), - ), + }) + ) ), }); export const issueGatewayContext = ( payload: GatewayContextRequest, - options: GatewayContextClientOptions = {}, + options: GatewayContextClientOptions = {} ): GatewayContextClientEffect => Schema.decodeEffect(GatewayContextRequestSchema)(payload).pipe( Effect.flatMap((decodedPayload) => gatewayContextClient.pipe( Effect.flatMap((client) => - client.gatewayContext.issueGatewayContext({ payload: decodedPayload }), - ), - ), + client.gatewayContext.issueGatewayContext({ payload: decodedPayload }) + ) + ) ), - Effect.provideService(GatewayContextRequestOptions, options), + Effect.provideService(GatewayContextRequestOptions, options) ); diff --git a/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts index 4a50594b4..193c3ba33 100644 --- a/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts @@ -1,9 +1,19 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Match, Option, Result, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { + DateTime, + Effect, + Match, + Option, + Predicate, + Result, + Schema, +} from 'effect'; + import type { AresAppliedEvidence } from '../../shared/domain/ares-application.ts'; import { AresAppliedEvidenceSchema } from '../../shared/domain/ares-application.ts'; import { partySubjectKeyFromString } from '../../shared/domain/identity-contracts.ts'; @@ -33,7 +43,8 @@ const partyId = '22222222-2222-4222-8222-222222222222'; const firstOwnerId = '33333333-3333-4333-8333-333333333333'; const secondOwnerId = '44444444-4444-4444-8444-444444444444'; const officialIdentifierId = '55555555-5555-4555-8555-555555555555'; -const instantAsDate = (instant: string): Date => DateTime.toDateUtc(DateTime.makeUnsafe(instant)); +const instantAsDate = (instant: string): Date => + DateTime.toDateUtc(DateTime.makeUnsafe(instant)); const appliedEvidence = Result.getOrThrow( Schema.decodeUnknownResult(AresAppliedEvidenceSchema)({ authorityPolicyKey: 'party_registry.ares_enrichment', @@ -50,7 +61,7 @@ const appliedEvidence = Result.getOrThrow( queryIco: '27074358', reasonCode: 'selected_missing_fact_confirmed', servedAt: '2026-01-01T00:00:00.000Z', - }), + }) ); const partyRow = (overrides: Partial = {}) => ({ @@ -65,7 +76,9 @@ const partyRow = (overrides: Partial = {}) => ({ ...overrides, }); -const identifierRow = (overrides: Partial = {}) => ({ +const identifierRow = ( + overrides: Partial = {} +) => ({ identifierTypeKey: 'ICO', isCurrent: true, namespace: 'CZ:ICO', @@ -83,7 +96,7 @@ const identifierRow = (overrides: Partial = {}) = const transactionHarness = ( selectResponses: readonly unknown[][], updateResponses: readonly unknown[][] = [], - insertResponses: readonly unknown[][] = [], + insertResponses: readonly unknown[][] = [] ) => { const queuedSelects = [...selectResponses]; const queuedUpdates = [...updateResponses]; @@ -108,7 +121,7 @@ const transactionHarness = ( return chain; }, where: () => chain, - }, + } ); return chain; }; @@ -126,10 +139,9 @@ const transactionHarness = ( }), select: (selection: unknown) => { selectSelections.push(selection); - // eslint-disable-next-line anti-slop/no-runtime-typeof -- The overloaded local Drizzle test double distinguishes SQL lock selections from ordinary query selections. - if (selection !== null && typeof selection === 'object' && 'lock' in selection) { + if (Predicate.isObject(selection) && 'lock' in selection) { // SAFETY: All lock selections emitted by these owner services contain a Drizzle SQL expression. - const lockQuery = new PgDialect().sqlToQuery(selection.lock as SQL); + const lockQuery = new PgDialect().sqlToQuery(selection['lock'] as SQL); if (lockQuery.params[0] === tenantIdentityWriteLockKey(tenantId)) { return query(() => []); } @@ -138,17 +150,27 @@ const transactionHarness = ( }, update: () => query(() => queuedUpdates.shift() ?? []), } as unknown as Parameters[0]; - return { deletedTargets, insertedValues, selectSelections, transaction, updateSets }; + return { + deletedTargets, + insertedValues, + selectSelections, + transaction, + updateSets, + }; }; /* eslint-enable anti-slop/no-unknown-parameters, anti-slop/no-unknown-returns, anti-slop/no-chained-type-assertions */ -const assertNoIdentityWrites = (harness: ReturnType) => { +const assertNoIdentityWrites = ( + harness: ReturnType +) => { assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); assert.deepEqual(harness.deletedTargets, []); }; -const assertTenantLockIsFirst = (harness: ReturnType) => { +const assertTenantLockIsFirst = ( + harness: ReturnType +) => { // SAFETY: Every service under test first calls the tenant lock with one Drizzle SQL lock selection. const selection = harness.selectSelections[0] as { readonly lock: SQL }; const query = new PgDialect().sqlToQuery(selection.lock); @@ -157,7 +179,10 @@ const assertTenantLockIsFirst = (harness: ReturnType) }; test('ended Party facts are made non-current as part of the same transition', () => { - assert.deepEqual(endedPartyFactTransition, { isCurrent: false, state: 'ENDED' }); + assert.deepEqual(endedPartyFactTransition, { + isCurrent: false, + state: 'ENDED', + }); }); test('unnamed Party insertion persists no fabricated display-name assertion', () => @@ -167,12 +192,13 @@ test('unnamed Party insertion persists no fabricated display-name assertion', () ...appliedEvidence, fact: 'PARTY_CANDIDATE', }; - const encodedCandidateEvidence = - yield* Schema.encodeEffect(AresAppliedEvidenceSchema)(candidateEvidence); + const encodedCandidateEvidence = yield* Schema.encodeEffect( + AresAppliedEvidenceSchema + )(candidateEvidence); const harness = transactionHarness( [], [], - [[partyRow({ archivedAt: null, currentDisplayName: null })], []], + [[partyRow({ archivedAt: null, currentDisplayName: null })], []] ); const result = yield* insertPartyRecord( harness.transaction, @@ -180,7 +206,11 @@ test('unnamed Party insertion persists no fabricated display-name assertion', () { evidenceRefs: ['source:official-record'], officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, + { + identifierType: 'ICO', + value: '27074358', + verification: 'VERIFIED', + }, ], partyType: 'ORGANIZATION', provenance: { @@ -204,24 +234,28 @@ test('unnamed Party insertion persists no fabricated display-name assertion', () actionInvocationId: '66666666-6666-4666-8666-666666666666', policyVersion: 'party-identity.v1', principalId: '77777777-7777-4777-8777-777777777777', - }, + } ); assert.ok(Option.isNone(result.displayName)); assertTenantLockIsFirst(harness); // SAFETY: The first insert captured by insertPartyRecord targets the parties table. assert.equal( - (harness.insertedValues[0] as typeof parties.$inferInsert).currentDisplayName, - null, + (harness.insertedValues[0] as typeof parties.$inferInsert) + .currentDisplayName, + null ); // SAFETY: The second insert captured by insertPartyRecord targets the typed fact-assertion table. const assertions = harness .insertedValues[1] as readonly (typeof partyFactAssertions.$inferInsert)[]; assert.deepEqual( assertions.map((assertion) => assertion.factKind), - ['PARTY_TYPE'], + ['PARTY_TYPE'] ); - assert.deepEqual(assertions[0]?.externalEvidence, encodedCandidateEvidence); - }), + assert.deepEqual( + assertions[0]?.externalEvidence, + encodedCandidateEvidence + ); + }) )); test('identity updates close the preceding assertion before accepting its replacement', () => @@ -230,22 +264,27 @@ test('identity updates close the preceding assertion before accepting its replac const current = partyRow({ archivedAt: null }); const harness = transactionHarness( [[current], [], [{ partyId }]], - [[{ ...current, currentDisplayName: 'New name', revision: 5 }], []], + [[{ ...current, currentDisplayName: 'New name', revision: 5 }], []] ); - const encodedAppliedEvidence = - yield* Schema.encodeEffect(AresAppliedEvidenceSchema)(appliedEvidence); + const encodedAppliedEvidence = yield* Schema.encodeEffect( + AresAppliedEvidenceSchema + )(appliedEvidence); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - displayName: 'New name', - expectedRevision: 4, - externalEvidence: appliedEvidence, - partyId, - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: '2026-01-01T00:00:00.000Z', - }); + const result = yield* updatePartyIdentityRecord( + harness.transaction, + tenantId, + { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + displayName: 'New name', + expectedRevision: 4, + externalEvidence: appliedEvidence, + partyId, + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: '2026-01-01T00:00:00.000Z', + } + ); assert.equal(result._tag, 'found'); assertTenantLockIsFirst(harness); assert.deepEqual(harness.updateSets[1], { @@ -258,17 +297,20 @@ test('identity updates close the preceding assertion before accepting its replac const assertions = harness .insertedValues[0] as readonly (typeof partyFactAssertions.$inferInsert)[]; assert.deepEqual(assertions[0]?.externalEvidence, encodedAppliedEvidence); - }), + }) )); test('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { assert.deepEqual(classifyUnarchiveClaimOwners(partyId, [{}, { partyId }]), { _tag: 'available', }); - assert.deepEqual(classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }]), { - _tag: 'identity_conflict', - conflictingPartyId: firstOwnerId, - }); + assert.deepEqual( + classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }]), + { + _tag: 'identity_conflict', + conflictingPartyId: firstOwnerId, + } + ); assert.deepEqual( classifyUnarchiveClaimOwners(partyId, [ { partyId: secondOwnerId }, @@ -278,28 +320,36 @@ test('unarchive owner classification distinguishes conflict from ambiguity deter { _tag: 'identity_ambiguous', candidatePartyIds: [firstOwnerId, secondOwnerId], - }, + } ); }); test('future-effective identity updates do not replace current facts early', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { - const harness = transactionHarness([[partyRow({ archivedAt: null })], [], [{ partyId }]]); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - displayName: 'Future name', - expectedRevision: 4, - partyId, - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: '2999-01-01T00:00:00.000Z', - }); + const harness = transactionHarness([ + [partyRow({ archivedAt: null })], + [], + [{ partyId }], + ]); + const result = yield* updatePartyIdentityRecord( + harness.transaction, + tenantId, + { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + displayName: 'Future name', + expectedRevision: 4, + partyId, + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: '2999-01-01T00:00:00.000Z', + } + ); assert.equal(result._tag, 'conflict'); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('unarchive keeps the Party archived when an exact claim belongs to another Party', () => @@ -315,7 +365,12 @@ test('unarchive keeps the Party archived when an exact claim belongs to another [{ partyId: firstOwnerId }], ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.deepEqual(result, { _tag: 'identity_conflict', @@ -324,7 +379,7 @@ test('unarchive keeps the Party archived when an exact claim belongs to another assertTenantLockIsFirst(harness); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('blocked unarchive persists a case and decision without mutating Party, then reuses the case on a fresh attempt', () => @@ -359,19 +414,19 @@ test('blocked unarchive persists a case and decision without mutating Party, the [], ], [], - [[caseRow], [], [{ matchDecisionId: decisionId }]], + [[caseRow], [], [{ matchDecisionId: decisionId }]] ); const result = yield* unarchivePartyWithReview( harness.transaction, tenantId, partyId, 4, - decisionId, + decisionId ); assert.equal(result._tag, 'blocked'); const blocked = Match.value(result).pipe( Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected unarchive to be blocked')), + Match.orElse(() => assert.fail('Expected unarchive to be blocked')) ); assertTenantLockIsFirst(harness); assert.deepEqual(harness.updateSets, []); @@ -384,17 +439,21 @@ test('blocked unarchive persists a case and decision without mutating Party, the const members = harness .insertedValues[1] as readonly (typeof duplicateCandidateCaseParties.$inferInsert)[]; // SAFETY: The third insert is the durable Action-linked match decision. - const decision = harness.insertedValues[2] as typeof partyMatchDecisions.$inferInsert; + const decision = harness + .insertedValues[2] as typeof partyMatchDecisions.$inferInsert; assert.equal(persistedCase.candidateSnapshot.intent, 'UNARCHIVE'); - assert.deepEqual(persistedCase.candidateSnapshot.names, ['Archived organization']); + assert.deepEqual(persistedCase.candidateSnapshot.names, [ + 'Archived organization', + ]); assert.equal( - persistedCase.candidateSnapshot.officialIdentifiers?.[0]?.normalizedValue, - '27074358', + persistedCase.candidateSnapshot.officialIdentifiers?.[0] + ?.normalizedValue, + '27074358' ); assert.match(persistedCase.evaluationFingerprint, /^[0-9a-f]{64}$/u); assert.deepEqual( members.map((member) => member.partyId), - [partyId, firstOwnerId], + [partyId, firstOwnerId] ); assert.equal(decision.actionInvocationId, decisionId); assert.equal(decision.candidateCaseId, candidateCaseId); @@ -403,40 +462,50 @@ test('blocked unarchive persists a case and decision without mutating Party, the assert.ok(Option.isSome(blocked.party.archivedAt)); assert.equal( DateTime.formatIso(Option.getOrThrow(blocked.party.archivedAt)), - '2026-01-01T00:00:00.000Z', + '2026-01-01T00:00:00.000Z' ); assert.equal(blocked.party.revision, 4); const secondDecisionId = '88888888-8888-4888-8888-888888888888'; const retryHarness = transactionHarness( - [[partyRow()], [], [{ partyId }], [{ candidateCaseId }], [partyRow()], [caseRow]], + [ + [partyRow()], + [], + [{ partyId }], + [{ candidateCaseId }], + [partyRow()], + [caseRow], + ], [], - [[{ matchDecisionId: secondDecisionId }]], + [[{ matchDecisionId: secondDecisionId }]] ); const retry = yield* unarchivePartyWithReview( retryHarness.transaction, tenantId, partyId, 4, - secondDecisionId, + secondDecisionId ); assert.equal(retry._tag, 'blocked'); const retryBlocked = Match.value(retry).pipe( Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected retry to be blocked')), + Match.orElse(() => assert.fail('Expected retry to be blocked')) ); assert.equal(retryHarness.insertedValues.length, 1); assert.deepEqual(retryHarness.updateSets, []); assert.deepEqual(retryBlocked.caseRef, blocked.caseRef); assert.notDeepEqual(retryBlocked.decisionRef, blocked.decisionRef); assert.deepEqual(retryBlocked.party, blocked.party); - }), + }) )); test('unresolved unnamed unarchive review persists no invented display-name evidence', () => runEffectTestPromise( Effect.gen(function* verifyUnresolvedUnarchiveReview() { - const current = partyRow({ currentDisplayName: null, currentType: 'UNRESOLVED' }); + const current = partyRow({ + currentDisplayName: null, + currentType: 'UNRESOLVED', + }); const caseRow = { candidateCaseId: firstOwnerId, candidateFingerprint: 'b'.repeat(64), @@ -446,19 +515,19 @@ test('unresolved unnamed unarchive review persists no invented display-name evid const harness = transactionHarness( [[current], [], [{ partyId }], [], [], [current], [], [], []], [], - [[caseRow], [], [{ matchDecisionId: secondOwnerId }]], + [[caseRow], [], [{ matchDecisionId: secondOwnerId }]] ); const result = yield* unarchivePartyWithReview( harness.transaction, tenantId, partyId, 4, - secondOwnerId, + secondOwnerId ); assert.equal(result._tag, 'blocked'); const blocked = Match.value(result).pipe( Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected unarchive to be blocked')), + Match.orElse(() => assert.fail('Expected unarchive to be blocked')) ); // SAFETY: The first captured insert is the immutable candidate case. const persistedCase = harness @@ -467,7 +536,7 @@ test('unresolved unnamed unarchive review persists no invented display-name evid assert.deepEqual(persistedCase.candidateSnapshot.officialIdentifiers, []); assert.deepEqual(harness.updateSets, []); assert.equal(blocked.reasonCode, 'UNRESOLVED_IDENTITY'); - }), + }) )); test('archive acquires the tenant identity lock before any Party row lock', () => @@ -479,11 +548,11 @@ test('archive acquires the tenant identity lock before any Party row lock', () = tenantId, partyId, 4, - 'ARCHIVED', + 'ARCHIVED' ); assert.equal(result._tag, 'not_found'); assertTenantLockIsFirst(harness); - }), + }) )); test('unarchive restores an unclaimed eligible identifier before activating the Party', () => @@ -492,10 +561,15 @@ test('unarchive restores an unclaimed eligible identifier before activating the const activeParty = partyRow({ archivedAt: null, revision: 5 }); const harness = transactionHarness( [[partyRow()], [], [{ partyId }], [], [identifierRow()], [{}], []], - [[activeParty]], + [[activeParty]] ); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.equal(result._tag, 'found'); assert.deepEqual(harness.insertedValues, [ @@ -515,12 +589,12 @@ test('unarchive restores an unclaimed eligible identifier before activating the assert.deepEqual( Object.fromEntries( Object.entries(harness.updateSets[0] as Partial).filter( - ([key]) => key !== 'updatedAt', - ), + ([key]) => key !== 'updatedAt' + ) ), - { archivedAt: null, revision: 5 }, + { archivedAt: null, revision: 5 } ); - }), + }) )); test('unarchive rejects an alias rather than forwarding the write to its survivor', () => @@ -534,12 +608,12 @@ test('unarchive rejects an alias rather than forwarding the write to its survivo ]); const error = yield* Effect.flip( - unarchivePartyRecord(harness.transaction, tenantId, partyId, 4), + unarchivePartyRecord(harness.transaction, tenantId, partyId, 4) ); assert.equal(error._tag, 'PartyAliasWriteRejected'); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('unarchive reports ambiguous exact claims without changing archived state', () => @@ -564,14 +638,19 @@ test('unarchive reports ambiguous exact claims without changing archived state', [{ partyId: secondOwnerId }], ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.deepEqual(result, { _tag: 'identity_ambiguous', candidatePartyIds: [firstOwnerId, secondOwnerId], }); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('unarchive does not promote a PERSON ICO into an exclusive strong claim', () => @@ -580,14 +659,19 @@ test('unarchive does not promote a PERSON ICO into an exclusive strong claim', ( const currentParty = partyRow({ currentType: 'PERSON' }); const harness = transactionHarness( [[currentParty], [], [{ partyId }], [], [identifierRow()]], - [[{ ...currentParty, archivedAt: null, revision: 5 }]], + [[{ ...currentParty, archivedAt: null, revision: 5 }]] ); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.equal(result._tag, 'found'); assert.deepEqual(harness.insertedValues, []); assert.equal(harness.updateSets.length, 1); - }), + }) )); test('unarchive requires review while a duplicate case involving the Party remains open', () => @@ -600,7 +684,12 @@ test('unarchive requires review while a duplicate case involving the Party remai [{ partyId }], [{ candidateCaseId: caseId }], ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.deepEqual(result, { _tag: 'review_required', caseIds: [caseId], @@ -608,7 +697,7 @@ test('unarchive requires review while a duplicate case involving the Party remai }); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('unarchive requires review for unresolved identity without any eligible strong claim', () => @@ -621,7 +710,12 @@ test('unarchive requires review for unresolved identity without any eligible str [], [], ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 + ); assert.deepEqual(result, { _tag: 'review_required', caseIds: [], @@ -629,7 +723,7 @@ test('unarchive requires review for unresolved identity without any eligible str }); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); test('reviewed UNRESOLVED Party can unarchive using retained accepted creation evidence', () => @@ -646,13 +740,18 @@ test('reviewed UNRESOLVED Party can unarchive using retained accepted creation e [], [], ], - [[{ ...current, archivedAt: null, revision: 5 }]], + [[{ ...current, archivedAt: null, revision: 5 }]] + ); + const result = yield* unarchivePartyRecord( + harness.transaction, + tenantId, + partyId, + 4 ); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); assert.equal(result._tag, 'found'); assert.equal(harness.updateSets.length, 1); assert.deepEqual(harness.insertedValues, []); - }), + }) )); test('Party type enrichment refuses another owner of a newly eligible identifier', () => @@ -668,29 +767,33 @@ test('Party type enrichment refuses another owner of a newly eligible identifier [{}], [{ partyId: firstOwnerId }], ]); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - expectedRevision: 4, - partyId, - partyType: 'ORGANIZATION', - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT', - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'ORGANIZATION', - statement: 'Reviewed this external organization', - subjectKey: partySubjectKeyFromString('one-subject'), - }, - ], - validFrom: '2026-01-01T00:00:00.000Z', - }); + const result = yield* updatePartyIdentityRecord( + harness.transaction, + tenantId, + { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + expectedRevision: 4, + partyId, + partyType: 'ORGANIZATION', + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT', + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'ORGANIZATION', + statement: 'Reviewed this external organization', + subjectKey: partySubjectKeyFromString('one-subject'), + }, + ], + validFrom: '2026-01-01T00:00:00.000Z', + } + ); assert.equal(result._tag, 'conflict'); assertNoIdentityWrites(harness); - }), + }) )); test('Party type enrichment atomically claims identifiers that newly qualify', () => @@ -699,28 +802,32 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); const harness = transactionHarness( [[current], [], [{ partyId }], [], [identifierRow()], [{}], [], []], - [[{ ...current, currentType: 'ORGANIZATION', revision: 5 }], []], + [[{ ...current, currentType: 'ORGANIZATION', revision: 5 }], []] + ); + const result = yield* updatePartyIdentityRecord( + harness.transaction, + tenantId, + { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + expectedRevision: 4, + partyId, + partyType: 'ORGANIZATION', + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT', + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'ORGANIZATION', + statement: 'Reviewed this external organization', + subjectKey: partySubjectKeyFromString('one-subject'), + }, + ], + validFrom: '2026-01-01T00:00:00.000Z', + } ); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - expectedRevision: 4, - partyId, - partyType: 'ORGANIZATION', - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT', - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'ORGANIZATION', - statement: 'Reviewed this external organization', - subjectKey: partySubjectKeyFromString('one-subject'), - }, - ], - validFrom: '2026-01-01T00:00:00.000Z', - }); assert.equal(result._tag, 'found'); assert.deepEqual(harness.insertedValues[0], [ { @@ -733,7 +840,7 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( }, ]); assert.equal(harness.updateSets.length, 2); - }), + }) )); test('type correction reconciliation releases an ICO claim no longer eligible for a PERSON', () => @@ -757,13 +864,13 @@ test('type correction reconciliation releases an ICO claim no longer eligible fo harness.transaction, tenantId, partyId, - 'PERSON', + 'PERSON' ); assert.deepEqual(result, { _tag: 'available', eligibleClaimCount: 0 }); assert.equal(harness.deletedTargets.length, 1); assert.deepEqual(harness.insertedValues, []); assertTenantLockIsFirst(harness); - }), + }) )); test('identity updates reject a historical end earlier than the assertion being replaced', () => @@ -775,19 +882,23 @@ test('identity updates reject a historical end earlier than the assertion being [{ partyId }], [{ validFrom: instantAsDate('2026-05-01T00:00:00.000Z') }], ]); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - displayName: 'Historical name', - expectedRevision: 4, - partyId, - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: '2026-01-01T00:00:00.000Z', - }); + const result = yield* updatePartyIdentityRecord( + harness.transaction, + tenantId, + { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + displayName: 'Historical name', + expectedRevision: 4, + partyId, + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: '2026-01-01T00:00:00.000Z', + } + ); assert.equal(result._tag, 'conflict'); assertNoIdentityWrites(harness); - }), + }) )); test('type enrichment rejects unevidenced type before accepting facts or claims', () => @@ -805,10 +916,10 @@ test('type enrichment rejects unevidenced type before accepting facts or claims' provenanceMethod: 'MANUAL', provenanceSource: 'review', validFrom: '2026-01-01T00:00:00.000Z', - }), + }) ); assert.equal(error._tag, 'PartyEvidenceInsufficient'); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); - }), + }) )); From 4519c6051635df00b7cb7c7377e4e7a2caad3d5b Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 18:28:25 +0200 Subject: [PATCH 07/11] style: apply the configured formatter preset Isolate preset-produced formatting from functional migration commits. Import, package and Tailwind sorting remain enabled; no custom style overrides or formatter suppressions. Co-Authored-By: Claude Code --- app/AGENTS.md | 21 +- app/DEVELOPMENT.md | 45 +- app/README.md | 214 +- .../api/auth/api-key-service.ts | 409 ++-- app/apps/shell-super-app/api/auth/config.ts | 141 +- .../api/auth/configuration-provider.ts | 12 +- .../shell-super-app/api/auth/db/catalog.ts | 4 +- .../shell-super-app/api/auth/db/client.ts | 88 +- .../api/auth/db/connection-error.ts | 2 +- .../shell-super-app/api/auth/db/schema.ts | 91 +- app/apps/shell-super-app/api/auth/db/types.ts | 1 + .../api/auth/environment-file-provider.ts | 21 +- app/apps/shell-super-app/api/auth/errors.ts | 66 +- .../api/auth/gateway-issuer-config.ts | 59 +- .../api/auth/gateway-issuer.ts | 308 +-- .../api/auth/identity-lifecycle.ts | 369 +-- .../api/auth/impersonation-service.ts | 617 +++-- .../api/auth/legal-entity-selection.ts | 50 +- .../api/auth/runtime-infrastructure.ts | 6 +- app/apps/shell-super-app/api/auth/service.ts | 421 ++-- .../api/auth/stage-demo-bootstrap-contract.ts | 68 +- ...e-demo-bootstrap-runtime-infrastructure.ts | 269 ++- .../api/auth/support-auth-provider-service.ts | 5 +- .../support-impersonation-correlation-id.ts | 2 +- .../support-impersonation-store-service.ts | 27 +- app/apps/shell-super-app/api/index.ts | 1983 ++++++++++------- .../api/modules/deployment-allowlist.ts | 119 +- .../api/modules/installed-module-catalog.ts | 177 +- .../api/modules/installed-outbox-matcher.ts | 33 +- .../api/modules/shell-composition.ts | 395 ++-- .../modules/shell-governed-read-schemas.ts | 6 +- .../api/modules/shell-governed-reads.ts | 156 +- .../api/modules/shell-resources.ts | 583 +++-- .../api/verticals/installed-verticals.ts | 25 +- app/apps/shell-super-app/modern.config.ts | 171 +- .../module-deployment-allowlist.config.ts | 45 +- .../module-federation.config.ts | 147 +- app/apps/shell-super-app/playwright.config.ts | 16 +- app/apps/shell-super-app/rstest.config.ts | 45 +- .../scripts/bootstrap-stage-demo.mts | 23 +- .../scripts/verify-auth-db-schema.mts | 29 +- app/apps/shell-super-app/shared/api.ts | 519 +++-- .../shared/ultramodern-build.ts | 6 +- .../shell-super-app/src/api/auth-client.ts | 252 ++- .../src/api/vertical-clients.ts | 30 +- .../shell-super-app/src/modern.runtime.ts | 10 +- .../src/routes/[lang]/contacts/page.tsx | 1 + .../src/routes/[lang]/login/page.tsx | 58 +- .../[lang]/modules/[moduleId]/page.data.ts | 73 +- .../routes/[lang]/modules/[moduleId]/page.tsx | 38 +- .../src/routes/[lang]/page.data.ts | 110 +- .../src/routes/[lang]/page.tsx | 37 +- .../[resourceType]/[resourceId]/page.data.ts | 51 +- .../[resourceType]/[resourceId]/page.tsx | 56 +- .../src/routes/[lang]/search/page.data.ts | 48 +- .../src/routes/[lang]/search/page.tsx | 21 +- .../shell-super-app/src/routes/layout.tsx | 1 + .../src/routes/module-entrypoint-loader.ts | 94 +- .../shell-authentication-client-options.ts | 8 +- .../src/routes/shell-content-layout.tsx | 1 + .../src/routes/shell-frame.tsx | 111 +- .../src/routes/ultramodern-route-head.tsx | 48 +- .../src/routes/use-shell-controls.ts | 91 +- .../src/runtime/browser-effect-runtime.ts | 4 +- .../shell-super-app/tests/e2e/auth-fixture.ts | 64 +- .../shell-super-app/tests/e2e/login.spec.ts | 340 ++- .../tests/integration/auth-runtime.test.ts | 948 +++++--- .../integration/generated-owner-fixture.ts | 150 +- .../generated-owner-isolation.test.ts | 836 ++++--- .../identity-modes-runtime.test.ts | 372 ++-- .../module-catalog-runtime.test.ts | 125 +- .../module-federation-i18n-runtime.test.ts | 31 +- .../integration/stage-demo-bootstrap.test.ts | 30 +- .../tests/support/action-runtime-double.ts | 30 +- .../tests/support/context-access-double.ts | 10 +- .../tests/support/identity-service-doubles.ts | 23 +- .../support/impersonation-service-doubles.ts | 15 +- .../tests/unit/api-index.test.ts | 1 + .../tests/unit/auth-boundary.test.ts | 25 +- .../tests/unit/auth-config.test.ts | 36 +- .../tests/unit/auth-contract.test.ts | 191 +- .../tests/unit/auth-db-client.test.ts | 5 +- .../tests/unit/auth-schema.test.ts | 38 +- .../tests/unit/browser-effect-runtime.test.ts | 11 +- .../tests/unit/configuration-provider.test.ts | 61 +- .../tests/unit/deployment-allowlist.test.ts | 87 +- .../tests/unit/gateway-issuer.test.ts | 181 +- .../tests/unit/identity-lifecycle.test.ts | 129 +- .../tests/unit/impersonation-service.test.ts | 238 +- .../unit/installed-module-catalog.test.ts | 165 +- .../unit/installed-outbox-matcher.test.ts | 18 +- .../tests/unit/installed-verticals.test.ts | 30 +- .../tests/unit/layout.test.tsx | 200 +- .../tests/unit/legal-entity-selection.test.ts | 98 +- .../unit/module-entrypoint-loader.test.ts | 224 +- .../tests/unit/routes/home/loader.test.ts | 89 +- .../tests/unit/routes/login/locales.test.ts | 13 +- .../tests/unit/routes/modules/loader.test.ts | 59 +- .../tests/unit/routes/modules/page.test.tsx | 131 +- .../tests/unit/shell-composition.test.ts | 179 +- .../unit/shell-governed-read-schemas.test.ts | 19 +- .../tests/unit/shell-resources.test.ts | 234 +- .../tests/unit/stage-demo-bootstrap.test.ts | 94 +- app/docs/architecture/ACTIONS.md | 129 +- .../architecture/COMMERCE_APPLICATIONS.md | 28 +- app/docs/architecture/DATABASE.md | 138 +- .../architecture/DATABASE_TRUST_BOUNDARIES.md | 117 +- app/docs/architecture/DATA_ACCESS.md | 103 +- app/docs/architecture/DEPLOYMENT.md | 273 +-- app/docs/architecture/DRIZZLE_V1_UPGRADE.md | 209 +- .../EFFECT_V4_LINT_ENFORCEMENT.md | 256 +-- app/docs/architecture/ERRORS.md | 98 +- app/docs/architecture/MICROVERTICALS.md | 115 +- app/docs/architecture/MODULE_ENTRYPOINTS.md | 164 +- app/docs/architecture/MODULE_MANIFESTS.md | 171 +- app/docs/architecture/OUTBOX_WORKERS.md | 54 +- app/docs/architecture/PARTY_REGISTRY.md | 293 +-- app/docs/architecture/VALUE_OBJECTS.md | 28 +- app/docs/frontend/FRONTEND.md | 11 +- app/docs/integrations/ares.md | 66 +- app/docs/quality-audit.md | 8 +- app/oxlint.config.ts | 66 +- app/packages/core-runtime/MIGRATIONS.md | 20 +- .../core-runtime/scripts/verify-db-schema.mts | 152 +- .../core-runtime/src/actions/collector.ts | 254 ++- .../core-runtime/src/actions/context.ts | 38 +- .../core-runtime/src/actions/definition.ts | 229 +- .../core-runtime/src/actions/error-schema.ts | 7 +- .../core-runtime/src/actions/errors.ts | 215 +- .../core-runtime/src/actions/events.ts | 46 +- .../core-runtime/src/actions/policy.ts | 59 +- .../src/actions/principal-context.ts | 48 +- .../core-runtime/src/actions/repository.ts | 795 ++++--- .../core-runtime/src/actions/runtime.ts | 1326 ++++++----- .../src/actions/string-schemas.ts | 16 +- .../src/actions/transaction-error.ts | 25 +- ...-assertion-redemption-unavailable-error.ts | 2 +- .../src/auth/gateway-assertion-redemption.ts | 7 +- .../auth/gateway-assertion-replay-error.ts | 2 +- .../auth/identity-lifecycle-conflict-error.ts | 2 +- .../identity-persistence-unavailable-error.ts | 5 +- .../src/auth/identity-target-invalid-error.ts | 2 +- .../legal-entity-context-ambiguous-error.ts | 2 +- .../legal-entity-context-inactive-error.ts | 2 +- .../legal-entity-context-invalid-error.ts | 2 +- .../legal-entity-context-missing-error.ts | 2 +- .../legal-entity-context-unavailable-error.ts | 2 +- .../src/auth/legal-entity-context.ts | 100 +- .../auth/principal-administration-reads.ts | 141 +- .../auth/principal-binding-ambiguous-error.ts | 2 +- .../auth/principal-binding-inactive-error.ts | 2 +- .../auth/principal-binding-missing-error.ts | 2 +- .../src/auth/principal-inactive-error.ts | 2 +- .../src/auth/principal-management-errors.ts | 5 +- .../src/auth/principal-management.ts | 321 ++- .../principal-resolver-unavailable-error.ts | 2 +- .../src/auth/principal-resolver.ts | 263 ++- ...recovery-principal-context-denied-error.ts | 5 +- ...ery-principal-context-unavailable-error.ts | 5 +- .../support-recovery-principal-context.ts | 66 +- .../system-principal-context-denied-error.ts | 5 +- .../system-principal-context-invalid-error.ts | 5 +- .../system-principal-context-provenance.ts | 111 +- ...tem-principal-context-unavailable-error.ts | 5 +- .../src/auth/system-principal-context.ts | 51 +- ...tem-workload-registration-invalid-error.ts | 2 +- .../src/auth/tenant-inactive-error.ts | 2 +- .../entrypoint-classification.ts | 27 +- .../src/authorization/rollout-decision.ts | 15 +- .../src/database/driver-failure.ts | 104 +- .../src/database/postgres-failure.ts | 31 +- app/packages/core-runtime/src/db/catalog.ts | 22 +- app/packages/core-runtime/src/db/client.ts | 81 +- .../core-runtime/src/db/config-error.ts | 2 +- app/packages/core-runtime/src/db/config.ts | 206 +- .../core-runtime/src/db/connection-error.ts | 2 +- .../core-runtime/src/db/pool-configuration.ts | 97 +- app/packages/core-runtime/src/db/schema.ts | 385 +++- .../core-runtime/src/db/scoped-transaction.ts | 84 +- app/packages/core-runtime/src/db/types.ts | 5 +- .../src/environment/dotenv-provider.ts | 71 +- .../src/environment/drizzle-config.ts | 15 +- .../src/environment/workspace-environment.ts | 26 +- .../core-runtime/src/http/governed-read.ts | 63 +- .../src/http/http-instrumentation-seam.ts | 72 +- .../src/http/principal-authentication.ts | 48 +- app/packages/core-runtime/src/index.ts | 53 +- .../action-authorization-provisioning.ts | 209 +- .../src/install/context-bootstrap-shared.ts | 31 +- .../src/install/spicedb-database-config.ts | 32 +- .../src/install/stage-context-bootstrap.ts | 386 ++-- .../actions/bind-managed-api-key.action.ts | 81 +- .../actions/bind-self-api-key.action.ts | 77 +- .../src/modules/actions/catalog.ts | 26 +- .../actions/change-principal-status.action.ts | 40 +- .../change-tenant-module-state.action.ts | 47 +- .../create-non-human-principal.action.ts | 38 +- .../record-support-impersonation.action.ts | 44 +- ...t-managed-api-key-binding-status.action.ts | 26 +- .../set-self-api-key-binding-status.action.ts | 28 +- .../src/modules/application-composition.ts | 439 ++-- .../core-runtime/src/modules/catalog.ts | 171 +- .../core-runtime/src/modules/manifest.ts | 214 +- .../src/modules/module-entrypoint-gateway.ts | 55 +- .../src/modules/module-entrypoint.ts | 115 +- .../module-state-check-unavailable-error.ts | 5 +- .../src/modules/module-state-denied-error.ts | 2 +- .../src/modules/module-state-gate-errors.ts | 4 +- .../src/modules/module-state-gate.ts | 205 +- .../src/modules/module-state-snapshot.ts | 6 +- .../src/modules/runtime-registration.ts | 106 +- .../src/modules/shell-contribution.ts | 142 +- ...nt-module-state-concurrent-change-error.ts | 5 +- ...ule-state-persistence-unavailable-error.ts | 5 +- ...ant-module-state-read-unavailable-error.ts | 5 +- .../modules/tenant-module-state-service.ts | 213 +- ...enant-module-state-tenant-missing-error.ts | 5 +- .../tenant-module-state-unchanged-error.ts | 5 +- ...enant-module-state-unknown-module-error.ts | 5 +- ...e-state-unsupported-change-source-error.ts | 5 +- ...nt-module-state-unsupported-state-error.ts | 5 +- ...dule-state-validation-unavailable-error.ts | 5 +- .../core-runtime/src/operations/context.ts | 226 +- .../operation-authentication-required.ts | 5 +- .../operations/operation-context-denied.ts | 2 +- .../operations/operation-context-invalid.ts | 2 +- .../operation-context-unavailable.ts | 5 +- .../src/operations/repository-context.ts | 7 +- .../core-runtime/src/outbox/definition.ts | 165 +- .../core-runtime/src/outbox/errors.ts | 4 +- .../core-runtime/src/outbox/health.ts | 78 +- .../src/outbox/outbox-claim-lost-error.ts | 2 +- .../outbox/outbox-handler-execution-error.ts | 5 +- .../src/outbox/outbox-payload-decode-error.ts | 2 +- .../src/outbox/outbox-poller-config-error.ts | 5 +- .../outbox/outbox-worker-descriptor-error.ts | 5 +- .../core-runtime/src/outbox/poller.ts | 131 +- .../core-runtime/src/outbox/process.ts | 72 +- .../core-runtime/src/outbox/repository.ts | 252 ++- .../core-runtime/src/outbox/runtime.ts | 368 +-- .../src/outbox/worker-entrypoint.ts | 25 +- .../core-runtime/src/permissions/client.ts | 57 +- .../src/permissions/config-error.ts | 2 +- .../core-runtime/src/permissions/config.ts | 197 +- .../src/permissions/context-access.ts | 140 +- .../core-runtime/src/permissions/service.ts | 96 +- .../core-runtime/src/reads/context.ts | 54 +- .../core-runtime/src/reads/definition.ts | 222 +- app/packages/core-runtime/src/reads/errors.ts | 2 +- .../reads/read-evidence-persistence-error.ts | 5 +- .../reads/read-evidence-validation-error.ts | 2 +- .../src/reads/read-handler-execution-error.ts | 5 +- .../src/reads/read-handler-not-found.ts | 2 +- .../src/reads/read-handler-unavailable.ts | 2 +- .../src/reads/read-input-validation-error.ts | 2 +- .../src/reads/read-permission-denied.ts | 2 +- .../src/reads/read-permission-unavailable.ts | 2 +- .../src/reads/read-policy-denied.ts | 15 +- .../src/reads/read-policy-evaluation-error.ts | 5 +- .../src/reads/read-result-validation-error.ts | 2 +- .../core-runtime/src/reads/repository.ts | 24 +- .../core-runtime/src/reads/runtime.ts | 479 ++-- .../core-runtime/src/search/ingestion.ts | 54 +- .../core-runtime/src/search/persistence.ts | 603 +++-- .../src/search/projection-store.ts | 22 +- .../core-runtime/src/search/projection.ts | 440 ++-- .../core-runtime/src/search/query-runtime.ts | 7 +- .../src/search/worker-snapshot.ts | 340 +-- .../core-runtime/src/testing/actions.ts | 230 +- .../src/testing/live-operations.ts | 853 ++++--- .../tests/fixtures/operational-scope.ts | 16 +- .../fixtures/outbox-worker-process.fixture.ts | 12 +- .../integration/action-permission.test.ts | 349 +-- .../tests/integration/action-runtime.test.ts | 721 +++--- .../contacts-identity-migration.test.ts | 352 +-- .../tests/integration/context-access.test.ts | 239 +- .../integration/identity-runtime.test.ts | 415 ++-- .../integration/legal-entity-context.test.ts | 27 +- .../integration/module-state-gate.test.ts | 125 +- .../tests/integration/outbox-runtime.test.ts | 349 ++- .../tests/integration/pool-deadlines.test.ts | 43 +- .../integration/principal-management.test.ts | 96 +- .../integration/principal-resolver.test.ts | 56 +- .../tests/integration/read-runtime.test.ts | 90 +- .../integration/search-persistence.test.ts | 163 +- .../search-worker-snapshot.test.ts | 179 +- .../integration/tenant-isolation.test.ts | 293 ++- .../integration/tenant-module-state.test.ts | 316 ++- .../tests/support/action-runtime-options.ts | 5 +- .../tests/support/database-faults.ts | 105 +- .../core-runtime/tests/support/database.ts | 35 +- .../tests/support/effect-runtime.ts | 20 +- .../tests/support/fixture-cleanup.ts | 5 +- .../tests/support/installed-catalog.ts | 19 +- .../support/open-module-entrypoint-gateway.ts | 22 +- .../tests/support/open-module-state-gate.ts | 14 +- .../tests/support/sql-connection.ts | 8 +- .../unit/action-authorization-rollout.test.ts | 20 +- .../tests/unit/action-collector.test.ts | 95 +- .../tests/unit/action-definition.test.ts | 175 +- .../tests/unit/action-errors.test.ts | 4 +- .../tests/unit/action-http-runner.test.ts | 54 +- .../tests/unit/action-identity.test.ts | 84 +- .../tests/unit/action-permission.test.ts | 137 +- .../tests/unit/action-policy.test.ts | 31 +- .../tests/unit/action-public-surface.test.ts | 12 +- .../tests/unit/action-runtime.test.ts | 756 ++++--- .../tests/unit/action-testing-harness.test.ts | 82 +- .../unit/application-composition.test.ts | 178 +- .../tests/unit/catalog-contract.test.ts | 39 +- .../unit/commit-recovery-metadata.test.ts | 90 +- .../core-runtime/tests/unit/config.test.ts | 57 +- .../tests/unit/context-access.test.ts | 156 +- .../unit/database-driver-failure.test.ts | 76 +- .../unit/entrypoint-classification.test.ts | 13 +- .../tests/unit/fixture-cleanup.test.ts | 20 +- .../tests/unit/governed-read-http.test.ts | 159 +- .../http-principal-authentication.test.ts | 84 +- .../tests/unit/legal-entity-context.test.ts | 101 +- .../tests/unit/module-catalog.test.ts | 99 +- .../tests/unit/module-manifest.test.ts | 152 +- .../tests/unit/module-state-gate.test.ts | 216 +- .../unit/native-transaction-context.test.ts | 54 +- .../tests/unit/native-transaction.test.ts | 75 +- .../tests/unit/operation-context.test.ts | 112 +- .../tests/unit/outbox-definition.test.ts | 34 +- .../tests/unit/outbox-errors.test.ts | 145 +- .../tests/unit/outbox-health.test.ts | 52 +- .../tests/unit/outbox-poller.test.ts | 50 +- .../tests/unit/outbox-process.test.ts | 30 +- .../tests/unit/outbox-runtime.test.ts | 160 +- .../tests/unit/pool-configuration.test.ts | 41 +- .../tests/unit/principal-management.test.ts | 107 +- .../tests/unit/principal-resolver.test.ts | 188 +- .../tests/unit/read-definition.test.ts | 57 +- .../tests/unit/read-runtime.test.ts | 264 ++- .../tests/unit/schema-contract.test.ts | 48 +- .../tests/unit/scoped-transaction.test.ts | 26 +- .../tests/unit/search-ingestion.test.ts | 177 +- .../tests/unit/search-projection.test.ts | 455 ++-- .../tests/unit/search-schema.test.ts | 49 +- .../tests/unit/search-worker-snapshot.test.ts | 291 ++- .../tests/unit/service-public-surface.test.ts | 3 +- .../tests/unit/shell-contribution.test.ts | 65 +- .../tests/unit/spicedb-client.test.ts | 13 +- .../unit/spicedb-database-bootstrap.test.ts | 27 +- .../unit/stage-context-bootstrap.test.ts | 1 + .../unit/system-principal-context.test.ts | 102 +- .../tests/unit/tenant-module-state.test.ts | 99 +- .../gateway-principal-verifier/src/server.ts | 365 +-- .../unit/gateway-principal-verifier.test.ts | 95 +- .../shared-contracts/src/client-runtime.ts | 58 +- .../src/effect-bff-runtime.ts | 24 +- .../src/effect-bff-runtime.type-test.ts | 50 +- app/packages/shared-contracts/src/index.ts | 196 +- .../src/microvertical-api-baseline.ts | 23 +- .../shared-contracts/src/operation-gateway.ts | 29 +- .../src/operation-gateway.type-test.ts | 38 +- .../shared-contracts/src/problem-details.ts | 118 +- .../shared-contracts/src/ultramodern-build.ts | 2 +- .../tests/unit/client-runtime.test.ts | 75 +- .../tests/unit/effect-bff-runtime.test.ts | 44 +- .../tests/unit/gateway-context.test.ts | 91 +- .../tests/unit/governed-runtime.test.ts | 86 +- .../unit/microvertical-api-baseline.test.ts | 42 +- .../tests/unit/operation-gateway.test.ts | 80 +- .../tests/unit/problem-details.test.ts | 122 +- .../tests/unit/ultramodern-build.test.ts | 7 +- .../shared-contracts/tooling/modern-config.ts | 41 +- app/quality-audit/knip-model.mts | 827 ++++--- app/quality-audit/knip-reporter.mts | 8 +- app/quality-audit/knip-runtime-model.mts | 466 ++-- app/scripts/assert-mf-types.mts | 8 +- .../protected-entrypoint-inventory.mts | 103 +- .../authorization/rollout-contract.mts | 75 +- app/scripts/bootstrap-agent-skills.mts | 25 +- app/scripts/boundary-source-structure.mts | 12 +- app/scripts/check-authorization-readiness.mts | 290 ++- .../check-database-access-boundaries.mts | 188 +- .../check-module-entrypoint-boundaries.mts | 598 +++-- app/scripts/check-ontos-module-contracts.mts | 220 +- .../check-ultramodern-api-boundaries.mts | 433 ++-- app/scripts/database-trust-audit/report.mts | 173 +- .../generate-node-backend-federation.mts | 13 +- .../generate-ontos-module-contract.mts | 486 ++-- .../generate-public-surface-assets.mts | 13 +- .../generated-governed-http-boundary.mts | 905 +++++--- app/scripts/generated-module-api-boundary.mts | 782 +++++-- app/scripts/initialize-local-development.mts | 861 ++++--- app/scripts/local-environment-values.mts | 24 +- app/scripts/materialize-outbox-worker.mjs | 132 +- app/scripts/materialize-zerops-runtime.mjs | 382 +++- .../microvertical-api-baseline-boundary.mts | 286 ++- .../migrate-contacts-authorization.mts | 268 ++- app/scripts/migrate-strict-effect.mts | 8 +- .../module-federation-bridge-boundary.mts | 50 +- app/scripts/prepare-dev-module-contract.mts | 52 +- app/scripts/proof-cloudflare-version.mts | 8 +- app/scripts/proof-node-backend-federation.mts | 43 +- app/scripts/proof-workerd-ssr.mts | 792 ++++--- ...provision-current-action-authorization.mts | 271 ++- app/scripts/published-outbox-contracts.mts | 161 +- app/scripts/quality-audit-gate.mts | 119 +- app/scripts/quality-audit.mts | 1117 ++++++---- app/scripts/quality-cli-lifecycle.mts | 13 +- .../scaffolding/action-service/scaffold.mts | 43 +- app/scripts/scaffolding/action/scaffold.mts | 286 +-- app/scripts/scaffolding/cli.mts | 330 ++- .../external-http-adapter/scaffold.mts | 62 +- .../governed-contribution/scaffold.mts | 977 ++++---- .../scaffold.mts | 103 +- .../microvertical-page/scaffold.mts | 651 ++++-- .../scaffolding/module-contract/scaffold.mts | 233 +- .../scaffolding/outbox-message/scaffold.mts | 130 +- .../scaffolding/outbox-worker/scaffold.mts | 280 ++- app/scripts/scaffolding/policy/scaffold.mts | 118 +- app/scripts/scaffolding/resource/scaffold.mts | 177 +- .../retire-contribution/scaffold.mts | 574 +++-- .../search-provider-access/scaffold.mts | 161 +- app/scripts/scaffolding/shared.mts | 894 +++++--- app/scripts/scaffolding/tailwind-prefix.mts | 16 +- .../scaffolding/tests/fixture-files.mts | 15 +- .../tests/resource-generator.test.mts | 172 +- .../tests/retire-contribution.test.mts | 124 +- app/scripts/shared/core-node-services.mts | 11 +- app/scripts/shared/ultramodern-command.mts | 25 +- app/scripts/shared/ultramodern-launch.mts | 2 +- .../shared/ultramodern-wrapper-source.mts | 19 +- .../tests/boundary-source-structure.test.mts | 59 +- app/scripts/tests/code-tools-i18n.test.mts | 67 +- .../tests/dependency-declarations.test.mts | 71 +- .../tests/generated-slot-entries.test.mts | 9 +- .../initialize-local-development.test.mts | 158 +- .../module-entrypoint-boundaries.test.mts | 1022 ++++++--- .../tests/plan-deployment-impact.test.mts | 177 +- ...sion-current-action-authorization.test.mts | 345 ++- .../tests/quality-audit-count-domain.test.mts | 14 +- app/scripts/tests/quality-audit-gate.test.mts | 84 +- .../tests/quality-audit-model.test.mts | 317 ++- .../quality-audit-runtime-model.test.mts | 177 +- app/scripts/tests/quality-audit.test.mts | 409 ++-- .../tests/quality-cli-lifecycle.test.mts | 71 +- app/scripts/tests/root-environment.test.mts | 15 +- .../server-environment-paths.fixture.mts | 11 +- .../typescript-api-contract-boundary.mts | 936 ++++++-- .../ultramodern-api-boundary-rules.mts | 557 +++-- app/scripts/ultramodern-command-failure.mts | 7 +- .../ultramodern-performance-readiness.mts | 8 +- app/scripts/ultramodern-typecheck.mts | 8 +- app/scripts/verify-cloudflare-output.mts | 11 +- ...ore-add-external-http-adapter-generator.md | 189 +- .../chore-ci-current-system-contracts.md | 328 +-- ...crm-customer-business-fields-regression.md | 59 +- ...ore-extend-microvertical-page-generator.md | 71 +- ...hore-postgres-drizzle-schema-foundation.md | 515 ++--- ...re-rename-crm-microvertical-to-contacts.md | 137 +- ...ore-support-dynamic-microvertical-pages.md | 90 +- app/specs/feature-action-permissions.md | 682 ++---- app/specs/feature-action-policies.md | 4 +- app/specs/feature-add-generators.md | 4 +- .../feature-authenticated-dashboard-layout.md | 91 +- app/specs/feature-betterauth-tenant-login.md | 159 +- app/specs/feature-complete-identity-modes.md | 754 ++----- ...lete-protected-entrypoint-authorization.md | 4 +- ...ture-complete-shell-runtime-composition.md | 4 +- ...e-coresdk-tenant-legal-entity-isolation.md | 131 +- app/specs/feature-crm-ares-adapter.md | 32 +- app/specs/feature-crm-ares-lookup-bff.md | 89 +- app/specs/feature-crm-contact-create-page.md | 102 +- app/specs/feature-crm-contact-detail-page.md | 77 +- app/specs/feature-crm-contact-edit-page.md | 102 +- .../feature-crm-customer-action-fields.md | 88 +- app/specs/feature-crm-customer-ares-loader.md | 31 +- .../feature-crm-customer-business-fields.md | 74 +- .../feature-crm-customer-contact-actions.md | 155 +- ...eature-crm-customer-contact-persistence.md | 57 +- .../feature-crm-customer-contacts-table.md | 4 +- .../feature-crm-customer-contracts-reads.md | 27 +- ...eature-crm-customer-create-ares-prefill.md | 32 +- app/specs/feature-crm-customer-create-page.md | 86 +- ...ure-crm-customer-detail-business-fields.md | 78 +- app/specs/feature-crm-customer-detail-page.md | 80 +- ...ature-crm-customer-edit-business-fields.md | 31 +- app/specs/feature-crm-customer-edit-page.md | 133 +- ...ature-crm-customer-form-business-fields.md | 33 +- app/specs/feature-crm-customers-list-page.md | 88 +- ...e-deployment-safe-ontos-module-manifest.md | 211 +- ...eature-fail-closed-action-authorization.md | 107 +- ...e-generic-microvertical-action-identity.md | 134 +- app/specs/feature-login-page.md | 419 +--- .../feature-module-aware-shell-composition.md | 4 +- .../feature-shell-core-action-runtime.md | 151 +- ...feature-tenant-microvertical-state-list.md | 176 +- app/specs/feature-tenant-switcher.md | 574 ++--- .../feature-universal-module-state-gate.md | 215 +- app/tools/oxlint/effect-native/README.md | 104 +- app/tools/oxlint/effect-native/index.ts | 12 +- .../oxlint/effect-native/report.config.ts | 4 +- app/tools/oxlint/effect-native/report.mts | 32 +- .../rules/no-ad-hoc-argv-in-scripts.ts | 105 +- .../effect-native/rules/no-ambient-date.ts | 245 +- .../rules/no-ambient-process-env.ts | 124 +- .../rules/no-async-script-program.ts | 106 +- .../effect-native/rules/no-bare-effect-run.ts | 184 +- .../rules/no-console-in-scripts.ts | 59 +- .../rules/no-dependency-parameters.ts | 182 +- .../rules/no-direct-node-io-in-scripts.ts | 109 +- .../effect-native/rules/no-dotenv-loading.ts | 196 +- .../rules/no-driver-failure-inspection.ts | 276 ++- .../rules/no-duplicate-literal-vocabulary.ts | 130 +- .../rules/no-effect-provide-in-library.ts | 169 +- .../rules/no-effect-run-in-scripts.ts | 293 ++- .../rules/no-effect-run-in-tests.ts | 159 +- .../rules/no-environment-record-type.ts | 111 +- .../no-failure-discarding-error-callback.ts | 160 +- .../no-hand-built-http-server-in-tests.ts | 148 +- .../rules/no-hand-built-problem-details.ts | 178 +- .../rules/no-hand-parsed-environment-value.ts | 291 ++- .../rules/no-hand-rolled-tagged-union.ts | 114 +- .../rules/no-imperative-loop-in-effect-gen.ts | 88 +- .../rules/no-interface-first-codec.ts | 195 +- .../no-json-schema-as-document-contract.ts | 105 +- .../effect-native/rules/no-layer-fresh.ts | 85 +- .../rules/no-layer-or-die-outside-root.ts | 195 +- .../rules/no-layer-provide-in-library.ts | 132 +- .../rules/no-literal-union-type-alias.ts | 128 +- .../rules/no-local-defect-seam.ts | 51 +- .../no-manual-config-in-scaffold-templates.ts | 42 +- .../rules/no-manual-cookie-serialization.ts | 169 +- ...al-error-handling-in-scaffold-templates.ts | 43 +- .../rules/no-manual-identity-annotations.ts | 146 +- .../rules/no-manual-route-param-parsing.ts | 182 +- .../rules/no-manual-tag-comparison.ts | 263 ++- .../rules/no-native-error-construction.ts | 100 +- .../rules/no-native-json-parse.ts | 45 +- .../rules/no-native-json-stringify.ts | 75 +- .../effect-native/rules/no-native-timers.ts | 158 +- .../rules/no-nested-effect-run.ts | 152 +- .../rules/no-nullable-schema-field.ts | 302 ++- .../rules/no-nullable-service-outcome.ts | 134 +- .../rules/no-per-operation-http-api-client.ts | 282 ++- .../rules/no-per-request-key-material.ts | 270 ++- .../no-process-exit-outside-script-entry.ts | 98 +- .../no-promise-first-scaffold-templates.ts | 112 +- .../rules/no-promise-shaped-port.ts | 233 +- .../rules/no-raw-effect-adt-tag-check.ts | 144 +- .../rules/no-refinement-outside-schema.ts | 309 ++- .../rules/no-route-local-error-classifier.ts | 204 +- .../no-runtime-construction-outside-root.ts | 158 +- .../rules/no-scattered-browser-effect-run.ts | 176 +- .../rules/no-sequential-independent-yields.ts | 165 +- .../rules/no-string-timestamp-schema.ts | 328 ++- .../rules/no-structural-document-walking.ts | 207 +- .../no-symbol-slotted-operation-record.ts | 204 +- .../rules/no-sync-schema-codec.ts | 62 +- .../no-threaded-correlation-parameter.ts | 190 +- .../rules/no-throw-in-configuration-parser.ts | 381 +++- .../rules/no-throw-in-effect-callback.ts | 127 +- .../rules/no-throw-in-scripts.ts | 50 +- .../rules/no-unbranded-identifier-schema.ts | 255 ++- ...-unjustified-file-wide-lint-suppression.ts | 81 +- .../rules/no-unmanaged-mutable-state.ts | 195 +- .../rules/no-unredacted-secret-field.ts | 181 +- .../rules/no-wide-factory-signature.ts | 155 +- .../rules/prefer-effect-fn-for-operations.ts | 224 +- .../rules/prefer-match-over-tag-switch.ts | 113 +- .../rules/require-concurrency-option.ts | 137 +- ...e-context-service-for-service-interface.ts | 242 +- ...re-observability-layers-at-runtime-root.ts | 169 +- .../require-timeout-on-external-effect.ts | 215 +- app/tools/oxlint/effect-native/shared/ast.ts | 95 +- .../oxlint/effect-native/shared/bindings.ts | 28 +- .../effect-native/shared/discover-rules.ts | 12 +- .../effect-native/shared/effect-identity.ts | 90 +- .../effect-native/shared/effect-imports.ts | 7 +- .../oxlint/effect-native/shared/imports.ts | 87 +- .../effect-native/shared/json-globals.ts | 12 +- .../effect-native/shared/json-rule-scope.ts | 5 +- .../oxlint/effect-native/shared/options.ts | 52 +- .../oxlint/effect-native/shared/paths.ts | 47 +- .../oxlint/effect-native/shared/provenance.ts | 92 +- .../shared/reference-positions.ts | 40 +- .../oxlint/effect-native/shared/reporting.ts | 23 +- .../effect-native/shared/rule-file-policy.ts | 5 +- .../effect-native/shared/scaffold-text.ts | 19 +- .../shared/schema-constructor.ts | 18 +- .../effect-native/shared/schema-identity.ts | 68 +- .../shared/schema-rule-support.ts | 7 +- .../effect-native/shared/script-entry.ts | 39 +- .../effect-native/shared/source-rule-scope.ts | 5 +- .../tests/discover-rules.test.mts | 29 +- .../effect-native/tests/fixtures.test.mts | 44 +- .../tests/json-rule-scope.test.mts | 66 +- .../effect-native/tests/launcher.test.mts | 20 +- .../oxlint/effect-native/tests/oxlint.mts | 65 +- .../effect-native/tests/oxlint.test.mts | 25 +- .../tests/production-fixture.config.ts | 6 +- .../tests/production-options.test.mts | 36 +- .../effect-native/tests/registration.test.mts | 46 +- .../tests/rule-file-policy.test.mts | 36 +- .../effect-native/tests/run-on-repo.mts | 20 +- .../tests/scaffold-text-unicode.test.mts | 2 +- .../tests/scaffold-unicode.test.mts | 29 +- .../effect-native/tests/script-scope.test.mts | 41 +- .../tests/shared-helpers.test.mts | 148 +- .../tests/source-rule-scope.test.mts | 55 +- .../tests/temporary-workspace.mts | 4 +- .../tests/temporary-workspace.test.mts | 19 +- .../party-registry/api/action-http-runner.ts | 9 +- .../api/ares-lookup-read-server.ts | 5 +- .../api/auth/action-principal.ts | 8 +- .../api/counterparties-search-server.ts | 5 +- .../api/counterparty-read-read-server.ts | 5 +- .../counterparty-role-history-read-server.ts | 5 +- .../duplicate-candidate-detail-read-server.ts | 5 +- .../api/engagement-profile-problems.ts | 76 +- .../api/engagement-profile-server.ts | 70 +- .../api/fail-authenticated-problem.ts | 17 +- app/verticals/party-registry/api/index.ts | 148 +- ...nization-engagement-profile-read-server.ts | 5 +- .../api/parties-search-server.ts | 5 +- .../api/party-command-problems.ts | 113 +- .../api/party-command-registrations.ts | 7 +- .../api/party-command-server.ts | 347 +-- .../party-contact-point-detail-read-server.ts | 5 +- .../api/party-contact-points-read-server.ts | 5 +- .../api/party-correction-read-server.ts | 5 +- .../api/party-detail-read-server.ts | 5 +- .../api/party-match-decision-read-server.ts | 5 +- .../api/party-match-read-server.ts | 5 +- .../api/party-merge-readiness-read-server.ts | 5 +- ...-official-identifier-detail-read-server.ts | 5 +- ...official-identifier-history-read-server.ts | 5 +- .../api/party-registry-foundation.ts | 10 +- .../api/party-registry-production-layers.ts | 29 +- .../party-relationship-detail-read-server.ts | 5 +- .../person-engagement-profile-read-server.ts | 5 +- .../party-registry/api/read-server-support.ts | 19 +- .../backend-federation.config.ts | 53 +- app/verticals/party-registry/modern.config.ts | 164 +- .../module-federation.config.ts | 123 +- app/verticals/party-registry/rstest.config.ts | 1 + .../party-registry/scripts/outbox-worker.ts | 11 +- .../scripts/prepare-contacts-migration.mts | 112 +- .../scripts/verify-db-schema.mts | 79 +- .../scripts/verify-engagement-db-schema.mts | 65 +- .../shared/actions/add-contact-point.ts | 1 + .../actions/add-party-official-identifier.ts | 9 +- .../shared/actions/archive-party.ts | 5 +- .../actions/confirm-duplicate-parties.ts | 6 +- .../shared/actions/counterparty-create.ts | 4 +- .../shared/actions/counterparty-role-add.ts | 14 +- .../shared/actions/counterparty-role-end.ts | 6 +- .../shared/actions/create-party.ts | 9 +- .../shared/actions/end-contact-point.ts | 6 +- .../actions/end-party-official-identifier.ts | 7 +- .../shared/actions/match-party.ts | 1 + .../shared/actions/request-search-rebuild.ts | 4 +- .../resolve-duplicate-candidate-create.ts | 3 +- .../resolve-duplicate-candidate-match.ts | 1 + .../shared/actions/unarchive-party.ts | 8 +- .../shared/actions/update-contact-point.ts | 18 +- .../update-party-official-identifier.ts | 8 +- .../shared/actions/update-party.ts | 10 +- app/verticals/party-registry/shared/api.ts | 27 +- .../party-registry/shared/apis/ares-lookup.ts | 40 +- .../shared/apis/counterparties-search.ts | 76 +- .../shared/apis/counterparty-read.ts | 42 +- .../shared/apis/counterparty-role-history.ts | 72 +- .../shared/apis/duplicate-candidate-detail.ts | 81 +- .../apis/organization-engagement-profile.ts | 72 +- .../shared/apis/parties-search.ts | 48 +- .../shared/apis/party-contact-point-detail.ts | 72 +- .../shared/apis/party-contact-points.ts | 57 +- .../shared/apis/party-correction.ts | 39 +- .../shared/apis/party-detail.ts | 44 +- .../shared/apis/party-match-decision.ts | 59 +- .../party-registry/shared/apis/party-match.ts | 31 +- .../shared/apis/party-merge-readiness.ts | 76 +- .../apis/party-official-identifier-detail.ts | 72 +- .../apis/party-official-identifier-history.ts | 111 +- .../shared/apis/party-relationship-detail.ts | 75 +- .../shared/apis/person-engagement-profile.ts | 75 +- .../party-registry/shared/command-api.ts | 439 ++-- .../shared/domain/ares-application.ts | 202 +- .../shared/domain/ares-evidence.ts | 56 +- .../shared/domain/canonical-utc-timestamp.ts | 10 +- .../domain/claim-owned-by-different-party.ts | 2 +- .../contact-point-errors/already-exists.ts | 3 +- .../correction-required.ts | 3 +- .../domain/contact-point-errors/invalid.ts | 3 +- .../lifecycle-conflict.ts | 3 +- .../domain/contact-point-errors/not-found.ts | 3 +- .../contact-point-errors/party-not-found.ts | 3 +- .../persistence-unavailable.ts | 3 +- .../contact-point-errors/revision-conflict.ts | 8 +- .../domain/contact-point-errors/shared.ts | 2 +- .../shared/domain/contact-point.ts | 125 +- .../shared/domain/correction-contracts.ts | 115 +- .../shared/domain/counterparty-contract.ts | 52 +- .../evidence-insufficient.ts | 2 +- .../domain/counterparty-errors/not-found.ts | 3 +- .../counterparty-errors/party-archived.ts | 3 +- .../counterparty-errors/party-not-found.ts | 3 +- .../persistence-unavailable.ts | 2 +- .../counterparty-errors/role-already-ended.ts | 3 +- .../counterparty-errors/role-overlap.ts | 2 +- .../role-period-not-found.ts | 3 +- .../counterparty-errors/scope-mismatch.ts | 2 +- .../counterparty-errors/temporal-conflict.ts | 2 +- .../shared/domain/counterparty-role-period.ts | 33 +- .../domain/duplicate-candidate-conflict.ts | 2 +- .../engagement-profile-errors/conflict.ts | 2 +- .../engagement-profile-errors/not-found.ts | 8 +- .../party-registry-reference-unavailable.ts | 2 +- .../persistence-unavailable.ts | 2 +- .../shared/domain/engagement-profile.ts | 25 +- .../shared/domain/identifier-contracts.ts | 42 +- .../identifier-errors/claim-conflict.ts | 2 +- .../domain/identifier-errors/invalid.ts | 2 +- .../shared/domain/identity-contracts.ts | 76 +- .../shared/domain/matching-contracts.ts | 135 +- .../broken-chain.ts | 3 +- .../cross-tenant.ts | 3 +- .../merge-alias-resolution-errors/cycle.ts | 3 +- .../merge-alias-resolution-errors/shared.ts | 8 +- .../unavailable.ts | 2 +- .../write-rejected.ts | 3 +- .../shared/domain/merge-readiness.ts | 15 +- .../shared/domain/merge-selection.ts | 55 +- .../party-create-recovery-unavailable.ts | 2 +- .../shared/domain/relationship-contract.ts | 67 +- .../correction-required.ts | 10 +- .../relationship-errors/endpoint-not-found.ts | 3 +- .../endpoint-type-mismatch.ts | 3 +- .../domain/relationship-errors/index.ts | 1 + .../relationship-errors/invalid-interval.ts | 3 +- .../domain/relationship-errors/not-found.ts | 3 +- .../relationship-errors/overlap-conflict.ts | 3 +- .../persistence-unavailable.ts | 3 +- .../relationship-errors/revision-conflict.ts | 8 +- .../domain/relationship-errors/shared.ts | 2 +- .../relationship-errors/type-unsupported.ts | 3 +- .../shared/domain/relationship-temporal.ts | 80 +- .../shared/domain/search-projection-error.ts | 2 +- .../domain/search-projection-gateway.ts | 28 +- .../shared/domain/search-result.ts | 25 +- .../shared/domain/search-semantics.ts | 117 +- .../shared/engagement-profile-api.ts | 153 +- .../party-registry-contact-point-added-v1.ts | 1 + .../party-registry-contact-point-ended-v1.ts | 1 + ...party-registry-contact-point-updated-v1.ts | 1 + .../party-registry-counterparty-created-v1.ts | 1 + ...rty-registry-counterparty-role-added-v1.ts | 7 +- ...rty-registry-counterparty-role-ended-v1.ts | 3 +- ...y-registry-official-identifier-added-v1.ts | 4 +- ...y-registry-official-identifier-ended-v1.ts | 4 +- ...registry-official-identifier-updated-v1.ts | 4 +- .../party-registry-party-archived-v1.ts | 1 + .../outbox/party-registry-party-created-v1.ts | 1 + .../party-registry-party-fact-corrected-v1.ts | 1 + .../party-registry-party-unarchived-v1.ts | 1 + .../outbox/party-registry-party-updated-v1.ts | 1 + ...ty-registry-search-rebuild-requested-v1.ts | 5 +- .../shared/party-registry-references.ts | 5 +- .../resources/counterparty-role-period.ts | 4 +- .../shared/resources/counterparty.ts | 4 +- .../resources/duplicate-candidate-case.ts | 3 +- .../organization-engagement-profile.ts | 8 +- .../shared/resources/party-alias.ts | 14 +- .../shared/resources/party-contact-point.ts | 1 + .../shared/resources/party-merge.ts | 50 +- .../resources/party-official-identifier.ts | 3 +- .../shared/resources/party-relationship.ts | 4 +- .../party-registry/shared/resources/party.ts | 1 + .../resources/person-engagement-profile.ts | 4 +- .../resources/resource-ref-identifiers.ts | 12 +- .../shared/resources/timeline-resource.ts | 13 +- .../shared/ultramodern-build.ts | 2 +- .../src/actions/add-contact-point.action.ts | 144 +- ...y-contact-point-added-v1.outbox-message.ts | 13 +- .../add-party-official-identifier.action.ts | 165 +- ...cial-identifier-added-v1.outbox-message.ts | 13 +- .../archive-organization-engagement.action.ts | 24 +- .../src/actions/archive-party.action.ts | 75 +- ...gistry-party-archived-v1.outbox-message.ts | 8 +- .../archive-person-engagement.action.ts | 22 +- .../src/actions/attach-engagement-handler.ts | 33 +- .../attach-organization-engagement.action.ts | 26 +- .../attach-person-engagement.action.ts | 18 +- .../attached-official-identifier-events.ts | 9 +- .../confirm-duplicate-parties.action.ts | 15 +- .../src/actions/correct-party-fact.action.ts | 100 +- ...-party-fact-corrected-v1.outbox-message.ts | 17 +- .../src/actions/counterparty-create.action.ts | 214 +- ...-counterparty-created-v1.outbox-message.ts | 17 +- .../counterparty-role-action-support.ts | 5 +- .../actions/counterparty-role-add.action.ts | 208 +- ...unterparty-role-added-v1.outbox-message.ts | 17 +- .../actions/counterparty-role-end.action.ts | 247 +- ...unterparty-role-ended-v1.outbox-message.ts | 17 +- .../create-party-relationship.action.ts | 34 +- ...-relationship-created-v1.outbox-message.ts | 18 +- .../src/actions/create-party.action.ts | 112 +- ...egistry-party-created-v1.outbox-message.ts | 8 +- .../dismiss-duplicate-candidate.action.ts | 15 +- .../duplicate-case-resolution-handler.ts | 10 +- .../duplicate-case-resolution-service.ts | 5 +- .../src/actions/end-contact-point.action.ts | 136 +- ...y-contact-point-ended-v1.outbox-message.ts | 13 +- .../end-party-official-identifier.action.ts | 120 +- ...cial-identifier-ended-v1.outbox-message.ts | 13 +- .../actions/end-party-relationship.action.ts | 38 +- ...ry-relationship-ended-v1.outbox-message.ts | 17 +- .../actions/engagement-lifecycle-handler.ts | 30 +- .../engagement-lifecycle-registration.ts | 17 +- .../src/actions/engagement-lifecycle.ts | 18 +- ...plicate-candidate-needs-evidence.action.ts | 18 +- .../src/actions/match-party.action.ts | 40 +- .../actions/party-lifecycle-action-helpers.ts | 27 +- .../src/actions/relationship-event-payload.ts | 5 +- .../actions/request-search-rebuild.action.ts | 26 +- ...rch-rebuild-requested-v1.outbox-message.ts | 13 +- ...solve-duplicate-candidate-create.action.ts | 107 +- ...esolve-duplicate-candidate-match.action.ts | 87 +- ...narchive-organization-engagement.action.ts | 24 +- .../src/actions/unarchive-party.action.ts | 170 +- ...stry-party-unarchived-v1.outbox-message.ts | 8 +- .../unarchive-person-engagement.action.ts | 22 +- .../actions/update-contact-point.action.ts | 239 +- ...contact-point-updated-v1.outbox-message.ts | 13 +- ...update-party-official-identifier.action.ts | 230 +- .../update-party-relationship.action.ts | 40 +- ...-relationship-updated-v1.outbox-message.ts | 13 +- .../src/actions/update-party.action.ts | 83 +- ...egistry-party-updated-v1.outbox-message.ts | 8 +- .../party-registry/src/api/action-gateway.ts | 454 ++-- .../src/api/ares-lookup-client.ts | 22 +- .../src/api/ares-lookup.read.ts | 52 +- .../src/api/counterparties-search-client.ts | 26 +- .../src/api/counterparty-read-client.ts | 33 +- .../src/api/counterparty-read-support.ts | 19 +- .../src/api/counterparty-read.read.ts | 32 +- .../api/counterparty-role-history-client.ts | 35 +- .../src/api/counterparty-role-history.read.ts | 25 +- .../api/duplicate-candidate-detail-client.ts | 35 +- .../api/duplicate-candidate-detail.read.ts | 30 +- .../src/api/engagement-profile-client.ts | 55 +- .../organization-engagement-profile-client.ts | 29 +- .../organization-engagement-profile.read.ts | 35 +- .../src/api/parties-search-client.ts | 20 +- .../src/api/party-command-client.ts | 436 ++-- .../api/party-contact-point-detail-client.ts | 35 +- .../api/party-contact-point-detail.read.ts | 28 +- .../src/api/party-contact-points-client.ts | 33 +- .../src/api/party-contact-points.read.ts | 16 +- .../src/api/party-correction-client.ts | 33 +- .../src/api/party-correction.read.ts | 19 +- .../src/api/party-detail-client.ts | 33 +- .../src/api/party-detail.read.ts | 149 +- .../src/api/party-match-client.ts | 22 +- .../src/api/party-match-decision-client.ts | 33 +- .../src/api/party-match-decision.read.ts | 43 +- .../src/api/party-match.read.ts | 13 +- .../src/api/party-merge-readiness-client.ts | 33 +- .../src/api/party-merge-readiness.read.ts | 3 +- ...party-official-identifier-detail-client.ts | 29 +- .../party-official-identifier-detail.read.ts | 27 +- ...arty-official-identifier-history-client.ts | 24 +- .../party-official-identifier-history.read.ts | 20 +- .../src/api/party-registry-client.ts | 18 +- .../src/api/party-registry-http-client.ts | 46 +- .../api/party-relationship-detail-client.ts | 35 +- .../src/api/party-relationship-detail.read.ts | 57 +- .../api/person-engagement-profile-client.ts | 35 +- .../src/api/person-engagement-profile.read.ts | 29 +- .../party-registry/src/api/read-outcome.ts | 9 +- .../gateway-assertion-redemption-runtime.ts | 44 +- .../party-registry/src/db/catalog.ts | 2 +- app/verticals/party-registry/src/db/client.ts | 81 +- .../src/db/compare-table-catalog.ts | 2 +- .../party-registry/src/db/connection-error.ts | 2 +- .../src/db/engagement-catalog.ts | 12 +- .../src/db/engagement-schema.ts | 61 +- .../party-registry/src/db/engagement-types.ts | 5 +- app/verticals/party-registry/src/db/schema.ts | 526 +++-- app/verticals/party-registry/src/db/types.ts | 5 +- .../src/federation/page-contacts.tsx | 1 + .../party-registry/src/i18n/resources.ts | 13 +- .../integrations/ares/ares-subject.service.ts | 237 +- .../src/merge/canonical-survivor-selection.ts | 107 +- .../src/merge/merge-collision-analysis.ts | 59 +- .../src/merge/merge-readiness.ts | 109 +- .../merge/party-alias-resolution.service.ts | 108 +- .../src/merge/party-alias-resolution.ts | 60 +- .../src/merge/reference-preservation-plan.ts | 48 +- .../party-registry/src/modern.runtime.ts | 1 + ...-party-without-strong-identifier.policy.ts | 22 +- .../src/routes/[lang]/contacts/page.tsx | 1 + .../src/routes/ultramodern-route-metadata.ts | 3 +- .../src/search-normalization.ts | 11 +- .../src/search/counterparties.provider.ts | 42 +- .../src/search/parties.provider.ts | 167 +- .../counterparty-persistence.service.ts | 381 ++-- .../engagement-profile-persistence.service.ts | 103 +- ...engagement-reference-validation.service.ts | 96 +- ...party-contact-point-persistence.service.ts | 1380 +++++++----- .../src/services/party-correction.service.ts | 1387 ++++++------ .../party-detail-persistence.service.ts | 29 +- .../party-identifier-claim.service.ts | 145 +- .../party-identity-persistence.service.ts | 576 +++-- .../party-matching-persistence.service.ts | 1650 ++++++++------ ...official-identifier-persistence.service.ts | 205 +- .../party-relationship-persistence.service.ts | 331 +-- .../party-search-projection-source.service.ts | 199 +- .../party-search-projection.service.ts | 201 +- .../src/workers/party-search-worker.ts | 20 +- ...ct-contact-point-added-to-search.worker.ts | 41 +- ...ct-contact-point-ended-to-search.worker.ts | 41 +- ...-contact-point-updated-to-search.worker.ts | 40 +- ...t-counterparty-created-to-search.worker.ts | 42 +- ...ounterparty-role-added-to-search.worker.ts | 43 +- ...ounterparty-role-ended-to-search.worker.ts | 43 +- ...icial-identifier-added-to-search.worker.ts | 8 +- ...icial-identifier-ended-to-search.worker.ts | 41 +- ...ial-identifier-updated-to-search.worker.ts | 8 +- ...project-party-archived-to-search.worker.ts | 41 +- .../project-party-created-to-search.worker.ts | 41 +- ...t-party-fact-corrected-to-search.worker.ts | 41 +- ...oject-party-unarchived-to-search.worker.ts | 41 +- .../project-party-updated-to-search.worker.ts | 41 +- .../src/workers/rebuild-search.worker.ts | 38 +- .../tests/components/contacts-page.test.tsx | 32 +- .../tests/integration/ares-governed.test.ts | 413 ++-- .../integration/database-boundary.test.ts | 339 ++- .../engagement-database-boundary.test.ts | 72 +- .../integration/governed-identity.test.ts | 432 ++-- .../integration/identity-concurrency.test.ts | 111 +- .../tests/support/command-assertion-fetch.ts | 4 +- .../tests/support/database-boundary.ts | 13 +- .../api-integration-ares-application.test.ts | 358 +-- .../unit/api-integration-client-url.test.ts | 31 +- .../api-integration-command-client.test.ts | 40 +- .../api-integration-command-contract.test.ts | 38 +- .../api-integration-command-recovery.test.ts | 181 +- .../api-integration-command-runtime.test.ts | 976 +++++--- .../unit/api-integration-contract.test.ts | 72 +- .../api-integration-correction-client.test.ts | 69 +- .../unit/api-integration-runtime.test.ts | 209 +- .../unit/ares-application-policy.test.ts | 172 +- .../tests/unit/ares-evidence-contract.test.ts | 48 +- .../tests/unit/ares-lookup-read.test.ts | 146 +- .../tests/unit/ares-subject.service.test.ts | 162 +- .../unit/attach-engagement-handler.test.ts | 8 +- .../unit/audit-evidence-contract.test.ts | 45 +- .../tests/unit/catalog-contract.test.ts | 28 +- .../tests/unit/contact-point-contract.test.ts | 126 +- .../contact-point-correction-action.test.ts | 15 +- .../contact-point-persistence.service.test.ts | 327 ++- .../tests/unit/correction-contract.test.ts | 179 +- .../tests/unit/cors-origin.test.ts | 10 +- .../tests/unit/counterparty-contract.test.ts | 169 +- .../counterparty-persistence.service.test.ts | 114 +- .../unit/counterparty-read-support.test.ts | 38 +- .../unit/counterparty-role-lifecycle.test.ts | 104 +- .../tests/unit/database-client.test.ts | 24 +- .../unit/engagement-catalog-contract.test.ts | 17 +- .../unit/engagement-lifecycle-handler.test.ts | 36 +- .../engagement-lifecycle-registration.test.ts | 1 + .../engagement-profile-api-contract.test.ts | 76 +- ...gement-profile-persistence-service.test.ts | 126 +- .../engagement-reference-validation.test.ts | 16 +- .../unit/engagement-schema-contract.test.ts | 36 +- ...teway-assertion-redemption-runtime.test.ts | 69 +- .../tests/unit/identifier-contract.test.ts | 57 +- .../identifier-persistence.service.test.ts | 166 +- .../unit/identifier-update-outbox.test.ts | 75 +- .../unit/identity-action-evidence.test.ts | 81 +- .../tests/unit/identity-contract.test.ts | 38 +- ...y-create-without-strong-identifier.test.ts | 68 +- .../unit/identity-party-detail-alias.test.ts | 47 +- .../identity-party-detail-history.test.ts | 71 +- .../tests/unit/matching-contract.test.ts | 114 +- .../tests/unit/matching-persistence.test.ts | 363 +-- .../merge-alias-resolution-service.test.ts | 63 +- .../tests/unit/merge-alias-resolution.test.ts | 48 +- .../unit/merge-collision-reference.test.ts | 117 +- .../unit/merge-readiness-contract.test.ts | 112 +- .../unit/merge-survivor-selection.test.ts | 101 +- .../tests/unit/party-search-worker.test.ts | 21 +- .../unit/prepare-contacts-migration.test.ts | 36 +- .../tests/unit/read-outcome.test.ts | 29 +- .../unit/relationship-domain-contract.test.ts | 160 +- .../relationship-operation-contract.test.ts | 103 +- .../relationship-persistence.service.test.ts | 196 +- .../tests/unit/runtime-locales.test.ts | 8 +- .../tests/unit/schema-contract.test.ts | 505 +++-- .../tests/unit/search-contract.test.ts | 88 +- .../tests/unit/search-core-adapter.test.ts | 76 +- .../tests/unit/search-provider.test.ts | 43 +- .../tests/unit/search-rebuild-request.test.ts | 46 +- .../tests/unit/search-semantics.test.ts | 116 +- .../tests/unit/search-source.test.ts | 151 +- .../unit/search-worker-registration.test.ts | 4 +- .../unit/timeline-resource-contract.test.ts | 16 +- .../party-registry/vertical.manifest.ts | 104 +- .../party-registry/vertical.registration.ts | 54 +- 1007 files changed, 67969 insertions(+), 42115 deletions(-) diff --git a/app/AGENTS.md b/app/AGENTS.md index 49dbb7cc0..c5175994e 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -1,23 +1,14 @@ # OntOS application guardrails -Before changing files under `app/`, read [the application coding guide](./README.md). It owns -setup, generator commands, coding conventions, validation, and trigger-based links to focused -architecture. +Before changing files under `app/`, read [the application coding guide](./README.md). It owns setup, generator commands, coding conventions, validation, and trigger-based links to focused architecture. Never read an `.env` file. ## Before editing -1. Read only the specification explicitly named by the task or GitHub issue. A specification with - `status: done`, `status: complete`, or `status: superseded` is historical evidence; stop unless - the task explicitly requests provenance. -2. Use the routing table in `README.md`. Open only documents whose concern matches the changed - files or behavior, plus matching product contexts when semantics are relevant. Do not browse - `app/specs/`, `app/docs/`, or root `docs/` for general background. -3. Start every supported business artifact with its Codesmith generator. If the category has no - approved generator or governed gateway, stop and get that boundary approved. -4. Never import another deployment's private source, registration, data access, or executable - behavior. If the task appears to require that, stop and resolve the MicroVertical contract. +1. Read only the specification explicitly named by the task or GitHub issue. A specification with `status: done`, `status: complete`, or `status: superseded` is historical evidence; stop unless the task explicitly requests provenance. +2. Use the routing table in `README.md`. Open only documents whose concern matches the changed files or behavior, plus matching product contexts when semantics are relevant. Do not browse `app/specs/`, `app/docs/`, or root `docs/` for general background. +3. Start every supported business artifact with its Codesmith generator. If the category has no approved generator or governed gateway, stop and get that boundary approved. +4. Never import another deployment's private source, registration, data access, or executable behavior. If the task appears to require that, stop and resolve the MicroVertical contract. -All remaining coding and command rules are owned by `README.md` and the focused documents selected -by its routing table. +All remaining coding and command rules are owned by `README.md` and the focused documents selected by its routing table. diff --git a/app/DEVELOPMENT.md b/app/DEVELOPMENT.md index 69849d224..97f773655 100644 --- a/app/DEVELOPMENT.md +++ b/app/DEVELOPMENT.md @@ -2,28 +2,22 @@ ## Branches -`main` is the canonical development branch and the default base and pull-request target. Do not -start new work from `develop`; it exists only for the one-time transition back to `main` and may be -removed after that transition. +`main` is the canonical development branch and the default base and pull-request target. Do not start new work from `develop`; it exists only for the one-time transition back to `main` and may be removed after that transition. -Promote releases from `main` to the protected `stage` branch. Feature sandboxes start from the -current committed `main` workflow below. +Promote releases from `main` to the protected `stage` branch. Feature sandboxes start from the current committed `main` workflow below. ## Repository-managed tooling - `.mise.toml` and `package.json#packageManager` own the local Node and pnpm toolchain. - `package.json#scripts` owns command names and composition. - `.agents/skills-lock.json` owns tracked skill sources; `.codex/skills/` is generated local output. -- Read-only reference repositories are opt-in through - `mise exec -- pnpm agents:refs:install`. +- Read-only reference repositories are opt-in through `mise exec -- pnpm agents:refs:install`. Do not copy versions, current package inventory, or generated skill state into prose. ## Locki -[Locki](https://github.com/JanPokorny/locki) creates isolated development sandboxes backed by Git -worktrees and containers. Each feature gets its own branch, dependencies, services, database, and -AI session without changing the primary checkout. +[Locki](https://github.com/JanPokorny/locki) creates isolated development sandboxes backed by Git worktrees and containers. Each feature gets its own branch, dependencies, services, database, and AI session without changing the primary checkout. Install Locki globally; the current directory does not matter: @@ -37,8 +31,7 @@ Run the one-time setup to select the AI harness and editor: locki setup ``` -Do not copy the entire `~/.codex` directory when prompted; it can contain large Codex worktrees. -Authenticate the selected harness inside Locki when required. +Do not copy the entire `~/.codex` directory when prompted; it can contain large Codex worktrees. Authenticate the selected harness inside Locki when required. ## Feature sandbox workflow @@ -48,10 +41,7 @@ Create and prepare a sandbox from `main` while in the primary `app/` directory: mise exec -- pnpm sandbox:new -- customer-search ``` -Replace `customer-search` with the feature slug. The command creates the branch and worktree, -copies `app/.env`, installs dependencies, starts containers, runs Drizzle migrations, initializes -the local tenant, legal entity, user, and Party Registry MicroVertical, verifies the database, and opens -the configured AI harness. Record the printed sandbox ID. +Replace `customer-search` with the feature slug. The command creates the branch and worktree, copies `app/.env`, installs dependencies, starts containers, runs Drizzle migrations, initializes the local tenant, legal entity, user, and Party Registry MicroVertical, verifies the database, and opens the configured AI harness. Record the printed sandbox ID. Forward application ports from macOS to the sandbox: @@ -71,28 +61,19 @@ mise exec -- pnpm dev `pnpm dev` occupies that terminal until stopped. -Party Registry owns Contacts, counterparties, and engagement profiles in one MicroVertical. Start -the Shell and Party Registry processes before exercising engagement-profile writes; there is no -separate Contacts deployment or cross-MicroVertical validation call. `mise exec -- pnpm -env:local:ensure` materializes the shared local infrastructure values while preserving explicit -values and printing no secrets. +Party Registry owns Contacts, counterparties, and engagement profiles in one MicroVertical. Start the Shell and Party Registry processes before exercising engagement-profile writes; there is no separate Contacts deployment or cross-MicroVertical validation call. `mise exec -- pnpm env:local:ensure` materializes the shared local infrastructure values while preserving explicit values and printing no secrets. ### Fail-closed Action authorization checkpoint -Sandbox preparation creates the fixed development context and Tenant membership but does not -provision Action executor relationships. For authorization changes, keep one sandbox unchanged -and verify this order: +Sandbox preparation creates the fixed development context and Tenant membership but does not provision Action executor relationships. For authorization changes, keep one sandbox unchanged and verify this order: 1. invoke a representative Party Registry engagement mutation as `demo@test.com`; -2. confirm a localized error Toast and `403`, one rejected invocation/audit record, and no - business write or handler effect; +2. confirm a localized error Toast and `403`, one rejected invocation/audit record, and no business write or handler effect; 3. run `mise exec -- pnpm authorization:provision-current-actions` twice to prove idempotence; 4. retry the mutation and confirm normal success without a denial Toast; 5. confirm a Principal outside the fixed development Tenant remains denied. -The provisioning command discovers current Actions and grants executor relations only to the -fixed development Tenant membership set. It accepts no caller-supplied scope and never writes -stage from a development sandbox. +The provisioning command discovers current Actions and grants executor relations only to the fixed development Tenant membership set. It accepts no caller-supplied scope and never writes stage from a development sandbox. When the feature sandbox is no longer needed, stop its running processes and remove it: @@ -100,11 +81,9 @@ When the feature sandbox is no longer needed, stop its running processes and rem locki rm --match 1aixi9oo --branches ``` -Locki refuses removal when uncommitted changes exist. This removes the container, worktree, port -forwards, and sandbox branches. +Locki refuses removal when uncommitted changes exist. This removes the container, worktree, port forwards, and sandbox branches. -Delete the shared Locki VM only when its containers, images, volumes, and caches are no longer -needed: +Delete the shared Locki VM only when its containers, images, volumes, and caches are no longer needed: ```sh locki vm delete diff --git a/app/README.md b/app/README.md index 96f339e59..14a16bc05 100644 --- a/app/README.md +++ b/app/README.md @@ -1,64 +1,47 @@ # OntOS application -> [!IMPORTANT] -> Read [Development](./DEVELOPMENT.md) before feature work. Use an isolated Locki sandbox and -> treat `app/` as the application root. +> [!IMPORTANT] Read [Development](./DEVELOPMENT.md) before feature work. Use an isolated Locki sandbox and treat `app/` as the application root. ## Read by trigger Read this guide, then only the rows that govern the task. -| Concern | Current authority | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| MicroVertical seams, BFFs, and staff identity | [MicroVertical Architecture](docs/architecture/MICROVERTICALS.md) | -| State-changing operations | [Action Execution](docs/architecture/ACTIONS.md) | -| Typed failures and HTTP responses | [Effect Error and HTTP Contracts](docs/architecture/ERRORS.md) | -| PostgreSQL ownership and governed access | [Database Architecture](docs/architecture/DATABASE.md) and [Governed Data Access](docs/architecture/DATA_ACCESS.md) | -| Drizzle cohort, migration layout, and re-proof steps | [Drizzle v1 Upgrade](docs/architecture/DRIZZLE_V1_UPGRADE.md) | -| Asynchronous consumers | [Outbox Worker Architecture](docs/architecture/OUTBOX_WORKERS.md) | -| Pages, APIs, components, search, reports, and workers | [Module Entrypoints](docs/architecture/MODULE_ENTRYPOINTS.md) | -| Deployment contracts and module identity | [Module Manifests](docs/architecture/MODULE_MANIFESTS.md) | -| Commerce surfaces | [Commerce Application Boundaries](docs/architecture/COMMERCE_APPLICATIONS.md) | -| Shared Party identity and operational boundaries | [Party Registry](docs/architecture/PARTY_REGISTRY.md) | -| Entities versus value objects | [Value Objects](docs/architecture/VALUE_OBJECTS.md) | -| Deployment and release work | [Deployment Playbook](docs/architecture/DEPLOYMENT.md) | -| Frontend work, including Figma | [Frontend Architecture](docs/frontend/FRONTEND.md) | -| ARES provider lookup and Party application | [ARES reference](docs/integrations/ares.md) and [Party Registry](docs/architecture/PARTY_REGISTRY.md) | - -Read a specification only when the task or GitHub issue names it. A specification with -`status: done`, `status: complete`, or `status: superseded` is historical implementation evidence, -not current guidance. Do not browse `specs/` for background. +| Concern | Current authority | +| --- | --- | +| MicroVertical seams, BFFs, and staff identity | [MicroVertical Architecture](docs/architecture/MICROVERTICALS.md) | +| State-changing operations | [Action Execution](docs/architecture/ACTIONS.md) | +| Typed failures and HTTP responses | [Effect Error and HTTP Contracts](docs/architecture/ERRORS.md) | +| PostgreSQL ownership and governed access | [Database Architecture](docs/architecture/DATABASE.md) and [Governed Data Access](docs/architecture/DATA_ACCESS.md) | +| Drizzle cohort, migration layout, and re-proof steps | [Drizzle v1 Upgrade](docs/architecture/DRIZZLE_V1_UPGRADE.md) | +| Asynchronous consumers | [Outbox Worker Architecture](docs/architecture/OUTBOX_WORKERS.md) | +| Pages, APIs, components, search, reports, and workers | [Module Entrypoints](docs/architecture/MODULE_ENTRYPOINTS.md) | +| Deployment contracts and module identity | [Module Manifests](docs/architecture/MODULE_MANIFESTS.md) | +| Commerce surfaces | [Commerce Application Boundaries](docs/architecture/COMMERCE_APPLICATIONS.md) | +| Shared Party identity and operational boundaries | [Party Registry](docs/architecture/PARTY_REGISTRY.md) | +| Entities versus value objects | [Value Objects](docs/architecture/VALUE_OBJECTS.md) | +| Deployment and release work | [Deployment Playbook](docs/architecture/DEPLOYMENT.md) | +| Frontend work, including Figma | [Frontend Architecture](docs/frontend/FRONTEND.md) | +| ARES provider lookup and Party application | [ARES reference](docs/integrations/ares.md) and [Party Registry](docs/architecture/PARTY_REGISTRY.md) | + +Read a specification only when the task or GitHub issue names it. A specification with `status: done`, `status: complete`, or `status: superseded` is historical implementation evidence, not current guidance. Do not browse `specs/` for background. ## Non-negotiable rules -- Use Effect for application behavior, I/O, resource management, concurrency, dependencies, BFF - contracts and clients, schemas, and expected failures. Pure synchronous transformations and - reusable presentation may stay plain TypeScript or React. -- Model expected failures as tagged Effect errors. Do not throw, reject a Promise, return an - untyped error object, or collapse an expected failure into a string. -- Preserve strict independently deployable MicroVertical seams. Never import another deployment's - private manifest, registration, table, handler, repository, route, migration, fixture, or test. -- Frontends call only the owning MicroVertical's generated Effect BFF client. Run an Effect at the - framework edge without erasing its typed failure channel. -- All public writes and governed reads pass through Core-owned operation lifecycles. Tenant/system - entrypoint scope and legal-entity scope are independent; invalid or indeterminate context fails - closed before private code resolves. -- Prefer direct values and typed references over stringly typed metadata. Reuse existing concepts - and files; add an abstraction only for a concrete reuse case. -- Business handlers receive owner-local transaction-scoped services, never a raw database - executor. -- Start supported business artifacts with Codesmith. If a category has no approved generator or - gateway, extend or approve that boundary before creating the artifact. +- Use Effect for application behavior, I/O, resource management, concurrency, dependencies, BFF contracts and clients, schemas, and expected failures. Pure synchronous transformations and reusable presentation may stay plain TypeScript or React. +- Model expected failures as tagged Effect errors. Do not throw, reject a Promise, return an untyped error object, or collapse an expected failure into a string. +- Preserve strict independently deployable MicroVertical seams. Never import another deployment's private manifest, registration, table, handler, repository, route, migration, fixture, or test. +- Frontends call only the owning MicroVertical's generated Effect BFF client. Run an Effect at the framework edge without erasing its typed failure channel. +- All public writes and governed reads pass through Core-owned operation lifecycles. Tenant/system entrypoint scope and legal-entity scope are independent; invalid or indeterminate context fails closed before private code resolves. +- Prefer direct values and typed references over stringly typed metadata. Reuse existing concepts and files; add an abstraction only for a concrete reuse case. +- Business handlers receive owner-local transaction-scoped services, never a raw database executor. +- Start supported business artifacts with Codesmith. If a category has no approved generator or gateway, extend or approve that boundary before creating the artifact. - Infrastructure and architecture files may be created directly when no generator applies. -- Keep third-party HTTP adapters private to their owner. Define provider schemas, typed failures, - request construction, resilience, diagnostics, and business mapping; use the generated Effect - `HttpClient` service as the test seam. +- Keep third-party HTTP adapters private to their owner. Define provider schemas, typed failures, request construction, resilience, diagnostics, and business mapping; use the generated Effect `HttpClient` service as the test seam. - Run pnpm commands from `app/` through `mise exec -- pnpm`. ## Codesmith -`package.json#scripts` is the command source of truth. Inspect supported flags with -`mise exec -- pnpm - )} + {jsonLd === undefined ? null : } )} diff --git a/app/apps/shell-super-app/src/routes/use-shell-controls.ts b/app/apps/shell-super-app/src/routes/use-shell-controls.ts index e1b46cfbe..ad8d416e5 100644 --- a/app/apps/shell-super-app/src/routes/use-shell-controls.ts +++ b/app/apps/shell-super-app/src/routes/use-shell-controls.ts @@ -3,83 +3,55 @@ import { useNavigate } from '@modern-js/plugin-tanstack/runtime'; import { Effect, Match, Schema } from 'effect'; import { useState } from 'react'; -import { - SwitchLegalEntityPayloadSchema, - SwitchTenantPayloadSchema, -} from '../../shared/api.ts'; -import { - signOut, - switchLegalEntity, - switchTenant, -} from '../api/auth-client.ts'; -import type { - SwitchLegalEntityClientError, - SwitchTenantClientError, -} from '../api/auth-client.ts'; +import { SwitchLegalEntityPayloadSchema, SwitchTenantPayloadSchema } from '../../shared/api.ts'; +import { signOut, switchLegalEntity, switchTenant } from '../api/auth-client.ts'; +import type { SwitchLegalEntityClientError, SwitchTenantClientError } from '../api/auth-client.ts'; import { browserRuntime } from '../runtime/browser-effect-runtime.ts'; import type { AuthenticatedHomePageModel } from './[lang]/page.data.ts'; -const SwitchFailureStateSchema = Schema.Literals([ - 'authentication-required', - 'failed', -]); +const SwitchFailureStateSchema = Schema.Literals(['authentication-required', 'failed']); type SwitchFailureState = typeof SwitchFailureStateSchema.Type; -const tenantSwitchFailureState = ( - error: SwitchTenantClientError -): SwitchFailureState => +const tenantSwitchFailureState = (error: SwitchTenantClientError): SwitchFailureState => Match.value(error).pipe( - Match.tag( - 'TenantAuthenticationRequiredProblem', - () => 'authentication-required' as const - ), + Match.tag('TenantAuthenticationRequiredProblem', () => 'authentication-required' as const), Match.tag( 'HttpClientError', 'SchemaError', 'TenantAccessForbiddenProblem', 'TenantCapabilityUnavailableProblem', 'TenantInternalProblem', - () => 'failed' as const + () => 'failed' as const, ), - Match.exhaustive + Match.exhaustive, ); -const legalEntitySwitchFailureState = ( - error: SwitchLegalEntityClientError -): SwitchFailureState => +const legalEntitySwitchFailureState = (error: SwitchLegalEntityClientError): SwitchFailureState => Match.value(error).pipe( - Match.tag( - 'TenantAuthenticationRequiredProblem', - () => 'authentication-required' as const - ), + Match.tag('TenantAuthenticationRequiredProblem', () => 'authentication-required' as const), Match.tag( 'HttpClientError', 'LegalEntityAccessForbiddenProblem', 'SchemaError', 'TenantCapabilityUnavailableProblem', 'TenantInternalProblem', - () => 'failed' as const + () => 'failed' as const, ), - Match.exhaustive + Match.exhaustive, ); -export const useShellControls = ( - model: AuthenticatedHomePageModel | undefined -) => { +export const useShellControls = (model: AuthenticatedHomePageModel | undefined) => { const { language } = useModernI18n(); const navigate = useNavigate(); const [logoutPending, setLogoutPending] = useState(false); const [logoutFailed, setLogoutFailed] = useState(false); const [tenantSwitchPending, setTenantSwitchPending] = useState(false); const [tenantSwitchFailed, setTenantSwitchFailed] = useState(false); - const [legalEntitySwitchPending, setLegalEntitySwitchPending] = - useState(false); + const [legalEntitySwitchPending, setLegalEntitySwitchPending] = useState(false); const [legalEntitySwitchFailed, setLegalEntitySwitchFailed] = useState(false); const reload = () => - Effect.tryPromise(() => navigate({ reloadDocument: true, to: '.' })).pipe( - Effect.timeout('10 seconds') - ); + Effect.tryPromise(() => navigate({ reloadDocument: true, to: '.' })).pipe(Effect.timeout('10 seconds')); const handleLogout = () => { if (logoutPending) { @@ -90,9 +62,9 @@ export const useShellControls = ( void browserRuntime.runPromise( signOut({ locale: language }).pipe( Effect.andThen( - Effect.tryPromise(() => - navigate({ reloadDocument: true, to: `/${language}/login` }) - ).pipe(Effect.timeout('10 seconds')) + Effect.tryPromise(() => navigate({ reloadDocument: true, to: `/${language}/login` })).pipe( + Effect.timeout('10 seconds'), + ), ), Effect.matchEffect({ onFailure: (error) => @@ -102,8 +74,8 @@ export const useShellControls = ( }), onSuccess: Effect.succeed, }), - Effect.ensuring(Effect.sync(() => setLogoutPending(false))) - ) + Effect.ensuring(Effect.sync(() => setLogoutPending(false))), + ), ); }; @@ -111,7 +83,7 @@ export const useShellControls = ( switching: Effect.Effect, switchFailureState: (error: NoInfer) => SwitchFailureState, setPending: (pending: boolean) => void, - setFailed: (failed: boolean) => void + setFailed: (failed: boolean) => void, ) => { setPending(true); setFailed(false); @@ -124,7 +96,7 @@ export const useShellControls = ( Effect.flatMap((outcome) => outcome === 'authentication-required' || outcome === 'switched' ? reload() - : Effect.sync(() => setFailed(true)) + : Effect.sync(() => setFailed(true)), ), Effect.matchEffect({ onFailure: (error) => @@ -134,49 +106,36 @@ export const useShellControls = ( }), onSuccess: Effect.succeed, }), - Effect.ensuring(Effect.sync(() => setPending(false))) - ) + Effect.ensuring(Effect.sync(() => setPending(false))), + ), ); }; const handleLegalEntityChange = (legalEntityId: string) => { - if ( - model === undefined || - legalEntitySwitchPending || - legalEntityId === model.selectedLegalEntityId - ) { + if (model === undefined || legalEntitySwitchPending || legalEntityId === model.selectedLegalEntityId) { return; } runSwitch( Schema.decodeEffect(SwitchLegalEntityPayloadSchema)({ legalEntityId, - }).pipe( - Effect.flatMap((payload) => - switchLegalEntity(payload, { locale: language }) - ) - ), + }).pipe(Effect.flatMap((payload) => switchLegalEntity(payload, { locale: language }))), legalEntitySwitchFailureState, setLegalEntitySwitchPending, - setLegalEntitySwitchFailed + setLegalEntitySwitchFailed, ); }; const handleTenantChange = (tenantId: string) => { - if ( - model === undefined || - tenantSwitchPending || - tenantId.length === 0 || - tenantId === model.identity.tenantId - ) { + if (model === undefined || tenantSwitchPending || tenantId.length === 0 || tenantId === model.identity.tenantId) { return; } runSwitch( Schema.decodeEffect(SwitchTenantPayloadSchema)({ tenantId }).pipe( - Effect.flatMap((payload) => switchTenant(payload, { locale: language })) + Effect.flatMap((payload) => switchTenant(payload, { locale: language })), ), tenantSwitchFailureState, setTenantSwitchPending, - setTenantSwitchFailed + setTenantSwitchFailed, ); }; diff --git a/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts b/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts index aa4c4eea2..3e72701dd 100644 --- a/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts +++ b/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts @@ -8,6 +8,6 @@ export const browserRuntime = ManagedRuntime.make( Layer.mergeAll( Logger.layer([Logger.defaultLogger]), Layer.succeed(Tracer.Tracer, browserTracer), - Layer.succeed(References.MinimumLogLevel, 'Info') - ) + Layer.succeed(References.MinimumLogLevel, 'Info'), + ), ); diff --git a/app/apps/shell-super-app/tests/e2e/auth-fixture.ts b/app/apps/shell-super-app/tests/e2e/auth-fixture.ts index 30f7fde60..7beb383dd 100644 --- a/app/apps/shell-super-app/tests/e2e/auth-fixture.ts +++ b/app/apps/shell-super-app/tests/e2e/auth-fixture.ts @@ -23,10 +23,7 @@ import { } from '../../../../packages/core-runtime/src/permissions/context-access.ts'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { loadAuthConfig } from '../../api/auth/config.ts'; -import { - acquirePoolResource, - makeAuthDatabase, -} from '../../api/auth/db/client.ts'; +import { acquirePoolResource, makeAuthDatabase } from '../../api/auth/db/client.ts'; import { account, session, user } from '../../api/auth/db/schema.ts'; const contactsModuleId = 'party.registry'; @@ -42,7 +39,7 @@ const e2ePoolDeadlines = { class E2eAuthorizationFixtureError extends Schema.TaggedError()( 'E2eAuthorizationFixtureError', - { reason: Schema.String } + { reason: Schema.String }, ) {} interface FixtureTenant { @@ -59,172 +56,121 @@ interface FixtureTenants { // Database module activation does not grant access. Own the complete authorization // chain for these disposable E2E identities instead of relying on bootstrap seeds. -const provisionContactsAccess = Effect.fn('provisionContactsAccess')( - function* provisionContactsAccessEffect(e2eTenants: FixtureTenants) { - const configuration = yield* loadSpiceDbConfig(); - if (!configuration.endpoint.startsWith('localhost:')) { - return yield* Effect.fail( - new E2eAuthorizationFixtureError({ - reason: 'E2E authorization fixtures require localhost SpiceDB', - }) - ); - } - const client = yield* Effect.acquireRelease( - Effect.sync(() => - v1.NewClient( - configuration.preSharedKey, - configuration.endpoint, - configuration.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE, - undefined, - { interceptors: [deadlineInterceptor(5000)] } - ) - ), - (acquired) => Effect.sync(() => acquired.close()) +const provisionContactsAccess = Effect.fn('provisionContactsAccess')(function* provisionContactsAccessEffect( + e2eTenants: FixtureTenants, +) { + const configuration = yield* loadSpiceDbConfig(); + if (!configuration.endpoint.startsWith('localhost:')) { + return yield* Effect.fail( + new E2eAuthorizationFixtureError({ + reason: 'E2E authorization fixtures require localhost SpiceDB', + }), ); - const relationships = yield* Effect.forEach( - Object.values(e2eTenants), - ({ legalEntityId, principalId, tenantId }) => - Effect.gen(function* makeContactsRelationships() { - const entityObject = toLegalEntityAccessObjectId( - tenantId, - legalEntityId - ); - const moduleObject = toModuleAccessObjectId( - tenantId, - legalEntityId, - contactsModuleId - ); - if (entityObject === undefined || moduleObject === undefined) { - return yield* Effect.fail( - new E2eAuthorizationFixtureError({ - reason: 'Invalid E2E authorization object identifier', - }) - ); - } - return ( - [ - ['tenant', tenantId, 'member', 'principal', principalId], - ['legal_entity', entityObject, 'tenant', 'tenant', tenantId], - [ - 'legal_entity', - entityObject, - 'member', - 'principal', - principalId, - ], - [ - 'module_access', - moduleObject, - 'legal_entity', - 'legal_entity', - entityObject, - ], - [ - 'module_access', - moduleObject, - 'accessor', - 'principal', - principalId, - ], - ] as const - ).map( - ([resourceType, resourceId, relation, subjectType, subjectId]) => - v1.Relationship.create({ - relation, - resource: { objectId: resourceId, objectType: resourceType }, - subject: { - object: { objectId: subjectId, objectType: subjectType }, - }, - }) + } + const client = yield* Effect.acquireRelease( + Effect.sync(() => + v1.NewClient( + configuration.preSharedKey, + configuration.endpoint, + configuration.insecureLocal ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED : v1.ClientSecurity.SECURE, + undefined, + { interceptors: [deadlineInterceptor(5000)] }, + ), + ), + (acquired) => Effect.sync(() => acquired.close()), + ); + const relationships = yield* Effect.forEach( + Object.values(e2eTenants), + ({ legalEntityId, principalId, tenantId }) => + Effect.gen(function* makeContactsRelationships() { + const entityObject = toLegalEntityAccessObjectId(tenantId, legalEntityId); + const moduleObject = toModuleAccessObjectId(tenantId, legalEntityId, contactsModuleId); + if (entityObject === undefined || moduleObject === undefined) { + return yield* Effect.fail( + new E2eAuthorizationFixtureError({ + reason: 'Invalid E2E authorization object identifier', + }), ); - }), - { concurrency: 'unbounded' } - ); - const update = (operation: v1.RelationshipUpdate_Operation) => - Effect.tryPromise( - async () => - await client.promises.writeRelationships( - v1.WriteRelationshipsRequest.create({ - updates: relationships - .flat() - .map((relationship) => ({ operation, relationship })), - }) - ) - ); - // Register first so even an indeterminate write acknowledgement is cleaned up. - yield* Effect.addFinalizer(() => - update(v1.RelationshipUpdate_Operation.DELETE).pipe(Effect.orDie) - ); - return yield* update(v1.RelationshipUpdate_Operation.TOUCH).pipe( - Effect.uninterruptible + } + return ( + [ + ['tenant', tenantId, 'member', 'principal', principalId], + ['legal_entity', entityObject, 'tenant', 'tenant', tenantId], + ['legal_entity', entityObject, 'member', 'principal', principalId], + ['module_access', moduleObject, 'legal_entity', 'legal_entity', entityObject], + ['module_access', moduleObject, 'accessor', 'principal', principalId], + ] as const + ).map(([resourceType, resourceId, relation, subjectType, subjectId]) => + v1.Relationship.create({ + relation, + resource: { objectId: resourceId, objectType: resourceType }, + subject: { + object: { objectId: subjectId, objectType: subjectType }, + }, + }), + ); + }), + { concurrency: 'unbounded' }, + ); + const update = (operation: v1.RelationshipUpdate_Operation) => + Effect.tryPromise( + async () => + await client.promises.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: relationships.flat().map((relationship) => ({ operation, relationship })), + }), + ), ); - } -); + // Register first so even an indeterminate write acknowledgement is cleaned up. + yield* Effect.addFinalizer(() => update(v1.RelationshipUpdate_Operation.DELETE).pipe(Effect.orDie)); + return yield* update(v1.RelationshipUpdate_Operation.TOUCH).pipe(Effect.uninterruptible); +}); -export const createAuthenticationFixture = Effect.fn( - 'createAuthenticationFixture' -)(function* createAuthenticationFixtureEffect() { - const crypto = yield* Crypto.Crypto; - const fixtureId = yield* crypto.randomUUIDv4; - const e2eCredentials = { - email: `e2e.${fixtureId}@example.test`, - password: 'e2e-correct-horse-battery-staple', - }; - const makeTenant = (name: string) => - Effect.all({ - legalEntityId: crypto.randomUUIDv4, - name: Effect.succeed(name), - principalId: crypto.randomUUIDv4, - tenantId: crypto.randomUUIDv4, +export const createAuthenticationFixture = Effect.fn('createAuthenticationFixture')( + function* createAuthenticationFixtureEffect() { + const crypto = yield* Crypto.Crypto; + const fixtureId = yield* crypto.randomUUIDv4; + const e2eCredentials = { + email: `e2e.${fixtureId}@example.test`, + password: 'e2e-correct-horse-battery-staple', + }; + const makeTenant = (name: string) => + Effect.all({ + legalEntityId: crypto.randomUUIDv4, + name: Effect.succeed(name), + principalId: crypto.randomUUIDv4, + tenantId: crypto.randomUUIDv4, + }); + const e2eTenants = yield* Effect.all({ + first: makeTenant('E2E Alpha tenant'), + second: makeTenant('E2E Zeta tenant'), }); - const e2eTenants = yield* Effect.all({ - first: makeTenant('E2E Alpha tenant'), - second: makeTenant('E2E Zeta tenant'), - }); - const { - baseUrl: baseURL, - connectionString, - secret, - } = yield* loadAuthConfig({ envPath: APP_ENV_PATH }); + const { baseUrl: baseURL, connectionString, secret } = yield* loadAuthConfig({ envPath: APP_ENV_PATH }); - const corePoolConfiguration = yield* configureDatabasePool( - Redacted.make(connectionString), - e2ePoolDeadlines - ); - const corePool = yield* acquirePoolResource( - () => new Pool(corePoolConfiguration) - ); - const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); - const { adapter, executor: authDatabase } = yield* makeAuthDatabase({ - connectionString, - }); - const authentication = betterAuth({ - baseURL, - database: adapter, - emailAndPassword: { - autoSignIn: false, - enabled: true, - }, - secret, - trustedOrigins: [baseURL, 'http://127.0.0.1:3020'], - }); + const corePoolConfiguration = yield* configureDatabasePool(Redacted.make(connectionString), e2ePoolDeadlines); + const corePool = yield* acquirePoolResource(() => new Pool(corePoolConfiguration)); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const { adapter, executor: authDatabase } = yield* makeAuthDatabase({ + connectionString, + }); + const authentication = betterAuth({ + baseURL, + database: adapter, + emailAndPassword: { + autoSignIn: false, + enabled: true, + }, + secret, + trustedOrigins: [baseURL, 'http://127.0.0.1:3020'], + }); - const cleanup = Effect.fn('cleanupAuthenticationFixture')( - function* cleanupAuthenticationFixtureEffect() { - const tenantIds = Object.values(e2eTenants).map( - ({ tenantId }) => tenantId - ); - const principalIds = Object.values(e2eTenants).map( - ({ principalId }) => principalId - ); + const cleanup = Effect.fn('cleanupAuthenticationFixture')(function* cleanupAuthenticationFixtureEffect() { + const tenantIds = Object.values(e2eTenants).map(({ tenantId }) => tenantId); + const principalIds = Object.values(e2eTenants).map(({ principalId }) => principalId); // Authenticated shell reads write evidence asynchronously. Let those writes // settle, then remove their E2E-owned rows before the referenced identities. yield* Effect.sleep('250 millis'); - yield* coreDatabase - .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.principalId, principalIds)); + yield* coreDatabase.delete(dataAccessEvents).where(inArray(dataAccessEvents.principalId, principalIds)); const existingUsers = yield* authDatabase .select({ id: user.id }) .from(user) @@ -233,158 +179,139 @@ export const createAuthenticationFixture = Effect.fn( yield* Effect.all( existingUsers.map((existingUser) => Effect.gen(function* removeExistingAuthUser() { - yield* authDatabase - .delete(session) - .where(eq(session.userId, existingUser.id)); - yield* authDatabase - .delete(account) - .where(eq(account.userId, existingUser.id)); - yield* authDatabase - .delete(user) - .where(eq(user.id, existingUser.id)); - }) + yield* authDatabase.delete(session).where(eq(session.userId, existingUser.id)); + yield* authDatabase.delete(account).where(eq(account.userId, existingUser.id)); + yield* authDatabase.delete(user).where(eq(user.id, existingUser.id)); + }), ), - { concurrency: 'unbounded', discard: true } + { concurrency: 'unbounded', discard: true }, ); // A page read can finish its asynchronous evidence write while auth rows // are being removed. Clear that final E2E-owned batch before deleting the // binding referenced by the evidence foreign key. - yield* coreDatabase - .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.principalId, principalIds)); + yield* coreDatabase.delete(dataAccessEvents).where(inArray(dataAccessEvents.principalId, principalIds)); yield* Effect.all( existingUsers.map((existingUser) => coreDatabase .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)) + .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), ), - { concurrency: 'unbounded', discard: true } + { concurrency: 'unbounded', discard: true }, ); - yield* coreDatabase - .delete(principalAuthBindings) - .where(inArray(principalAuthBindings.principalId, principalIds)); - yield* coreDatabase - .delete(tenantModuleStates) - .where(inArray(tenantModuleStates.tenantId, tenantIds)); - yield* coreDatabase - .delete(legalEntities) - .where(inArray(legalEntities.tenantId, tenantIds)); - yield* coreDatabase - .delete(principals) - .where(inArray(principals.principalId, principalIds)); - yield* coreDatabase - .delete(tenants) - .where(inArray(tenants.tenantId, tenantIds)); - } - ); + yield* coreDatabase.delete(principalAuthBindings).where(inArray(principalAuthBindings.principalId, principalIds)); + yield* coreDatabase.delete(tenantModuleStates).where(inArray(tenantModuleStates.tenantId, tenantIds)); + yield* coreDatabase.delete(legalEntities).where(inArray(legalEntities.tenantId, tenantIds)); + yield* coreDatabase.delete(principals).where(inArray(principals.principalId, principalIds)); + yield* coreDatabase.delete(tenants).where(inArray(tenants.tenantId, tenantIds)); + }); - yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); - const createdUser = yield* Effect.tryPromise( - async () => - await authentication.api.signUpEmail({ - body: { - email: e2eCredentials.email, - name: 'E2E user', - password: e2eCredentials.password, - }, - }) - ).pipe(Effect.uninterruptible); - yield* coreDatabase.insert(tenants).values([ - { - defaultLocale: 'en', - name: e2eTenants.first.name, - slug: `e2e-alpha-${fixtureId}`, - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - defaultLocale: 'en', - name: e2eTenants.second.name, - slug: `e2e-zeta-${fixtureId}`, - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - yield* coreDatabase.insert(principals).values([ - { - displayName: 'E2E user', - kind: 'human', - principalId: e2eTenants.first.principalId, - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - displayName: 'E2E user second tenant', - kind: 'human', - principalId: e2eTenants.second.principalId, - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - yield* coreDatabase.insert(legalEntities).values([ - { - legalEntityId: e2eTenants.first.legalEntityId, - legalName: 'E2E Alpha company', - registrationCountry: 'CZ', - registrationNumber: 'E2E-ALPHA', - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - legalEntityId: e2eTenants.second.legalEntityId, - legalName: 'E2E Zeta company', - registrationCountry: 'CZ', - registrationNumber: 'E2E-ZETA', - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - yield* coreDatabase.insert(principalAuthBindings).values([ - { - createdAt: new Date('2026-01-01T00:00:00.000Z'), - principalId: e2eTenants.first.principalId, - provider: 'better_auth', - providerSubjectId: createdUser.user.id, - status: 'active', - subjectType: 'user', - tenantId: e2eTenants.first.tenantId, - }, - { - createdAt: new Date('2026-02-01T00:00:00.000Z'), - principalId: e2eTenants.second.principalId, - provider: 'better_auth', - providerSubjectId: createdUser.user.id, - status: 'active', - subjectType: 'user', - tenantId: e2eTenants.second.tenantId, - }, - ]); - yield* coreDatabase.insert(tenantModuleStates).values([ - { - moduleKey: contactsModuleId, - state: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - moduleKey: contactsModuleId, - state: 'active', - tenantId: e2eTenants.second.tenantId, - }, - { - moduleKey: 'e2e-first-module', - state: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - moduleKey: 'e2e-second-module', - state: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - yield* provisionContactsAccess(e2eTenants); - return { credentials: e2eCredentials, tenants: e2eTenants }; -}, Effect.provide(NodeCrypto.layer)); + yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); + const createdUser = yield* Effect.tryPromise( + async () => + await authentication.api.signUpEmail({ + body: { + email: e2eCredentials.email, + name: 'E2E user', + password: e2eCredentials.password, + }, + }), + ).pipe(Effect.uninterruptible); + yield* coreDatabase.insert(tenants).values([ + { + defaultLocale: 'en', + name: e2eTenants.first.name, + slug: `e2e-alpha-${fixtureId}`, + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + defaultLocale: 'en', + name: e2eTenants.second.name, + slug: `e2e-zeta-${fixtureId}`, + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(principals).values([ + { + displayName: 'E2E user', + kind: 'human', + principalId: e2eTenants.first.principalId, + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + displayName: 'E2E user second tenant', + kind: 'human', + principalId: e2eTenants.second.principalId, + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(legalEntities).values([ + { + legalEntityId: e2eTenants.first.legalEntityId, + legalName: 'E2E Alpha company', + registrationCountry: 'CZ', + registrationNumber: 'E2E-ALPHA', + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + legalEntityId: e2eTenants.second.legalEntityId, + legalName: 'E2E Zeta company', + registrationCountry: 'CZ', + registrationNumber: 'E2E-ZETA', + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(principalAuthBindings).values([ + { + createdAt: new Date('2026-01-01T00:00:00.000Z'), + principalId: e2eTenants.first.principalId, + provider: 'better_auth', + providerSubjectId: createdUser.user.id, + status: 'active', + subjectType: 'user', + tenantId: e2eTenants.first.tenantId, + }, + { + createdAt: new Date('2026-02-01T00:00:00.000Z'), + principalId: e2eTenants.second.principalId, + provider: 'better_auth', + providerSubjectId: createdUser.user.id, + status: 'active', + subjectType: 'user', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(tenantModuleStates).values([ + { + moduleKey: contactsModuleId, + state: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + moduleKey: contactsModuleId, + state: 'active', + tenantId: e2eTenants.second.tenantId, + }, + { + moduleKey: 'e2e-first-module', + state: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + moduleKey: 'e2e-second-module', + state: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* provisionContactsAccess(e2eTenants); + return { credentials: e2eCredentials, tenants: e2eTenants }; + }, + Effect.provide(NodeCrypto.layer), +); -export type AuthenticationFixture = Effect.Success< - ReturnType ->; +export type AuthenticationFixture = Effect.Success>; diff --git a/app/apps/shell-super-app/tests/e2e/login.spec.ts b/app/apps/shell-super-app/tests/e2e/login.spec.ts index 70b60229c..4f579a3ee 100644 --- a/app/apps/shell-super-app/tests/e2e/login.spec.ts +++ b/app/apps/shell-super-app/tests/e2e/login.spec.ts @@ -6,28 +6,21 @@ import { shellAuthenticationApiContract } from '../../shared/api.ts'; import { createAuthenticationFixture } from './auth-fixture.ts'; import type { AuthenticationFixture } from './auth-fixture.ts'; -const hydratedLoginForm = (page: Page) => - page.locator('form[data-e2e-hydrated-login="true"]'); +const hydratedLoginForm = (page: Page) => page.locator('form[data-e2e-hydrated-login="true"]'); // SSR already exposes a visible trigger. Zag's menu becomes interactive after hydration // installs React props and moves its matching content portal to document.body. const waitForInteractiveAccountMenu = async (page: Page) => { await page.waitForFunction(() => { - const trigger = document.querySelector( - 'button[data-scope="menu"][data-part="trigger"]' - ); + const trigger = document.querySelector('button[data-scope="menu"][data-part="trigger"]'); if (trigger === null) { return false; } if (!Object.keys(trigger).some((key) => key.startsWith('__reactProps$'))) { return false; } - const positioner = document.querySelector( - '[data-scope="menu"][data-part="positioner"]' - ); - const content = positioner?.querySelector( - '[data-scope="menu"][data-part="content"]' - ); + const positioner = document.querySelector('[data-scope="menu"][data-part="positioner"]'); + const content = positioner?.querySelector('[data-scope="menu"][data-part="content"]'); return ( positioner?.parentElement === document.body && content?.getAttribute('id') === trigger.getAttribute('aria-controls') @@ -38,9 +31,9 @@ const waitForInteractiveAccountMenu = async (page: Page) => { const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { await page.goto(`/${language}/login`); await page.waitForFunction(() => { - const forms = [ - ...document.querySelectorAll('form'), - ].filter((form) => form.querySelector('input[name="login"]') !== null); + const forms = [...document.querySelectorAll('form')].filter( + (form) => form.querySelector('input[name="login"]') !== null, + ); let hydratedForm: HTMLFormElement | undefined; for (const form of forms) { if (Object.keys(form).some((key) => key.startsWith('__reactProps$'))) { @@ -66,11 +59,7 @@ const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { }); }; -const login = async ( - page: Page, - language: 'cs' | 'en', - credentials: AuthenticationFixture['credentials'] -) => { +const login = async (page: Page, language: 'cs' | 'en', credentials: AuthenticationFixture['credentials']) => { await gotoHydratedLogin(page, language); const form = hydratedLoginForm(page); const labels = @@ -87,9 +76,7 @@ const login = async ( submit: 'Přihlásit se', url: /\/cs\/?$/u, }; - await form - .getByRole('textbox', { name: labels.login }) - .fill(credentials.email); + await form.getByRole('textbox', { name: labels.login }).fill(credentials.email); await form.getByLabel(labels.password).fill(credentials.password); await form.getByRole('button', { name: labels.submit }).click(); await expect(page).toHaveURL(labels.url); @@ -103,29 +90,21 @@ const login = async ( // timeouts stay enabled. Foreign operations retain their real server deadlines. const workerFixtureAcquisitionTimeout = '25 seconds'; -const test = base.extend< - Record, - { authentication: AuthenticationFixture } ->({ +const test = base.extend, { authentication: AuthenticationFixture }>({ authentication: [ async ({ browserName: _browserName }, use) => { await Effect.runPromise( Effect.gen(function* useAuthenticationFixture() { - const fixture = yield* Effect.timeout( - createAuthenticationFixture(), - workerFixtureAcquisitionTimeout - ); + const fixture = yield* Effect.timeout(createAuthenticationFixture(), workerFixtureAcquisitionTimeout); yield* Effect.tryPromise(async () => await use(fixture)); - }).pipe(Effect.scoped) + }).pipe(Effect.scoped), ); }, { scope: 'worker', timeout: 0 }, ], }); -test('renders the exact anonymous English and Czech home states', async ({ - page, -}) => { +test('renders the exact anonymous English and Czech home states', async ({ page }) => { const expectAnonymousHome = async (language: string, label: string) => { await page.goto(`/${language}/`); await Promise.all([ @@ -142,9 +121,7 @@ test('renders the exact anonymous English and Czech home states', async ({ await expectAnonymousHome('cs', 'Přihlásit se'); }); -test('keeps English and Czech login pages free of authenticated dashboard chrome', async ({ - page, -}) => { +test('keeps English and Czech login pages free of authenticated dashboard chrome', async ({ page }) => { const expectDashboardAbsent = async () => await Promise.all([ expect(page.locator('header[aria-label]')).toHaveCount(0), @@ -158,45 +135,30 @@ test('keeps English and Czech login pages free of authenticated dashboard chrome await expectDashboardAbsent(); }); -test('shows one generic error for invalid English credentials', async ({ - authentication, - page, -}) => +test('shows one generic error for invalid English credentials', async ({ authentication, page }) => await gotoHydratedLogin(page, 'en') .then( async () => await hydratedLoginForm(page) .getByRole('textbox', { name: /^Login\s*\*$/u }) - .fill(authentication.credentials.email) + .fill(authentication.credentials.email), ) .then( async () => await hydratedLoginForm(page) .getByLabel(/^Password/u) - .fill('wrong-password') - ) - .then( - async () => - await hydratedLoginForm(page) - .getByRole('button', { name: 'Login' }) - .click() + .fill('wrong-password'), ) + .then(async () => await hydratedLoginForm(page).getByRole('button', { name: 'Login' }).click()) .then( async () => await Promise.all([ - expect( - page.getByText('The email address or password is invalid.') - ).toHaveCount(1), - expect( - page.getByRole('textbox', { name: /^Login\s*\*$/u }) - ).toBeFocused(), - ]) + expect(page.getByText('The email address or password is invalid.')).toHaveCount(1), + expect(page.getByRole('textbox', { name: /^Login\s*\*$/u })).toBeFocused(), + ]), )); -test('logs a user in without any server-error response', async ({ - authentication, - page, -}, testInfo) => { +test('logs a user in without any server-error response', async ({ authentication, page }, testInfo) => { const { baseURL } = testInfo.project.use; if (!Predicate.isString(baseURL)) { throw new TypeError('The login E2E test requires a configured base URL'); @@ -207,56 +169,36 @@ test('logs a user in without any server-error response', async ({ page.on('response', (response) => { const responseURL = new URL(response.url()); - if ( - responseURL.origin === applicationOrigin && - response.status() >= 500 && - response.status() < 600 - ) { + if (responseURL.origin === applicationOrigin && response.status() >= 500 && response.status() < 600) { serverErrors.push( - `${response.request().method()} ${responseURL.pathname}${responseURL.search} returned ${response.status()}` + `${response.request().method()} ${responseURL.pathname}${responseURL.search} returned ${response.status()}`, ); } }); await gotoHydratedLogin(page, 'en'); const form = hydratedLoginForm(page); - await form - .getByRole('textbox', { name: /^Login\s*\*$/u }) - .fill(authentication.credentials.email); + await form.getByRole('textbox', { name: /^Login\s*\*$/u }).fill(authentication.credentials.email); await form.getByLabel(/^Password/u).fill(authentication.credentials.password); const signInResponsePromise = page.waitForResponse( (response) => - new URL(response.url()).pathname === - shellAuthenticationApiContract.signInPath && - response.request().method() === 'POST' + new URL(response.url()).pathname === shellAuthenticationApiContract.signInPath && + response.request().method() === 'POST', ); await form.getByRole('button', { name: 'Login' }).click(); const signInResponse = await signInResponsePromise; - expect( - signInResponse.status(), - 'The sign-in endpoint should accept valid credentials' - ).toBe(200); + expect(signInResponse.status(), 'The sign-in endpoint should accept valid credentials').toBe(200); await expect(page).toHaveURL(/\/en\/?$/u); await expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible(); await expect(page.getByText(authentication.credentials.email)).toBeVisible(); - await expect( - page.getByRole('complementary', { name: 'Dashboard sidebar' }) - ).toBeVisible(); - await expect( - page.locator('header[aria-label="Dashboard header"]') - ).toBeVisible(); - expect( - serverErrors, - 'Login and the authenticated page must not return HTTP 5xx' - ).toEqual([]); + await expect(page.getByRole('complementary', { name: 'Dashboard sidebar' })).toBeVisible(); + await expect(page.locator('header[aria-label="Dashboard header"]')).toBeVisible(); + expect(serverErrors, 'Login and the authenticated page must not return HTTP 5xx').toEqual([]); }); -test('loads localized English and Czech Contacts pages only after login', async ({ - authentication, - page, -}) => { +test('loads localized English and Czech Contacts pages only after login', async ({ authentication, page }) => { const pageErrors: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); @@ -266,9 +208,7 @@ test('loads localized English and Czech Contacts pages only after login', async await expect(page.getByRole('heading', { name: 'Contacts' })).toHaveCount(0); await login(page, 'cs', authentication.credentials); - await expect( - page.getByText('Nasazení modulu je dočasně nedostupné.') - ).toHaveCount(0); + await expect(page.getByText('Nasazení modulu je dočasně nedostupné.')).toHaveCount(0); const contactsLink = page.locator('a[href="/cs/contacts"]'); await expect(contactsLink).toHaveAttribute('href', '/cs/contacts'); @@ -283,58 +223,41 @@ test('loads localized English and Czech Contacts pages only after login', async url: RegExp; }) => { await expect(page).toHaveURL(content.url); - await expect( - page.getByRole('heading', { name: content.heading }) - ).toBeVisible(); - await expect( - page.getByRole('heading', { name: content.module }) - ).toHaveCount(0); + await expect(page.getByRole('heading', { name: content.heading })).toBeVisible(); + await expect(page.getByRole('heading', { name: content.module })).toHaveCount(0); await expect(page.getByText(content.description)).toBeVisible(); await expect(page.getByText(content.placeholder)).toHaveCount(0); await expect(page.getByText(content.empty)).toHaveCount(0); }; await expectContacts({ - description: - 'Party Registry uchovává kanonické strany, protistrany a jejich profily zapojení v jednom modulu.', + description: 'Party Registry uchovává kanonické strany, protistrany a jejich profily zapojení v jednom modulu.', empty: 'Zatím zde není žádný obsah.', heading: 'Kontakty', module: 'Modul', placeholder: 'Tato stránka je připravena k implementaci.', url: /\/cs\/contacts\/?$/u, }); - await expect( - page.getByRole('complementary', { name: 'Postranní panel přehledu' }) - ).toBeVisible(); + await expect(page.getByRole('complementary', { name: 'Postranní panel přehledu' })).toBeVisible(); await page.goto('/en/contacts'); await expectContacts({ - description: - 'Party Registry keeps canonical Parties, Counterparties, and their engagement profiles in one module.', + description: 'Party Registry keeps canonical Parties, Counterparties, and their engagement profiles in one module.', empty: 'No content has been added yet.', heading: 'Contacts', module: 'Module', placeholder: 'This page is ready for implementation.', url: /\/en\/contacts\/?$/u, }); - await expect( - page.getByText('The module is temporarily unavailable. Try again.') - ).toHaveCount(0); + await expect(page.getByText('The module is temporarily unavailable. Try again.')).toHaveCount(0); const dashboardSidebar = page.getByRole('complementary', { name: 'Dashboard sidebar', }); await expect(dashboardSidebar).toBeVisible(); - const [sidebarBox, mainBox] = await Promise.all([ - dashboardSidebar.boundingBox(), - page.locator('main').boundingBox(), - ]); + const [sidebarBox, mainBox] = await Promise.all([dashboardSidebar.boundingBox(), page.locator('main').boundingBox()]); expect(sidebarBox?.width).toBe(256); expect(mainBox?.x).toBe(256); expect( - pageErrors.filter((message) => - message.includes( - 'FederatedI18nBoundary must be used within ModernI18nProvider' - ) - ) + pageErrors.filter((message) => message.includes('FederatedI18nBoundary must be used within ModernI18nProvider')), ).toEqual([]); }); @@ -346,22 +269,15 @@ test('keeps authenticated Shell chrome on search and guarded direct-target route const expectPersistentShell = async (path: string, status: string) => { await page.goto(path); - await expect( - page.getByRole('complementary', { name: 'Dashboard sidebar' }) - ).toBeVisible(); - await expect( - page.locator('header[aria-label="Dashboard header"]') - ).toBeVisible(); + await expect(page.getByRole('complementary', { name: 'Dashboard sidebar' })).toBeVisible(); + await expect(page.locator('header[aria-label="Dashboard header"]')).toBeVisible(); await expect(page.getByText(status)).toBeVisible(); }; await expectPersistentShell('/en/search', 'No authorized results found.'); - await expectPersistentShell( - '/en/modules/not-installed', - 'You do not have permission to open this module.' - ); + await expectPersistentShell('/en/modules/not-installed', 'You do not have permission to open this module.'); await expectPersistentShell( '/en/resources/not-installed/example/missing', - 'You do not have permission to view this resource.' + 'You do not have permission to view this resource.', ); }); @@ -374,45 +290,29 @@ test('persists an English session, logs out, clears the cookie, and stays anonym async () => await hydratedLoginForm(page) .getByRole('textbox', { name: /^Login\s*\*$/u }) - .fill(authentication.credentials.email) + .fill(authentication.credentials.email), ) .then( async () => await hydratedLoginForm(page) .getByLabel(/^Password/u) - .fill(authentication.credentials.password) - ) - .then( - async () => - await hydratedLoginForm(page) - .getByRole('button', { name: 'Login' }) - .click() + .fill(authentication.credentials.password), ) + .then(async () => await hydratedLoginForm(page).getByRole('button', { name: 'Login' }).click()) .then(async () => await expect(page).toHaveURL(/\/en\/?$/u)) .then( async () => await Promise.all([ expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible(), - expect( - page.getByText(authentication.credentials.email) - ).toBeVisible(), + expect(page.getByText(authentication.credentials.email)).toBeVisible(), expect(page.getByRole('link', { name: 'Home' })).toHaveCount(1), - ]) + ]), ) .then(async () => await page.reload()) .then(async () => await waitForInteractiveAccountMenu(page)) - .then( - async () => - await expect( - page.getByRole('button', { name: 'E2E user' }) - ).toBeVisible() - ) - .then( - async () => await page.getByRole('button', { name: 'E2E user' }).click() - ) - .then( - async () => await page.getByRole('menuitem', { name: 'Logout' }).click() - ) + .then(async () => await expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible()) + .then(async () => await page.getByRole('button', { name: 'E2E user' }).click()) + .then(async () => await page.getByRole('menuitem', { name: 'Logout' }).click()) .then(async () => await expect(page).toHaveURL(/\/en\/login\/?$/u)) .then( async () => @@ -421,13 +321,10 @@ test('persists an English session, logs out, clears the cookie, and stays anonym expect(page.getByRole('button', { name: 'Login' })).toBeVisible(), expect(page.locator('header[aria-label]')).toHaveCount(0), expect(page.getByRole('complementary')).toHaveCount(0), - ]) + ]), ) .then(async () => await page.reload()) - .then( - async () => - await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible() - )); + .then(async () => await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible())); test('switches tenant by pointer, fully reloads, and persists the selected context', async ({ authentication, @@ -437,46 +334,33 @@ test('switches tenant by pointer, fully reloads, and persists the selected conte const tenant = page.getByRole('combobox', { name: 'Current tenant' }); await expect(tenant).toContainText(authentication.tenants.first.name); - await expect( - page.getByText(authentication.tenants.first.tenantId) - ).toBeVisible(); + await expect(page.getByText(authentication.tenants.first.tenantId)).toBeVisible(); await tenant.click(); const switchResponsePromise = page.waitForResponse( (response) => - new URL(response.url()).pathname === - shellAuthenticationApiContract.switchTenantPath && - response.request().method() === 'POST' + new URL(response.url()).pathname === shellAuthenticationApiContract.switchTenantPath && + response.request().method() === 'POST', ); await Promise.all([ page.waitForEvent('framenavigated', { predicate: (frame) => frame === page.mainFrame(), }), - page - .getByRole('option', { name: authentication.tenants.second.name }) - .click(), + page.getByRole('option', { name: authentication.tenants.second.name }).click(), ]); const switchResponse = await switchResponsePromise; expect(switchResponse.status()).toBe(200); - await expect( - page.getByRole('combobox', { name: 'Current tenant' }) - ).toContainText(authentication.tenants.second.name); - await expect( - page.getByRole('button', { name: 'E2E user second tenant' }) - ).toBeVisible(); - await expect( - page.getByText(authentication.tenants.second.principalId) - ).toBeVisible(); - await expect( - page.getByText(authentication.tenants.second.tenantId) - ).toBeVisible(); + await expect(page.getByRole('combobox', { name: 'Current tenant' })).toContainText( + authentication.tenants.second.name, + ); + await expect(page.getByRole('button', { name: 'E2E user second tenant' })).toBeVisible(); + await expect(page.getByText(authentication.tenants.second.principalId)).toBeVisible(); + await expect(page.getByText(authentication.tenants.second.tenantId)).toBeVisible(); await page.reload(); - await expect( - page.getByRole('combobox', { name: 'Current tenant' }) - ).toContainText(authentication.tenants.second.name); - await expect( - page.getByRole('button', { name: 'E2E user second tenant' }) - ).toBeVisible(); + await expect(page.getByRole('combobox', { name: 'Current tenant' })).toContainText( + authentication.tenants.second.name, + ); + await expect(page.getByRole('button', { name: 'E2E user second tenant' })).toBeVisible(); }); test('retains Czech tenant context after one failed switch and supports keyboard retry', async ({ @@ -485,31 +369,22 @@ test('retains Czech tenant context after one failed switch and supports keyboard }) => { let failSwitch = true; await login(page, 'cs', authentication.credentials); - await page.route( - `**${shellAuthenticationApiContract.switchTenantPath}`, - async (route) => { - if (failSwitch) { - failSwitch = false; - await route.abort('failed'); - return; - } - await route.continue(); + await page.route(`**${shellAuthenticationApiContract.switchTenantPath}`, async (route) => { + if (failSwitch) { + failSwitch = false; + await route.abort('failed'); + return; } - ); + await route.continue(); + }); const tenant = page.getByRole('combobox', { name: 'Aktuální tenant' }); await expect(tenant).toContainText(authentication.tenants.first.name); await tenant.click(); - await page - .getByRole('option', { name: authentication.tenants.second.name }) - .click(); - await expect( - page.getByText('Přepnutí tenantu selhalo. Zkuste to znovu.') - ).toBeVisible(); + await page.getByRole('option', { name: authentication.tenants.second.name }).click(); + await expect(page.getByText('Přepnutí tenantu selhalo. Zkuste to znovu.')).toBeVisible(); await expect(tenant).toContainText(authentication.tenants.first.name); - await expect( - page.getByText(authentication.tenants.first.tenantId) - ).toBeVisible(); + await expect(page.getByText(authentication.tenants.first.tenantId)).toBeVisible(); await tenant.focus(); await page.keyboard.press('Enter'); @@ -521,96 +396,63 @@ test('retains Czech tenant context after one failed switch and supports keyboard await tenantListbox.press('End'); const secondTenantOptionId = await secondTenantOption.getAttribute('id'); expect(secondTenantOptionId).not.toBeNull(); - await expect(tenantListbox).toHaveAttribute( - 'aria-activedescendant', - secondTenantOptionId ?? '' - ); + await expect(tenantListbox).toHaveAttribute('aria-activedescendant', secondTenantOptionId ?? ''); await Promise.all([ page.waitForEvent('framenavigated', { predicate: (frame) => frame === page.mainFrame(), }), tenantListbox.press('Enter'), ]); - await expect( - page.getByRole('combobox', { name: 'Aktuální tenant' }) - ).toContainText(authentication.tenants.second.name); + await expect(page.getByRole('combobox', { name: 'Aktuální tenant' })).toContainText( + authentication.tenants.second.name, + ); }); -test('keeps keyboard logout operable after a Czech failure and succeeds on retry', async ({ - authentication, - page, -}) => { +test('keeps keyboard logout operable after a Czech failure and succeeds on retry', async ({ authentication, page }) => { let failLogout = true; await gotoHydratedLogin(page, 'cs') .then( - async () => - await hydratedLoginForm(page) - .locator('input[name="login"]') - .fill(authentication.credentials.email) + async () => await hydratedLoginForm(page).locator('input[name="login"]').fill(authentication.credentials.email), ) .then( async () => - await hydratedLoginForm(page) - .locator('input[name="password"]') - .fill(authentication.credentials.password) - ) - .then( - async () => - await hydratedLoginForm(page) - .getByRole('button', { name: 'Přihlásit se' }) - .click() + await hydratedLoginForm(page).locator('input[name="password"]').fill(authentication.credentials.password), ) + .then(async () => await hydratedLoginForm(page).getByRole('button', { name: 'Přihlásit se' }).click()) .then(async () => await expect(page).toHaveURL(/\/cs\/?$/u)) .then( async () => - await page.route( - '**/shell-super-app-api/auth/sign-out', - async (route) => { - if (failLogout) { - failLogout = false; - await route.abort('failed'); - return; - } - await route.continue(); + await page.route('**/shell-super-app-api/auth/sign-out', async (route) => { + if (failLogout) { + failLogout = false; + await route.abort('failed'); + return; } - ) - ) - .then( - async () => await page.getByRole('button', { name: 'E2E user' }).focus() + await route.continue(); + }), ) + .then(async () => await page.getByRole('button', { name: 'E2E user' }).focus()) .then(async () => await page.keyboard.press('Enter')) .then( async () => - await expect( - page.getByRole('menuitem', { name: 'Odhlásit se' }) - ).toHaveAttribute('data-highlighted', '') - ) - .then( - async () => - await page.getByRole('menuitem', { name: 'Odhlásit se' }).click() + await expect(page.getByRole('menuitem', { name: 'Odhlásit se' })).toHaveAttribute('data-highlighted', ''), ) + .then(async () => await page.getByRole('menuitem', { name: 'Odhlásit se' }).click()) .then( async () => await Promise.all([ expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible(), - expect( - page.getByText('Odhlášení selhalo. Zkuste to znovu.') - ).toBeVisible(), + expect(page.getByText('Odhlášení selhalo. Zkuste to znovu.')).toBeVisible(), expect(page.getByRole('button', { name: 'E2E user' })).toBeFocused(), - ]) + ]), ) .then(async () => await page.keyboard.press('Enter')) .then( async () => - await expect( - page.getByRole('menuitem', { name: 'Odhlásit se' }) - ).toHaveAttribute('data-highlighted', '') - ) - .then( - async () => - await page.getByRole('menuitem', { name: 'Odhlásit se' }).click() + await expect(page.getByRole('menuitem', { name: 'Odhlásit se' })).toHaveAttribute('data-highlighted', ''), ) + .then(async () => await page.getByRole('menuitem', { name: 'Odhlásit se' }).click()) .then(async () => await expect(page).toHaveURL(/\/cs\/login\/?$/u)); }); @@ -646,19 +488,13 @@ test('keeps the authenticated dashboard reachable without horizontal overflow at await login(page, 'en', authentication.credentials); await page.route( `**${shellAuthenticationApiContract.switchTenantPath}`, - async (route) => await route.abort('failed') + async (route) => await route.abort('failed'), ); - await expect( - page.getByRole('complementary', { name: 'Dashboard sidebar' }) - ).toBeInViewport(); - await expect( - page.locator('header[aria-label="Dashboard header"]') - ).toBeInViewport(); + await expect(page.getByRole('complementary', { name: 'Dashboard sidebar' })).toBeInViewport(); + await expect(page.locator('header[aria-label="Dashboard header"]')).toBeInViewport(); await expect(page.getByRole('button', { name: 'E2E user' })).toBeInViewport(); - await expect( - page.getByRole('region', { name: 'Authenticated identity' }) - ).toBeInViewport(); + await expect(page.getByRole('region', { name: 'Authenticated identity' })).toBeInViewport(); await expect(page.getByRole('link', { name: 'Home' })).toBeInViewport(); const tenant = page.getByRole('combobox', { name: 'Current tenant' }); await expect(tenant).toBeInViewport(); @@ -668,15 +504,9 @@ test('keeps the authenticated dashboard reachable without horizontal overflow at }); await expect(secondTenant).toBeInViewport(); await secondTenant.click(); - await expect( - page.getByText('Tenant switching failed. Try again.') - ).toBeInViewport(); + await expect(page.getByText('Tenant switching failed. Try again.')).toBeInViewport(); await expect(tenant).toContainText(authentication.tenants.first.name); - expect( - await page.evaluate( - () => - document.documentElement.scrollWidth <= - document.documentElement.clientWidth - ) - ).toBe(true); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe( + true, + ); }); diff --git a/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts b/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts index c40e776dc..0d679493b 100644 --- a/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts +++ b/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts @@ -5,10 +5,7 @@ import { Cause, Effect } from 'effect'; // acquisition must finalize before reporting its typed timeout and must never complete. const stalledAcquisitionDeadline = '250 millis'; -const test = base.extend< - Record, - { stalledAcquisition: readonly string[] } ->({ +const test = base.extend, { stalledAcquisition: readonly string[] }>({ stalledAcquisition: [ async ({ browserName: _browserName }, use) => { const events: string[] = []; @@ -17,7 +14,7 @@ const test = base.extend< yield* Effect.addFinalizer(() => Effect.sync(() => { events.push('finalizer'); - }) + }), ); return yield* Effect.never; }); @@ -26,8 +23,8 @@ const test = base.extend< Effect.gen(function* useStalledAcquisition() { yield* Effect.timeout(acquisition, stalledAcquisitionDeadline); events.push('acquired'); - }).pipe(Effect.scoped) - ) + }).pipe(Effect.scoped), + ), ); expect(failure).toBeInstanceOf(Cause.TimeoutError); events.push('reported timeout'); @@ -37,8 +34,6 @@ const test = base.extend< ], }); -test('finishes installed finalizers before reporting a stalled acquisition', ({ - stalledAcquisition, -}) => { +test('finishes installed finalizers before reporting a stalled acquisition', ({ stalledAcquisition }) => { expect(stalledAcquisition).toEqual(['finalizer', 'reported timeout']); }); diff --git a/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts b/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts index 583633e71..30850f4ef 100644 --- a/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts @@ -39,24 +39,14 @@ import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/suppor import { renderActionPrincipalServer } from '../../../../scripts/scaffolding/microvertical-action-boundary/scaffold.mts'; import { AuthConfig, loadAuthConfig } from '../../api/auth/config.ts'; import { AuthDatabase, makeAuthDatabase } from '../../api/auth/db/client.ts'; -import { - account, - authRelations, - session, - user, -} from '../../api/auth/db/schema.ts'; +import { account, authRelations, session, user } from '../../api/auth/db/schema.ts'; import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; import { makeGatewayIssuerLayer } from '../../api/auth/gateway-issuer.ts'; import type { GatewayIssuerLayerOptions } from '../../api/auth/gateway-issuer.ts'; -import { - AuthenticationService, - makeAuthenticationService, -} from '../../api/auth/service.ts'; +import { AuthenticationService, makeAuthenticationService } from '../../api/auth/service.ts'; import { makeShellAuthenticationApiRuntime } from '../../api/index.ts'; -type AuthenticationRuntimeHandler = ReturnType< - ReturnType['createHandler'] ->; +type AuthenticationRuntimeHandler = ReturnType['createHandler']>; const email = 'better-auth-runtime@example.test'; const password = 'correct-horse-battery-staple'; const tenantId = '30000000-0000-4000-8000-000000000001'; @@ -74,9 +64,7 @@ const IdentityResponseSchema = Schema.Struct({ }); const ProblemStatusSchema = Schema.Struct({ status: Schema.Number }); const SessionResponseSchema = Schema.Struct({ - identity: Schema.optional( - Schema.Struct({ principalId: Schema.optional(PrincipalIdSchema) }) - ), + identity: Schema.optional(Schema.Struct({ principalId: Schema.optional(PrincipalIdSchema) })), }); const RetryableProblemSchema = Schema.Struct({ retryable: Schema.optional(Schema.Boolean), @@ -87,23 +75,13 @@ const DefectProblemSchema = Schema.Struct({ }); const legalEntitySelectionOptions = { contextAccess: { - legalEntities: ({ - legalEntityIds, - }: { - readonly legalEntityIds: readonly string[]; - }) => - Effect.succeed( - legalEntityIds.map((key) => ({ decision: 'allowed' as const, key })) - ), + legalEntities: ({ legalEntityIds }: { readonly legalEntityIds: readonly string[] }) => + Effect.succeed(legalEntityIds.map((key) => ({ decision: 'allowed' as const, key }))), modules: ({ moduleIds }: { readonly moduleIds: readonly string[] }) => - Effect.succeed( - moduleIds.map((key) => ({ decision: 'allowed' as const, key })) - ), + Effect.succeed(moduleIds.map((key) => ({ decision: 'allowed' as const, key }))), resources: () => Effect.succeed([]), tenants: ({ tenantIds }: { readonly tenantIds: readonly string[] }) => - Effect.succeed( - tenantIds.map((key) => ({ decision: 'allowed' as const, key })) - ), + Effect.succeed(tenantIds.map((key) => ({ decision: 'allowed' as const, key }))), }, legalEntityContext: { listActiveForTenant: () => @@ -119,69 +97,52 @@ const legalEntitySelectionOptions = { : Effect.die('missing fixture legal entity'), }, } as const; -const contextAccessLayer = Layer.succeed( - ContextAccess, - legalEntitySelectionOptions.contextAccess -); +const contextAccessLayer = Layer.succeed(ContextAccess, legalEntitySelectionOptions.contextAccess); const authenticationContextLayer = Layer.mergeAll( contextAccessLayer, - Layer.succeed( - LegalEntityContext, - legalEntitySelectionOptions.legalEntityContext - ) + Layer.succeed(LegalEntityContext, legalEntitySelectionOptions.legalEntityContext), ); const cookieHeader = (setCookieHeaders: readonly string[]) => setCookieHeaders.map((header) => header.split(';')[0]).join('; '); -const headerValue = (headers: Headers, name: string): string => - headers.get(name) ?? ''; +const headerValue = (headers: Headers, name: string): string => headers.get(name) ?? ''; const optionalText = (value: string | undefined): string => value ?? ''; /** A revoked or missing binding must fail closed on the next session resolution. */ -const assertSessionForbidden = Effect.fnUntraced( - function* assertSessionForbidden( - resolution: Effect.Effect - ) { - const failure = yield* Effect.flip(resolution); - expect(Predicate.isTagged(failure, 'OntosIdentityForbiddenError')).toBe( - true - ); - } -); +const assertSessionForbidden = Effect.fnUntraced(function* assertSessionForbidden( + resolution: Effect.Effect, +) { + const failure = yield* Effect.flip(resolution); + expect(Predicate.isTagged(failure, 'OntosIdentityForbiddenError')).toBe(true); +}); /** Verify the issued assertion while retaining each caller's principal expectations. */ -const verifiedGatewayAssertion = Effect.fnUntraced( - function* verifiedGatewayAssertion( - assertionResponse: Response, - publicKey: Parameters[1] - ) { - const assertion = yield* Schema.decodeUnknownEffect(TokenResponseSchema)( - yield* Effect.tryPromise(() => assertionResponse.json()) - ); - const verified = yield* Effect.tryPromise(() => - jwtVerify(assertion.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'inventory-stock', - currentDate: new Date(1_700_000_001_000), - issuer: 'https://shell.example.test', - }) - ); - return { - principal: yield* Schema.decodeUnknownEffect( - TrustedPrincipalContextSchema - )(verified.payload['principal']), - token: assertion.token, - }; - } -); +const verifiedGatewayAssertion = Effect.fnUntraced(function* verifiedGatewayAssertion( + assertionResponse: Response, + publicKey: Parameters[1], +) { + const assertion = yield* Schema.decodeUnknownEffect(TokenResponseSchema)( + yield* Effect.tryPromise(() => assertionResponse.json()), + ); + const verified = yield* Effect.tryPromise(() => + jwtVerify(assertion.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'inventory-stock', + currentDate: new Date(1_700_000_001_000), + issuer: 'https://shell.example.test', + }), + ); + return { + principal: yield* Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)(verified.payload['principal']), + token: assertion.token, + }; +}); const assertOptionalField = ( value: Value | null | undefined, key: Key, - expected: string | null + expected: string | null, ): void => { expect(value?.[key]).toBe(expected); }; -const installedCatalog = ( - moduleIds: readonly string[] -): InstalledModuleCatalog => +const installedCatalog = (moduleIds: readonly string[]): InstalledModuleCatalog => Object.freeze({ contracts: Object.freeze([]), deploymentAppIds: Object.freeze([]), @@ -297,15 +258,10 @@ it.live( Effect.fnUntraced(function* runIntegration1() { const configuration = yield* loadAuthConfig(); const corePool = yield* Effect.acquireRelease( - Effect.sync( - () => new Pool({ connectionString: configuration.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const coreDatabase = yield* makeTestDatabaseFromPool( - corePool, - coreRelations + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (pool) => Effect.promise(() => pool.end()), ); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); const authPersistence = yield* makeAuthDatabase(configuration); const authDatabase = authPersistence.executor; const resolver = makePrincipalResolver({ executor: coreDatabase }); @@ -314,76 +270,44 @@ it.live( }).pipe( Effect.provideService(AuthConfig, configuration), Effect.provideService(AuthDatabase, authPersistence), - Effect.provideService(PrincipalResolver, resolver) - ); - const authenticationLayer = Layer.succeed( - AuthenticationService, - authentication + Effect.provideService(PrincipalResolver, resolver), ); + const authenticationLayer = Layer.succeed(AuthenticationService, authentication); const moduleStateLayer = Layer.succeed( TenantModuleStateService, - makeTenantModuleStateService({ executor: coreDatabase }) + makeTenantModuleStateService({ executor: coreDatabase }), ); const handlers: AuthenticationRuntimeHandler[] = []; - const generatedFixtureRoot = yield* Effect.tryPromise(() => - mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-')) - ); + const generatedFixtureRoot = yield* Effect.tryPromise(() => mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-'))); const cleanup = Effect.fnUntraced(function* runIntegration2() { yield* purgeFixtureRows([ - coreDatabase - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, tenantId)), - coreDatabase - .delete(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - coreDatabase - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)), + coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), + coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), + coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), ]); - const existingUsers = yield* authDatabase - .select({ id: user.id }) - .from(user) - .where(eq(user.email, email)); + const existingUsers = yield* authDatabase.select({ id: user.id }).from(user).where(eq(user.email, email)); yield* Effect.all( existingUsers.map( Effect.fnUntraced(function* runIntegration3(existingUser) { yield* purgeFixtureRows([ coreDatabase .delete(principalAuthBindings) - .where( - eq(principalAuthBindings.providerSubjectId, existingUser.id) - ), - authDatabase - .delete(session) - .where(eq(session.userId, existingUser.id)), - authDatabase - .delete(account) - .where(eq(account.userId, existingUser.id)), + .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), + authDatabase.delete(session).where(eq(session.userId, existingUser.id)), + authDatabase.delete(account).where(eq(account.userId, existingUser.id)), authDatabase.delete(user).where(eq(user.id, existingUser.id)), ]); - }) - ) + }), + ), ); yield* purgeFixtureRows([ - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.principalId, principalId)), - coreDatabase - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantId)), - coreDatabase - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, foreignTenantId)), - coreDatabase - .delete(principals) - .where(eq(principals.principalId, principalId)), - coreDatabase - .delete(legalEntities) - .where(eq(legalEntities.legalEntityId, fixtureLegalEntityId)), + coreDatabase.delete(principalAuthBindings).where(eq(principalAuthBindings.principalId, principalId)), + coreDatabase.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), + coreDatabase.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, foreignTenantId)), + coreDatabase.delete(principals).where(eq(principals.principalId, principalId)), + coreDatabase.delete(legalEntities).where(eq(legalEntities.legalEntityId, fixtureLegalEntityId)), coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId)), - coreDatabase - .delete(tenants) - .where(eq(tenants.tenantId, foreignTenantId)), + coreDatabase.delete(tenants).where(eq(tenants.tenantId, foreignTenantId)), ]); }); yield* Effect.acquireRelease( @@ -393,21 +317,15 @@ it.live( handlers.map( Effect.fnUntraced(function* runIntegration5({ dispose }) { return yield* Effect.tryPromise(() => dispose()); - }) - ) - ); - yield* Effect.tryPromise(() => - rm(generatedFixtureRoot, { force: true, recursive: true }) + }), + ), ); + yield* Effect.tryPromise(() => rm(generatedFixtureRoot, { force: true, recursive: true })); yield* cleanup(); - }, Effect.orDie) + }, Effect.orDie), ); yield* cleanup(); - const betterAuthUserId = yield* authentication.createFixtureUser( - email, - 'Runtime fixture', - password - ); + const betterAuthUserId = yield* authentication.createFixtureUser(email, 'Runtime fixture', password); yield* coreDatabase.insert(tenants).values({ defaultLocale: 'en', name: 'Authentication runtime tenant', @@ -456,9 +374,7 @@ it.live( const requestHeaders = new Headers({ origin: configuration.baseUrl, }); - const invalid = yield* Effect.flip( - authentication.signIn(email, 'wrong-password', requestHeaders) - ); + const invalid = yield* Effect.flip(authentication.signIn(email, 'wrong-password', requestHeaders)); expect(Predicate.isTagged(invalid, 'InvalidCredentialsError')).toBe(true); const anonymousRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, @@ -471,7 +387,7 @@ it.live( moduleStateLayer, Effect.succeed(installedCatalog(['testing1'])), false, - contextAccessLayer + contextAccessLayer, ); const unavailableHandler = anonymousRuntime.createHandler(); handlers.push(unavailableHandler); @@ -484,24 +400,20 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(anonymousGatewayResponse.status).toBe(401); - expect( - headerValue(anonymousGatewayResponse.headers, 'www-authenticate') - ).toMatch(/^Bearer/u); + expect(headerValue(anonymousGatewayResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); const anonymousModulesResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: { origin: configuration.baseUrl }, - }) - ) + }), + ), ); expect(anonymousModulesResponse.status).toBe(401); - expect( - headerValue(anonymousModulesResponse.headers, 'www-authenticate') - ).toMatch(/^Bearer/u); + expect(headerValue(anonymousModulesResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); const anonymousPageResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/module-target`, { @@ -514,13 +426,11 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(anonymousPageResponse.status).toBe(401); - expect( - headerValue(anonymousPageResponse.headers, 'www-authenticate') - ).toMatch(/^Bearer/u); + expect(headerValue(anonymousPageResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); const invalidSignInResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( new Request(`${configuration.baseUrl}/auth/sign-in`, { @@ -530,15 +440,13 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(invalidSignInResponse.status).toBe(401); - expect(headerValue(invalidSignInResponse.headers, 'content-type')).toMatch( - /^application\/problem\+json/u - ); + expect(headerValue(invalidSignInResponse.headers, 'content-type')).toMatch(/^application\/problem\+json/u); const invalidSignInProblem = Schema.decodeUnknownSync(ProblemStatusSchema)( - yield* Effect.tryPromise(() => invalidSignInResponse.json()) + yield* Effect.tryPromise(() => invalidSignInResponse.json()), ); expect(invalidSignInProblem.status).toBe(401); const signInResponse = yield* Effect.tryPromise(() => @@ -550,12 +458,12 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(signInResponse.status).toBe(200); const signedIn = Schema.decodeUnknownSync(IdentityResponseSchema)( - yield* Effect.tryPromise(() => signInResponse.json()) + yield* Effect.tryPromise(() => signInResponse.json()), ); const signedInCookies = signInResponse.headers.getSetCookie(); expect(signedIn.identity.email).toBe(email); @@ -591,12 +499,10 @@ it.live( moduleStateLayer, Effect.succeed(installedPageCatalog()), false, - contextAccessLayer + contextAccessLayer, ).createHandler(); handlers.push(pageRuntime); - const exactPageResponse = yield* Effect.tryPromise(() => - pageRuntime.handler(exactPageRequest()) - ); + const exactPageResponse = yield* Effect.tryPromise(() => pageRuntime.handler(exactPageRequest())); expect(exactPageResponse.status).toBe(200); expect(yield* Effect.tryPromise(() => exactPageResponse.json())).toEqual({ appId: 'inventory-stock', @@ -606,7 +512,7 @@ it.live( writable: true, }); const missingPageResponse = yield* Effect.tryPromise(() => - pageRuntime.handler(exactPageRequest('testing.pages.page.missing')) + pageRuntime.handler(exactPageRequest('testing.pages.page.missing')), ); expect(missingPageResponse.status).toBe(404); const authenticatedContext = yield* authentication @@ -614,9 +520,7 @@ it.live( .pipe(Effect.provide(authenticationContextLayer)); expect(authenticatedContext.state).toBe('authenticated'); if (authenticatedContext.state !== 'authenticated') { - throw new Error( - 'The exact-page boundary fixture must resolve an authenticated context' - ); + throw new Error('The exact-page boundary fixture must resolve an authenticated context'); } const selectionRequiredRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, { @@ -644,17 +548,17 @@ it.live( moduleStateLayer, Effect.succeed(installedPageCatalog()), false, - contextAccessLayer + contextAccessLayer, ).createHandler(); handlers.push(selectionRequiredRuntime); const selectionRequiredPageResponse = yield* Effect.tryPromise(() => - selectionRequiredRuntime.handler(exactPageRequest()) + selectionRequiredRuntime.handler(exactPageRequest()), ); expect(selectionRequiredPageResponse.status).toBe(409); expect( Schema.decodeUnknownSync(ProblemStatusSchema)( - yield* Effect.tryPromise(() => selectionRequiredPageResponse.json()) - ).status + yield* Effect.tryPromise(() => selectionRequiredPageResponse.json()), + ).status, ).toBe(409); const deniedPageRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, @@ -670,33 +574,26 @@ it.live( Layer.succeed(ContextAccess, { ...legalEntitySelectionOptions.contextAccess, modules: ({ moduleIds }: { readonly moduleIds: readonly string[] }) => - Effect.succeed( - moduleIds.map((key) => ({ decision: 'denied' as const, key })) - ), - }) + Effect.succeed(moduleIds.map((key) => ({ decision: 'denied' as const, key }))), + }), ).createHandler(); handlers.push(deniedPageRuntime); - const deniedPageResponse = yield* Effect.tryPromise(() => - deniedPageRuntime.handler(exactPageRequest()) - ); + const deniedPageResponse = yield* Effect.tryPromise(() => deniedPageRuntime.handler(exactPageRequest())); expect(deniedPageResponse.status).toBe(403); expect( - Schema.decodeUnknownSync(ProblemStatusSchema)( - yield* Effect.tryPromise(() => deniedPageResponse.json()) - ).status + Schema.decodeUnknownSync(ProblemStatusSchema)(yield* Effect.tryPromise(() => deniedPageResponse.json())).status, ).toBe(403); const currentSessionResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( new Request(`${configuration.baseUrl}/auth/session`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(currentSessionResponse.status).toBe(200); expect( - Schema.decodeUnknownSync(SessionResponseSchema)( - yield* Effect.tryPromise(() => currentSessionResponse.json()) - ).identity?.principalId + Schema.decodeUnknownSync(SessionResponseSchema)(yield* Effect.tryPromise(() => currentSessionResponse.json())) + .identity?.principalId, ).toBe(principalId); const missingIdempotencyResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( @@ -708,50 +605,46 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(missingIdempotencyResponse.status).toBe(428); - const deniedIdentityAdministrationRuntime = - makeShellAuthenticationApiRuntime( - authenticationLayer, - makeGatewayIssuerLayer({ - currentTimeSeconds: Effect.succeed(1_700_000_000), - generateJti: Effect.succeed('60000000-0000-4000-8000-000000000002'), - loadAudiences: Effect.succeed(new Set(['inventory-stock'])), - loadConfig: parseGatewayIssuerConfig({}), - }), - moduleStateLayer, - Effect.succeed(installedCatalog(['testing1'])), - false, - Layer.succeed(ContextAccess, { - ...legalEntitySelectionOptions.contextAccess, - tenants: ({ - permission, - tenantIds, - }: { - readonly permission: - | 'access' - | 'impersonate' - | 'manage_identity' - | 'manage_party_identity' - | 'manage_party_relationships' - | 'merge_party_identity' - | 'read_party_identity' - | 'review_party_identity'; - readonly tenantIds: readonly string[]; - }) => - Effect.succeed( - tenantIds.map((key) => ({ - decision: - permission === 'manage_identity' - ? ('denied' as const) - : ('allowed' as const), - key, - })) - ), - }) - ).createHandler(); + const deniedIdentityAdministrationRuntime = makeShellAuthenticationApiRuntime( + authenticationLayer, + makeGatewayIssuerLayer({ + currentTimeSeconds: Effect.succeed(1_700_000_000), + generateJti: Effect.succeed('60000000-0000-4000-8000-000000000002'), + loadAudiences: Effect.succeed(new Set(['inventory-stock'])), + loadConfig: parseGatewayIssuerConfig({}), + }), + moduleStateLayer, + Effect.succeed(installedCatalog(['testing1'])), + false, + Layer.succeed(ContextAccess, { + ...legalEntitySelectionOptions.contextAccess, + tenants: ({ + permission, + tenantIds, + }: { + readonly permission: + | 'access' + | 'impersonate' + | 'manage_identity' + | 'manage_party_identity' + | 'manage_party_relationships' + | 'merge_party_identity' + | 'read_party_identity' + | 'review_party_identity'; + readonly tenantIds: readonly string[]; + }) => + Effect.succeed( + tenantIds.map((key) => ({ + decision: permission === 'manage_identity' ? ('denied' as const) : ('allowed' as const), + key, + })), + ), + }), + ).createHandler(); handlers.push(deniedIdentityAdministrationRuntime); const deniedIdentityAdministrationResponse = yield* Effect.tryPromise(() => deniedIdentityAdministrationRuntime.handler( @@ -767,8 +660,8 @@ it.live( origin: configuration.baseUrl, }, method: 'POST', - }) - ) + }), + ), ); expect(deniedIdentityAdministrationResponse.status).toBe(403); const [deniedIdentityInvocation] = yield* coreDatabase @@ -781,9 +674,7 @@ it.live( .limit(1); assertOptionalField(deniedIdentityInvocation, 'status', 'rejected'); if (deniedIdentityInvocation === undefined) { - throw new Error( - 'The denied identity Action did not persist its invocation' - ); + throw new Error('The denied identity Action did not persist its invocation'); } const [deniedIdentityAudit] = yield* coreDatabase .select({ @@ -791,12 +682,7 @@ it.live( outcomeCode: auditEvents.outcomeCode, }) .from(auditEvents) - .where( - eq( - auditEvents.actionInvocationId, - deniedIdentityInvocation.actionInvocationId - ) - ) + .where(eq(auditEvents.actionInvocationId, deniedIdentityInvocation.actionInvocationId)) .limit(1); expect(deniedIdentityAudit).toEqual({ eventType: 'action.rejected', @@ -806,13 +692,11 @@ it.live( unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(activeModulesResponse.status).toBe(200); - expect( - yield* Effect.tryPromise(() => activeModulesResponse.json()) - ).toEqual({ + expect(yield* Effect.tryPromise(() => activeModulesResponse.json())).toEqual({ navigation: [], state: 'available', unavailableDeployments: [], @@ -830,11 +714,8 @@ it.live( .where( and( eq(dataAccessEvents.tenantId, tenantId), - eq( - dataAccessEvents.evidencePolicyKey, - 'core.shell.composition.evidence.v1' - ) - ) + eq(dataAccessEvents.evidencePolicyKey, 'core.shell.composition.evidence.v1'), + ), ); expect(compositionEvidence).toEqual({ authBindingId: fixtureAuthBindingId, @@ -872,8 +753,7 @@ it.live( }, principal: { authBindingId: '45000000-0000-4000-8000-000000000001', - authContextRef: - 'better-auth-session:45000000-0000-4000-8000-000000000001', + authContextRef: 'better-auth-session:45000000-0000-4000-8000-000000000001', authMethod: 'session' as const, legalEntityId: fixtureLegalEntityId, principalId: current.identity.principalId, @@ -892,19 +772,15 @@ it.live( moduleStateLayer, Effect.succeed(installedCatalog(['testing1'])), false, - contextAccessLayer + contextAccessLayer, ).createHandler(); handlers.push(refreshingRuntime); const refreshedModulesResponse = yield* Effect.tryPromise(() => - refreshingRuntime.handler( - new Request(`${configuration.baseUrl}/shell/composition`) - ) + refreshingRuntime.handler(new Request(`${configuration.baseUrl}/shell/composition`)), ); expect(refreshedModulesResponse.status).toBe(200); expect( - refreshedModulesResponse.headers - .getSetCookie() - .some((header) => header.startsWith('refreshed-session=value')) + refreshedModulesResponse.headers.getSetCookie().some((header) => header.startsWith('refreshed-session=value')), ).toBe(true); const unavailableModuleStates = { getTenantModuleStates: () => @@ -912,21 +788,21 @@ it.live( new TenantModuleStateReadUnavailableError({ code: 'tenant_module_state_read_unavailable', reason: `secret SQL failure for ${tenantId}`, - }) + }), ), listActiveTenantModules: () => Effect.fail( new TenantModuleStateReadUnavailableError({ code: 'tenant_module_state_read_unavailable', reason: `secret SQL failure for ${tenantId}`, - }) + }), ), listTenantModuleStates: () => Effect.fail( new TenantModuleStateReadUnavailableError({ code: 'tenant_module_state_read_unavailable', reason: `secret SQL failure for ${tenantId}`, - }) + }), ), }; const unavailableModulesHandler = makeShellAuthenticationApiRuntime( @@ -942,29 +818,26 @@ it.live( false, contextAccessLayer, undefined, - () => unavailableModuleStates + () => unavailableModuleStates, ).createHandler(); handlers.push(unavailableModulesHandler); const unavailableModulesResponse = yield* Effect.tryPromise(() => unavailableModulesHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(unavailableModulesResponse.status).toBe(503); - const unavailableModulesProblem = yield* Effect.tryPromise(() => - unavailableModulesResponse.text() - ); + const unavailableModulesProblem = yield* Effect.tryPromise(() => unavailableModulesResponse.text()); expect(unavailableModulesProblem).not.toMatch(/SQL|30000000|40000000/u); const unavailablePageResponse = yield* Effect.tryPromise(() => - unavailableModulesHandler.handler(exactPageRequest()) + unavailableModulesHandler.handler(exactPageRequest()), ); expect(unavailablePageResponse.status).toBe(503); expect( - Schema.decodeUnknownSync(ProblemStatusSchema)( - yield* Effect.tryPromise(() => unavailablePageResponse.json()) - ).status + Schema.decodeUnknownSync(ProblemStatusSchema)(yield* Effect.tryPromise(() => unavailablePageResponse.json())) + .status, ).toBe(503); const unavailableGatewayResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( @@ -976,24 +849,18 @@ it.live( origin: configuration.baseUrl, }), method: 'POST', - }) - ) + }), + ), ); expect(unavailableGatewayResponse.status).toBe(503); - expect( - headerValue(unavailableGatewayResponse.headers, 'content-type') - ).toMatch(/application\/problem\+json/u); + expect(headerValue(unavailableGatewayResponse.headers, 'content-type')).toMatch(/application\/problem\+json/u); expect( Schema.decodeUnknownSync(RetryableProblemSchema)( - yield* Effect.tryPromise(() => unavailableGatewayResponse.json()) - ).retryable + yield* Effect.tryPromise(() => unavailableGatewayResponse.json()), + ).retryable, ).toBe(true); - const pair = yield* Effect.tryPromise(() => - generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }) - ); - const privateJwk = yield* Effect.tryPromise(() => - exportJWK(pair.privateKey) - ); + const pair = yield* Effect.tryPromise(() => generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true })); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(pair.privateKey)); const publicJwk = yield* Effect.tryPromise(() => exportJWK(pair.publicKey)); const issuerDependencies: GatewayIssuerLayerOptions = { currentTimeSeconds: Effect.succeed(1_700_000_000), @@ -1018,7 +885,7 @@ it.live( moduleStateLayer, Effect.succeed(installedCatalog(['testing1'])), false, - contextAccessLayer + contextAccessLayer, ).createHandler(); handlers.push(issuingHandler); const assertionResponse = yield* Effect.tryPromise(() => @@ -1031,19 +898,16 @@ it.live( origin: configuration.baseUrl, }), method: 'POST', - }) - ) + }), + ), ); - expect( - assertionResponse.status, - yield* Effect.tryPromise(() => assertionResponse.clone().text()) - ).toBe(200); - const { principal: verifiedPrincipal, token: assertionToken } = - yield* verifiedGatewayAssertion(assertionResponse, pair.publicKey); - expect(verifiedPrincipal.authBindingId).toBe(fixtureAuthBindingId); - expect(optionalText(verifiedPrincipal.authContextRef)).toMatch( - /^better-auth-session:/u + expect(assertionResponse.status, yield* Effect.tryPromise(() => assertionResponse.clone().text())).toBe(200); + const { principal: verifiedPrincipal, token: assertionToken } = yield* verifiedGatewayAssertion( + assertionResponse, + pair.publicKey, ); + expect(verifiedPrincipal.authBindingId).toBe(fixtureAuthBindingId); + expect(optionalText(verifiedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); expect(verifiedPrincipal.authMethod).toBe('session'); expect(verifiedPrincipal.legalEntityId).toBe(fixtureLegalEntityId); expect(verifiedPrincipal.principalId).toBe(principalId); @@ -1051,60 +915,44 @@ it.live( yield* Effect.tryPromise(() => mkdir(path.join(generatedFixtureRoot, 'node_modules', '@app'), { recursive: true, - }) + }), ); yield* Effect.tryPromise(() => symlink( path.join(appRoot, 'packages/core-runtime'), path.join(generatedFixtureRoot, 'node_modules/@app/core-runtime'), - 'dir' - ) + 'dir', + ), ); yield* Effect.tryPromise(() => symlink( path.join(appRoot, 'packages/shared-contracts'), path.join(generatedFixtureRoot, 'node_modules/@app/shared-contracts'), - 'dir' - ) + 'dir', + ), ); yield* Effect.tryPromise(() => symlink( path.join(appRoot, 'packages/gateway-principal-verifier'), - path.join( - generatedFixtureRoot, - 'node_modules/@app/gateway-principal-verifier' - ), - 'dir' - ) + path.join(generatedFixtureRoot, 'node_modules/@app/gateway-principal-verifier'), + 'dir', + ), ); yield* Effect.tryPromise(() => - symlink( - path.join(appRoot, 'node_modules/effect'), - path.join(generatedFixtureRoot, 'node_modules/effect'), - 'dir' - ) + symlink(path.join(appRoot, 'node_modules/effect'), path.join(generatedFixtureRoot, 'node_modules/effect'), 'dir'), ); yield* Effect.tryPromise(() => symlink( path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), path.join(generatedFixtureRoot, 'node_modules/jose'), - 'dir' - ) - ); - const generatedVerifierPath = path.join( - generatedFixtureRoot, - 'action-principal.ts' + 'dir', + ), ); + const generatedVerifierPath = path.join(generatedFixtureRoot, 'action-principal.ts'); yield* Effect.tryPromise(() => - writeFile( - generatedVerifierPath, - renderActionPrincipalServer({ appId: 'inventory-stock' }), - 'utf-8' - ) - ); - const generatedVerifier = yield* Effect.tryPromise( - () => import(pathToFileURL(generatedVerifierPath).href) + writeFile(generatedVerifierPath, renderActionPrincipalServer({ appId: 'inventory-stock' }), 'utf-8'), ); + const generatedVerifier = yield* Effect.tryPromise(() => import(pathToFileURL(generatedVerifierPath).href)); type GeneratedVerifier = ( authorization: string, options: { @@ -1113,17 +961,13 @@ it.live( readonly redemption: { readonly consume: () => Effect.Effect; }; - } + }, ) => Effect.Effect; const verifyActionPrincipal = Schema.decodeUnknownSync( - Schema.declare((value): value is GeneratedVerifier => - Predicate.isFunction(value) - ) + Schema.declare((value): value is GeneratedVerifier => Predicate.isFunction(value)), )(generatedVerifier.verifyActionPrincipal); expect(Predicate.isFunction(verifyActionPrincipal)).toBe(true); - const generatedPrincipal = Schema.decodeUnknownSync( - TrustedPrincipalContextSchema - )( + const generatedPrincipal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( yield* verifyActionPrincipal(`Bearer ${assertionToken}`, { currentTimeSeconds: Effect.succeed(1_700_000_001), environment: { @@ -1140,12 +984,10 @@ it.live( }), }, redemption: { consume: () => Effect.void }, - }) + }), ); expect(generatedPrincipal.authBindingId).toBe(fixtureAuthBindingId); - expect(optionalText(generatedPrincipal.authContextRef)).toMatch( - /^better-auth-session:/u - ); + expect(optionalText(generatedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); expect(generatedPrincipal.authMethod).toBe('session'); expect(generatedPrincipal.legalEntityId).toBe(fixtureLegalEntityId); expect(generatedPrincipal.principalId).toBe(principalId); @@ -1160,8 +1002,8 @@ it.live( origin: configuration.baseUrl, }), method: 'POST', - }) - ) + }), + ), ); expect(invalidAudienceResponse.status).toBe(400); const defectHandler = makeShellAuthenticationApiRuntime( @@ -1173,7 +1015,7 @@ it.live( moduleStateLayer, Effect.succeed(installedCatalog(['testing1'])), false, - contextAccessLayer + contextAccessLayer, ).createHandler(); handlers.push(defectHandler); const defectResponse = yield* Effect.tryPromise(() => @@ -1187,30 +1029,20 @@ it.live( 'x-correlation-id': 'integration-correlation-id', }), method: 'POST', - }) - ) + }), + ), ); expect(defectResponse.status).toBe(500); - expect(headerValue(defectResponse.headers, 'content-type')).toMatch( - /application\/problem\+json/u - ); + expect(headerValue(defectResponse.headers, 'content-type')).toMatch(/application\/problem\+json/u); const defectProblem = Schema.decodeUnknownSync(DefectProblemSchema)( - yield* Effect.tryPromise(() => defectResponse.json()) - ); - expect(defectProblem.detail).toBe( - 'Gateway authentication could not complete.' - ); - expect(JSON.stringify(defectProblem)).not.toMatch( - /deliberate gateway test defect/u + yield* Effect.tryPromise(() => defectResponse.json()), ); + expect(defectProblem.detail).toBe('Gateway authentication could not complete.'); + expect(JSON.stringify(defectProblem)).not.toMatch(/deliberate gateway test defect/u); const stillAuthenticated = yield* authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(authenticationContextLayer)); - assertOptionalField( - stillAuthenticated.identity, - 'principalId', - principalId - ); + assertOptionalField(stillAuthenticated.identity, 'principalId', principalId); yield* coreDatabase .update(principalAuthBindings) .set({ @@ -1219,24 +1051,18 @@ it.live( }) .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)); yield* assertSessionForbidden( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)) + authentication.currentSession(authenticatedHeaders).pipe(Effect.provide(authenticationContextLayer)), ); const forbiddenModulesResponse = yield* Effect.tryPromise(() => unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(forbiddenModulesResponse.status).toBe(401); - expect( - headerValue(forbiddenModulesResponse.headers, 'www-authenticate') - ).toMatch(/^Bearer/u); - expect( - yield* Effect.tryPromise(() => forbiddenModulesResponse.text()) - ).not.toMatch(/30000000|40000000/u); + expect(headerValue(forbiddenModulesResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); + expect(yield* Effect.tryPromise(() => forbiddenModulesResponse.text())).not.toMatch(/30000000|40000000/u); yield* coreDatabase .update(principalAuthBindings) .set({ revokedAt: null, status: 'active' }) @@ -1246,21 +1072,19 @@ it.live( new Request(`${configuration.baseUrl}/auth/sign-out`, { headers: authenticatedHeaders, method: 'POST', - }) - ) + }), + ), ); expect(signOutResponse.status).toBe(200); const signedOutCookies = signOutResponse.headers.getSetCookie(); expect(signedOutCookies.length >= 3).toBe(true); - expect(signedOutCookies.every((header) => !header.includes(password))).toBe( - true - ); + expect(signedOutCookies.every((header) => !header.includes(password))).toBe(true); const anonymous = yield* authentication .currentSession( new Headers({ cookie: cookieHeader(signedOutCookies), origin: configuration.baseUrl, - }) + }), ) .pipe(Effect.provide(authenticationContextLayer)); expect(anonymous.identity).toBe(null); @@ -1268,14 +1092,12 @@ it.live( unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(expiredModulesResponse.status).toBe(401); - expect( - yield* Effect.tryPromise(() => expiredModulesResponse.text()) - ).not.toMatch(/30000000|40000000/u); - }) + expect(yield* Effect.tryPromise(() => expiredModulesResponse.text())).not.toMatch(/30000000|40000000/u); + }), ); it.live( 'selects, lists, switches, revalidates, and upgrades a multi-tenant session', @@ -1290,10 +1112,7 @@ it.live( const firstAuthBindingId = '46000000-0000-4000-8000-000000000001'; const secondAuthBindingId = '46000000-0000-4000-8000-000000000002'; const legalEntityByTenant = new Map([ - [ - firstTenantId, - { legalEntityId: firstLegalEntityId, legalName: 'First legal entity' }, - ], + [firstTenantId, { legalEntityId: firstLegalEntityId, legalName: 'First legal entity' }], [ secondTenantId, { @@ -1307,12 +1126,9 @@ it.live( legalEntityContext: { listActiveForTenant: (selectedTenantId: string) => Effect.succeed(legalEntityByTenant.get(selectedTenantId)).pipe( - Effect.map((selected) => (selected === undefined ? [] : [selected])) + Effect.map((selected) => (selected === undefined ? [] : [selected])), ), - validateSelection: ( - selectedTenantId: string, - legalEntityId: string - ) => { + validateSelection: (selectedTenantId: string, legalEntityId: string) => { const selected = legalEntityByTenant.get(selectedTenantId); return selected?.legalEntityId === legalEntityId ? Effect.succeed(selected) @@ -1320,16 +1136,10 @@ it.live( }, }, } as const; - const multiContextAccessLayer = Layer.succeed( - ContextAccess, - multiLegalEntitySelectionOptions.contextAccess - ); + const multiContextAccessLayer = Layer.succeed(ContextAccess, multiLegalEntitySelectionOptions.contextAccess); const multiAuthenticationContextLayer = Layer.mergeAll( multiContextAccessLayer, - Layer.succeed( - LegalEntityContext, - multiLegalEntitySelectionOptions.legalEntityContext - ) + Layer.succeed(LegalEntityContext, multiLegalEntitySelectionOptions.legalEntityContext), ); const configuration = yield* loadAuthConfig(); const databaseConnections = yield* loadDatabaseConnectionPair(); @@ -1338,89 +1148,54 @@ it.live( () => new Pool({ connectionString: databaseConnections.admin.connectionString, - }) + }), ), - (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie) + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), ); const corePool = yield* Effect.acquireRelease( - Effect.sync( - () => new Pool({ connectionString: configuration.connectionString }) - ), - (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie) - ); - const coreDatabase = yield* makeTestDatabaseFromPool( - corePool, - coreRelations + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), ); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); const authPersistence = yield* makeAuthDatabase(configuration); const authDatabase = authPersistence.executor; - const adminAuthDatabase = yield* makeTestDatabaseFromPool( - adminPool, - authRelations - ); + const adminAuthDatabase = yield* makeTestDatabaseFromPool(adminPool, authRelations); const resolver = makePrincipalResolver({ executor: coreDatabase }); const authentication = yield* makeAuthenticationService({ allowFixtureSignUp: true, }).pipe( Effect.provideService(AuthConfig, configuration), Effect.provideService(AuthDatabase, authPersistence), - Effect.provideService(PrincipalResolver, resolver) + Effect.provideService(PrincipalResolver, resolver), ); const moduleStateLayer = Layer.succeed( TenantModuleStateService, - makeTenantModuleStateService({ executor: coreDatabase }) + makeTenantModuleStateService({ executor: coreDatabase }), ); const handlers: AuthenticationRuntimeHandler[] = []; const fixtureTenants = [firstTenantId, secondTenantId]; // Ordered child-before-parent within the owned fixture rows. const cleanup = Effect.fnUntraced(function* runIntegration7() { - yield* coreDatabase - .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.tenantId, fixtureTenants)); - const existingUsers = yield* authDatabase - .select({ id: user.id }) - .from(user) - .where(eq(user.email, multiEmail)); + yield* coreDatabase.delete(dataAccessEvents).where(inArray(dataAccessEvents.tenantId, fixtureTenants)); + const existingUsers = yield* authDatabase.select({ id: user.id }).from(user).where(eq(user.email, multiEmail)); const existingUserIds = existingUsers.map(({ id }) => id); if (existingUserIds.length > 0) { yield* purgeFixtureRows([ coreDatabase .delete(principalAuthBindings) - .where( - inArray(principalAuthBindings.providerSubjectId, existingUserIds) - ), - authDatabase - .delete(session) - .where(inArray(session.userId, existingUserIds)), - authDatabase - .delete(account) - .where(inArray(account.userId, existingUserIds)), + .where(inArray(principalAuthBindings.providerSubjectId, existingUserIds)), + authDatabase.delete(session).where(inArray(session.userId, existingUserIds)), + authDatabase.delete(account).where(inArray(account.userId, existingUserIds)), authDatabase.delete(user).where(inArray(user.id, existingUserIds)), ]); } yield* purgeFixtureRows([ - coreDatabase - .delete(tenantModuleStates) - .where(inArray(tenantModuleStates.tenantId, fixtureTenants)), - coreDatabase - .delete(principals) - .where( - inArray(principals.principalId, [ - firstPrincipalId, - secondPrincipalId, - ]) - ), + coreDatabase.delete(tenantModuleStates).where(inArray(tenantModuleStates.tenantId, fixtureTenants)), + coreDatabase.delete(principals).where(inArray(principals.principalId, [firstPrincipalId, secondPrincipalId])), coreDatabase .delete(legalEntities) - .where( - inArray(legalEntities.legalEntityId, [ - firstLegalEntityId, - secondLegalEntityId, - ]) - ), - coreDatabase - .delete(tenants) - .where(inArray(tenants.tenantId, fixtureTenants)), + .where(inArray(legalEntities.legalEntityId, [firstLegalEntityId, secondLegalEntityId])), + coreDatabase.delete(tenants).where(inArray(tenants.tenantId, fixtureTenants)), ]); }); yield* Effect.acquireRelease( @@ -1430,18 +1205,14 @@ it.live( handlers.map( Effect.fnUntraced(function* runIntegration9({ dispose }) { return yield* Effect.tryPromise(() => dispose()); - }) - ) + }), + ), ); yield* cleanup(); - }, Effect.orDie) + }, Effect.orDie), ); yield* cleanup(); - const betterAuthUserId = yield* authentication.createFixtureUser( - multiEmail, - 'Multi tenant fixture', - password - ); + const betterAuthUserId = yield* authentication.createFixtureUser(multiEmail, 'Multi tenant fixture', password); yield* coreDatabase.insert(tenants).values([ { defaultLocale: 'en', @@ -1518,11 +1289,7 @@ it.live( { moduleKey: 'first-module', state: 'active', tenantId: firstTenantId }, { moduleKey: 'second-module', state: 'active', tenantId: secondTenantId }, ]); - const signIn = yield* authentication.signIn( - multiEmail, - password, - new Headers({ origin: configuration.baseUrl }) - ); + const signIn = yield* authentication.signIn(multiEmail, password, new Headers({ origin: configuration.baseUrl })); expect(signIn.identity.tenantId).toBe(firstTenantId); expect(signIn.identity.principalId).toBe(firstPrincipalId); const authenticatedCookie = cookieHeader(signIn.setCookieHeaders); @@ -1531,10 +1298,7 @@ it.live( origin: configuration.baseUrl, }); // Tenant switching varies only the target and optional correlation header. - const tenantSwitchRequest = ( - target: string, - extraHeaders: Record = {} - ) => + const tenantSwitchRequest = (target: string, extraHeaders: Record = {}) => new Request(`${configuration.baseUrl}/auth/tenant/switch`, { body: JSON.stringify({ tenantId: target }), headers: new Headers({ @@ -1553,12 +1317,8 @@ it.live( const initialSessions = yield* readActiveTenantIds(); assertOptionalField(initialSessions[0], 'activeTenantId', firstTenantId); - const pair = yield* Effect.tryPromise(() => - generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }) - ); - const privateJwk = yield* Effect.tryPromise(() => - exportJWK(pair.privateKey) - ); + const pair = yield* Effect.tryPromise(() => generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true })); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(pair.privateKey)); const runtime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, authentication), makeGatewayIssuerLayer({ @@ -1581,26 +1341,24 @@ it.live( moduleStateLayer, Effect.succeed(installedCatalog(['first-module', 'second-module'])), false, - multiContextAccessLayer + multiContextAccessLayer, ).createHandler(); handlers.push(runtime); const anonymousAvailableResponse = yield* Effect.tryPromise(() => runtime.handler( new Request(`${configuration.baseUrl}/auth/tenants`, { headers: { origin: configuration.baseUrl }, - }) - ) + }), + ), ); expect(anonymousAvailableResponse.status).toBe(401); - expect( - headerValue(anonymousAvailableResponse.headers, 'www-authenticate') - ).toMatch(/^Bearer /u); + expect(headerValue(anonymousAvailableResponse.headers, 'www-authenticate')).toMatch(/^Bearer /u); const availableResponse = yield* Effect.tryPromise(() => runtime.handler( new Request(`${configuration.baseUrl}/auth/tenants`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(availableResponse.status).toBe(200); expect(yield* Effect.tryPromise(() => availableResponse.json())).toEqual({ @@ -1612,15 +1370,13 @@ it.live( const availableTenants = yield* authentication .availableTenants(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)); - expect(JSON.stringify(availableTenants)).not.toMatch( - /principalId|sessionId|token|bindingId|password/u - ); + expect(JSON.stringify(availableTenants)).not.toMatch(/principalId|sessionId|token|bindingId|password/u); const firstModules = yield* Effect.tryPromise(() => runtime.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(yield* Effect.tryPromise(() => firstModules.json())).toEqual({ navigation: [], @@ -1628,38 +1384,24 @@ it.live( unavailableDeployments: [], }); const forbiddenResponse = yield* Effect.tryPromise(() => - runtime.handler( - tenantSwitchRequest('31000000-0000-4000-8000-000000000099') - ) + runtime.handler(tenantSwitchRequest('31000000-0000-4000-8000-000000000099')), ); expect(forbiddenResponse.status).toBe(403); const sessionsAfterForbiddenSwitch = yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterForbiddenSwitch[0], - 'activeTenantId', - firstTenantId - ); + assertOptionalField(sessionsAfterForbiddenSwitch[0], 'activeTenantId', firstTenantId); yield* coreDatabase .update(principals) .set({ status: 'disabled' }) .where(eq(principals.principalId, secondPrincipalId)); - const inactiveTargetResponse = yield* Effect.tryPromise(() => - runtime.handler(tenantSwitchRequest(secondTenantId)) - ); + const inactiveTargetResponse = yield* Effect.tryPromise(() => runtime.handler(tenantSwitchRequest(secondTenantId))); expect(inactiveTargetResponse.status).toBe(403); const sessionsAfterInactiveSwitch = yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterInactiveSwitch[0], - 'activeTenantId', - firstTenantId - ); + assertOptionalField(sessionsAfterInactiveSwitch[0], 'activeTenantId', firstTenantId); yield* coreDatabase .update(principals) .set({ status: 'active' }) .where(eq(principals.principalId, secondPrincipalId)); - const resolverUnavailableAuthentication = yield* makeAuthenticationService( - {} - ).pipe( + const resolverUnavailableAuthentication = yield* makeAuthenticationService({}).pipe( Effect.provideService(AuthConfig, configuration), Effect.provideService(AuthDatabase, authPersistence), Effect.provideService(PrincipalResolver, { @@ -1669,10 +1411,10 @@ it.live( ? Effect.fail( new PrincipalResolverUnavailableError({ reason: 'Injected resolver outage', - }) + }), ) : resolver.resolveBetterAuthUserForTenant(userId, selectedTenantId), - }) + }), ); const resolverUnavailableRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, resolverUnavailableAuthentication), @@ -1682,19 +1424,15 @@ it.live( loadAudiences: Effect.succeed(new Set()), loadConfig: parseGatewayIssuerConfig({}), }), - moduleStateLayer + moduleStateLayer, ).createHandler(); handlers.push(resolverUnavailableRuntime); const resolverUnavailableResponse = yield* Effect.tryPromise(() => - resolverUnavailableRuntime.handler(tenantSwitchRequest(secondTenantId)) + resolverUnavailableRuntime.handler(tenantSwitchRequest(secondTenantId)), ); expect(resolverUnavailableResponse.status).toBe(503); const sessionsAfterResolverFailure = yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterResolverFailure[0], - 'activeTenantId', - firstTenantId - ); + assertOptionalField(sessionsAfterResolverFailure[0], 'activeTenantId', firstTenantId); // Drizzle has no query-builder failure injection. This temporary trigger raises PostgreSQL's // connection-failure class for the fixed test tenant through the real Better Auth adapter path. yield* adminAuthDatabase.execute( @@ -1710,7 +1448,7 @@ it.live( RETURN NEW; END; $function$ - `) + `), ); yield* adminAuthDatabase.execute( sql.raw(` @@ -1718,7 +1456,7 @@ it.live( BEFORE UPDATE ON auth.session FOR EACH ROW EXECUTE FUNCTION auth.tenant_switch_test_fail_persistence() - `) + `), ); yield* Effect.scoped( Effect.gen(function* integrationEffect10() { @@ -1728,39 +1466,29 @@ it.live( yield* adminAuthDatabase.execute( sql.raw(` DROP TRIGGER IF EXISTS tenant_switch_test_persistence_failure ON auth.session - `) + `), ); yield* adminAuthDatabase.execute( sql.raw(` DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_persistence() - `) + `), ); - }, Effect.orDie) + }, Effect.orDie), ); const persistenceUnavailableResponse = yield* Effect.tryPromise(() => - runtime.handler(tenantSwitchRequest(secondTenantId)) + runtime.handler(tenantSwitchRequest(secondTenantId)), ); expect(persistenceUnavailableResponse.status).toBe(503); const sessionsAfterPersistenceFailure = yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterPersistenceFailure[0], - 'activeTenantId', - firstTenantId - ); - }) + assertOptionalField(sessionsAfterPersistenceFailure[0], 'activeTenantId', firstTenantId); + }), ); const sessionsBeforeSwitch = yield* authDatabase .select({ activeLegalEntityId: session.activeLegalEntityId }) .from(session) .where(eq(session.userId, betterAuthUserId)); - assertOptionalField( - sessionsBeforeSwitch[0], - 'activeLegalEntityId', - firstLegalEntityId - ); - const switchResponse = yield* Effect.tryPromise(() => - runtime.handler(tenantSwitchRequest(secondTenantId)) - ); + assertOptionalField(sessionsBeforeSwitch[0], 'activeLegalEntityId', firstLegalEntityId); + const switchResponse = yield* Effect.tryPromise(() => runtime.handler(tenantSwitchRequest(secondTenantId))); expect(switchResponse.status).toBe(200); expect(yield* Effect.tryPromise(() => switchResponse.json())).toEqual({ selectedTenantId: secondTenantId, @@ -1772,20 +1500,12 @@ it.live( }) .from(session) .where(eq(session.userId, betterAuthUserId)); - assertOptionalField( - sessionsAfterSwitch[0], - 'activeTenantId', - secondTenantId - ); + assertOptionalField(sessionsAfterSwitch[0], 'activeTenantId', secondTenantId); assertOptionalField(sessionsAfterSwitch[0], 'activeLegalEntityId', null); const currentSessionAfterSwitch = yield* authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)); - assertOptionalField( - currentSessionAfterSwitch.identity, - 'principalId', - secondPrincipalId - ); + assertOptionalField(currentSessionAfterSwitch.identity, 'principalId', secondPrincipalId); const idempotentSwitch = yield* authentication .switchTenant(secondTenantId, authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)); @@ -1794,8 +1514,8 @@ it.live( runtime.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, - }) - ) + }), + ), ); expect(yield* Effect.tryPromise(() => secondModules.json())).toEqual({ navigation: [], @@ -1812,17 +1532,12 @@ it.live( origin: configuration.baseUrl, }), method: 'POST', - }) - ) - ); - const { principal: verifiedPrincipal } = yield* verifiedGatewayAssertion( - assertionResponse, - pair.publicKey + }), + ), ); + const { principal: verifiedPrincipal } = yield* verifiedGatewayAssertion(assertionResponse, pair.publicKey); expect(verifiedPrincipal.authBindingId).toBe(secondAuthBindingId); - expect(optionalText(verifiedPrincipal.authContextRef)).toMatch( - /^better-auth-session:/u - ); + expect(optionalText(verifiedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); expect(verifiedPrincipal.authMethod).toBe('session'); expect(verifiedPrincipal.legalEntityId).toBe(secondLegalEntityId); expect(verifiedPrincipal.principalId).toBe(secondPrincipalId); @@ -1842,7 +1557,7 @@ it.live( RETURN NEW; END; $function$ - `) + `), ); yield* adminAuthDatabase.execute( sql.raw(` @@ -1850,7 +1565,7 @@ it.live( BEFORE UPDATE ON auth.session FOR EACH ROW EXECUTE FUNCTION auth.tenant_switch_test_fail_internal_persistence() - `) + `), ); yield* Effect.scoped( Effect.gen(function* integrationEffect12() { @@ -1860,37 +1575,29 @@ it.live( yield* adminAuthDatabase.execute( sql.raw(` DROP TRIGGER IF EXISTS tenant_switch_test_internal_persistence_failure ON auth.session - `) + `), ); yield* adminAuthDatabase.execute( sql.raw(` DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_internal_persistence() - `) + `), ); - }, Effect.orDie) + }, Effect.orDie), ); const unexpectedSwitchResponse = yield* Effect.tryPromise(() => runtime.handler( tenantSwitchRequest(firstTenantId, { 'x-correlation-id': 'unexpected-switch-persistence-test', - }) - ) + }), + ), ); expect(unexpectedSwitchResponse.status).toBe(500); - expect( - yield* Effect.tryPromise(() => unexpectedSwitchResponse.text()) - ).not.toMatch(/secret auth persistence defect|P0001/u); - const sessionsAfterUnexpectedSwitchFailure = - yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterUnexpectedSwitchFailure[0], - 'activeTenantId', - secondTenantId + expect(yield* Effect.tryPromise(() => unexpectedSwitchResponse.text())).not.toMatch( + /secret auth persistence defect|P0001/u, ); - yield* authDatabase - .update(session) - .set({ activeTenantId: null }) - .where(eq(session.userId, betterAuthUserId)); + const sessionsAfterUnexpectedSwitchFailure = yield* readActiveTenantIds(); + assertOptionalField(sessionsAfterUnexpectedSwitchFailure[0], 'activeTenantId', secondTenantId); + yield* authDatabase.update(session).set({ activeTenantId: null }).where(eq(session.userId, betterAuthUserId)); const unexpectedLegacyUpgradeResponse = yield* Effect.tryPromise(() => runtime.handler( new Request(`${configuration.baseUrl}/auth/session`, { @@ -1899,32 +1606,23 @@ it.live( origin: configuration.baseUrl, 'x-correlation-id': 'unexpected-legacy-upgrade-test', }), - }) - ) + }), + ), ); expect(unexpectedLegacyUpgradeResponse.status).toBe(500); - expect( - yield* Effect.tryPromise(() => unexpectedLegacyUpgradeResponse.text()) - ).not.toMatch(/secret auth persistence defect|P0001/u); - const sessionsAfterUnexpectedLegacyUpgrade = - yield* readActiveTenantIds(); - assertOptionalField( - sessionsAfterUnexpectedLegacyUpgrade[0], - 'activeTenantId', - null + expect(yield* Effect.tryPromise(() => unexpectedLegacyUpgradeResponse.text())).not.toMatch( + /secret auth persistence defect|P0001/u, ); - }) + const sessionsAfterUnexpectedLegacyUpgrade = yield* readActiveTenantIds(); + assertOptionalField(sessionsAfterUnexpectedLegacyUpgrade[0], 'activeTenantId', null); + }), ); const upgradedSession = yield* authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)); assertOptionalField(upgradedSession.identity, 'tenantId', firstTenantId); const upgradedSessionRows = yield* readActiveTenantIds(); - assertOptionalField( - upgradedSessionRows[0], - 'activeTenantId', - firstTenantId - ); + assertOptionalField(upgradedSessionRows[0], 'activeTenantId', firstTenantId); yield* authentication .switchTenant(secondTenantId, authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)); @@ -1936,9 +1634,7 @@ it.live( }) .where(eq(principalAuthBindings.tenantId, secondTenantId)); yield* assertSessionForbidden( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)) + authentication.currentSession(authenticatedHeaders).pipe(Effect.provide(multiAuthenticationContextLayer)), ); yield* coreDatabase .update(principalAuthBindings) @@ -1951,17 +1647,11 @@ it.live( // Production evidence retains referenced bindings. Clear only this fixture's evidence so the // resolver can still prove that an existing selected session rejects a genuinely missing row. yield* purgeFixtureRows([ - coreDatabase - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, secondTenantId)), - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, secondTenantId)), + coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, secondTenantId)), + coreDatabase.delete(principalAuthBindings).where(eq(principalAuthBindings.tenantId, secondTenantId)), ]); yield* assertSessionForbidden( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)) + authentication.currentSession(authenticatedHeaders).pipe(Effect.provide(multiAuthenticationContextLayer)), ); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts b/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts index 0ec167be2..54cf9eb59 100644 --- a/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts +++ b/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts @@ -17,93 +17,78 @@ export const GENERATED_OWNER = { slug: 'isolation-owner', } as const; -const json = (value: Value): string => - `${JSON.stringify(value, null, 2)}\n`; +const json = (value: Value): string => `${JSON.stringify(value, null, 2)}\n`; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..'); -const writeFixtureFile = Effect.fn('writeFixtureFile')( - function* writeFixtureFileEffect( - root: string, - relativePath: string, - content: string - ) { - const fileSystem = yield* FileSystem.FileSystem; - const filePath = path.join(root, relativePath); - yield* fileSystem.makeDirectory(path.dirname(filePath), { - recursive: true, - }); - yield* fileSystem.writeFileString(filePath, content); - } -); +const writeFixtureFile = Effect.fn('writeFixtureFile')(function* writeFixtureFileEffect( + root: string, + relativePath: string, + content: string, +) { + const fileSystem = yield* FileSystem.FileSystem; + const filePath = path.join(root, relativePath); + yield* fileSystem.makeDirectory(path.dirname(filePath), { + recursive: true, + }); + yield* fileSystem.writeFileString(filePath, content); +}); -const replaceRequired = ( - source: string, - current: string, - replacement: string -): string => { +const replaceRequired = (source: string, current: string, replacement: string): string => { if (!source.includes(current)) { - throw new Error( - `Generated isolation fixture no longer contains ${JSON.stringify(current)}` - ); + throw new Error(`Generated isolation fixture no longer contains ${JSON.stringify(current)}`); } return source.replace(current, replacement); }; -const createWorkspace = Effect.fn('createWorkspace')( - function* createWorkspaceEffect(root: string) { - yield* writeFixtureFile( - root, - 'package.json', - json({ name: 'generated-owner-fixture', private: true }) - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/module-federation.config.ts`, - 'export default { exposes: {} };\n' - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/tsconfig.json`, - json({ - compilerOptions: { composite: true }, - include: ['api', 'shared', 'src'], - }) - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/package.json`, - json({ - dependencies: {}, - modernjs: { - apiRuntime: 'effect', - appId: GENERATED_OWNER.appId, - preset: 'presetUltramodern', - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: '@app/isolation-owner', - private: true, - scripts: { - build: - 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', - 'cloudflare:build': - 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', - }, - type: 'module', - version: '0.0.0', - }) - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/shared/api.ts`, - `import { HttpApi } from '@modern-js/plugin-bff/effect-client'; +const createWorkspace = Effect.fn('createWorkspace')(function* createWorkspaceEffect(root: string) { + yield* writeFixtureFile(root, 'package.json', json({ name: 'generated-owner-fixture', private: true })); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/module-federation.config.ts`, + 'export default { exposes: {} };\n', + ); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/tsconfig.json`, + json({ + compilerOptions: { composite: true }, + include: ['api', 'shared', 'src'], + }), + ); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/package.json`, + json({ + dependencies: {}, + modernjs: { + apiRuntime: 'effect', + appId: GENERATED_OWNER.appId, + preset: 'presetUltramodern', + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', + }, + name: '@app/isolation-owner', + private: true, + scripts: { + build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + 'cloudflare:build': + 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', + }, + type: 'module', + version: '0.0.0', + }), + ); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/shared/api.ts`, + `import { HttpApi } from '@modern-js/plugin-bff/effect-client'; export const isolationOwnerApi = HttpApi.make('IsolationOwnerApi'); -` - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/api/index.ts`, - `import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; +`, + ); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/api/index.ts`, + `import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import type { EffectRuntimeLayer } from '@modern-js/plugin-bff/effect-edge'; import { isolationOwnerApi } from '../shared/api.ts'; @@ -111,100 +96,86 @@ const layer = HttpApiBuilder.layer(isolationOwnerApi).pipe( Layer.provide(Layer.empty), ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: isolationOwnerApi, layer }); -` - ); - yield* writeFixtureFile( - root, - `verticals/${GENERATED_OWNER.slug}/src/routes/ultramodern-route-head.tsx`, - 'export const UltramodernRouteHead = () => null;\n' - ); - yield* writeFixtureFile( - root, - 'topology/reference-topology.json', - json({ - schemaVersion: 1, - verticals: [ - { - domain: 'isolation', - id: GENERATED_OWNER.appId, - kind: 'vertical', - moduleFederation: { - name: 'verticalIsolationOwner', - role: 'remote', - }, - package: '@app/isolation-owner', - path: `verticals/${GENERATED_OWNER.slug}`, +`, + ); + yield* writeFixtureFile( + root, + `verticals/${GENERATED_OWNER.slug}/src/routes/ultramodern-route-head.tsx`, + 'export const UltramodernRouteHead = () => null;\n', + ); + yield* writeFixtureFile( + root, + 'topology/reference-topology.json', + json({ + schemaVersion: 1, + verticals: [ + { + domain: 'isolation', + id: GENERATED_OWNER.appId, + kind: 'vertical', + moduleFederation: { + name: 'verticalIsolationOwner', + role: 'remote', }, - ], - }) - ); - } -); - -const linkRuntimeDependencies = Effect.fn('linkRuntimeDependencies')( - function* linkRuntimeDependenciesEffect(root: string) { - const fileSystem = yield* FileSystem.FileSystem; - yield* fileSystem.makeDirectory(path.join(root, 'node_modules', '@app'), { - recursive: true, - }); - yield* fileSystem.makeDirectory( - path.join(root, 'node_modules', '@modern-js'), - { - recursive: true, - } - ); - yield* Effect.all( - [ - fileSystem.symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(root, 'node_modules/@app/core-runtime') - ), - fileSystem.symlink( - path.join(appRoot, 'packages/shared-contracts'), - path.join(root, 'node_modules/@app/shared-contracts') - ), - fileSystem.symlink( - path.join(appRoot, 'packages/gateway-principal-verifier'), - path.join(root, 'node_modules/@app/gateway-principal-verifier') - ), - fileSystem.symlink( - path.join( - appRoot, - 'apps/shell-super-app/node_modules/@modern-js/plugin-bff' - ), - path.join(root, 'node_modules/@modern-js/plugin-bff') - ), - fileSystem.symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/drizzle-orm'), - path.join(root, 'node_modules/drizzle-orm') - ), - fileSystem.symlink( - path.join(appRoot, 'node_modules/effect'), - path.join(root, 'node_modules/effect') - ), - fileSystem.symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), - path.join(root, 'node_modules/jose') - ), + package: '@app/isolation-owner', + path: `verticals/${GENERATED_OWNER.slug}`, + }, ], - { concurrency: 'unbounded', discard: true } - ); - } -); + }), + ); +}); -const addResourceType = Effect.fn('addResourceType')( - function* addResourceTypeEffect(root: string) { - const fileSystem = yield* FileSystem.FileSystem; - const manifestPath = path.join( - root, - `verticals/${GENERATED_OWNER.slug}/vertical.manifest.ts` - ); - const manifest = yield* fileSystem.readFileString(manifestPath); - const withResourceType = replaceRequired( - manifest, - ` ${MODULE_MANIFEST_RESOURCE_SLOT_START} +const linkRuntimeDependencies = Effect.fn('linkRuntimeDependencies')(function* linkRuntimeDependenciesEffect( + root: string, +) { + const fileSystem = yield* FileSystem.FileSystem; + yield* fileSystem.makeDirectory(path.join(root, 'node_modules', '@app'), { + recursive: true, + }); + yield* fileSystem.makeDirectory(path.join(root, 'node_modules', '@modern-js'), { + recursive: true, + }); + yield* Effect.all( + [ + fileSystem.symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(root, 'node_modules/@app/core-runtime'), + ), + fileSystem.symlink( + path.join(appRoot, 'packages/shared-contracts'), + path.join(root, 'node_modules/@app/shared-contracts'), + ), + fileSystem.symlink( + path.join(appRoot, 'packages/gateway-principal-verifier'), + path.join(root, 'node_modules/@app/gateway-principal-verifier'), + ), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-bff'), + path.join(root, 'node_modules/@modern-js/plugin-bff'), + ), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/drizzle-orm'), + path.join(root, 'node_modules/drizzle-orm'), + ), + fileSystem.symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), + path.join(root, 'node_modules/jose'), + ), + ], + { concurrency: 'unbounded', discard: true }, + ); +}); + +const addResourceType = Effect.fn('addResourceType')(function* addResourceTypeEffect(root: string) { + const fileSystem = yield* FileSystem.FileSystem; + const manifestPath = path.join(root, `verticals/${GENERATED_OWNER.slug}/vertical.manifest.ts`); + const manifest = yield* fileSystem.readFileString(manifestPath); + const withResourceType = replaceRequired( + manifest, + ` ${MODULE_MANIFEST_RESOURCE_SLOT_START} ${MODULE_MANIFEST_RESOURCE_SLOT_END}`, - ` ${MODULE_MANIFEST_RESOURCE_SLOT_START} + ` ${MODULE_MANIFEST_RESOURCE_SLOT_START} { capabilities: { graphVisible: false, @@ -218,12 +189,12 @@ const addResourceType = Effect.fn('addResourceType')( label: 'Isolation record', owningModuleId: '${GENERATED_OWNER.moduleId}', }, - ${MODULE_MANIFEST_RESOURCE_SLOT_END}` - ); - const withResourceDetail = replaceRequired( - withResourceType, - ' resourceDetails: [],', - ` resourceDetails: [ + ${MODULE_MANIFEST_RESOURCE_SLOT_END}`, + ); + const withResourceDetail = replaceRequired( + withResourceType, + ' resourceDetails: [],', + ` resourceDetails: [ { apiKey: '${GENERATED_OWNER.moduleId}.resource-detail', contributionKey: '${GENERATED_OWNER.moduleId}.detail.record', @@ -237,14 +208,14 @@ const addResourceType = Effect.fn('addResourceType')( }, resourceType: '${GENERATED_OWNER.resourceType}', }, - ],` - ); - yield* fileSystem.writeFileString( - manifestPath, - replaceRequired( - withResourceDetail, - ' timelines: [],', - ` timelines: [ + ],`, + ); + yield* fileSystem.writeFileString( + manifestPath, + replaceRequired( + withResourceDetail, + ' timelines: [],', + ` timelines: [ { apiKey: '${GENERATED_OWNER.moduleId}.resource-list', contributionKey: '${GENERATED_OWNER.moduleId}.timeline.record', @@ -258,34 +229,30 @@ const addResourceType = Effect.fn('addResourceType')( }, resourceType: '${GENERATED_OWNER.resourceType}', }, - ],` - ) - ); - } -); + ],`, + ), + ); +}); const adaptContract = Effect.fn('adaptContract')(function* adaptContractEffect( root: string, name: 'resource-detail' | 'resource-list', request: string, - response: string + response: string, ) { const fileSystem = yield* FileSystem.FileSystem; - const contractPath = path.join( - root, - `verticals/${GENERATED_OWNER.slug}/shared/apis/${name}.ts` - ); + const contractPath = path.join(root, `verticals/${GENERATED_OWNER.slug}/shared/apis/${name}.ts`); let contract = yield* fileSystem.readFileString(contractPath); const type = name === 'resource-detail' ? 'ResourceDetail' : 'ResourceList'; contract = replaceRequired( contract, `export const ${type}RequestSchema = Schema.Struct({});`, - `export const ${type}RequestSchema = ${request};` + `export const ${type}RequestSchema = ${request};`, ); contract = replaceRequired( contract, `export const ${type}ResponseSchema = Schema.Struct({ ok: Schema.Literal(true) });`, - `export const ${type}ResponseSchema = ${response};` + `export const ${type}ResponseSchema = ${response};`, ); yield* fileSystem.writeFileString(contractPath, contract); }); @@ -552,148 +519,125 @@ export const createRecordAction = defineAction( ); `; -const adaptGeneratedOwner = Effect.fn('adaptGeneratedOwner')( - function* adaptGeneratedOwnerEffect(root: string, schemaName: string) { - const verticalRoot = `verticals/${GENERATED_OWNER.slug}`; - yield* adaptContract( - root, - 'resource-detail', - 'Schema.Struct({ resourceId: Schema.String.check(Schema.isUUID()) })', - `Schema.Struct({ +const adaptGeneratedOwner = Effect.fn('adaptGeneratedOwner')(function* adaptGeneratedOwnerEffect( + root: string, + schemaName: string, +) { + const verticalRoot = `verticals/${GENERATED_OWNER.slug}`; + yield* adaptContract( + root, + 'resource-detail', + 'Schema.Struct({ resourceId: Schema.String.check(Schema.isUUID()) })', + `Schema.Struct({ fields: Schema.Array(Schema.Struct({ label: Schema.String, value: Schema.String })), title: Schema.String, -})` - ); - yield* adaptContract( - root, - 'resource-list', - 'Schema.Struct({ resourceId: Schema.String.check(Schema.isUUID()) })', - `Schema.Struct({ +})`, + ); + yield* adaptContract( + root, + 'resource-list', + 'Schema.Struct({ resourceId: Schema.String.check(Schema.isUUID()) })', + `Schema.Struct({ entries: Schema.Array(Schema.Struct({ occurredAt: Schema.String, summary: Schema.String, timelineEntryId: Schema.String, })), projectionLagging: Schema.Boolean, -})` - ); - yield* Effect.all( - [ - writeFixtureFile( - root, - `${verticalRoot}/src/isolation/instrumentation.ts`, - instrumentationSource - ), - writeFixtureFile( - root, - `${verticalRoot}/src/isolation/owner-repository.ts`, - ownerRepositorySource(schemaName) - ), - writeFixtureFile( - root, - `${verticalRoot}/src/api/resource-detail.read.ts`, - detailReadSource - ), - writeFixtureFile( - root, - `${verticalRoot}/src/api/resource-list.read.ts`, - listReadSource - ), - writeFixtureFile( - root, - `${verticalRoot}/src/search/records.provider.ts`, - searchReadSource - ), - writeFixtureFile( - root, - `${verticalRoot}/src/actions/create-record.action.ts`, - actionSource - ), - ], - { concurrency: 'unbounded', discard: true } - ); - } -); - -export const createGeneratedOwnerFixture = Effect.fn( - 'createGeneratedOwnerFixture' -)(function* createGeneratedOwnerFixtureEffect(schemaName: string) { - const fileSystem = yield* FileSystem.FileSystem; - const root = yield* fileSystem.makeTempDirectoryScoped({ - directory: tmpdir(), - prefix: 'ontos-generated-owner-', - }); - yield* createWorkspace(root); - yield* runScaffoldEffect( - 'module-contract', - ['--vertical', GENERATED_OWNER.slug, '--module', GENERATED_OWNER.moduleId], - { workspaceRoot: root } +})`, ); - yield* addResourceType(root); - yield* runScaffoldEffect( - 'action', + yield* Effect.all( [ - '--vertical', - GENERATED_OWNER.slug, - '--action', - 'create-record', - '--authorization', - 'action_execution', - '--legal-entity-scope', - 'required', - '--provisioning', - 'tenant_membership_default', + writeFixtureFile(root, `${verticalRoot}/src/isolation/instrumentation.ts`, instrumentationSource), + writeFixtureFile(root, `${verticalRoot}/src/isolation/owner-repository.ts`, ownerRepositorySource(schemaName)), + writeFixtureFile(root, `${verticalRoot}/src/api/resource-detail.read.ts`, detailReadSource), + writeFixtureFile(root, `${verticalRoot}/src/api/resource-list.read.ts`, listReadSource), + writeFixtureFile(root, `${verticalRoot}/src/search/records.provider.ts`, searchReadSource), + writeFixtureFile(root, `${verticalRoot}/src/actions/create-record.action.ts`, actionSource), ], - { workspaceRoot: root } + { concurrency: 'unbounded', discard: true }, ); - yield* runScaffoldEffect( - 'module-api', - [ - '--vertical', - GENERATED_OWNER.slug, - '--name', - 'resource-detail', - '--authorization', - 'context_permission', - '--permission', - 'module.access', - ], - { workspaceRoot: root } - ); - yield* runScaffoldEffect( - 'module-api', - [ - '--vertical', - GENERATED_OWNER.slug, - '--name', - 'resource-list', - '--authorization', - 'context_permission', - '--permission', - 'module.access', - ], - { workspaceRoot: root } - ); - yield* runScaffoldEffect( - 'search-provider', - [ - '--vertical', - GENERATED_OWNER.slug, - '--name', - 'records', - '--resource', - 'record', - '--authorization', - 'context_permission', - '--permission', - 'module.access', - ], - { workspaceRoot: root } - ); - yield* adaptGeneratedOwner(root, schemaName); - yield* linkRuntimeDependencies(root); - return { - root, - verticalRoot: path.join(root, 'verticals', GENERATED_OWNER.slug), - }; }); + +export const createGeneratedOwnerFixture = Effect.fn('createGeneratedOwnerFixture')( + function* createGeneratedOwnerFixtureEffect(schemaName: string) { + const fileSystem = yield* FileSystem.FileSystem; + const root = yield* fileSystem.makeTempDirectoryScoped({ + directory: tmpdir(), + prefix: 'ontos-generated-owner-', + }); + yield* createWorkspace(root); + yield* runScaffoldEffect( + 'module-contract', + ['--vertical', GENERATED_OWNER.slug, '--module', GENERATED_OWNER.moduleId], + { workspaceRoot: root }, + ); + yield* addResourceType(root); + yield* runScaffoldEffect( + 'action', + [ + '--vertical', + GENERATED_OWNER.slug, + '--action', + 'create-record', + '--authorization', + 'action_execution', + '--legal-entity-scope', + 'required', + '--provisioning', + 'tenant_membership_default', + ], + { workspaceRoot: root }, + ); + yield* runScaffoldEffect( + 'module-api', + [ + '--vertical', + GENERATED_OWNER.slug, + '--name', + 'resource-detail', + '--authorization', + 'context_permission', + '--permission', + 'module.access', + ], + { workspaceRoot: root }, + ); + yield* runScaffoldEffect( + 'module-api', + [ + '--vertical', + GENERATED_OWNER.slug, + '--name', + 'resource-list', + '--authorization', + 'context_permission', + '--permission', + 'module.access', + ], + { workspaceRoot: root }, + ); + yield* runScaffoldEffect( + 'search-provider', + [ + '--vertical', + GENERATED_OWNER.slug, + '--name', + 'records', + '--resource', + 'record', + '--authorization', + 'context_permission', + '--permission', + 'module.access', + ], + { workspaceRoot: root }, + ); + yield* adaptGeneratedOwner(root, schemaName); + yield* linkRuntimeDependencies(root); + return { + root, + verticalRoot: path.join(root, 'verticals', GENERATED_OWNER.slug), + }; + }, +); diff --git a/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts b/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts index f8db4fe6c..666ba4da8 100644 --- a/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts +++ b/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts @@ -24,22 +24,9 @@ import type { } from '@app/core-runtime'; import { v1 } from '@authzed/authzed-node'; import { NodeServices } from '@effect/platform-node'; -import { - defineEffectBff, - HttpApiBuilder, -} from '@modern-js/plugin-bff/effect-edge'; +import { defineEffectBff, HttpApiBuilder } from '@modern-js/plugin-bff/effect-edge'; import type { EffectRuntimeLayer } from '@modern-js/plugin-bff/effect-edge'; -import { - Clock, - Config, - ConfigProvider, - Effect, - Layer, - Logger, - Predicate, - Redacted, - Schema, -} from 'effect'; +import { Clock, Config, ConfigProvider, Effect, Layer, Logger, Predicate, Redacted, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { TestClock } from 'effect/testing'; import { HttpApi } from 'effect/unstable/httpapi'; @@ -79,16 +66,10 @@ import { import { GatewayPrincipalVerifierLive } from '../../../../packages/gateway-principal-verifier/src/server.ts'; import { deriveOntosModuleDeploymentContract } from '../../../../scripts/generate-ontos-module-contract.mts'; import type { GatewayIssuerConfigValue } from '../../api/auth/gateway-issuer-config.ts'; -import { - issueGatewayContextAssertion, - makeGatewayIssuerLayer, -} from '../../api/auth/gateway-issuer.ts'; +import { issueGatewayContextAssertion, makeGatewayIssuerLayer } from '../../api/auth/gateway-issuer.ts'; import { ShellInstalledModuleCatalog } from '../../api/modules/installed-module-catalog.ts'; import { ShellCompositionFactoryLive } from '../../api/modules/shell-composition.ts'; -import { - ShellGovernedReads, - createShellGovernedReadsLayer, -} from '../../api/modules/shell-governed-reads.ts'; +import { ShellGovernedReads, createShellGovernedReadsLayer } from '../../api/modules/shell-governed-reads.ts'; import { ResourceRefSchema, ShellProviderUnavailableError, @@ -97,60 +78,40 @@ import { } from '../../api/modules/shell-resources.ts'; import type { ShellResourceGateways } from '../../api/modules/shell-resources.ts'; import { makeContextAccessDouble } from '../support/context-access-double.ts'; -import { - GENERATED_OWNER, - createGeneratedOwnerFixture, -} from './generated-owner-fixture.ts'; +import { GENERATED_OWNER, createGeneratedOwnerFixture } from './generated-owner-fixture.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const TestSpiceDbConfig = Config.all({ - endpoint: Config.string('SPICEDB_ENDPOINT').pipe( - Config.withDefault('localhost:50051') - ), - insecureLocal: Config.boolean('SPICEDB_INSECURE').pipe( - Config.withDefault(true) - ), + endpoint: Config.string('SPICEDB_ENDPOINT').pipe(Config.withDefault('localhost:50051')), + insecureLocal: Config.boolean('SPICEDB_INSECURE').pipe(Config.withDefault(true)), preSharedKey: Config.redacted('SPICEDB_PRESHARED_KEY').pipe( - Config.withDefault(Redacted.make('ontos-local-development-key')) + Config.withDefault(Redacted.make('ontos-local-development-key')), ), }).pipe( Effect.map(({ endpoint, insecureLocal, preSharedKey }) => ({ endpoint, insecureLocal, preSharedKey: Redacted.value(preSharedKey), - })) + })), ); const testGatewayAssertionRedemption: GatewayAssertionRedemption = { consume: () => Effect.void, }; -type OwnerHttpHandler = ReturnType< - ReturnType['createHandler'] ->; +type OwnerHttpHandler = ReturnType['createHandler']>; const disposeOwnerHandlers = (handlers: readonly OwnerHttpHandler[]) => Effect.forEach( handlers, - (handler) => - Effect.tryPromise(() => handler.dispose()).pipe( - Effect.catchCause(() => Effect.void) - ), - { concurrency: 'unbounded', discard: true } + (handler) => Effect.tryPromise(() => handler.dispose()).pipe(Effect.catchCause(() => Effect.void)), + { concurrency: 'unbounded', discard: true }, ); const OwnerDetailSchema = Schema.Struct({ - fields: Schema.Array( - Schema.Struct({ label: Schema.String, value: Schema.String }) - ), + fields: Schema.Array(Schema.Struct({ label: Schema.String, value: Schema.String })), title: Schema.String, }); const OwnerTimelineSchema = Schema.Struct({ @@ -159,13 +120,11 @@ const OwnerTimelineSchema = Schema.Struct({ occurredAt: Schema.DateTimeUtcFromString, summary: Schema.String, timelineEntryId: Schema.String.pipe(Schema.brand('TimelineEntryId')), - }) + }), ), projectionLagging: Schema.Boolean, }); -const OwnerSearchSchema = Schema.Array( - Schema.Struct({ ref: ResourceRefSchema, title: Schema.String }) -); +const OwnerSearchSchema = Schema.Array(Schema.Struct({ ref: ResourceRefSchema, title: Schema.String })); interface GeneratedOwnerModules { // Generated source is imported from a temporary path, so TypeScript cannot retain the private // Action-registration symbols across the dynamic module boundary. Runtime checks below prove it. @@ -184,7 +143,7 @@ interface GeneratedOwnerModules { options: { readonly environment: Readonly>; readonly redemption: GatewayAssertionRedemption; - } + }, ) => Effect.Effect; readonly wiring: { readonly action: boolean; @@ -198,50 +157,37 @@ type OwnerGroupLayer = Layer.Layer; const DynamicModuleSchema = Schema.Record(Schema.String, Schema.Unknown); const OwnerApiSchema = Schema.declare(HttpApi.isHttpApi); const OwnerGroupLayerSchema = Schema.declare(Layer.isLayer); -const VerticalRuntimeRegistrationSchema = - Schema.declare( - (value): value is VerticalRuntimeRegistration => - Predicate.isObjectKeyword(value) - ); +const VerticalRuntimeRegistrationSchema = Schema.declare( + (value): value is VerticalRuntimeRegistration => Predicate.isObjectKeyword(value), +); const OwnerCountsSchema = Schema.Struct({ action: Schema.Number, detail: Schema.Number, list: Schema.Number, search: Schema.Number, }); -const OwnerVerifierSchema = Schema.declare< - GeneratedOwnerModules['verifyActionPrincipal'] ->((value): value is GeneratedOwnerModules['verifyActionPrincipal'] => - Predicate.isFunction(value) +const OwnerVerifierSchema = Schema.declare( + (value): value is GeneratedOwnerModules['verifyActionPrincipal'] => Predicate.isFunction(value), ); -const EffectRuntimeLayerSchema = Schema.declare( - (value): value is EffectRuntimeLayer => Predicate.isObjectKeyword(value) +const EffectRuntimeLayerSchema = Schema.declare((value): value is EffectRuntimeLayer => + Predicate.isObjectKeyword(value), ); const isEffectRuntimeLayer = Schema.is(EffectRuntimeLayerSchema); -const requiredValue = ( - value: Value | null | undefined, - label: string -): Value => { +const requiredValue = (value: Value | null | undefined, label: string): Value => { if (value === undefined || value === null) { throw new TypeError(`${label} is required by the generated-owner fixture`); } return value; }; -const isOperationContextDenied = Schema.is( - Schema.Struct({ _tag: Schema.Literal('OperationContextDenied') }) -); -const isCreateRecordRejected = Schema.is( - Schema.Struct({ _tag: Schema.Literal('CreateRecordRejected') }) -); -const isActionHandlerExecutionError = Schema.is( - Schema.Struct({ _tag: Schema.Literal('ActionHandlerExecutionError') }) -); +const isOperationContextDenied = Schema.is(Schema.Struct({ _tag: Schema.Literal('OperationContextDenied') })); +const isCreateRecordRejected = Schema.is(Schema.Struct({ _tag: Schema.Literal('CreateRecordRejected') })); +const isActionHandlerExecutionError = Schema.is(Schema.Struct({ _tag: Schema.Literal('ActionHandlerExecutionError') })); const relationship = ( resourceType: string, resourceId: string, relation: string, subjectType: string, - subjectId: string + subjectId: string, ) => v1.Relationship.create({ relation, @@ -256,35 +202,25 @@ const relationship = ( }), }), }); -const makeCatalog = ( - contract: OntosModuleDeploymentContract -): InstalledModuleCatalog => - buildInstalledModuleCatalog([ - { contract, expectedAppId: GENERATED_OWNER.appId }, - ]); +const makeCatalog = (contract: OntosModuleDeploymentContract): InstalledModuleCatalog => + buildInstalledModuleCatalog([{ contract, expectedAppId: GENERATED_OWNER.appId }]); const makeOwnerHandler = ( api: OwnerApi, group: OwnerGroupLayer, runtime: ReadRuntimeService, loggerLayer: Layer.Layer, - configLayer: Layer.Layer + configLayer: Layer.Layer, ) => { const loggedRuntime: ReadRuntimeService = { - runRead: (input) => - runtime.runRead(input).pipe(Effect.provide(loggerLayer)), + runRead: (input) => runtime.runRead(input).pipe(Effect.provide(loggerLayer)), }; const ownerLayerCandidate = HttpApiBuilder.layer(api).pipe( Layer.provide(group), Layer.provide(GatewayPrincipalVerifierLive), - Layer.provide( - Layer.succeed( - GatewayAssertionRedemptionService, - testGatewayAssertionRedemption - ) - ), + Layer.provide(Layer.succeed(GatewayAssertionRedemptionService, testGatewayAssertionRedemption)), Layer.provide(Layer.succeed(ReadRuntime, loggedRuntime)), Layer.provide(loggerLayer), - Layer.provide(configLayer) + Layer.provide(configLayer), ); if (!isEffectRuntimeLayer(ownerLayerCandidate)) { throw new TypeError('Generated owner BFF Layer is invalid'); @@ -295,48 +231,29 @@ const makeOwnerHandler = ( return handler; }; const loadClientWiring = Effect.fnUntraced(function* loadClientWiring( - entrypoints: ReturnType + entrypoints: ReturnType, ) { const [detailClient, listClient, searchClient] = yield* Effect.all( [ - Effect.tryPromise(() => - Promise.resolve(entrypoints.api['resource-detail']?.()) - ), - Effect.tryPromise(() => - Promise.resolve(entrypoints.api['resource-list']?.()) - ), - Effect.tryPromise(() => - Promise.resolve(entrypoints.search['records']?.()) - ), + Effect.tryPromise(() => Promise.resolve(entrypoints.api['resource-detail']?.())), + Effect.tryPromise(() => Promise.resolve(entrypoints.api['resource-list']?.())), + Effect.tryPromise(() => Promise.resolve(entrypoints.search['records']?.())), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); return { action: true, detailClient: detailClient !== undefined && Predicate.isFunction( - Object.getOwnPropertyDescriptor( - detailClient, - 'executeResourceDetailWithAuthorization' - )?.value + Object.getOwnPropertyDescriptor(detailClient, 'executeResourceDetailWithAuthorization')?.value, ), listClient: listClient !== undefined && - Predicate.isFunction( - Object.getOwnPropertyDescriptor( - listClient, - 'executeResourceListWithAuthorization' - )?.value - ), + Predicate.isFunction(Object.getOwnPropertyDescriptor(listClient, 'executeResourceListWithAuthorization')?.value), searchClient: searchClient !== undefined && - Predicate.isFunction( - Object.getOwnPropertyDescriptor( - searchClient, - 'loadRecordsClientWithAuthorization' - )?.value - ), + Predicate.isFunction(Object.getOwnPropertyDescriptor(searchClient, 'loadRecordsClientWithAuthorization')?.value), }; }); @@ -344,97 +261,64 @@ const loadGeneratedOwner = Effect.fnUntraced(function* runIntegration1( verticalRoot: string, runtime: ReadRuntimeService, loggerLayer: Layer.Layer, - configLayer: Layer.Layer + configLayer: Layer.Layer, ) { - const load = Effect.fnUntraced(function* runIntegration2( - relativePath: string - ) { + const load = Effect.fnUntraced(function* runIntegration2(relativePath: string) { const importedModule: unknown = yield* Effect.tryPromise( - () => import(pathToFileURL(`${verticalRoot}/${relativePath}`).href) - ); - return yield* Schema.decodeUnknownEffect(DynamicModuleSchema)( - importedModule + () => import(pathToFileURL(`${verticalRoot}/${relativePath}`).href), ); + return yield* Schema.decodeUnknownEffect(DynamicModuleSchema)(importedModule); }); - const [ - detailApi, - detailServer, - listApi, - listServer, - searchApi, - searchServer, - verifier, - state, - registrationOwner, - ] = yield* Effect.all( - [ - load('shared/apis/resource-detail.ts'), - load('api/resource-detail-read-server.ts'), - load('shared/apis/resource-list.ts'), - load('api/resource-list-read-server.ts'), - load('shared/apis/records-search.ts'), - load('api/records-search-server.ts'), - load('api/auth/action-principal.ts'), - load('src/isolation/instrumentation.ts'), - load('vertical.registration.ts'), - ], - { concurrency: 'unbounded' } + const [detailApi, detailServer, listApi, listServer, searchApi, searchServer, verifier, state, registrationOwner] = + yield* Effect.all( + [ + load('shared/apis/resource-detail.ts'), + load('api/resource-detail-read-server.ts'), + load('shared/apis/resource-list.ts'), + load('api/resource-list-read-server.ts'), + load('shared/apis/records-search.ts'), + load('api/records-search-server.ts'), + load('api/auth/action-principal.ts'), + load('src/isolation/instrumentation.ts'), + load('vertical.registration.ts'), + ], + { concurrency: 'unbounded' }, + ); + const registration = yield* Schema.decodeUnknownEffect(VerticalRuntimeRegistrationSchema)( + registrationOwner['isolationOwnerRegistration'], ); - const registration = yield* Schema.decodeUnknownEffect( - VerticalRuntimeRegistrationSchema - )(registrationOwner['isolationOwnerRegistration']); const actions = getVerticalRuntimeActions(registration); const entrypoints = getVerticalRuntimeEntrypoints(registration); const wiring = yield* loadClientWiring(entrypoints); - const generatedAction = actions.find( - ({ descriptor }) => descriptor.actionKey === GENERATED_OWNER.actionKey - ); + const generatedAction = actions.find(({ descriptor }) => descriptor.actionKey === GENERATED_OWNER.actionKey); if (generatedAction === undefined) { - throw new TypeError( - 'Generated Action is missing from the owner runtime registration' - ); + throw new TypeError('Generated Action is missing from the owner runtime registration'); } return { action: generatedAction, - counts: yield* Schema.decodeUnknownEffect(OwnerCountsSchema)( - state['generatedOwnerHandlerCounts'] - ), + counts: yield* Schema.decodeUnknownEffect(OwnerCountsSchema)(state['generatedOwnerHandlerCounts']), detail: makeOwnerHandler( - yield* Schema.decodeUnknownEffect(OwnerApiSchema)( - detailApi['ResourceDetailApi'] - ), - yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)( - detailServer['resourceDetailReadApiLive'] - ), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(detailApi['ResourceDetailApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)(detailServer['resourceDetailReadApiLive']), runtime, loggerLayer, - configLayer + configLayer, ), list: makeOwnerHandler( - yield* Schema.decodeUnknownEffect(OwnerApiSchema)( - listApi['ResourceListApi'] - ), - yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)( - listServer['resourceListReadApiLive'] - ), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(listApi['ResourceListApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)(listServer['resourceListReadApiLive']), runtime, loggerLayer, - configLayer + configLayer, ), search: makeOwnerHandler( - yield* Schema.decodeUnknownEffect(OwnerApiSchema)( - searchApi['RecordsSearchApi'] - ), - yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)( - searchServer['recordsReadApiLive'] - ), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(searchApi['RecordsSearchApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)(searchServer['recordsReadApiLive']), runtime, loggerLayer, - configLayer + configLayer, ), - verifyActionPrincipal: yield* Schema.decodeUnknownEffect( - OwnerVerifierSchema - )(verifier['verifyActionPrincipal']), + verifyActionPrincipal: yield* Schema.decodeUnknownEffect(OwnerVerifierSchema)(verifier['verifyActionPrincipal']), wiring, }; }); @@ -443,7 +327,7 @@ const requestOwner = Effect.fnUntraced(function* runIntegration3( path: string, payload: Payload, authorization: string, - correlationId: string + correlationId: string, ) { return yield* Effect.tryPromise(() => handler.handler( @@ -455,21 +339,16 @@ const requestOwner = Effect.fnUntraced(function* runIntegration3( 'x-correlation-id': correlationId, }, method: 'POST', - }) - ) + }), + ), ); }); const decodeResponse = Effect.fnUntraced(function* runIntegration4< ResponseSchema extends Schema.ConstraintDecoder, >(response: Response, schema: ResponseSchema) { - return yield* Schema.decodeUnknownEffect(schema)( - yield* Effect.tryPromise(() => response.json()) - ); + return yield* Schema.decodeUnknownEffect(schema)(yield* Effect.tryPromise(() => response.json())); }); -const createOwnerSchema = Effect.fnUntraced(function* runIntegration5( - admin: Pool, - schemaName: string -) { +const createOwnerSchema = Effect.fnUntraced(function* runIntegration5(admin: Pool, schemaName: string) { const tenantPredicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid`; const entityPredicate = `${tenantPredicate} and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; // Dynamic identifiers are generated locally from UUID hex and never accept external input. @@ -482,7 +361,7 @@ const createOwnerSchema = Effect.fnUntraced(function* runIntegration5( title text not null, primary key (tenant_id, resource_id) ) - `) + `), ); yield* Effect.tryPromise(() => admin.query(` @@ -493,55 +372,39 @@ const createOwnerSchema = Effect.fnUntraced(function* runIntegration5( title text not null, primary key (tenant_id, legal_entity_id, resource_id) ) - `) + `), ); - const configureTable = Effect.fnUntraced(function* runIntegration6( - table: string, - predicate: string - ) { - yield* Effect.tryPromise(() => - admin.query( - `alter table ${schemaName}.${table} enable row level security` - ) - ); - yield* Effect.tryPromise(() => - admin.query(`alter table ${schemaName}.${table} force row level security`) - ); + const configureTable = Effect.fnUntraced(function* runIntegration6(table: string, predicate: string) { + yield* Effect.tryPromise(() => admin.query(`alter table ${schemaName}.${table} enable row level security`)); + yield* Effect.tryPromise(() => admin.query(`alter table ${schemaName}.${table} force row level security`)); yield* Effect.tryPromise(() => admin.query( - `create policy ${table}_select on ${schemaName}.${table} for select to ontos_runtime using (${predicate})` - ) + `create policy ${table}_select on ${schemaName}.${table} for select to ontos_runtime using (${predicate})`, + ), ); yield* Effect.tryPromise(() => admin.query( - `create policy ${table}_insert on ${schemaName}.${table} for insert to ontos_runtime with check (${predicate})` - ) + `create policy ${table}_insert on ${schemaName}.${table} for insert to ontos_runtime with check (${predicate})`, + ), ); yield* Effect.tryPromise(() => admin.query( - `create policy ${table}_update on ${schemaName}.${table} for update to ontos_runtime using (${predicate}) with check (${predicate})` - ) + `create policy ${table}_update on ${schemaName}.${table} for update to ontos_runtime using (${predicate}) with check (${predicate})`, + ), ); yield* Effect.tryPromise(() => admin.query( - `create policy ${table}_delete on ${schemaName}.${table} for delete to ontos_runtime using (${predicate})` - ) + `create policy ${table}_delete on ${schemaName}.${table} for delete to ontos_runtime using (${predicate})`, + ), ); }); yield* Effect.all( - [ - configureTable('tenant_records', tenantPredicate), - configureTable('entity_records', entityPredicate), - ], - { concurrency: 'unbounded' } + [configureTable('tenant_records', tenantPredicate), configureTable('entity_records', entityPredicate)], + { concurrency: 'unbounded' }, ); + yield* Effect.tryPromise(() => admin.query(`grant usage on schema ${schemaName} to ontos_runtime`)); yield* Effect.tryPromise(() => - admin.query(`grant usage on schema ${schemaName} to ontos_runtime`) - ); - yield* Effect.tryPromise(() => - admin.query( - `grant select, insert, update, delete on all tables in schema ${schemaName} to ontos_runtime` - ) + admin.query(`grant select, insert, update, delete on all tables in schema ${schemaName} to ontos_runtime`), ); }); type CoreDatabaseService = Parameters[0]; @@ -555,15 +418,11 @@ type RuntimeActionRegistration = ActionRegistration< string, unknown >; -const RuntimeActionRegistrationSchema = - Schema.declare( - (value): value is RuntimeActionRegistration => - Predicate.isObjectKeyword(value) - ); +const RuntimeActionRegistrationSchema = Schema.declare( + (value): value is RuntimeActionRegistration => Predicate.isObjectKeyword(value), +); const isRuntimeActionRegistration = Schema.is(RuntimeActionRegistrationSchema); -const failingEvidenceDatabase = ( - database: CoreDatabaseService -): CoreDatabaseService => { +const failingEvidenceDatabase = (database: CoreDatabaseService): CoreDatabaseService => { const transactionOverride = { transaction: (runInTransaction, configuration) => database.executor.transaction( @@ -577,17 +436,17 @@ const failingEvidenceDatabase = ( cause: new Error('Injected SQL failure'), message: 'Injected evidence persistence failure', }), - }) + }), ) - : Effect.void - ) + : Effect.void, + ), ), - configuration + configuration, ), } satisfies Pick; const executor: CoreDatabaseService['executor'] = Object.assign( Object.create(database.executor), - transactionOverride + transactionOverride, ); return { executor }; }; @@ -597,14 +456,13 @@ const capturedLoggerLayer = (entries: string[]) => entries.push(JSON.stringify(Logger.formatStructured.log(options))); }), ]); -const ignoreOperationFailure = ( - operation: () => Effect.Effect -): Effect.Effect => operation().pipe(Effect.ignore); +const ignoreOperationFailure = (operation: () => Effect.Effect): Effect.Effect => + operation().pipe(Effect.ignore); const principal = ( tenantId: string, legalEntityId: string, principalId: string, - authBindingId: string + authBindingId: string, ): TrustedPrincipalContext => ({ authBindingId, authContextRef: `better-auth-session:${authBindingId}`, @@ -616,41 +474,30 @@ const principal = ( it.live( 'Codesmith composes the disposable owner Action and receiving read BFFs', Effect.fnUntraced(function* runIntegration7() { - const fixture = yield* createGeneratedOwnerFixture( - `generated_owner_${randomUUID().replaceAll('-', '')}` - ).pipe(Effect.provide(NodeServices.layer)); + const fixture = yield* createGeneratedOwnerFixture(`generated_owner_${randomUUID().replaceAll('-', '')}`).pipe( + Effect.provide(NodeServices.layer), + ); const contract = yield* deriveOntosModuleDeploymentContract({ vertical: GENERATED_OWNER.slug, workspaceRoot: fixture.root, }).pipe(Effect.provide(NodeServices.layer)); const compileRuntime: ReadRuntimeService = { - runRead: () => - Effect.die( - new Error('The compile fixture must not execute a governed read') - ), + runRead: () => Effect.die(new Error('The compile fixture must not execute a governed read')), }; const generated = yield* loadGeneratedOwner( fixture.verticalRoot, compileRuntime, capturedLoggerLayer([]), - TestClock.layer() + TestClock.layer(), ); yield* Effect.acquireRelease( Effect.void, Effect.fnUntraced(function* integrationEffect8() { - yield* disposeOwnerHandlers([ - generated.detail, - generated.list, - generated.search, - ]); - }, Effect.orDie) - ); - expect( - makeCatalog(contract).getByModuleId(GENERATED_OWNER.moduleId) - ).toEqual(contract); - expect(generated.action.descriptor.actionKey).toBe( - GENERATED_OWNER.actionKey + yield* disposeOwnerHandlers([generated.detail, generated.list, generated.search]); + }, Effect.orDie), ); + expect(makeCatalog(contract).getByModuleId(GENERATED_OWNER.moduleId)).toEqual(contract); + expect(generated.action.descriptor.actionKey).toBe(GENERATED_OWNER.actionKey); expect(generated.action.descriptor.legalEntityScope).toBe('required'); expect(generated.counts).toEqual({ action: 0, @@ -664,7 +511,7 @@ it.live( listClient: true, searchClient: true, }); - }) + }), ); it.live( 'generated owner enforces tenant and legal-entity isolation through Shell, BFF, CoreSDK, SpiceDB, and RLS', @@ -686,10 +533,8 @@ it.live( const connections = yield* loadDatabaseConnectionPair(); expect(connections.runtime.user).toBe('ontos_runtime'); const admin = yield* Effect.acquireRelease( - Effect.sync( - () => new Pool({ connectionString: connections.admin.connectionString }) - ), - (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie) + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), ); // Shell and the independently deployed owner hold separate nested read transactions in this // in-process fixture, so the shared test pool needs more than one physical connection. @@ -699,16 +544,12 @@ it.live( new Pool({ connectionString: connections.runtime.connectionString, max: 4, - }) + }), ), - (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie) - ); - const runtimeDatabase = yield* makeFaultInjectableCoreDatabase( - connections.runtime - ); - const fixture = yield* createGeneratedOwnerFixture(schemaName).pipe( - Effect.provide(NodeServices.layer) + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), ); + const runtimeDatabase = yield* makeFaultInjectableCoreDatabase(connections.runtime); + const fixture = yield* createGeneratedOwnerFixture(schemaName).pipe(Effect.provide(NodeServices.layer)); const contract = yield* deriveOntosModuleDeploymentContract({ vertical: GENERATED_OWNER.slug, workspaceRoot: fixture.root, @@ -719,37 +560,18 @@ it.live( const spiceAdmin = v1.NewClient( testSpiceDb.preSharedKey, testSpiceDb.endpoint, - testSpiceDb.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE - ); - const permissionClient = createSpiceDbPermissionClient( - testSpiceDb, - SPICEDB_CHECK_TIMEOUT_MS + testSpiceDb.insecureLocal ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED : v1.ClientSecurity.SECURE, ); + const permissionClient = createSpiceDbPermissionClient(testSpiceDb, SPICEDB_CHECK_TIMEOUT_MS); const contextAccess = makeContextAccess(permissionClient); const moduleStates = makeTenantModuleStateService(runtimeDatabase); const moduleStateGate = makeModuleStateGate(moduleStates); const moduleGateway = makeModuleEntrypointGateway(moduleStateGate); - const scopeResolver = makeOperationalScopeResolver( - makeOperationalScopeRepository(runtimeDatabase), - contextAccess - ); - const readRuntime = makeReadRuntime( - runtimeDatabase, - moduleGateway, - scopeResolver, - contextAccess - ); - const keyPair = yield* Effect.tryPromise(() => - generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }) - ); - const privateJwk = yield* Effect.tryPromise(() => - exportJWK(keyPair.privateKey) - ); - const publicJwk = yield* Effect.tryPromise(() => - exportJWK(keyPair.publicKey) - ); + const scopeResolver = makeOperationalScopeResolver(makeOperationalScopeRepository(runtimeDatabase), contextAccess); + const readRuntime = makeReadRuntime(runtimeDatabase, moduleGateway, scopeResolver, contextAccess); + const keyPair = yield* Effect.tryPromise(() => generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true })); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(keyPair.privateKey)); + const publicJwk = yield* Effect.tryPromise(() => exportJWK(keyPair.publicKey)); const issuerConfiguration: GatewayIssuerConfigValue = { issuer: 'https://shell.isolation.test', privateJwk: { @@ -777,23 +599,12 @@ it.live( }; const verifierConfigLayer = Layer.merge( testClockLayer, - ConfigProvider.layer(ConfigProvider.fromUnknown(verifierEnvironment)) + ConfigProvider.layer(ConfigProvider.fromUnknown(verifierEnvironment)), ); - const generated = yield* loadGeneratedOwner( - fixture.verticalRoot, - readRuntime, - loggerLayer, - verifierConfigLayer - ); - const handlers: OwnerHttpHandler[] = [ - generated.detail, - generated.list, - generated.search, - ]; + const generated = yield* loadGeneratedOwner(fixture.verticalRoot, readRuntime, loggerLayer, verifierConfigLayer); + const handlers: OwnerHttpHandler[] = [generated.detail, generated.list, generated.search]; let assertionCount = 0; - const issueAuthorization = Effect.fnUntraced(function* runIntegration10( - principalContext: TrustedPrincipalContext - ) { + const issueAuthorization = Effect.fnUntraced(function* runIntegration10(principalContext: TrustedPrincipalContext) { return yield* issueGatewayContextAssertion({ audience: GENERATED_OWNER.appId, principal: principalContext, @@ -801,7 +612,7 @@ it.live( Effect.provide( makeGatewayIssuerLayer({ currentTimeSeconds: Clock.currentTimeMillis.pipe( - Effect.map((milliseconds) => Math.floor(milliseconds / 1000)) + Effect.map((milliseconds) => Math.floor(milliseconds / 1000)), ), generateJti: Effect.sync(() => { assertionCount += 1; @@ -809,49 +620,47 @@ it.live( }), loadAudiences: Effect.succeed(new Set([GENERATED_OWNER.appId])), loadConfig: Effect.succeed(issuerConfiguration), - }) + }), ), Effect.map(({ token }) => `Bearer ${token}`), - Effect.provide(testClockLayer) + Effect.provide(testClockLayer), ); }); const principalA1 = principal(tenantA, entityA1, principalA, bindingA); const principalB1 = principal(tenantB, entityB1, principalB, bindingB); - const issueProviderAuthorization = Effect.fnUntraced( - function* runIntegration11(context: TrustedPrincipalContext) { - return yield* issueAuthorization( + const issueProviderAuthorization = Effect.fnUntraced(function* runIntegration11(context: TrustedPrincipalContext) { + return yield* issueAuthorization( + withOptionalProperty( withOptionalProperty( withOptionalProperty( withOptionalProperty( - withOptionalProperty( - { - authMethod: context.authMethod, - principalId: context.principalId, - tenantId: context.tenantId, - }, - context.authBindingId !== undefined, - 'authBindingId', - context.authBindingId, - {} - ), - context.authContextRef !== undefined, - 'authContextRef', - context.authContextRef, - {} + { + authMethod: context.authMethod, + principalId: context.principalId, + tenantId: context.tenantId, + }, + context.authBindingId !== undefined, + 'authBindingId', + context.authBindingId, + {}, ), - context.impersonatedByPrincipalId !== undefined, - 'impersonatedByPrincipalId', - context.impersonatedByPrincipalId, - {} + context.authContextRef !== undefined, + 'authContextRef', + context.authContextRef, + {}, ), - context.legalEntityId !== undefined, - 'legalEntityId', - context.legalEntityId, - {} - ) - ); - } - ); + context.impersonatedByPrincipalId !== undefined, + 'impersonatedByPrincipalId', + context.impersonatedByPrincipalId, + {}, + ), + context.legalEntityId !== undefined, + 'legalEntityId', + context.legalEntityId, + {}, + ), + ); + }); const resourceRef = yield* Schema.decodeUnknownEffect(ResourceRefSchema)({ moduleId: GENERATED_OWNER.moduleId, resourceId: collidingResourceId, @@ -860,48 +669,18 @@ it.live( const touchedObjects: readonly [string, string][] = [ ['tenant', tenantA], ['tenant', tenantB], - [ - 'legal_entity', - requiredValue( - toLegalEntityAccessObjectId(tenantA, entityA1), - 'Tenant A legal entity' - ), - ], - [ - 'legal_entity', - requiredValue( - toLegalEntityAccessObjectId(tenantB, entityB1), - 'Tenant B legal entity' - ), - ], + ['legal_entity', requiredValue(toLegalEntityAccessObjectId(tenantA, entityA1), 'Tenant A legal entity')], + ['legal_entity', requiredValue(toLegalEntityAccessObjectId(tenantB, entityB1), 'Tenant B legal entity')], [ 'module_access', - requiredValue( - toModuleAccessObjectId(tenantA, entityA1, GENERATED_OWNER.moduleId), - 'Tenant A module access' - ), + requiredValue(toModuleAccessObjectId(tenantA, entityA1, GENERATED_OWNER.moduleId), 'Tenant A module access'), ], [ 'module_access', - requiredValue( - toModuleAccessObjectId(tenantB, entityB1, GENERATED_OWNER.moduleId), - 'Tenant B module access' - ), - ], - [ - 'resource', - requiredValue( - toResourceAccessObjectId(tenantA, entityA1, resourceRef), - 'Tenant A resource' - ), - ], - [ - 'resource', - requiredValue( - toResourceAccessObjectId(tenantB, entityB1, resourceRef), - 'Tenant B resource' - ), + requiredValue(toModuleAccessObjectId(tenantB, entityB1, GENERATED_OWNER.moduleId), 'Tenant B module access'), ], + ['resource', requiredValue(toResourceAccessObjectId(tenantA, entityA1, resourceRef), 'Tenant A resource')], + ['resource', requiredValue(toResourceAccessObjectId(tenantB, entityB1, resourceRef), 'Tenant B resource')], ['action', toSpiceDbActionObjectId(GENERATED_OWNER.actionKey)], ]; yield* Effect.acquireRelease( @@ -918,112 +697,80 @@ it.live( optionalResourceId: resourceId, resourceType, }), - }) - ) + }), + ), ).pipe(Effect.catchCause(() => Effect.void)), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); permissionClient.close(); spiceAdmin.close(); const cleanupQueries = [ Effect.fnUntraced(function* runIntegration15() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.outbox_messages where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.outbox_messages where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration16() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.domain_events where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.domain_events where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration17() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.data_access_events where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.data_access_events where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration18() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.audit_events where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.audit_events where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration19() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.action_invocations where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.action_invocations where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration20() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.tenant_module_states where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.tenant_module_states where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration21() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.principal_auth_bindings where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.principal_auth_bindings where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration22() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.principals where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.principals where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration23() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.legal_entities where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.legal_entities where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration24() { return yield* Effect.tryPromise(() => - admin.query( - 'delete from core.tenants where tenant_id in ($1, $2)', - [tenantA, tenantB] - ) + admin.query('delete from core.tenants where tenant_id in ($1, $2)', [tenantA, tenantB]), ); }), Effect.fnUntraced(function* runIntegration25() { - return yield* Effect.tryPromise(() => - admin.query(`drop schema if exists ${schemaName} cascade`) - ); + return yield* Effect.tryPromise(() => admin.query(`drop schema if exists ${schemaName} cascade`)); }), ]; yield* Effect.forEach(cleanupQueries, ignoreOperationFailure, { concurrency: 1, discard: true, }); - }, Effect.orDie) + }, Effect.orDie), ); yield* createOwnerSchema(admin, schemaName); yield* Effect.tryPromise(() => admin.query( `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Generated tenant A', 'active', 'en'), ($2, $4, 'Generated tenant B', 'active', 'en')`, - [tenantA, tenantB, `generated-a-${tenantA}`, `generated-b-${tenantB}`] - ) + [tenantA, tenantB, `generated-a-${tenantA}`, `generated-b-${tenantB}`], + ), ); yield* Effect.tryPromise(() => admin.query( @@ -1039,80 +786,51 @@ it.live( `A2-${entityA2}`, `B1-${entityB1}`, `B2-${entityB2}`, - ] - ) + ], + ), ); yield* Effect.tryPromise(() => admin.query( `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Generated principal A', 'active'), ($2, $4, 'human', 'Generated principal B', 'active')`, - [principalA, principalB, tenantA, tenantB] - ) + [principalA, principalB, tenantA, tenantB], + ), ); yield* Effect.tryPromise(() => admin.query( `insert into core.principal_auth_bindings (principal_auth_binding_id, tenant_id, principal_id, provider, subject_type, provider_subject_id, status) values ($1, $3, $5, 'better_auth', 'user', $7, 'active'), ($2, $4, $6, 'better_auth', 'user', $8, 'active')`, - [ - bindingA, - bindingB, - tenantA, - tenantB, - principalA, - principalB, - `user-${principalA}`, - `user-${principalB}`, - ] - ) + [bindingA, bindingB, tenantA, tenantB, principalA, principalB, `user-${principalA}`, `user-${principalB}`], + ), ); yield* Effect.tryPromise(() => admin.query( `insert into core.tenant_module_states (tenant_id, module_key, state) values ($1, $3, 'active'), ($2, $3, 'active')`, - [tenantA, tenantB, GENERATED_OWNER.moduleId] - ) + [tenantA, tenantB, GENERATED_OWNER.moduleId], + ), ); yield* Effect.tryPromise(() => admin.query( `insert into ${schemaName}.tenant_records (tenant_id, resource_id, title) values ($1, $3, 'Tenant A list'), ($2, $3, 'Tenant B list')`, - [tenantA, tenantB, collidingResourceId] - ) + [tenantA, tenantB, collidingResourceId], + ), ); yield* Effect.tryPromise(() => admin.query( `insert into ${schemaName}.entity_records (tenant_id, legal_entity_id, resource_id, title) values ($1, $2, $7, 'A1 searchable'), ($1, $3, $7, 'A2 searchable'), ($4, $5, $7, 'B1 searchable'), ($4, $6, $7, 'B2 searchable')`, - [ - tenantA, - entityA1, - entityA2, - tenantB, - entityB1, - entityB2, - collidingResourceId, - ] - ) - ); - const legalA = requiredValue( - toLegalEntityAccessObjectId(tenantA, entityA1), - 'Tenant A legal entity' - ); - const legalB = requiredValue( - toLegalEntityAccessObjectId(tenantB, entityB1), - 'Tenant B legal entity' + [tenantA, entityA1, entityA2, tenantB, entityB1, entityB2, collidingResourceId], + ), ); + const legalA = requiredValue(toLegalEntityAccessObjectId(tenantA, entityA1), 'Tenant A legal entity'); + const legalB = requiredValue(toLegalEntityAccessObjectId(tenantB, entityB1), 'Tenant B legal entity'); const moduleA = requiredValue( toModuleAccessObjectId(tenantA, entityA1, GENERATED_OWNER.moduleId), - 'Tenant A module access' + 'Tenant A module access', ); const moduleB = requiredValue( toModuleAccessObjectId(tenantB, entityB1, GENERATED_OWNER.moduleId), - 'Tenant B module access' - ); - const resourceA = requiredValue( - toResourceAccessObjectId(tenantA, entityA1, resourceRef), - 'Tenant A resource' - ); - const resourceB = requiredValue( - toResourceAccessObjectId(tenantB, entityB1, resourceRef), - 'Tenant B resource' + 'Tenant B module access', ); + const resourceA = requiredValue(toResourceAccessObjectId(tenantA, entityA1, resourceRef), 'Tenant A resource'); + const resourceB = requiredValue(toResourceAccessObjectId(tenantB, entityB1, resourceRef), 'Tenant B resource'); const actionId = toSpiceDbActionObjectId(GENERATED_OWNER.actionKey); const relationships = [ relationship('tenant', tenantA, 'member', 'principal', principalA), @@ -1121,34 +839,10 @@ it.live( relationship('legal_entity', legalA, 'member', 'principal', principalA), relationship('legal_entity', legalB, 'tenant', 'tenant', tenantB), relationship('legal_entity', legalB, 'member', 'principal', principalB), - relationship( - 'module_access', - moduleA, - 'legal_entity', - 'legal_entity', - legalA - ), - relationship( - 'module_access', - moduleA, - 'accessor', - 'principal', - principalA - ), - relationship( - 'module_access', - moduleB, - 'legal_entity', - 'legal_entity', - legalB - ), - relationship( - 'module_access', - moduleB, - 'accessor', - 'principal', - principalB - ), + relationship('module_access', moduleA, 'legal_entity', 'legal_entity', legalA), + relationship('module_access', moduleA, 'accessor', 'principal', principalA), + relationship('module_access', moduleB, 'legal_entity', 'legal_entity', legalB), + relationship('module_access', moduleB, 'accessor', 'principal', principalB), relationship('resource', resourceA, 'module', 'module_access', moduleA), relationship('resource', resourceA, 'reader', 'principal', principalA), relationship('resource', resourceB, 'module', 'module_access', moduleB), @@ -1163,28 +857,21 @@ it.live( v1.RelationshipUpdate.create({ operation: v1.RelationshipUpdate_Operation.TOUCH, relationship: item, - }) + }), ), - }) - ) + }), + ), ); const ownerSearchProbe = yield* requestOwner( generated.search, `/${GENERATED_OWNER.moduleId}/search/records`, { query: 'searchable' }, yield* issueAuthorization(principalA1), - randomUUID() - ); - const ownerSearchProbeBody = yield* decodeResponse( - ownerSearchProbe, - OwnerSearchSchema - ); - expect(ownerSearchProbe.status, JSON.stringify(ownerSearchProbeBody)).toBe( - 200 + randomUUID(), ); - expect(ownerSearchProbeBody.map(({ title }) => title)).toEqual([ - 'A1 searchable', - ]); + const ownerSearchProbeBody = yield* decodeResponse(ownerSearchProbe, OwnerSearchSchema); + expect(ownerSearchProbe.status, JSON.stringify(ownerSearchProbeBody)).toBe(200); + expect(ownerSearchProbeBody.map(({ title }) => title)).toEqual(['A1 searchable']); const catalog = makeCatalog(contract); const gateway = { resource: { @@ -1199,16 +886,14 @@ it.live( '/reads/resource-detail', { resourceId: ref.resourceId }, authorization, - correlationId + correlationId, ); if (!response.ok) { throw new Error('Owner detail request failed'); } return yield* decodeResponse(response, OwnerDetailSchema); }, - Effect.catchCause(() => - Effect.fail(new ShellProviderUnavailableError()) - ) + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), ), timeline: Effect.fnUntraced( function* integrationEffect27({ @@ -1221,20 +906,15 @@ it.live( '/reads/resource-list', { resourceId: ref.resourceId }, authorization, - correlationId + correlationId, ); if (!response.ok) { throw new Error('Owner list request failed'); } - const timeline = yield* decodeResponse( - response, - OwnerTimelineSchema - ); + const timeline = yield* decodeResponse(response, OwnerTimelineSchema); return Schema.encodeSync(OwnerTimelineSchema)(timeline); }, - Effect.catchCause(() => - Effect.fail(new ShellProviderUnavailableError()) - ) + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), ), }, search: { @@ -1249,31 +929,27 @@ it.live( `/${GENERATED_OWNER.moduleId}/search/records`, { query }, authorization, - correlationId + correlationId, ); if (!response.ok) { throw new Error('Owner search request failed'); } return yield* decodeResponse(response, OwnerSearchSchema); }, - Effect.catchCause(() => - Effect.fail(new ShellProviderUnavailableError()) - ) + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), ), }, } satisfies ShellResourceGateways; - expect( - yield* moduleStates.getTenantModuleStates(tenantA, [ - GENERATED_OWNER.moduleId, - ]) - ).toEqual([{ moduleKey: GENERATED_OWNER.moduleId, state: 'active' }]); + expect(yield* moduleStates.getTenantModuleStates(tenantA, [GENERATED_OWNER.moduleId])).toEqual([ + { moduleKey: GENERATED_OWNER.moduleId, state: 'active' }, + ]); expect( yield* contextAccess.modules({ legalEntityId: entityA1, moduleIds: [GENERATED_OWNER.moduleId], principalId: principalA, tenantId: tenantA, - }) + }), ).toEqual([{ decision: 'allowed', key: GENERATED_OWNER.moduleId }]); expect( yield* contextAccess.resources({ @@ -1281,7 +957,7 @@ it.live( principalId: principalA, resources: [resourceRef], tenantId: tenantA, - }) + }), ).toEqual([ { decision: 'allowed', @@ -1295,7 +971,7 @@ it.live( correlationId: randomUUID(), query: 'searchable', searchKey: `${GENERATED_OWNER.moduleId}.records`, - }) + }), ).toEqual([ { ref: resourceRef, @@ -1307,20 +983,14 @@ it.live( catalog: Effect.succeed(catalog), contextAccess, issueAssertion: Effect.fnUntraced( - function* integrationEffect29({ - context, - }: { - readonly context: TrustedPrincipalContext; - }) { + function* integrationEffect29({ context }: { readonly context: TrustedPrincipalContext }) { return yield* issueProviderAuthorization(context); }, - Effect.catchCause(() => - Effect.fail(new ShellProviderUnavailableError()) - ) + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), ), moduleStates, }, - gateway.search + gateway.search, ); expect( yield* directShellSearch.search( @@ -1329,8 +999,8 @@ it.live( correlationId: randomUUID(), legalEntityId: entityA1, }, - 'searchable' - ) + 'searchable', + ), ).toEqual({ partial: false, results: [{ kind: 'resource', ref: resourceRef, title: 'A1 searchable' }], @@ -1339,19 +1009,13 @@ it.live( gateway, { issueAssertion: Effect.fnUntraced( - function* integrationEffect30({ - context, - }: { - readonly context: TrustedPrincipalContext; - }) { + function* integrationEffect30({ context }: { readonly context: TrustedPrincipalContext }) { return yield* issueProviderAuthorization(context); }, - Effect.catchCause(() => - Effect.fail(new ShellProviderUnavailableError()) - ) + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), ), }, - (transaction) => makeTenantModuleStateService({ executor: transaction }) + (transaction) => makeTenantModuleStateService({ executor: transaction }), ).pipe( Layer.provide( Layer.mergeAll( @@ -1362,13 +1026,11 @@ it.live( load: Effect.succeed(catalog), }), ShellCompositionFactoryLive, - ShellResourceServicesFactoryLive - ) - ) - ); - const shellReads = yield* ShellGovernedReads.pipe( - Effect.provide(shellLayer) + ShellResourceServicesFactoryLive, + ), + ), ); + const shellReads = yield* ShellGovernedReads.pipe(Effect.provide(shellLayer)); const searchA = yield* shellReads.search({ correlationId: randomUUID(), principal: principalA1, @@ -1389,24 +1051,13 @@ it.live( principal: principalB1, ref: resourceRef, }); - expect(searchA.results.map(({ title }) => title)).toEqual([ - 'A1 searchable', - ]); + expect(searchA.results.map(({ title }) => title)).toEqual(['A1 searchable']); expect(detailA.detail.title).toBe('A1 searchable'); - expect(detailA.timeline.map(({ summary }) => summary)).toEqual([ - 'Tenant A list', - ]); - expect(searchB.results.map(({ title }) => title)).toEqual([ - 'B1 searchable', - ]); + expect(detailA.timeline.map(({ summary }) => summary)).toEqual(['Tenant A list']); + expect(searchB.results.map(({ title }) => title)).toEqual(['B1 searchable']); expect(detailB.detail.title).toBe('B1 searchable'); - expect(detailB.timeline.map(({ summary }) => summary)).toEqual([ - 'Tenant B list', - ]); - expect( - assertionCount, - 'every provider attempt must receive a fresh assertion' - ).toBe(9); + expect(detailB.timeline.map(({ summary }) => summary)).toEqual(['Tenant B list']); + expect(assertionCount, 'every provider attempt must receive a fresh assertion').toBe(9); capturedLogs.length = 0; const beforeForgedShell = { ...generated.counts }; expect( @@ -1416,9 +1067,9 @@ it.live( correlationId: randomUUID(), principal: principal(tenantA, entityA2, principalA, bindingA), ref: resourceRef, - }) - ) - ) + }), + ), + ), ).toBe(true); expect( isOperationContextDenied( @@ -1427,17 +1078,14 @@ it.live( correlationId: randomUUID(), principal: principal(tenantB, entityB1, principalA, bindingA), ref: resourceRef, - }) - ) - ) + }), + ), + ), ).toBe(true); expect(generated.counts).toEqual(beforeForgedShell); expect(assertionCount).toBe(9); yield* Effect.all( - [ - principal(tenantA, entityA2, principalA, bindingA), - principal(tenantB, entityB1, principalA, bindingA), - ].map( + [principal(tenantA, entityA2, principalA, bindingA), principal(tenantB, entityB1, principalA, bindingA)].map( Effect.fnUntraced(function* runIntegration31(forgedPrincipal) { const authorization = yield* issueAuthorization(forgedPrincipal); const response = yield* requestOwner( @@ -1445,20 +1093,14 @@ it.live( '/reads/resource-detail', { resourceId: collidingResourceId }, authorization, - randomUUID() + randomUUID(), ); expect(response.status).toBe(403); - const problem = JSON.stringify( - yield* Effect.tryPromise(() => response.json()) - ); - expect(problem).not.toMatch( - new RegExp([tenantA, tenantB, entityA2, entityB1].join('|'), 'u') - ); - expect(problem).not.toMatch( - /postgres|spicedb|permission check|row-level/iu - ); - }) - ) + const problem = JSON.stringify(yield* Effect.tryPromise(() => response.json())); + expect(problem).not.toMatch(new RegExp([tenantA, tenantB, entityA2, entityB1].join('|'), 'u')); + expect(problem).not.toMatch(/postgres|spicedb|permission check|row-level/iu); + }), + ), ); expect(generated.counts).toEqual(beforeForgedShell); const deniedBefore = generated.counts.detail; @@ -1468,7 +1110,7 @@ it.live( '/reads/resource-detail', { resourceId: deniedResourceId }, deniedAuthorization, - randomUUID() + randomUUID(), ); expect(deniedResponse.status).toBe(403); expect(generated.counts.detail).toBe(deniedBefore); @@ -1480,8 +1122,8 @@ it.live( result_count: number; }>( `select outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and target_resource_id = $2`, - [tenantA, deniedResourceId] - ) + [tenantA, deniedResourceId], + ), ); expect(deniedEvidence.rows).toEqual([ { @@ -1492,82 +1134,69 @@ it.live( }, ]); const unavailableContextAccess = makeContextAccessDouble('unavailable'); - const unavailableResolver: OperationalScopeResolverService = - makeOperationalScopeResolver( - makeOperationalScopeRepository(runtimeDatabase), - unavailableContextAccess - ); + const unavailableResolver: OperationalScopeResolverService = makeOperationalScopeResolver( + makeOperationalScopeRepository(runtimeDatabase), + unavailableContextAccess, + ); const unavailableRuntime = makeReadRuntime( runtimeDatabase, moduleGateway, unavailableResolver, - unavailableContextAccess + unavailableContextAccess, ); const unavailableOwner = yield* loadGeneratedOwner( fixture.verticalRoot, unavailableRuntime, loggerLayer, - verifierConfigLayer - ); - handlers.push( - unavailableOwner.detail, - unavailableOwner.list, - unavailableOwner.search + verifierConfigLayer, ); + handlers.push(unavailableOwner.detail, unavailableOwner.list, unavailableOwner.search); const unavailableBefore = generated.counts.detail; const unavailableResponse = yield* requestOwner( unavailableOwner.detail, '/reads/resource-detail', { resourceId: collidingResourceId }, yield* issueAuthorization(principalA1), - randomUUID() + randomUUID(), ); expect(unavailableResponse.status).toBe(503); expect(generated.counts.detail).toBe(unavailableBefore); - expect( - JSON.stringify(yield* Effect.tryPromise(() => unavailableResponse.json())) - ).not.toMatch(/postgres|spicedb|permission check|row-level/iu); + expect(JSON.stringify(yield* Effect.tryPromise(() => unavailableResponse.json()))).not.toMatch( + /postgres|spicedb|permission check|row-level/iu, + ); const evidenceFailureRuntime = makeReadRuntime( failingEvidenceDatabase(runtimeDatabase), moduleGateway, scopeResolver, - contextAccess + contextAccess, ); const evidenceFailureOwner = yield* loadGeneratedOwner( fixture.verticalRoot, evidenceFailureRuntime, loggerLayer, - verifierConfigLayer - ); - handlers.push( - evidenceFailureOwner.detail, - evidenceFailureOwner.list, - evidenceFailureOwner.search + verifierConfigLayer, ); + handlers.push(evidenceFailureOwner.detail, evidenceFailureOwner.list, evidenceFailureOwner.search); const evidenceFailureResponse = yield* requestOwner( evidenceFailureOwner.detail, '/reads/resource-detail', { resourceId: collidingResourceId }, yield* issueAuthorization(principalA1), - randomUUID() + randomUUID(), ); expect(evidenceFailureResponse.status).toBe(503); - expect( - JSON.stringify( - yield* Effect.tryPromise(() => evidenceFailureResponse.json()) - ) - ).not.toMatch(/A1 searchable/u); + expect(JSON.stringify(yield* Effect.tryPromise(() => evidenceFailureResponse.json()))).not.toMatch( + /A1 searchable/u, + ); const actionRuntime = makeActionRuntime( runtimeDatabase, makeActionRepository(), makeActionPermissionService(permissionClient), scopeResolver, - { moduleEntrypointGateway: moduleGateway, moduleStateGate } + { moduleEntrypointGateway: moduleGateway, moduleStateGate }, ); if (!isRuntimeActionRegistration(generated.action)) { - throw new TypeError( - 'Generated Action registration is missing its runtime handler' - ); + throw new TypeError('Generated Action registration is missing its runtime handler'); } const actionRegistration = generated.action; const invokeAction = Effect.fnUntraced(function* runIntegration32( @@ -1578,7 +1207,7 @@ it.live( readonly tenantId: string; readonly title: string; }, - idempotencyKey: string + idempotencyKey: string, ) { const authorization = yield* issueAuthorization(trustedPrincipal); const verified = yield* generated @@ -1612,8 +1241,8 @@ it.live( tenantId: tenantA, title: 'A1 action write', }, - randomUUID() - ) + randomUUID(), + ), ).toEqual({ created: true }); yield* Effect.all( [ @@ -1631,15 +1260,11 @@ it.live( }, ].map( Effect.fnUntraced(function* runIntegration33(payload) { - expect( - isCreateRecordRejected( - yield* Effect.flip( - invokeAction(principalA1, payload, randomUUID()) - ) - ) - ).toBe(true); - }) - ) + expect(isCreateRecordRejected(yield* Effect.flip(invokeAction(principalA1, payload, randomUUID())))).toBe( + true, + ); + }), + ), ); const beforeForgedAction = generated.counts.action; expect( @@ -1653,10 +1278,10 @@ it.live( tenantId: tenantA, title: 'forged action scope', }, - randomUUID() - ) - ) - ) + randomUUID(), + ), + ), + ), ).toBe(true); expect(generated.counts.action).toBe(beforeForgedAction); expect( @@ -1670,26 +1295,20 @@ it.live( tenantId: tenantA, title: 'trigger safe logging defect', }, - randomUUID() - ) - ) - ) + randomUUID(), + ), + ), + ), ).toBe(true); const ownerRows = yield* Effect.tryPromise(() => admin.query<{ legal_entity_id: string; tenant_id: string; title: string; - }>( - `select tenant_id, legal_entity_id, title from ${schemaName}.entity_records order by title` - ) + }>(`select tenant_id, legal_entity_id, title from ${schemaName}.entity_records order by title`), ); - expect( - ownerRows.rows.some(({ title }) => title === 'A1 action write') - ).toBe(true); - expect( - ownerRows.rows.some(({ title }) => title.startsWith('forbidden')) - ).toBe(false); + expect(ownerRows.rows.some(({ title }) => title === 'A1 action write')).toBe(true); + expect(ownerRows.rows.some(({ title }) => title.startsWith('forbidden'))).toBe(false); const allowedEvidence = yield* Effect.tryPromise(() => admin.query<{ evidence_policy_key: string; @@ -1697,71 +1316,38 @@ it.live( query_hash: null; }>( `select evidence_policy_key, outcome, query_hash from core.data_access_events where tenant_id in ($1, $2) and outcome = 'allowed' order by evidence_policy_key`, - [tenantA, tenantB] - ) + [tenantA, tenantB], + ), ); expect(allowedEvidence.rows.length >= 10).toBe(true); - expect( - allowedEvidence.rows.every(({ outcome }) => outcome === 'allowed') - ).toBe(true); - expect( - allowedEvidence.rows.every(({ query_hash }) => query_hash === null) - ).toBe(true); + expect(allowedEvidence.rows.every(({ outcome }) => outcome === 'allowed')).toBe(true); + expect(allowedEvidence.rows.every(({ query_hash }) => query_hash === null)).toBe(true); const unscopedEntityRows = yield* Effect.tryPromise(() => - runtimePool.query(`select * from ${schemaName}.entity_records`) + runtimePool.query(`select * from ${schemaName}.entity_records`), ); - expect( - unscopedEntityRows.rowCount, - 'a reused pooled connection must not retain transaction-local scope' - ).toBe(0); + expect(unscopedEntityRows.rowCount, 'a reused pooled connection must not retain transaction-local scope').toBe(0); const unscopedTenantRows = yield* Effect.tryPromise(() => - runtimePool.query(`select * from ${schemaName}.tenant_records`) + runtimePool.query(`select * from ${schemaName}.tenant_records`), ); expect(unscopedTenantRows.rowCount).toBe(0); - expect( - capturedLogs.length > 0, - 'the generated-owner path must capture runtime logs' - ).toBe(true); + expect(capturedLogs.length > 0, 'the generated-owner path must capture runtime logs').toBe(true); const capturedLogText = capturedLogs.join('\n'); expect(capturedLogText).toMatch(/Unexpected Action execution defect/u); expect(capturedLogText).not.toMatch( - new RegExp( - [ - tenantB, - entityA2, - entityB1, - entityB2, - principalB, - bindingB, - deniedResourceId, - ].join('|'), - 'u' - ) - ); - expect(capturedLogText).not.toMatch( - /postgres|spicedb|row-level|database operation scope|permission check/iu + new RegExp([tenantB, entityA2, entityB1, entityB2, principalB, bindingB, deniedResourceId].join('|'), 'u'), ); + expect(capturedLogText).not.toMatch(/postgres|spicedb|row-level|database operation scope|permission check/iu); const generatedActionSource = yield* Effect.tryPromise(() => - readFile( - `${fixture.verticalRoot}/src/actions/create-record.action.ts`, - 'utf-8' - ) + readFile(`${fixture.verticalRoot}/src/actions/create-record.action.ts`, 'utf-8'), ); const generatedServerSource = yield* Effect.tryPromise(() => - readFile( - `${fixture.verticalRoot}/api/resource-detail-read-server.ts`, - 'utf-8' - ) - ); - expect(generatedActionSource).toMatch( - /@generated by OntOS Codesmith Action/u + readFile(`${fixture.verticalRoot}/api/resource-detail-read-server.ts`, 'utf-8'), ); + expect(generatedActionSource).toMatch(/@generated by OntOS Codesmith Action/u); expect(generatedActionSource).toMatch(/legalEntityScope: 'required'/u); expect(generatedServerSource).toMatch(/authenticateOperationPrincipal/u); expect(generatedServerSource).toMatch(/makeGovernedReadHttpHandler\(\{/u); expect(generatedServerSource).toMatch(/registration: resourceDetailRead/u); - expect(generatedServerSource).not.toMatch( - /yield\* ReadRuntime|\.runRead\(/u - ); - }) + expect(generatedServerSource).not.toMatch(/yield\* ReadRuntime|\.runRead\(/u); + }), ); diff --git a/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts b/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts index e89ae54de..367f601c7 100644 --- a/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts @@ -38,17 +38,8 @@ import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/suppor import { makeApiKeyService } from '../../api/auth/api-key-service.ts'; import { AuthConfig, loadAuthConfig } from '../../api/auth/config.ts'; import { AuthDatabase, makeAuthDatabase } from '../../api/auth/db/client.ts'; -import { - account, - apikey, - session, - supportImpersonationRecovery, - user, -} from '../../api/auth/db/schema.ts'; -import { - issueGatewayContextAssertion, - makeGatewayIssuerLayer, -} from '../../api/auth/gateway-issuer.ts'; +import { account, apikey, session, supportImpersonationRecovery, user } from '../../api/auth/db/schema.ts'; +import { issueGatewayContextAssertion, makeGatewayIssuerLayer } from '../../api/auth/gateway-issuer.ts'; import { makeIdentityLifecycleService } from '../../api/auth/identity-lifecycle.ts'; import { makeSupportAuthProvider, @@ -58,10 +49,7 @@ import { SupportImpersonationCorrelationId, SupportImpersonationStoreService, } from '../../api/auth/impersonation-service.ts'; -import { - AuthenticationService, - makeAuthenticationService, -} from '../../api/auth/service.ts'; +import { AuthenticationService, makeAuthenticationService } from '../../api/auth/service.ts'; const cookieHeader = (setCookieHeaders: readonly string[]): string => { const cookies = new Map(); @@ -88,32 +76,16 @@ it.live.each([ Effect.fnUntraced(function* runIntegration1({ pendingCleanupOnly }) { const baseConfiguration = yield* loadAuthConfig(); const corePool = yield* Effect.acquireRelease( - Effect.sync( - () => new Pool({ connectionString: baseConfiguration.connectionString }) - ), - (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie) + Effect.sync(() => new Pool({ connectionString: baseConfiguration.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), ); const authPersistence = yield* makeAuthDatabase(baseConfiguration); const authDatabase = authPersistence.executor; - const coreDatabase = yield* makeTestDatabaseFromPool( - corePool, - coreRelations - ); - const principalManagementRepository = - principalManagementRepositoryFromTransaction(coreDatabase); - const providePrincipalManagementRepository = < - Success, - Failure, - Requirements, - >( - effect: Effect.Effect - ) => - effect.pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepository - ) - ); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const principalManagementRepository = principalManagementRepositoryFromTransaction(coreDatabase); + const providePrincipalManagementRepository = ( + effect: Effect.Effect, + ) => effect.pipe(Effect.provideService(PrincipalManagementRepository, principalManagementRepository)); const tenantId = randomUUID(); const originalPrincipalId = randomUUID(); const targetPrincipalId = randomUUID(); @@ -149,53 +121,40 @@ it.live.each([ Effect.succeed( tenantIds.map((key) => ({ decision: - permission === 'impersonate' && !supportPermissionAllowed - ? ('denied' as const) - : ('allowed' as const), + permission === 'impersonate' && !supportPermissionAllowed ? ('denied' as const) : ('allowed' as const), key, - })) + })), ), }; const provideContextAccess = ( - effect: Effect.Effect - ) => - effect.pipe(Effect.provideService(ContextAccess, allowedContextAccess)); + effect: Effect.Effect, + ) => effect.pipe(Effect.provideService(ContextAccess, allowedContextAccess)); const operationalScope = makeOperationalScopeResolver( makeOperationalScopeRepository({ executor: coreDatabase }), - allowedContextAccess + allowedContextAccess, ); const actionRuntime = makeActionRuntime( { executor: coreDatabase }, makeActionRepository(), { checkActionPermission: () => Effect.succeed('allowed' as const) }, operationalScope, - { ...openActionRuntimeOptions, contextAccess: allowedContextAccess } + { ...openActionRuntimeOptions, contextAccess: allowedContextAccess }, ); const fixtureAuthentication = yield* makeAuthenticationService({ allowFixtureSignUp: true, }).pipe( Effect.provideService(AuthConfig, baseConfiguration), Effect.provideService(AuthDatabase, authPersistence), - Effect.provideService(PrincipalResolver, resolver) + Effect.provideService(PrincipalResolver, resolver), ); let originalUserId = ''; let targetUserId = ''; let secondAdministratorUserId = ''; const cleanup = Effect.fnUntraced(function* runIntegration2() { - if ( - originalUserId.length > 0 || - targetUserId.length > 0 || - secondAdministratorUserId.length > 0 - ) { - const ids = [ - originalUserId, - targetUserId, - secondAdministratorUserId, - ].filter((id) => id.length > 0); + if (originalUserId.length > 0 || targetUserId.length > 0 || secondAdministratorUserId.length > 0) { + const ids = [originalUserId, targetUserId, secondAdministratorUserId].filter((id) => id.length > 0); yield* purgeFixtureRows([ - authDatabase - .delete(supportImpersonationRecovery) - .where(eq(supportImpersonationRecovery.tenantId, tenantId)), + authDatabase.delete(supportImpersonationRecovery).where(eq(supportImpersonationRecovery.tenantId, tenantId)), authDatabase.delete(apikey).where(inArray(apikey.referenceId, ids)), authDatabase.delete(session).where(inArray(session.userId, ids)), authDatabase.delete(account).where(inArray(account.userId, ids)), @@ -203,21 +162,11 @@ it.live.each([ ]); } yield* purgeFixtureRows([ - coreDatabase - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, tenantId)), - coreDatabase - .delete(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - coreDatabase - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)), - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, tenantId)), - coreDatabase - .delete(principals) - .where(eq(principals.tenantId, tenantId)), + coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), + coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), + coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), + coreDatabase.delete(principalAuthBindings).where(eq(principalAuthBindings.tenantId, tenantId)), + coreDatabase.delete(principals).where(eq(principals.tenantId, tenantId)), coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId)), ]); }); @@ -225,22 +174,14 @@ it.live.each([ Effect.void, Effect.fnUntraced(function* integrationEffect3() { yield* cleanup(); - }, Effect.orDie) - ); - originalUserId = yield* fixtureAuthentication.createFixtureUser( - originalEmail, - 'Support original', - password - ); - targetUserId = yield* fixtureAuthentication.createFixtureUser( - targetEmail, - 'Support target', - password + }, Effect.orDie), ); + originalUserId = yield* fixtureAuthentication.createFixtureUser(originalEmail, 'Support original', password); + targetUserId = yield* fixtureAuthentication.createFixtureUser(targetEmail, 'Support target', password); secondAdministratorUserId = yield* fixtureAuthentication.createFixtureUser( secondAdministratorEmail, 'Second identity administrator', - password + password, ); yield* coreDatabase.insert(tenants).values({ defaultLocale: 'en', @@ -308,12 +249,12 @@ it.live.each([ const authentication = yield* makeAuthenticationService({}).pipe( Effect.provideService(AuthConfig, configuration), Effect.provideService(AuthDatabase, authPersistence), - Effect.provideService(PrincipalResolver, resolver) + Effect.provideService(PrincipalResolver, resolver), ); const signedIn = yield* authentication.signIn( originalEmail, password, - new Headers({ origin: configuration.baseUrl }) + new Headers({ origin: configuration.baseUrl }), ); const originalHeaders = new Headers({ cookie: cookieHeader(signedIn.setCookieHeaders), @@ -321,11 +262,9 @@ it.live.each([ }); const keys = yield* makeApiKeyService().pipe( Effect.provideService(AuthConfig, configuration), - Effect.provideService(AuthDatabase, authPersistence) - ); - const resolvedOriginal = yield* provideContextAccess( - authentication.resolveTenantContext(originalHeaders) + Effect.provideService(AuthDatabase, authPersistence), ); + const resolvedOriginal = yield* provideContextAccess(authentication.resolveTenantContext(originalHeaders)); expect(resolvedOriginal.state).toBe('authenticated'); if (resolvedOriginal.state !== 'authenticated') { throw new Error('The live original session did not resolve'); @@ -350,7 +289,7 @@ it.live.each([ lifecycleOperationId: randomUUID(), nowEpochMillis, tenantId, - }) + }), ).toEqual({ hasMore: false, providerKeyIds: [pending.providerKeyId] }); yield* authDatabase .update(apikey) @@ -369,18 +308,14 @@ it.live.each([ lifecycleOperationId: randomUUID(), nowEpochMillis, tenantId, - }) + }), ).toEqual({ hasMore: false, providerKeyIds: [pending.providerKeyId] }); yield* keys.setEnabled(pending.providerKeyId, false); yield* keys.clearPendingCleanup(pending.providerKeyId); }); return; } - const lifecycle = makeIdentityLifecycleService( - actionRuntime, - keys, - resolver - ); + const lifecycle = makeIdentityLifecycleService(actionRuntime, keys, resolver); const issued = yield* lifecycle.issue({ correlationId: randomUUID(), idempotencyKey: `identity-integration-key-${randomUUID()}`, @@ -390,14 +325,12 @@ it.live.each([ }); const verified = yield* keys.verify(issued.secret); const apiKeyAuthBindingId = issued.authBindingId; - const apiKeyIdentity = yield* resolver.resolveBetterAuthApiKey( - verified.providerKeyId - ); + const apiKeyIdentity = yield* resolver.resolveBetterAuthApiKey(verified.providerKeyId); const { privateKey, publicKey } = yield* Effect.tryPromise(() => generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true, - }) + }), ); const privateJwk = yield* Effect.tryPromise(() => exportJWK(privateKey)); const assertion = yield* issueGatewayContextAssertion({ @@ -427,8 +360,8 @@ it.live.each([ x: privateJwk.x ?? '', }, }), - }) - ) + }), + ), ); const verifiedAssertion = yield* Effect.tryPromise(() => jwtVerify(assertion.token, publicKey, { @@ -436,7 +369,7 @@ it.live.each([ audience: 'identity-integration', currentDate: new Date(1_800_000_001_000), issuer: 'https://shell.identity-integration.test', - }) + }), ); expect(verifiedAssertion.payload['principal']).toEqual({ authBindingId: apiKeyAuthBindingId, @@ -445,9 +378,7 @@ it.live.each([ principalId: originalPrincipalId, tenantId, }); - expect( - JSON.stringify(verifiedAssertion.payload).includes(issued.secret) - ).toBe(false); + expect(JSON.stringify(verifiedAssertion.payload).includes(issued.secret)).toBe(false); yield* providePrincipalManagementRepository( actionRuntime.runAction({ payload: { displayName: 'API-key evidence target', kind: 'service' }, @@ -463,13 +394,11 @@ it.live.each([ correlationId: randomUUID(), idempotencyKey: randomUUID(), }, - }) + }), ); yield* keys.setEnabled(verified.providerKeyId, false); const invalidKey = yield* Effect.flip(keys.verify(issued.secret)); - expect(Predicate.isTagged(invalidKey, 'ApiKeyCredentialInvalidError')).toBe( - true - ); + expect(Predicate.isTagged(invalidKey, 'ApiKeyCredentialInvalidError')).toBe(true); const managedPrincipal = yield* providePrincipalManagementRepository( lifecycle.createNonHumanPrincipal({ correlationId: randomUUID(), @@ -479,7 +408,7 @@ it.live.each([ kind: 'integration', }, principal: resolvedOriginal.principal, - }) + }), ); const managedKey = yield* lifecycle.issue({ correlationId: randomUUID(), @@ -492,15 +421,15 @@ it.live.each([ const secondAdministratorSignIn = yield* authentication.signIn( secondAdministratorEmail, password, - new Headers({ origin: configuration.baseUrl }) + new Headers({ origin: configuration.baseUrl }), ); const secondAdministratorContext = yield* provideContextAccess( authentication.resolveTenantContext( new Headers({ cookie: cookieHeader(secondAdministratorSignIn.setCookieHeaders), origin: configuration.baseUrl, - }) - ) + }), + ), ); expect(secondAdministratorContext.state).toBe('authenticated'); if (secondAdministratorContext.state !== 'authenticated') { @@ -518,29 +447,19 @@ it.live.each([ }); expect(crossAdminDisabled.enabled).toBe(false); expect(crossAdminDisabled.cleanupPending).toBe(false); - const supportRecoveryPrincipal = - makeSupportRecoveryPrincipalContextResolver({ - executor: coreDatabase, - }); + const supportRecoveryPrincipal = makeSupportRecoveryPrincipalContextResolver({ + executor: coreDatabase, + }); const support = makeSupportImpersonationService( Context.empty().pipe( Context.add(ActionRuntime, actionRuntime), Context.add(AuthenticationService, authentication), Context.add(AuthConfig, configuration), Context.add(PrincipalResolver, resolver), - Context.add( - SupportRecoveryPrincipalContextResolver, - supportRecoveryPrincipal - ), - Context.add( - SupportAuthProviderService, - makeSupportAuthProvider(configuration, authPersistence.adapter) - ), - Context.add( - SupportImpersonationStoreService, - makeSupportImpersonationStore(authDatabase) - ) - ) + Context.add(SupportRecoveryPrincipalContextResolver, supportRecoveryPrincipal), + Context.add(SupportAuthProviderService, makeSupportAuthProvider(configuration, authPersistence.adapter)), + Context.add(SupportImpersonationStoreService, makeSupportImpersonationStore(authDatabase)), + ), ); const started = yield* provideContextAccess( providePrincipalManagementRepository( @@ -551,13 +470,8 @@ it.live.each([ requestHeaders: originalHeaders, targetPrincipalId, }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - randomUUID() - ) - ) - ) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), + ), ); const impersonatedHeaders = new Headers({ cookie: cookieHeader(started.setCookieHeaders), @@ -566,37 +480,24 @@ it.live.each([ const [impersonationSession] = yield* authDatabase .select({ actionId: session.impersonationActionId, id: session.id }) .from(session) - .where( - and( - eq(session.userId, targetUserId), - eq(session.impersonatedBy, originalUserId) - ) - ) + .where(and(eq(session.userId, targetUserId), eq(session.impersonatedBy, originalUserId))) .limit(1); expect(impersonationSession).not.toBe(undefined); if (impersonationSession === undefined) { - throw new TypeError( - 'The support impersonation session was not persisted' - ); + throw new TypeError('The support impersonation session was not persisted'); } expect(Predicate.isString(impersonationSession.actionId)).toBe(true); if (!Predicate.isString(impersonationSession.actionId)) { - throw new TypeError( - 'The approved support start did not persist its Action correlation' - ); + throw new TypeError('The approved support start did not persist its Action correlation'); } yield* authDatabase .update(session) .set({ impersonationActionId: null }) .where(eq(session.id, impersonationSession.id)); const incompleteImpersonation = yield* Effect.flip( - provideContextAccess( - authentication.resolveTenantContext(impersonatedHeaders) - ) + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - expect( - Predicate.isTagged(incompleteImpersonation, 'OntosIdentityForbiddenError') - ).toBe(true); + expect(Predicate.isTagged(incompleteImpersonation, 'OntosIdentityForbiddenError')).toBe(true); yield* authDatabase .update(session) .set({ impersonationActionId: impersonationSession.actionId }) @@ -606,30 +507,19 @@ it.live.each([ .set({ impersonationReason: 'Tampered support reason' }) .where(eq(session.id, impersonationSession.id)); const mismatchedImpersonationReason = yield* Effect.flip( - provideContextAccess( - authentication.resolveTenantContext(impersonatedHeaders) - ) + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - expect( - Predicate.isTagged( - mismatchedImpersonationReason, - 'OntosIdentityForbiddenError' - ) - ).toBe(true); + expect(Predicate.isTagged(mismatchedImpersonationReason, 'OntosIdentityForbiddenError')).toBe(true); yield* authDatabase .update(session) .set({ impersonationReason: 'Investigating a tenant support request' }) .where(eq(session.id, impersonationSession.id)); - const impersonated = yield* provideContextAccess( - authentication.resolveTenantContext(impersonatedHeaders) - ); + const impersonated = yield* provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)); expect(impersonated.state).toBe('authenticated'); if (impersonated.state === 'authenticated') { expect(impersonated.principal.authMethod).toBe('support_impersonation'); expect(impersonated.principal.principalId).toBe(targetPrincipalId); - expect(impersonated.principal.impersonatedByPrincipalId).toBe( - originalPrincipalId - ); + expect(impersonated.principal.impersonatedByPrincipalId).toBe(originalPrincipalId); yield* providePrincipalManagementRepository( actionRuntime.runAction({ payload: { @@ -642,18 +532,14 @@ it.live.each([ correlationId: randomUUID(), idempotencyKey: randomUUID(), }, - }) + }), ); } supportPermissionAllowed = false; const revokedImpersonation = yield* Effect.flip( - provideContextAccess( - authentication.resolveTenantContext(impersonatedHeaders) - ) + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - expect( - Predicate.isTagged(revokedImpersonation, 'OntosIdentityForbiddenError') - ).toBe(true); + expect(Predicate.isTagged(revokedImpersonation, 'OntosIdentityForbiddenError')).toBe(true); const stopped = yield* provideContextAccess( providePrincipalManagementRepository( support @@ -661,13 +547,8 @@ it.live.each([ idempotencyKey: randomUUID(), requestHeaders: impersonatedHeaders, }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - randomUUID() - ) - ) - ) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), + ), ); expect(stopped.checkpointPending).toBe(false); expect(stopped.setCookieHeaders.length > 0).toBe(true); @@ -683,9 +564,9 @@ it.live.each([ 'checkpoint' in evidence && Predicate.isString(evidence.checkpoint) ? [evidence.checkpoint] - : [] + : [], ) - .toSorted() + .toSorted(), ).toEqual(['requested', 'started', 'stopped']); const identityEvidence = yield* coreDatabase .select({ @@ -702,8 +583,8 @@ it.live.each([ evidence.authMethod === 'api_key' && evidence.authBindingId === apiKeyAuthBindingId && evidence.principalId === originalPrincipalId && - evidence.impersonatedByPrincipalId === null - ) + evidence.impersonatedByPrincipalId === null, + ), ).toBe(true); expect( identityEvidence.some( @@ -711,12 +592,10 @@ it.live.each([ evidence.authMethod === 'support_impersonation' && evidence.authBindingId === targetAuthBindingId && evidence.principalId === targetPrincipalId && - evidence.impersonatedByPrincipalId === originalPrincipalId - ) + evidence.impersonatedByPrincipalId === originalPrincipalId, + ), ).toBe(true); - const recovery = yield* authDatabase - .select() - .from(supportImpersonationRecovery); + const recovery = yield* authDatabase.select().from(supportImpersonationRecovery); expect(recovery.length).toBe(0); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts b/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts index 6ddd08aaa..5dc889c03 100644 --- a/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts @@ -12,11 +12,7 @@ import { import { makeEffectHttpApiClient } from '@modern-js/plugin-bff/effect-client'; import { Effect, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - HttpApi, - HttpApiEndpoint, - HttpApiGroup, -} from 'effect/unstable/httpapi'; +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'; import { deriveDeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { makeInstalledModuleCatalogLoader } from '../../api/modules/installed-module-catalog.ts'; @@ -32,7 +28,7 @@ const contract = ( readonly components?: readonly object[]; readonly outboxSubscriptions?: readonly object[]; readonly shellContributions?: object; - } = {} + } = {}, ) => ({ deployment: { appId, buildMarker: `${appId}-independent-build` }, manifest: { @@ -40,15 +36,7 @@ const contract = ( defaultState: 'inactive', preservesHistoryWhenInactive: true, scope: 'tenant', - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], }, module: { description: `${moduleId} independently deployed module`, @@ -59,17 +47,12 @@ const contract = ( }, publicSurface: { actions: overrides.actions ?? [], - api: overrides.api ?? [ - { key: `${moduleId}.api`, operationKeys: ['read'] }, - ], + api: overrides.api ?? [{ key: `${moduleId}.api`, operationKeys: ['read'] }], components: overrides.components ?? [ { expose: './Dashboard', key: `${moduleId}.dashboard`, - mfBoundaryId: - appId === 'property-registry' - ? 'verticalPropertyRegistry' - : 'verticalDocumentsCenter', + mfBoundaryId: appId === 'property-registry' ? 'verticalPropertyRegistry' : 'verticalDocumentsCenter', }, ], events: [], @@ -92,14 +75,10 @@ const contract = ( schemaVersion: '2', }); const PropertyApi = HttpApi.make('PropertyApi').add( - HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')) -); -const PropertyRegistryUnitIdSchema = Schema.String.pipe( - Schema.brand('PropertyRegistryUnitId') -); -const DocumentsCenterDocumentIdSchema = Schema.String.pipe( - Schema.brand('DocumentsCenterDocumentId') + HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')), ); +const PropertyRegistryUnitIdSchema = Schema.String.pipe(Schema.brand('PropertyRegistryUnitId')); +const DocumentsCenterDocumentIdSchema = Schema.String.pipe(Schema.brand('DocumentsCenterDocumentId')); const PropertyAction = defineAction( { accessEvidencePolicy: { @@ -128,7 +107,7 @@ const PropertyAction = defineAction( resultSchema: Schema.Struct({ renamed: Schema.Boolean }), schemaVersion: '1', }, - () => Effect.succeed({ renamed: true }) + () => Effect.succeed({ renamed: true }), ); const PropertyOutboxWorker = defineOutboxWorker( { @@ -154,7 +133,7 @@ const PropertyOutboxWorker = defineOutboxWorker( topic: 'documents.center.document-created', workerKey: 'property.registry.index-document', }, - () => Effect.void + () => Effect.void, ); const PropertyDashboard = () => null; const propertyManifest = defineOntosModuleManifest({ @@ -196,17 +175,10 @@ const propertyRuntimeRegistration = defineVerticalRuntimeRegistration({ manifest: propertyManifest, outboxWorkers: [PropertyOutboxWorker], }); -const propertySafeRuntime = extractVerticalRuntimeSafeDescriptors( - propertyRuntimeRegistration -); -const ContractDocumentJsonSchema = Schema.fromJsonString( - OntosModuleDeploymentContractSchema -); +const propertySafeRuntime = extractVerticalRuntimeSafeDescriptors(propertyRuntimeRegistration); +const ContractDocumentJsonSchema = Schema.fromJsonString(OntosModuleDeploymentContractSchema); const makeContractFetch = - ( - documents: ReadonlyMap, - requests: Map - ): ModuleContractFetch => + (documents: ReadonlyMap, requests: Map): ModuleContractFetch => (input) => { const { url } = new Request(input); const document = documents.get(url); @@ -214,22 +186,18 @@ const makeContractFetch = return Promise.resolve(new Response(null, { status: 404 })); } requests.set(url, (requests.get(url) ?? 0) + 1); - const encodedDocument = Schema.encodeUnknownSync( - ContractDocumentJsonSchema - )(document); + const encodedDocument = Schema.encodeUnknownSync(ContractDocumentJsonSchema)(document); return Promise.resolve( new Response(encodedDocument, { headers: { 'content-type': 'application/json' }, - }) + }), ); }; it.effect( 'keeps discovered metadata separate from one complete owner-local runtime', Effect.fnUntraced(function* runIntegration1() { - const propertyUrl = - 'https://property-registry.test/.well-known/ontos-module-manifest.json'; - const documentsUrl = - 'https://documents-center.test/.well-known/ontos-module-manifest.json'; + const propertyUrl = 'https://property-registry.test/.well-known/ontos-module-manifest.json'; + const documentsUrl = 'https://documents-center.test/.well-known/ontos-module-manifest.json'; const requests = new Map(); const contractFetch = makeContractFetch( new Map([ @@ -255,7 +223,7 @@ it.effect( ], [documentsUrl, contract('documents-center', 'documents.center')], ]), - requests + requests, ); const allowlist = yield* deriveDeploymentAllowlist({ environment: 'development', @@ -280,12 +248,8 @@ it.effect( expect(first).toBe(second); expect(requests.get(propertyUrl)).toBe(1); expect(requests.get(documentsUrl)).toBe(1); - expect( - first.getByDeploymentAppId('property-registry')?.manifest.module.id - ).toBe('property.registry'); - expect(first.getByModuleId('property.registry')?.deployment.appId).toBe( - 'property-registry' - ); + expect(first.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe('property.registry'); + expect(first.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); expect(first.moduleIds).toEqual(['documents.center', 'property.registry']); const tenantStates = [ { moduleKey: 'property.registry', state: 'active' }, @@ -293,47 +257,35 @@ it.effect( ] as const; expect( tenantStates - .filter( - ({ moduleKey, state }) => - state === 'active' && first.moduleIds.includes(moduleKey) - ) - .map(({ moduleKey }) => moduleKey) + .filter(({ moduleKey, state }) => state === 'active' && first.moduleIds.includes(moduleKey)) + .map(({ moduleKey }) => moduleKey), ).toEqual(['property.registry']); - expect(getVerticalRuntimeActions(propertyRuntimeRegistration)[0]).toBe( - PropertyAction - ); - expect( - getVerticalRuntimeOutboxWorkers(propertyRuntimeRegistration)[0] - ).toBe(PropertyOutboxWorker); + expect(getVerticalRuntimeActions(propertyRuntimeRegistration)[0]).toBe(PropertyAction); + expect(getVerticalRuntimeOutboxWorkers(propertyRuntimeRegistration)[0]).toBe(PropertyOutboxWorker); expect(Object.keys(propertyRuntimeRegistration)).toEqual(['moduleId']); let matchedSubscriptions: readonly object[] = []; yield* matchInstalledOutboxMessagesOnce(first, (input) => { matchedSubscriptions = input.subscriptions; return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); }); - expect(matchedSubscriptions).toEqual( - propertySafeRuntime.outboxSubscriptions - ); + expect(matchedSubscriptions).toEqual(propertySafeRuntime.outboxSubscriptions); const propertyClientReference = makeEffectHttpApiClient(PropertyApi, { baseUrl: new URL('/api', propertyUrl), }); expect(Effect.isEffect(propertyClientReference)).toBe(true); - expect( - first.getByModuleId('property.registry')?.manifest.publicSurface - .components - ).toEqual([ + expect(first.getByModuleId('property.registry')?.manifest.publicSurface.components).toEqual([ { expose: './Dashboard', key: 'property.registry.dashboard', mfBoundaryId: 'verticalPropertyRegistry', }, ]); - const serialized = yield* Schema.encodeUnknownEffect( - ContractDocumentJsonSchema - )(first.getByModuleId('property.registry')); + const serialized = yield* Schema.encodeUnknownEffect(ContractDocumentJsonSchema)( + first.getByModuleId('property.registry'), + ); expect(serialized.includes('payloadSchema')).toBe(false); expect(serialized.includes('leaseDurationMs')).toBe(false); expect(serialized.includes('PropertyDashboard')).toBe(false); expect(serialized.includes('handler')).toBe(false); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts b/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts index 83fe4398f..6d599e541 100644 --- a/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts @@ -5,18 +5,14 @@ import { Effect } from 'effect'; import { describe, expect, it } from 'effect-rstest'; import * as Schema from 'effect/Schema'; -const shellConfigUrl = new URL( - '../../module-federation.config.ts', - import.meta.url -); +const shellConfigUrl = new URL('../../module-federation.config.ts', import.meta.url); const partyRegistryConfigUrl = new URL( '../../../../verticals/party-registry/module-federation.config.ts', - import.meta.url + import.meta.url, ); const applicationPackageJsonUrls = new Set([ new URL('../../package.json', import.meta.url).href, - new URL('../../../../verticals/party-registry/package.json', import.meta.url) - .href, + new URL('../../../../verticals/party-registry/package.json', import.meta.url).href, ]); registerHooks({ @@ -35,35 +31,25 @@ registerHooks({ }); describe('module-federation-i18n-runtime', () => { - it.effect( - 'Shell and Party Registry share the i18n runtime that owns the federated provider context', - () => - Effect.gen(function* sharesFederatedI18nRuntime() { - const [{ default: shellConfig }, { default: partyRegistryConfig }] = - yield* Effect.promise(() => - Promise.all([ - import(shellConfigUrl.href), - import(partyRegistryConfigUrl.href), - ]) - ); - const require = createRequire(shellConfigUrl); - const { version: i18nVersion } = Schema.decodeUnknownSync( - Schema.Struct({ version: Schema.String }) - )(require('@modern-js/plugin-i18n/package.json')); - const expectedSharedRuntime = { - import: '@modern-js/plugin-i18n/runtime/no-react-i18next', - requiredVersion: i18nVersion, - singleton: true, - strictVersion: true, - treeShaking: false, - }; + it.effect('Shell and Party Registry share the i18n runtime that owns the federated provider context', () => + Effect.gen(function* sharesFederatedI18nRuntime() { + const [{ default: shellConfig }, { default: partyRegistryConfig }] = yield* Effect.promise(() => + Promise.all([import(shellConfigUrl.href), import(partyRegistryConfigUrl.href)]), + ); + const require = createRequire(shellConfigUrl); + const { version: i18nVersion } = Schema.decodeUnknownSync(Schema.Struct({ version: Schema.String }))( + require('@modern-js/plugin-i18n/package.json'), + ); + const expectedSharedRuntime = { + import: '@modern-js/plugin-i18n/runtime/no-react-i18next', + requiredVersion: i18nVersion, + singleton: true, + strictVersion: true, + treeShaking: false, + }; - expect(shellConfig.shared?.['@modern-js/plugin-i18n/runtime']).toEqual( - expectedSharedRuntime - ); - expect( - partyRegistryConfig.shared?.['@modern-js/plugin-i18n/runtime'] - ).toEqual(expectedSharedRuntime); - }) + expect(shellConfig.shared?.['@modern-js/plugin-i18n/runtime']).toEqual(expectedSharedRuntime); + expect(partyRegistryConfig.shared?.['@modern-js/plugin-i18n/runtime']).toEqual(expectedSharedRuntime); + }), ); }); diff --git a/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts index 57a313a2a..72574f902 100644 --- a/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts +++ b/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts @@ -42,34 +42,23 @@ describe('stage-demo-bootstrap', () => { databaseAdminUrl: baseConfiguration.connectionString, } as const; const cleanup = Effect.gen(function* cleanupPasswordFixture() { - const users = yield* database - .select({ id: user.id }) - .from(user) - .where(eq(user.email, email)); + const users = yield* database.select({ id: user.id }).from(user).where(eq(user.email, email)); for (const existingUser of users) { - yield* database - .delete(session) - .where(eq(session.userId, existingUser.id)); - yield* database - .delete(account) - .where(eq(account.userId, existingUser.id)); + yield* database.delete(session).where(eq(session.userId, existingUser.id)); + yield* database.delete(account).where(eq(account.userId, existingUser.id)); yield* database.delete(user).where(eq(user.id, existingUser.id)); } }).pipe(Effect.orDie); yield* cleanup; yield* Effect.addFinalizer(() => cleanup); - const created = yield* ensureStageDemoAuthUser( - configuration, - configuration.accounts[0] - ).pipe(Effect.provideService(AuthDatabase, persistence)); - yield* database - .update(account) - .set({ password: randomUUID() }) - .where(eq(account.userId, created.userId)); + const created = yield* ensureStageDemoAuthUser(configuration, configuration.accounts[0]).pipe( + Effect.provideService(AuthDatabase, persistence), + ); + yield* database.update(account).set({ password: randomUUID() }).where(eq(account.userId, created.userId)); const sessionCreatedAt = yield* DateTime.nowAsDate; const sessionExpiresAt = DateTime.makeUnsafe(sessionCreatedAt).pipe( DateTime.add({ minutes: 1 }), - DateTime.toDateUtc + DateTime.toDateUtc, ); yield* database.insert(session).values({ createdAt: sessionCreatedAt, @@ -96,13 +85,13 @@ describe('stage-demo-bootstrap', () => { verifyPassword({ hash: credential?.password ?? '', password: replacementPassword, - }) + }), ); const initialMatches = yield* Effect.promise(() => verifyPassword({ hash: credential?.password ?? '', password: initialPassword, - }) + }), ); expect(replacementMatches).toBe(true); expect(initialMatches).toBe(false); @@ -111,8 +100,8 @@ describe('stage-demo-bootstrap', () => { .from(session) .where(eq(session.userId, created.userId)); expect(remainingSessions).toHaveLength(0); - }) - ) + }), + ), ); it.live( @@ -130,7 +119,7 @@ describe('stage-demo-bootstrap', () => { yield* Effect.addFinalizer(() => Effect.sync(() => { databaseClosed = true; - }) + }), ); const database = yield* makeAuthDatabase(configuration); return yield* bootstrapStageDemo({ @@ -154,48 +143,36 @@ describe('stage-demo-bootstrap', () => { adapter: (options) => { const adapter = sdkAdapter(options); const create = adapter.create.bind(adapter); - rstest - .spyOn(adapter, 'create') - .mockImplementation( - (input: Parameters[0]) => { - if (input.model === 'user') { - Deferred.doneUnsafe(sdkStarted, Effect.succeed(null)); - // oxlint-disable-next-line sonarjs/no-nested-functions -- SDK settlement continuation stays inside its adapter mock. - return sdkSettlement.promise.then(() => - create(input) - ); - } - return create(input); - } - ); + rstest.spyOn(adapter, 'create').mockImplementation((input: Parameters[0]) => { + if (input.model === 'user') { + Deferred.doneUnsafe(sdkStarted, Effect.succeed(null)); + // oxlint-disable-next-line sonarjs/no-nested-functions -- SDK settlement continuation stays inside its adapter mock. + return sdkSettlement.promise.then(() => create(input)); + } + return create(input); + }); return adapter; }, executor: database.executor, - }) + }), ); - }) + }), ); - const outcome = yield* Effect.gen( - function* interruptPendingBootstrap() { - const bootstrap = yield* program.pipe(Effect.forkChild); - yield* Deferred.await(sdkStarted).pipe( - Effect.raceFirst(Fiber.join(bootstrap)) - ); - const interruption = yield* Fiber.interrupt(bootstrap).pipe( - Effect.forkChild - ); - yield* Effect.yieldNow; - const pending = bootstrap.pollUnsafe() === undefined; - const closedBeforeSettlement = databaseClosed; - sdkSettlement.resolve(null); - yield* Fiber.join(interruption); - return { - closedBeforeSettlement, - exit: yield* Fiber.await(bootstrap), - pending, - }; - } - ).pipe(Effect.ensuring(Effect.sync(() => sdkSettlement.resolve(null)))); + const outcome = yield* Effect.gen(function* interruptPendingBootstrap() { + const bootstrap = yield* program.pipe(Effect.forkChild); + yield* Deferred.await(sdkStarted).pipe(Effect.raceFirst(Fiber.join(bootstrap))); + const interruption = yield* Fiber.interrupt(bootstrap).pipe(Effect.forkChild); + yield* Effect.yieldNow; + const pending = bootstrap.pollUnsafe() === undefined; + const closedBeforeSettlement = databaseClosed; + sdkSettlement.resolve(null); + yield* Fiber.join(interruption); + return { + closedBeforeSettlement, + exit: yield* Fiber.await(bootstrap), + pending, + }; + }).pipe(Effect.ensuring(Effect.sync(() => sdkSettlement.resolve(null)))); expect(outcome.pending).toBe(true); expect(outcome.closedBeforeSettlement).toBe(false); expect(Exit.isFailure(outcome.exit)).toBe(true); @@ -206,6 +183,6 @@ describe('stage-demo-bootstrap', () => { expect(store.user).toHaveLength(1); expect(store.account).toHaveLength(1); }), - 10_000 + 10_000, ); }); diff --git a/app/apps/shell-super-app/tests/support/action-runtime-double.ts b/app/apps/shell-super-app/tests/support/action-runtime-double.ts index eae446950..e9e4a3377 100644 --- a/app/apps/shell-super-app/tests/support/action-runtime-double.ts +++ b/app/apps/shell-super-app/tests/support/action-runtime-double.ts @@ -12,9 +12,7 @@ type TestActionOutcome = } | { readonly kind: 'success'; readonly value: JsonValue }; -export const actionCoreFailure = ( - error: ActionCoreError -): TestActionOutcome => ({ +export const actionCoreFailure = (error: ActionCoreError): TestActionOutcome => ({ error, kind: 'core-failure', }); @@ -24,31 +22,27 @@ export const actionDefect = (defect: Error | string): TestActionOutcome => ({ kind: 'defect', }); -export const actionDomainFailure = ( - error: Readonly<{ readonly _tag: string }> -): TestActionOutcome => ({ error, kind: 'domain-failure' }); +export const actionDomainFailure = (error: Readonly<{ readonly _tag: string }>): TestActionOutcome => ({ + error, + kind: 'domain-failure', +}); export const actionSuccess = (value: JsonValue): TestActionOutcome => ({ kind: 'success', value, }); -export const makeActionRuntimeDouble = ( - outcomes: readonly TestActionOutcome[] -) => { +export const makeActionRuntimeDouble = (outcomes: readonly TestActionOutcome[]) => { let invocation = 0; const payloads: unknown[] = []; const runtime: ActionRuntimeService = { - resolveActionCommit: () => - Effect.die('resolveActionCommit is not configured in this test'), + resolveActionCommit: () => Effect.die('resolveActionCommit is not configured in this test'), runAction: (input) => { payloads.push(input.payload); const outcome = outcomes[invocation]; invocation += 1; if (outcome === undefined) { - return Effect.die( - `Action invocation ${invocation} has no configured outcome` - ); + return Effect.die(`Action invocation ${invocation} has no configured outcome`); } if (outcome.kind === 'core-failure') { return Effect.fail(outcome.error); @@ -60,15 +54,9 @@ export const makeActionRuntimeDouble = ( const schema = input.registration.descriptor.domainErrorSchema; return Schema.is(schema)(outcome.error) ? Effect.fail(outcome.error) - : Effect.die( - 'Configured action domain failure does not match registration schema' - ); + : Effect.die('Configured action domain failure does not match registration schema'); } - return Effect.sync(() => - Schema.decodeUnknownSync(input.registration.descriptor.resultSchema)( - outcome.value - ) - ); + return Effect.sync(() => Schema.decodeUnknownSync(input.registration.descriptor.resultSchema)(outcome.value)); }, }; return { invocationCount: () => invocation, payloads, runtime }; diff --git a/app/apps/shell-super-app/tests/support/context-access-double.ts b/app/apps/shell-super-app/tests/support/context-access-double.ts index 4d03cda66..22a4ed542 100644 --- a/app/apps/shell-super-app/tests/support/context-access-double.ts +++ b/app/apps/shell-super-app/tests/support/context-access-double.ts @@ -1,20 +1,15 @@ import type { ContextAccessService } from '@app/core-runtime'; import { Effect } from 'effect'; -export const makeContextAccessDouble = ( - decision: 'allowed' | 'unavailable' -): ContextAccessService => ({ - legalEntities: ({ legalEntityIds }) => - Effect.succeed(legalEntityIds.map((key) => ({ decision, key }))), - modules: ({ moduleIds }) => - Effect.succeed(moduleIds.map((key) => ({ decision, key }))), +export const makeContextAccessDouble = (decision: 'allowed' | 'unavailable'): ContextAccessService => ({ + legalEntities: ({ legalEntityIds }) => Effect.succeed(legalEntityIds.map((key) => ({ decision, key }))), + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))), resources: ({ resources }) => Effect.succeed( resources.map(({ moduleId, resourceId, resourceType }) => ({ decision, key: `${moduleId}:${resourceType}:${resourceId}`, - })) + })), ), - tenants: ({ tenantIds }) => - Effect.succeed(tenantIds.map((key) => ({ decision, key }))), + tenants: ({ tenantIds }) => Effect.succeed(tenantIds.map((key) => ({ decision, key }))), }); diff --git a/app/apps/shell-super-app/tests/support/identity-service-doubles.ts b/app/apps/shell-super-app/tests/support/identity-service-doubles.ts index 803c4c656..9255c55d4 100644 --- a/app/apps/shell-super-app/tests/support/identity-service-doubles.ts +++ b/app/apps/shell-super-app/tests/support/identity-service-doubles.ts @@ -3,8 +3,7 @@ import { Effect } from 'effect'; import type { ApiKeyServiceContract } from '../../api/auth/api-key-service.ts'; -const unconfigured = (operation: string) => - Effect.die(`${operation} is not configured in this test`); +const unconfigured = (operation: string) => Effect.die(`${operation} is not configured in this test`); const apiKeyDefaults: ApiKeyServiceContract = { clearPendingCleanup: () => unconfigured('clearPendingCleanup'), @@ -17,26 +16,21 @@ const apiKeyDefaults: ApiKeyServiceContract = { const principalResolverDefaults: PrincipalResolverService = { listAvailableTenants: () => unconfigured('listAvailableTenants'), - loadApiKeyBindingForAdministration: () => - unconfigured('loadApiKeyBindingForAdministration'), - resolveApiKeyBindingSubject: () => - unconfigured('resolveApiKeyBindingSubject'), + loadApiKeyBindingForAdministration: () => unconfigured('loadApiKeyBindingForAdministration'), + resolveApiKeyBindingSubject: () => unconfigured('resolveApiKeyBindingSubject'), resolveBetterAuthApiKey: () => unconfigured('resolveBetterAuthApiKey'), - resolveBetterAuthUserForPrincipal: () => - unconfigured('resolveBetterAuthUserForPrincipal'), - resolveBetterAuthUserForTenant: () => - unconfigured('resolveBetterAuthUserForTenant'), - resolveDefaultBetterAuthUser: () => - unconfigured('resolveDefaultBetterAuthUser'), + resolveBetterAuthUserForPrincipal: () => unconfigured('resolveBetterAuthUserForPrincipal'), + resolveBetterAuthUserForTenant: () => unconfigured('resolveBetterAuthUserForTenant'), + resolveDefaultBetterAuthUser: () => unconfigured('resolveDefaultBetterAuthUser'), resolveProviderSubject: () => unconfigured('resolveProviderSubject'), - verifySupportImpersonationStarted: () => - unconfigured('verifySupportImpersonationStarted'), + verifySupportImpersonationStarted: () => unconfigured('verifySupportImpersonationStarted'), }; -export const makeApiKeyServiceDouble = ( - overrides: Partial = {} -): ApiKeyServiceContract => ({ ...apiKeyDefaults, ...overrides }); +export const makeApiKeyServiceDouble = (overrides: Partial = {}): ApiKeyServiceContract => ({ + ...apiKeyDefaults, + ...overrides, +}); export const makePrincipalResolverDouble = ( - overrides: Partial = {} + overrides: Partial = {}, ): PrincipalResolverService => ({ ...principalResolverDefaults, ...overrides }); diff --git a/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts b/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts index 1268deb3e..87ea350bb 100644 --- a/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts +++ b/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts @@ -1,14 +1,10 @@ import { Effect } from 'effect'; import { rs } from 'effect-rstest'; -import type { - SupportAuthProvider, - SupportImpersonationStore, -} from '../../api/auth/impersonation-service.ts'; +import type { SupportAuthProvider, SupportImpersonationStore } from '../../api/auth/impersonation-service.ts'; import type { AuthenticationServiceContract } from '../../api/auth/service.ts'; -const unconfiguredEffect = (operation: string) => - Effect.die(`${operation} is not configured in this test`); +const unconfiguredEffect = (operation: string) => Effect.die(`${operation} is not configured in this test`); const authenticationDefaults: AuthenticationServiceContract = { availableTenants: () => unconfiguredEffect('availableTenants'), createFixtureUser: () => unconfiguredEffect('createFixtureUser'), @@ -27,14 +23,10 @@ const providerDefaults: SupportAuthProvider['api'] = { .mockRejectedValue(new Error('getSession is not configured in this test')), impersonateUser: rs .fn() - .mockRejectedValue( - new Error('impersonateUser is not configured in this test') - ), + .mockRejectedValue(new Error('impersonateUser is not configured in this test')), stopImpersonating: rs .fn() - .mockRejectedValue( - new Error('stopImpersonating is not configured in this test') - ), + .mockRejectedValue(new Error('stopImpersonating is not configured in this test')), }; const storeDefaults: SupportImpersonationStore = { @@ -44,21 +36,20 @@ const storeDefaults: SupportImpersonationStore = { loadExpiredRecovery: () => unconfiguredEffect('loadExpiredRecovery'), loadOriginalSession: () => unconfiguredEffect('loadOriginalSession'), loadRecoveries: () => unconfiguredEffect('loadRecoveries'), - updateImpersonationSession: () => - unconfiguredEffect('updateImpersonationSession'), + updateImpersonationSession: () => unconfiguredEffect('updateImpersonationSession'), }; export const makeAuthenticationServiceDouble = ( - overrides: Partial = {} + overrides: Partial = {}, ): AuthenticationServiceContract => ({ ...authenticationDefaults, ...overrides, }); export const makeSupportAuthProviderDouble = ( - overrides: Partial = {} + overrides: Partial = {}, ): SupportAuthProvider => ({ api: { ...providerDefaults, ...overrides } }); export const makeSupportImpersonationStoreDouble = ( - overrides: Partial = {} + overrides: Partial = {}, ): SupportImpersonationStore => ({ ...storeDefaults, ...overrides }); diff --git a/app/apps/shell-super-app/tests/support/localized-link-double.tsx b/app/apps/shell-super-app/tests/support/localized-link-double.tsx index 4c833f906..6cd25b705 100644 --- a/app/apps/shell-super-app/tests/support/localized-link-double.tsx +++ b/app/apps/shell-super-app/tests/support/localized-link-double.tsx @@ -29,14 +29,11 @@ export interface LocalizedLinkRecording { const localisedUrlPatterns = new Map>>( Object.entries(ultramodernLocalisedUrls).map( - ([canonicalPattern, localisedPatterns]): readonly [ - string, - Readonly>, - ] => [ + ([canonicalPattern, localisedPatterns]): readonly [string, Readonly>] => [ canonicalPattern, { cs: localisedPatterns.cs, en: localisedPatterns.en }, - ] - ) + ], + ), ); /** @@ -47,19 +44,14 @@ const localisedUrlPatterns = new Map>>( const resolveLocalizedHref = ( to: string, params: Readonly> | undefined, - language: string + language: string, ): string => { const canonicalPattern = to.replaceAll('$', ':'); - const localisedPattern = - localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; + const localisedPattern = localisedUrlPatterns.get(canonicalPattern)?.[language] ?? canonicalPattern; const segments = localisedPattern .split('/') .filter(Boolean) - .map((segment) => - segment.startsWith(':') - ? encodeURIComponent(params?.[segment.slice(1)] ?? '') - : segment - ); + .map((segment) => (segment.startsWith(':') ? encodeURIComponent(params?.[segment.slice(1)] ?? '') : segment)); return `/${[language, ...segments].join('/')}`; }; @@ -70,14 +62,11 @@ const resolveLocalizedHref = ( */ export const renderLocalizedLinkDouble = ( { children, href, params, to, ...anchorProps }: LocalizedLinkDoubleProps, - recording: LocalizedLinkRecording + recording: LocalizedLinkRecording, ): ReactElement => { recording.calls.push({ href, params, to }); return ( - + {children} ); diff --git a/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts b/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts index 3df72419c..c15f04ad6 100644 --- a/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts @@ -6,13 +6,10 @@ import { expect, test } from 'effect-rstest'; const workspaceRoot = new URL('../../../../', import.meta.url); const readJson = >( relativePath: string, - schema: JsonSchema + schema: JsonSchema, ): JsonSchema['Type'] => - Schema.decodeUnknownSync(schema)( - JSON.parse(fs.readFileSync(new URL(relativePath, workspaceRoot), 'utf-8')) - ); -const readText = (relativePath: string) => - fs.readFileSync(new URL(relativePath, workspaceRoot), 'utf-8'); + Schema.decodeUnknownSync(schema)(JSON.parse(fs.readFileSync(new URL(relativePath, workspaceRoot), 'utf-8'))); +const readText = (relativePath: string) => fs.readFileSync(new URL(relativePath, workspaceRoot), 'utf-8'); const TopologySchema = Schema.Struct({ shell: Schema.Struct({ @@ -29,7 +26,7 @@ const TopologySchema = Schema.Struct({ Schema.Struct({ id: Schema.String, moduleFederation: Schema.Struct({ exposes: Schema.Array(Schema.String) }), - }) + }), ), }); @@ -48,9 +45,7 @@ test('keeps authentication in the existing Shell/Core ownership boundary', () => expect(installedVerticalIds).toEqual(['party-registry']); expect(verticalRefs).toEqual(installedVerticalIds); expect(browserRemoteIds).toEqual(['party-registry']); - expect(moduleFederation.remotes.map(({ id }) => id)).toEqual( - browserRemoteIds - ); + expect(moduleFederation.remotes.map(({ id }) => id)).toEqual(browserRemoteIds); expect(fs.existsSync(new URL('verticals/auth', workspaceRoot))).toBe(false); expect(shellPackageSource).not.toContain('@app/auth'); expect(ownershipSource).not.toContain('"id":"auth"'); @@ -59,15 +54,9 @@ test('keeps authentication in the existing Shell/Core ownership boundary', () => test('keeps the Contacts page in the Party Registry lazy browser allowlist', () => { const source = readText('apps/shell-super-app/src/api/vertical-clients.ts'); const shellConfig = readText('apps/shell-super-app/modern.config.ts'); - const lazyRemotes = [ - ...source.matchAll(/import\('(?[^']+)'\)/gu), - ].map((match) => match.groups?.['remote']); - const componentKeys = [ - ...source.matchAll(/componentKey: '(?[^']+)'/gu), - ].map((match) => match.groups?.['key']); + const lazyRemotes = [...source.matchAll(/import\('(?[^']+)'\)/gu)].map((match) => match.groups?.['remote']); + const componentKeys = [...source.matchAll(/componentKey: '(?[^']+)'/gu)].map((match) => match.groups?.['key']); expect(lazyRemotes).toEqual(['partyRegistry/PageContacts']); expect(componentKeys).toEqual(['party.registry.page-contacts']); - expect(shellConfig).toContain( - 'new rspack.NormalModuleReplacementPlugin(\n /^partyRegistry\\//u,' - ); + expect(shellConfig).toMatch(/new rspack\.NormalModuleReplacementPlugin\(\s*\/\^partyRegistry\\\/\/u,/u); }); diff --git a/app/apps/shell-super-app/tests/unit/auth-config.test.ts b/app/apps/shell-super-app/tests/unit/auth-config.test.ts index dc90f22e8..e0db63767 100644 --- a/app/apps/shell-super-app/tests/unit/auth-config.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-config.test.ts @@ -2,15 +2,11 @@ import { Effect, Predicate, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { parseAuthConfig } from '../../api/auth/config.ts'; -import { - GatewayIssuerConfigError, - parseGatewayIssuerConfig, -} from '../../api/auth/gateway-issuer-config.ts'; +import { GatewayIssuerConfigError, parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; const validEnvironment = { BETTER_AUTH_SECRET: 'a-secure-test-secret-with-more-than-32-characters', - BETTER_AUTH_TRUSTED_ORIGINS: - 'http://localhost:3020,https://preview.example.test', + BETTER_AUTH_TRUSTED_ORIGINS: 'http://localhost:3020,https://preview.example.test', BETTER_AUTH_URL: 'http://localhost:3020', DATABASE_URL: 'postgresql://ontos:ontos@localhost:5433/ontos', }; @@ -19,44 +15,37 @@ it.effect('parses trusted origins and derives local cookie security', () => Effect.gen(function* parsesOrigins() { const configuration = yield* parseAuthConfig(validEnvironment); expect(configuration.secureCookies).toBe(false); - expect(configuration.trustedOrigins).toEqual([ - 'http://localhost:3020', - 'https://preview.example.test', - ]); - }) + expect(configuration.trustedOrigins).toEqual(['http://localhost:3020', 'https://preview.example.test']); + }), ); -it.effect( - 'requires a strong secret and PostgreSQL URL in the typed error channel', - () => - Effect.gen(function* validatesCredentials() { - const [secretError, databaseError] = yield* Effect.all( - [ - Effect.flip( - parseAuthConfig({ - ...validEnvironment, - BETTER_AUTH_SECRET: 'short', - }) - ), - Effect.flip( - parseAuthConfig({ - ...validEnvironment, - DATABASE_URL: 'https://example.test/not-postgres', - }) - ), - ], - { concurrency: 'unbounded' } - ); - expect(Predicate.isTagged(secretError, 'AuthConfigError')).toBe(true); - expect(Predicate.isTagged(databaseError, 'AuthConfigError')).toBe(true); - }) +it.effect('requires a strong secret and PostgreSQL URL in the typed error channel', () => + Effect.gen(function* validatesCredentials() { + const [secretError, databaseError] = yield* Effect.all( + [ + Effect.flip( + parseAuthConfig({ + ...validEnvironment, + BETTER_AUTH_SECRET: 'short', + }), + ), + Effect.flip( + parseAuthConfig({ + ...validEnvironment, + DATABASE_URL: 'https://example.test/not-postgres', + }), + ), + ], + { concurrency: 'unbounded' }, + ); + expect(Predicate.isTagged(secretError, 'AuthConfigError')).toBe(true); + expect(Predicate.isTagged(databaseError, 'AuthConfigError')).toBe(true); + }), ); -it.effect( - 'keeps gateway signing configuration independent from Better Auth configuration', - () => - Effect.gen(function* independentSigning() { - const authentication = yield* parseAuthConfig(validEnvironment); - const gatewayError = yield* Effect.flip(parseGatewayIssuerConfig({})); - expect(authentication.baseUrl).toBe('http://localhost:3020'); - expect(Schema.is(GatewayIssuerConfigError)(gatewayError)).toBe(true); - }) +it.effect('keeps gateway signing configuration independent from Better Auth configuration', () => + Effect.gen(function* independentSigning() { + const authentication = yield* parseAuthConfig(validEnvironment); + const gatewayError = yield* Effect.flip(parseGatewayIssuerConfig({})); + expect(authentication.baseUrl).toBe('http://localhost:3020'); + expect(Schema.is(GatewayIssuerConfigError)(gatewayError)).toBe(true); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/auth-contract.test.ts b/app/apps/shell-super-app/tests/unit/auth-contract.test.ts index c8f8b6fcf..82fb129ef 100644 --- a/app/apps/shell-super-app/tests/unit/auth-contract.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-contract.test.ts @@ -35,9 +35,7 @@ const problemTag = (schema: Schema.Top): SchemaAST.LiteralValue => { if (!SchemaAST.isObjects(schema.ast)) { throw new Error('Expected an object problem schema'); } - const tag = schema.ast.propertySignatures.find( - ({ name }) => name === '_tag' - )?.type; + const tag = schema.ast.propertySignatures.find(({ name }) => name === '_tag')?.type; expect(tag !== undefined && SchemaAST.isLiteral(tag)).toBe(true); if (tag === undefined || !SchemaAST.isLiteral(tag)) { throw new Error('Expected a literal problem tag'); @@ -46,34 +44,15 @@ const problemTag = (schema: Schema.Top): SchemaAST.LiteralValue => { }; it('publishes authentication, identity lifecycle, and gateway operations', () => { - const authenticationEndpoints = Object.keys( - ShellAuthenticationApi.groups.authentication.endpoints - ).toSorted(); - const gatewayEndpoints = Object.keys( - ShellAuthenticationApi.groups.gatewayContext.endpoints - ); - const identityEndpoints = Object.keys( - ShellAuthenticationApi.groups.identity.endpoints - ).toSorted(); - const legalEntityEndpoints = Object.keys( - ShellAuthenticationApi.groups.legalEntities.endpoints - ).toSorted(); - const tenantEndpoints = Object.keys( - ShellAuthenticationApi.groups.tenants.endpoints - ).toSorted(); - const resourceEndpoints = Object.keys( - ShellAuthenticationApi.groups.resources.endpoints - ).toSorted(); + const authenticationEndpoints = Object.keys(ShellAuthenticationApi.groups.authentication.endpoints).toSorted(); + const gatewayEndpoints = Object.keys(ShellAuthenticationApi.groups.gatewayContext.endpoints); + const identityEndpoints = Object.keys(ShellAuthenticationApi.groups.identity.endpoints).toSorted(); + const legalEntityEndpoints = Object.keys(ShellAuthenticationApi.groups.legalEntities.endpoints).toSorted(); + const tenantEndpoints = Object.keys(ShellAuthenticationApi.groups.tenants.endpoints).toSorted(); + const resourceEndpoints = Object.keys(ShellAuthenticationApi.groups.resources.endpoints).toSorted(); - expect(authenticationEndpoints).toEqual([ - 'currentSession', - 'signIn', - 'signOut', - ]); - expect(gatewayEndpoints).toEqual([ - 'issueGatewayContext', - 'issueApiKeyGatewayContext', - ]); + expect(authenticationEndpoints).toEqual(['currentSession', 'signIn', 'signOut']); + expect(gatewayEndpoints).toEqual(['issueGatewayContext', 'issueApiKeyGatewayContext']); expect(identityEndpoints).toEqual([ 'changePrincipalStatus', 'createNonHumanPrincipal', @@ -88,25 +67,15 @@ it('publishes authentication, identity lifecycle, and gateway operations', () => 'startSupportImpersonation', 'stopSupportImpersonation', ]); - expect(legalEntityEndpoints).toEqual([ - 'availableLegalEntities', - 'switchLegalEntity', - ]); + expect(legalEntityEndpoints).toEqual(['availableLegalEntities', 'switchLegalEntity']); expect(tenantEndpoints).toEqual(['availableTenants', 'switchTenant']); - expect(resourceEndpoints).toEqual([ - 'attachMedia', - 'resourceDetail', - 'search', - ]); + expect(resourceEndpoints).toEqual(['attachMedia', 'resourceDetail', 'search']); expect( Object.fromEntries( Object.values(ShellAuthenticationApi.groups).flatMap((group) => - Object.entries(group.endpoints).map(([name, endpoint]) => [ - name, - endpoint.path, - ]) - ) - ) + Object.entries(group.endpoints).map(([name, endpoint]) => [name, endpoint.path]), + ), + ), ).toEqual({ attachMedia: '/shell/resource/media-attachment', availableLegalEntities: '/auth/legal-entities', @@ -140,9 +109,7 @@ it('publishes authentication, identity lifecycle, and gateway operations', () => signInPath: '/shell-super-app-api/auth/sign-in', switchTenantPath: '/shell-super-app-api/auth/tenant/switch', }); - expect( - [...authenticationEndpoints, ...gatewayEndpoints].join(':') - ).not.toMatch(/testing|actionKey/u); + expect([...authenticationEndpoints, ...gatewayEndpoints].join(':')).not.toMatch(/testing|actionKey/u); }); it('preserves migrated Shell Problem Details wire shapes and ordered membership', () => { @@ -169,43 +136,21 @@ it('preserves migrated Shell Problem Details wire shapes and ordered membership' title: 'Rate limited', type: 'https://ontos.dev/problems/shell-rate-limited', } as const; - const decodedUnavailable = Schema.decodeUnknownSync( - AuthenticationUnavailableProblemSchema - )(unavailable); - expect( - Schema.is(AuthenticationUnavailableProblemSchema)(decodedUnavailable) - ).toBe(true); - expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual( - Struct.omit(unavailable, ['_tag']) - ); - const decodedRetryable = Schema.decodeUnknownSync( - TenantCapabilityUnavailableProblemSchema - )(retryable); - expect( - Schema.is(TenantCapabilityUnavailableProblemSchema)(decodedRetryable) - ).toBe(true); - expect(Struct.omit(decodedRetryable, ['_tag'])).toEqual( - Struct.omit(retryable, ['_tag']) - ); - const decodedRateLimited = Schema.decodeUnknownSync( - ShellRateLimitedProblemSchema - )(rateLimited); - expect(Schema.is(ShellRateLimitedProblemSchema)(decodedRateLimited)).toBe( - true - ); - expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual( - Struct.omit(rateLimited, ['_tag']) - ); + const decodedUnavailable = Schema.decodeUnknownSync(AuthenticationUnavailableProblemSchema)(unavailable); + expect(Schema.is(AuthenticationUnavailableProblemSchema)(decodedUnavailable)).toBe(true); + expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual(Struct.omit(unavailable, ['_tag'])); + const decodedRetryable = Schema.decodeUnknownSync(TenantCapabilityUnavailableProblemSchema)(retryable); + expect(Schema.is(TenantCapabilityUnavailableProblemSchema)(decodedRetryable)).toBe(true); + expect(Struct.omit(decodedRetryable, ['_tag'])).toEqual(Struct.omit(retryable, ['_tag'])); + const decodedRateLimited = Schema.decodeUnknownSync(ShellRateLimitedProblemSchema)(rateLimited); + expect(Schema.is(ShellRateLimitedProblemSchema)(decodedRateLimited)).toBe(true); + expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual(Struct.omit(rateLimited, ['_tag'])); expect(() => Schema.decodeUnknownSync(AuthenticationUnavailableProblemSchema, { onExcessProperty: 'error', - })({ ...unavailable, retryable: true }) + })({ ...unavailable, retryable: true }), ).toThrow(); - expect( - [ - ...ShellAuthenticationApi.groups.authentication.endpoints.signIn.error, - ].map(problemTag) - ).toEqual([ + expect([...ShellAuthenticationApi.groups.authentication.endpoints.signIn.error].map(problemTag)).toEqual([ 'InvalidCredentialsProblem', 'OntosIdentityForbiddenProblem', 'AuthenticationUnavailableProblem', @@ -213,103 +158,89 @@ it('preserves migrated Shell Problem Details wire shapes and ordered membership' ]); }); -it.effect( - 'decodes a missing identity idempotency header so handlers can return declared 428', - () => - Effect.gen(function* testProgram1() { - expect( - yield* Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({}) - ).toEqual({}); - expect( - yield* Effect.flip( - Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({ - 'idempotency-key': '', - }) - ) - ).toBeDefined(); - }) +it.effect('decodes a missing identity idempotency header so handlers can return declared 428', () => + Effect.gen(function* testProgram1() { + expect(yield* Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({})).toEqual({}); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({ + 'idempotency-key': '', + }), + ), + ).toBeDefined(); + }), ); -it.effect( - 'publishes exact legal-entity endpoints with an ID-only switch payload', - () => - Effect.gen(function* testProgram2() { - const { availableLegalEntities, switchLegalEntity } = - ShellAuthenticationApi.groups.legalEntities.endpoints; - expect({ - method: availableLegalEntities.method, - path: availableLegalEntities.path, - }).toEqual({ - method: 'GET', - path: '/auth/legal-entities', - }); - expect({ - method: switchLegalEntity.method, - path: switchLegalEntity.path, - }).toEqual({ - method: 'POST', - path: '/auth/legal-entity/switch', - }); - const legalEntityId = '35000000-0000-4000-8000-000000000001'; - expect( - yield* Schema.decodeUnknownEffect(SwitchLegalEntityPayloadSchema)({ - authorization: 'must-not-pass', - legalEntityId, - tenantId: 'must-not-pass', - }) - ).toEqual({ legalEntityId }); - expect( - yield* Schema.decodeUnknownEffect(AvailableLegalEntitiesResponseSchema)( - { - legalEntities: [ - { legalEntityId, legalName: 'Alpha', token: 'must-not-pass' }, - ], - selectedLegalEntityId: legalEntityId, - state: 'authenticated', - } - ) - ).toEqual({ - legalEntities: [{ legalEntityId, legalName: 'Alpha' }], +it.effect('publishes exact legal-entity endpoints with an ID-only switch payload', () => + Effect.gen(function* testProgram2() { + const { availableLegalEntities, switchLegalEntity } = ShellAuthenticationApi.groups.legalEntities.endpoints; + expect({ + method: availableLegalEntities.method, + path: availableLegalEntities.path, + }).toEqual({ + method: 'GET', + path: '/auth/legal-entities', + }); + expect({ + method: switchLegalEntity.method, + path: switchLegalEntity.path, + }).toEqual({ + method: 'POST', + path: '/auth/legal-entity/switch', + }); + const legalEntityId = '35000000-0000-4000-8000-000000000001'; + expect( + yield* Schema.decodeUnknownEffect(SwitchLegalEntityPayloadSchema)({ + authorization: 'must-not-pass', + legalEntityId, + tenantId: 'must-not-pass', + }), + ).toEqual({ legalEntityId }); + expect( + yield* Schema.decodeUnknownEffect(AvailableLegalEntitiesResponseSchema)({ + legalEntities: [{ legalEntityId, legalName: 'Alpha', token: 'must-not-pass' }], selectedLegalEntityId: legalEntityId, state: 'authenticated', - }); - }) + }), + ).toEqual({ + legalEntities: [{ legalEntityId, legalName: 'Alpha' }], + selectedLegalEntityId: legalEntityId, + state: 'authenticated', + }); + }), ); -it.effect( - 'decodes an optional exact page entrypoint without accepting private routing fields', - () => - Effect.gen(function* testProgram3() { - expect( - yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ - entrypointKey: 'contacts.core.page.customers', - importPath: 'must-not-pass', - moduleId: 'contacts.core', - routePath: '/contacts/customers', - }) - ).toEqual({ +it.effect('decodes an optional exact page entrypoint without accepting private routing fields', () => + Effect.gen(function* testProgram3() { + expect( + yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ entrypointKey: 'contacts.core.page.customers', + importPath: 'must-not-pass', + moduleId: 'contacts.core', + routePath: '/contacts/customers', + }), + ).toEqual({ + entrypointKey: 'contacts.core.page.customers', + moduleId: 'contacts.core', + }); + expect( + yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ moduleId: 'contacts.core', - }); - expect( - yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ + }), + ).toEqual({ moduleId: 'contacts.core' }); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ + entrypointKey: '../private-page', moduleId: 'contacts.core', - }) - ).toEqual({ moduleId: 'contacts.core' }); - expect( - yield* Effect.flip( - Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ - entrypointKey: '../private-page', - moduleId: 'contacts.core', - }) - ) - ).toBeDefined(); - }) + }), + ), + ).toBeDefined(); + }), ); it('publishes exact tenant methods, paths, and declared failure statuses', () => { - const { availableTenants, switchTenant } = - ShellAuthenticationApi.groups.tenants.endpoints; + const { availableTenants, switchTenant } = ShellAuthenticationApi.groups.tenants.endpoints; expect({ method: availableTenants.method, path: availableTenants.path, @@ -326,16 +257,12 @@ it('publishes exact tenant methods, paths, and declared failure statuses', () => }); it('publishes the exhaustive identity failure status contract', () => { - for (const endpoint of Object.values( - ShellAuthenticationApi.groups.identity.endpoints - )) { + for (const endpoint of Object.values(ShellAuthenticationApi.groups.identity.endpoints)) { const identityStatuses = [...endpoint.error] .map((schema) => schema.ast.annotations?.['httpApiStatus']) .toSorted((left, right) => Number(left) - Number(right)); - expect(identityStatuses).toEqual([ - 400, 401, 403, 404, 409, 422, 428, 429, 500, 503, - ]); + expect(identityStatuses).toEqual([400, 401, 403, 404, 409, 422, 428, 429, 500, 503]); } }); @@ -354,27 +281,25 @@ it.effect('validates tenant UUIDs and strips all non-contract fields', () => token: 'must-not-pass', }, ], - }) + }), ).toEqual({ tenants: [{ name: 'Alpha tenant', tenantId }] }); expect( yield* Schema.decodeUnknownEffect(SwitchTenantResponseSchema)({ principalId: 'must-not-pass', selectedTenantId: tenantId, sessionId: 'must-not-pass', - }) + }), ).toEqual({ selectedTenantId: tenantId }); - expect( - yield* Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId }) - ).toEqual({ + expect(yield* Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId })).toEqual({ tenantId, }); const invalidPayload = yield* Effect.flip( Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId: 'not-a-uuid', - }) + }), ); expect(Predicate.isTagged(invalidPayload, 'SchemaError')).toBe(true); - }) + }), ); it.effect('rejects malformed credentials through Effect Schema', () => @@ -383,74 +308,64 @@ it.effect('rejects malformed credentials through Effect Schema', () => Schema.decodeUnknownEffect(SignInPayloadSchema)({ email: '', password: '', - }) + }), ); expect(Predicate.isTagged(error, 'SchemaError')).toBe(true); - }) + }), ); -it.effect( - 'requires lifecycle reasons and strips provider-private API key identifiers', - () => - Effect.gen(function* testProgram6() { - const principalId = '00000000-0000-4000-8000-000000000001'; - const authBindingId = '00000000-0000-4000-8000-000000000002'; - const createdAt = '2026-08-09T00:00:00.000Z'; - const missingPrincipalReason = yield* Effect.flip( - Schema.decodeUnknownEffect(ChangePrincipalStatusPayloadSchema)({ - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - }) - ); - const missingRevocationReason = yield* Effect.flip( - Schema.decodeUnknownEffect(SetApiKeyStatusPayloadSchema)({ - authBindingId, - expectedStatus: 'active', - newStatus: 'revoked', - }) - ); - expect(Predicate.isTagged(missingPrincipalReason, 'SchemaError')).toBe( - true - ); - expect(Predicate.isTagged(missingRevocationReason, 'SchemaError')).toBe( - true - ); - - const lifecycle = yield* Schema.decodeUnknownEffect( - ApiKeyLifecycleResponseSchema - )({ - authBindingId, - cleanupPending: false, - createdAt, - enabled: true, - expiresAt: null, - id: 'private-provider-key-id', - name: null, - providerKeyId: 'private-provider-key-id', - start: 'onto', - }); - expect(lifecycle).toEqual({ - authBindingId, - cleanupPending: false, - createdAt: DateTime.makeUnsafe(createdAt), - enabled: true, - expiresAt: null, - name: null, - start: 'onto', - }); - expect( - yield* Schema.encodeEffect(ApiKeyLifecycleResponseSchema)(lifecycle) - ).toEqual({ +it.effect('requires lifecycle reasons and strips provider-private API key identifiers', () => + Effect.gen(function* testProgram6() { + const principalId = '00000000-0000-4000-8000-000000000001'; + const authBindingId = '00000000-0000-4000-8000-000000000002'; + const createdAt = '2026-08-09T00:00:00.000Z'; + const missingPrincipalReason = yield* Effect.flip( + Schema.decodeUnknownEffect(ChangePrincipalStatusPayloadSchema)({ + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + }), + ); + const missingRevocationReason = yield* Effect.flip( + Schema.decodeUnknownEffect(SetApiKeyStatusPayloadSchema)({ authBindingId, - cleanupPending: false, - createdAt, - enabled: true, - expiresAt: null, - name: null, - start: 'onto', - }); - }) + expectedStatus: 'active', + newStatus: 'revoked', + }), + ); + expect(Predicate.isTagged(missingPrincipalReason, 'SchemaError')).toBe(true); + expect(Predicate.isTagged(missingRevocationReason, 'SchemaError')).toBe(true); + + const lifecycle = yield* Schema.decodeUnknownEffect(ApiKeyLifecycleResponseSchema)({ + authBindingId, + cleanupPending: false, + createdAt, + enabled: true, + expiresAt: null, + id: 'private-provider-key-id', + name: null, + providerKeyId: 'private-provider-key-id', + start: 'onto', + }); + expect(lifecycle).toEqual({ + authBindingId, + cleanupPending: false, + createdAt: DateTime.makeUnsafe(createdAt), + enabled: true, + expiresAt: null, + name: null, + start: 'onto', + }); + expect(yield* Schema.encodeEffect(ApiKeyLifecycleResponseSchema)(lifecycle)).toEqual({ + authBindingId, + cleanupPending: false, + createdAt, + enabled: true, + expiresAt: null, + name: null, + start: 'onto', + }); + }), ); it.effect('decodes only safe current-session identity fields', () => @@ -479,5 +394,5 @@ it.effect('decodes only safe current-session identity fields', () => }, state: 'authenticated', }); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts b/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts index 455637018..13325f4bd 100644 --- a/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts @@ -4,27 +4,25 @@ import { expect, it } from 'effect-rstest'; import type { PoolResource } from '../../api/auth/db/client.ts'; import { acquirePoolResource } from '../../api/auth/db/client.ts'; -it.effect( - 'ends the pool resource without arguments when its scope closes', - () => - Effect.gen(function* testProgram1() { - const recorded: number[] = []; - const fake: PoolResource = { - end(...args: []) { - recorded.push(args.length); - return Promise.resolve(); - }, - }; +it.effect('ends the pool resource without arguments when its scope closes', () => + Effect.gen(function* testProgram1() { + const recorded: number[] = []; + const fake: PoolResource = { + end(...args: []) { + recorded.push(args.length); + return Promise.resolve(); + }, + }; - const acquired = yield* Effect.scoped( - Effect.gen(function* acquireResource() { - const resource = yield* acquirePoolResource(() => fake); - expect(recorded).toEqual([]); - return resource; - }) - ); + const acquired = yield* Effect.scoped( + Effect.gen(function* acquireResource() { + const resource = yield* acquirePoolResource(() => fake); + expect(recorded).toEqual([]); + return resource; + }), + ); - expect(acquired).toBe(fake); - expect(recorded).toEqual([0]); - }) + expect(acquired).toBe(fake); + expect(recorded).toEqual([0]); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/auth-schema.test.ts b/app/apps/shell-super-app/tests/unit/auth-schema.test.ts index 3ddbebca8..d1c8d3398 100644 --- a/app/apps/shell-super-app/tests/unit/auth-schema.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-schema.test.ts @@ -1,10 +1,7 @@ import { getColumns } from 'drizzle-orm'; import { expect, test } from 'effect-rstest'; -import { - compareAuthCatalog, - expectedAuthTableCatalog, -} from '../../api/auth/db/catalog.ts'; +import { compareAuthCatalog, expectedAuthTableCatalog } from '../../api/auth/db/catalog.ts'; import { AUTH_SCHEMA_NAME, AUTH_TABLE_INVENTORY, @@ -71,9 +68,7 @@ test('matches the generated API Key and Admin plugin persistence fields', () => 'permissions', 'metadata', ]); - expect(Object.keys(getColumns(user))).toEqual( - expect.arrayContaining(['role', 'banned', 'banReason', 'banExpires']) - ); + expect(Object.keys(getColumns(user))).toEqual(expect.arrayContaining(['role', 'banned', 'banReason', 'banExpires'])); expect(Object.keys(getColumns(session))).toEqual( expect.arrayContaining([ 'impersonatedBy', @@ -83,21 +78,13 @@ test('matches the generated API Key and Admin plugin persistence fields', () => 'impersonationOriginalPrincipalId', 'impersonationOriginalSessionId', 'impersonationTargetPrincipalId', - ]) + ]), ); expect(getColumns(session).impersonationActionId.columnType).toBe('PgText'); - expect(getColumns(session).impersonationTargetPrincipalId.columnType).toBe( - 'PgUUID' - ); - expect( - getColumns(session).impersonationOriginalAuthBindingId.columnType - ).toBe('PgUUID'); - expect(getColumns(session).impersonationOriginalPrincipalId.columnType).toBe( - 'PgUUID' - ); - expect(getColumns(session).impersonationOriginalSessionId.columnType).toBe( - 'PgText' - ); + expect(getColumns(session).impersonationTargetPrincipalId.columnType).toBe('PgUUID'); + expect(getColumns(session).impersonationOriginalAuthBindingId.columnType).toBe('PgUUID'); + expect(getColumns(session).impersonationOriginalPrincipalId.columnType).toBe('PgUUID'); + expect(getColumns(session).impersonationOriginalSessionId.columnType).toBe('PgText'); expect(Object.keys(getColumns(supportImpersonationRecovery))).toEqual([ 'impersonationSessionId', 'originalAuthBindingId', @@ -113,27 +100,15 @@ test('matches the generated API Key and Admin plugin persistence fields', () => test('reports missing and unexpected authentication tables', () => { expect( - compareAuthCatalog([ - 'auth.user', - 'auth.session', - 'auth.account', - 'auth.unexpected', - 'auth.unexpected', - ]) + compareAuthCatalog(['auth.user', 'auth.session', 'auth.account', 'auth.unexpected', 'auth.unexpected']), ).toEqual({ - missing: [ - 'auth.apikey', - 'auth.support_impersonation_recovery', - 'auth.verification', - ], + missing: ['auth.apikey', 'auth.support_impersonation_recovery', 'auth.verification'], unexpected: ['auth.unexpected'], }); }); test('accepts unordered duplicate auth table rows without mutating the inventory', () => { - expect( - compareAuthCatalog([...expectedAuthTableCatalog.toReversed(), 'auth.user']) - ).toEqual({ + expect(compareAuthCatalog([...expectedAuthTableCatalog.toReversed(), 'auth.user'])).toEqual({ missing: [], unexpected: [], }); diff --git a/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts b/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts index c30804b86..062e072bf 100644 --- a/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts +++ b/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts @@ -3,19 +3,13 @@ import { expect, it } from 'effect-rstest'; import { browserRuntime } from '../../src/runtime/browser-effect-runtime.ts'; -class ExpectedFailure extends Schema.TaggedError()( - 'ExpectedFailure', - {} -) {} +class ExpectedFailure extends Schema.TaggedError()('ExpectedFailure', {}) {} /** Forks on the real browser runtime, interrupting on scope close so a failed assertion leaks no fiber. */ -const forkOnBrowserRuntime = ( - program: Effect.Effect, - options?: Effect.RunOptions -) => +const forkOnBrowserRuntime = (program: Effect.Effect, options?: Effect.RunOptions) => Effect.acquireRelease( Effect.sync(() => browserRuntime.runFork(program, options)), - (fiber) => Fiber.interrupt(fiber) + (fiber) => Fiber.interrupt(fiber), ); it.effect('carries success values out of the browser runtime', () => @@ -23,7 +17,7 @@ it.effect('carries success values out of the browser runtime', () => const fiber = yield* forkOnBrowserRuntime(Effect.succeed('ready')); expect(yield* Fiber.join(fiber)).toBe('ready'); - }) + }), ); it.effect('keeps the typed failure identity of a browser runtime program', () => @@ -33,7 +27,7 @@ it.effect('keeps the typed failure identity of a browser runtime program', () => const fiber = yield* forkOnBrowserRuntime(Effect.fail(failure)); expect(yield* Effect.flip(Fiber.join(fiber))).toBe(failure); - }) + }), ); it.effect('interrupts the running Effect when its AbortSignal is aborted', () => @@ -42,14 +36,11 @@ it.effect('interrupts the running Effect when its AbortSignal is aborted', () => const finalized: string[] = []; const finalizersInstalled = yield* Deferred.make<'installed'>(); const fiber = yield* forkOnBrowserRuntime( - Effect.andThen( - Deferred.succeed(finalizersInstalled, 'installed'), - Effect.never - ).pipe( + Effect.andThen(Deferred.succeed(finalizersInstalled, 'installed'), Effect.never).pipe( Effect.ensuring(Effect.sync(() => finalized.push('inner'))), - Effect.ensuring(Effect.sync(() => finalized.push('outer'))) + Effect.ensuring(Effect.sync(() => finalized.push('outer'))), ), - { signal: controller.signal } + { signal: controller.signal }, ); yield* Deferred.await(finalizersInstalled); @@ -58,5 +49,5 @@ it.effect('interrupts the running Effect when its AbortSignal is aborted', () => expect(Exit.hasInterrupts(exit)).toBe(true); expect(finalized).toEqual(['inner', 'outer']); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts b/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts index a417b5e89..781003a1b 100644 --- a/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts +++ b/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts @@ -7,91 +7,64 @@ import { loadEnvironmentFileProvider } from '../../api/auth/environment-file-pro import { loadGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; rs.mock('../../api/auth/environment-file-provider.ts', () => ({ - loadEnvironmentFileProvider: rs.fn(() => - Effect.succeed(ConfigProvider.fromEnvRecord({})) - ), + loadEnvironmentFileProvider: rs.fn(() => Effect.succeed(ConfigProvider.fromEnvRecord({}))), })); -it.effect( - 'explicit environment overrides file values and absent keys fall back to the file', - () => - Effect.gen(function* explicitEnvironment() { - rs.mocked(loadEnvironmentFileProvider).mockReturnValue( - Effect.succeed( - ConfigProvider.fromEnvRecord({ - SECRET: 'file-secret', - URL: 'file-url', - }) - ) - ); - const result = yield* loadConfigurationProvider( - { environment: { URL: 'explicit-url' }, envPath: 'fixture-path' }, - () => 'unreadable' - ).pipe( - Effect.flatMap((provider) => - Config.all({ - secret: Config.string('SECRET'), - url: Config.string('URL'), - }).parse(provider) - ) - ); - expect(result).toEqual({ secret: 'file-secret', url: 'explicit-url' }); - expect(loadEnvironmentFileProvider).toHaveBeenCalledWith( - 'fixture-path', - expect.any(Function) - ); - }) +it.effect('explicit environment overrides file values and absent keys fall back to the file', () => + Effect.gen(function* explicitEnvironment() { + rs.mocked(loadEnvironmentFileProvider).mockReturnValue( + Effect.succeed( + ConfigProvider.fromEnvRecord({ + SECRET: 'file-secret', + URL: 'file-url', + }), + ), + ); + const result = yield* loadConfigurationProvider( + { environment: { URL: 'explicit-url' }, envPath: 'fixture-path' }, + () => 'unreadable', + ).pipe( + Effect.flatMap((provider) => + Config.all({ + secret: Config.string('SECRET'), + url: Config.string('URL'), + }).parse(provider), + ), + ); + expect(result).toEqual({ secret: 'file-secret', url: 'explicit-url' }); + expect(loadEnvironmentFileProvider).toHaveBeenCalledWith('fixture-path', expect.any(Function)); + }), ); const read = ( options: { - readonly environment?: Readonly< - Partial> - >; - } = {} + readonly environment?: Readonly>>; + } = {}, ) => loadConfigurationProvider(options, () => 'unreadable').pipe( - Effect.flatMap((provider) => - Config.string('ONTOS_PROVIDER_TEST').parse(provider) - ) + Effect.flatMap((provider) => Config.string('ONTOS_PROVIDER_TEST').parse(provider)), ); -it.effect( - 'an explicit empty environment does not fall through to process values', - () => - Effect.gen(function* emptyEnvironment() { - yield* Effect.acquireRelease( - Effect.sync(() => rs.stubEnv('ONTOS_PROVIDER_TEST', 'process-value')), - () => Effect.sync(() => rs.unstubAllEnvs()) - ); - rs.mocked(loadEnvironmentFileProvider).mockReturnValue( - Effect.succeed( - ConfigProvider.fromEnvRecord({ ONTOS_PROVIDER_TEST: 'file-value' }) - ) - ); - expect(yield* read()).toBe('process-value'); - expect(yield* read({ environment: {} })).toBe('file-value'); - }) +it.effect('an explicit empty environment does not fall through to process values', () => + Effect.gen(function* emptyEnvironment() { + yield* Effect.acquireRelease( + Effect.sync(() => rs.stubEnv('ONTOS_PROVIDER_TEST', 'process-value')), + () => Effect.sync(() => rs.unstubAllEnvs()), + ); + rs.mocked(loadEnvironmentFileProvider).mockReturnValue( + Effect.succeed(ConfigProvider.fromEnvRecord({ ONTOS_PROVIDER_TEST: 'file-value' })), + ); + expect(yield* read()).toBe('process-value'); + expect(yield* read({ environment: {} })).toBe('file-value'); + }), ); -it.effect( - 'file loading failures retain the parser-specific typed error and safe reason', - () => - Effect.gen(function* fileFailure() { - rs.mocked(loadEnvironmentFileProvider).mockImplementation( - (_path, failure) => Effect.fail(failure()) - ); - const authFailure = yield* Effect.flip( - loadAuthConfig({ environment: {} }) - ); - const gatewayFailure = yield* Effect.flip( - loadGatewayIssuerConfig({ environment: {} }) - ); - expect(authFailure.reason).toBe( - 'Unable to load the root authentication environment' - ); - expect(gatewayFailure.reason).toBe( - 'Unable to load the Shell gateway signing environment' - ); - }) +it.effect('file loading failures retain the parser-specific typed error and safe reason', () => + Effect.gen(function* fileFailure() { + rs.mocked(loadEnvironmentFileProvider).mockImplementation((_path, failure) => Effect.fail(failure())); + const authFailure = yield* Effect.flip(loadAuthConfig({ environment: {} })); + const gatewayFailure = yield* Effect.flip(loadGatewayIssuerConfig({ environment: {} })); + expect(authFailure.reason).toBe('Unable to load the root authentication environment'); + expect(gatewayFailure.reason).toBe('Unable to load the Shell gateway signing environment'); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts b/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts index bb04b5706..905435715 100644 --- a/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts +++ b/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts @@ -11,59 +11,39 @@ const topology = { ], }; -const overlay = ( - ontosModuleManifests: Readonly>, - environment = 'development' -) => ({ +const overlay = (ontosModuleManifests: Readonly>, environment = 'development') => ({ environment, ontosModuleManifests, schemaVersion: 1, }); const validUrls = { - 'documents-center': - 'http://localhost:4102/.well-known/ontos-module-manifest.json', - 'property-registry': - 'http://127.0.0.1:4101/.well-known/ontos-module-manifest.json', + 'documents-center': 'http://localhost:4102/.well-known/ontos-module-manifest.json', + 'property-registry': 'http://127.0.0.1:4101/.well-known/ontos-module-manifest.json', }; -it.effect( - 'derives an immutable, topology-authorized and deterministically ordered allowlist', - () => - Effect.gen(function* testProgram1() { - const allowlist = yield* deriveDeploymentAllowlist({ - environment: 'development', - overlay: overlay(validUrls), - topology, - }); - expect(allowlist.entries.map(({ appId }) => appId)).toEqual([ - 'documents-center', - 'property-registry', - ]); - expect(Object.isFrozen(allowlist)).toBe(true); - expect(Object.isFrozen(allowlist.entries)).toBe(true); - }) +it.effect('derives an immutable, topology-authorized and deterministically ordered allowlist', () => + Effect.gen(function* testProgram1() { + const allowlist = yield* deriveDeploymentAllowlist({ + environment: 'development', + overlay: overlay(validUrls), + topology, + }); + expect(allowlist.entries.map(({ appId }) => appId)).toEqual(['documents-center', 'property-registry']); + expect(Object.isFrozen(allowlist)).toBe(true); + expect(Object.isFrozen(allowlist.entries)).toBe(true); + }), ); it.effect.each([ - [ - 'missing topology entry', - { 'property-registry': validUrls['property-registry'] }, - ], - [ - 'unknown shell entry', - { ...validUrls, 'shell-super-app': validUrls['property-registry'] }, - ], - [ - 'duplicate normalized URL', - { ...validUrls, 'documents-center': validUrls['property-registry'] }, - ], + ['missing topology entry', { 'property-registry': validUrls['property-registry'] }], + ['unknown shell entry', { ...validUrls, 'shell-super-app': validUrls['property-registry'] }], + ['duplicate normalized URL', { ...validUrls, 'documents-center': validUrls['property-registry'] }], [ 'credentials', { ...validUrls, - 'property-registry': - 'http://user:secret@localhost:4101/.well-known/ontos-module-manifest.json', + 'property-registry': 'http://user:secret@localhost:4101/.well-known/ontos-module-manifest.json', }, ], [ @@ -73,33 +53,26 @@ it.effect.each([ 'property-registry': `${validUrls['property-registry']}#private`, }, ], - [ - 'arbitrary path', - { ...validUrls, 'property-registry': 'http://localhost:4101/private.json' }, - ], -] as const)( - 'rejects %s configuration without authorizing a fetch', - ([_label, manifests]) => - Effect.gen(function* testProgram2() { - expect( - yield* Effect.flip( - deriveDeploymentAllowlist({ - environment: 'development', - overlay: overlay(manifests), - topology, - }) - ) - ).toMatchObject({ code: 'deployment_allowlist_invalid' }); - }) + ['arbitrary path', { ...validUrls, 'property-registry': 'http://localhost:4101/private.json' }], +] as const)('rejects %s configuration without authorizing a fetch', ([_label, manifests]) => + Effect.gen(function* testProgram2() { + expect( + yield* Effect.flip( + deriveDeploymentAllowlist({ + environment: 'development', + overlay: overlay(manifests), + topology, + }), + ), + ).toMatchObject({ code: 'deployment_allowlist_invalid' }); + }), ); it.effect('requires HTTPS outside loopback development', () => Effect.gen(function* testProgram3() { const productionUrls = { - 'documents-center': - 'https://documents.example.test/.well-known/ontos-module-manifest.json', - 'property-registry': - 'https://property.example.test/.well-known/ontos-module-manifest.json', + 'documents-center': 'https://documents.example.test/.well-known/ontos-module-manifest.json', + 'property-registry': 'https://property.example.test/.well-known/ontos-module-manifest.json', }; expect( yield* Effect.flip( @@ -110,20 +83,20 @@ it.effect('requires HTTPS outside loopback development', () => ...productionUrls, 'property-registry': validUrls['property-registry'], }, - 'production' + 'production', ), topology, - }) - ) + }), + ), ).toMatchObject({ code: 'deployment_allowlist_invalid' }); expect( yield* deriveDeploymentAllowlist({ environment: 'production', overlay: overlay(productionUrls, 'production'), topology, - }) + }), ).toMatchObject({ entries: expect.any(Array) }); - }) + }), ); it('builds production discovery from deployment URL configuration, never the development overlay', () => { @@ -141,13 +114,10 @@ it('builds production discovery from deployment URL configuration, never the dev const configured = createModuleDeploymentAllowlistBuildInput({ cloudflareDeployEnabled: true, developmentOverlay: overlay({ - 'property-registry': - 'http://localhost:4101/.well-known/ontos-module-manifest.json', + 'property-registry': 'http://localhost:4101/.well-known/ontos-module-manifest.json', }), readEnvironment: (name) => - name === 'ULTRAMODERN_PUBLIC_URL_PROPERTY_REGISTRY' - ? 'https://property.example.test' - : undefined, + name === 'ULTRAMODERN_PUBLIC_URL_PROPERTY_REGISTRY' ? 'https://property.example.test' : undefined, topology: productionTopology, }); @@ -155,8 +125,7 @@ it('builds production discovery from deployment URL configuration, never the dev expect(configured.overlay).toEqual({ environment: 'production', ontosModuleManifests: { - 'property-registry': - 'https://property.example.test/.well-known/ontos-module-manifest.json', + 'property-registry': 'https://property.example.test/.well-known/ontos-module-manifest.json', }, schemaVersion: 1, }); @@ -166,6 +135,6 @@ it('builds production discovery from deployment URL configuration, never the dev developmentOverlay: overlay({}), readEnvironment: () => 'http://localhost:4101', topology: productionTopology, - }) + }), ).toThrow(/credential-free HTTPS origin/u); }); diff --git a/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts b/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts index 99bcb3c78..15ce453d3 100644 --- a/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts +++ b/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts @@ -1,36 +1,20 @@ import { Effect, Exit, Fiber, Predicate } from 'effect'; import { expect, rs, it } from 'effect-rstest'; import { TestClock } from 'effect/testing'; -import { - decodeJwt, - decodeProtectedHeader, - exportJWK, - generateKeyPair, - jwtVerify, -} from 'jose'; +import { decodeJwt, decodeProtectedHeader, exportJWK, generateKeyPair, jwtVerify } from 'jose'; import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; import type { GatewayIssuerConfigValue } from '../../api/auth/gateway-issuer-config.ts'; -import { - GatewayIssuer, - issueGatewayContextAssertion, - makeGatewayIssuerLayer, -} from '../../api/auth/gateway-issuer.ts'; +import { GatewayIssuer, issueGatewayContextAssertion, makeGatewayIssuerLayer } from '../../api/auth/gateway-issuer.ts'; import type { GatewayIssuerLayerOptions } from '../../api/auth/gateway-issuer.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const issuer = 'https://shell.example.test'; const principal = { @@ -52,7 +36,7 @@ const makeConfiguration = (): Effect.Effect<{ generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true, - }) + }), ); const privateJwk = yield* Effect.promise(() => exportJWK(privateKey)); return { @@ -74,7 +58,7 @@ const makeConfiguration = (): Effect.Effect<{ const dependencies = ( configuration: GatewayIssuerConfigValue, - overrides: Partial = {} + overrides: Partial = {}, ): GatewayIssuerLayerOptions => ({ currentTimeSeconds: Effect.succeed(1_700_000_000), generateJti: Effect.succeed('60000000-0000-4000-8000-000000000001'), @@ -88,72 +72,67 @@ const issueGatewayContextAssertionWith = ( readonly audience: string; readonly principal: Principal; }, - options: GatewayIssuerLayerOptions -) => - issueGatewayContextAssertion(input).pipe( - Effect.provide(makeGatewayIssuerLayer(options)) - ); + options: GatewayIssuerLayerOptions, +) => issueGatewayContextAssertion(input).pipe(Effect.provide(makeGatewayIssuerLayer(options))); -it.effect( - 'memoises configuration within the refresh window and issues signed assertions', - () => - Effect.gen(function* testProgram2() { - const { configuration, publicKey } = yield* makeConfiguration(); - let configurationLoads = 0; - const layer = makeGatewayIssuerLayer( - dependencies(configuration, { - loadConfig: Effect.sync(() => { - configurationLoads += 1; - return configuration; - }), - }) - ); - const [result] = yield* Effect.all( - [ - issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }), - issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }), - ], - { concurrency: 2 } - ).pipe(Effect.provide(layer)); - const header = decodeProtectedHeader(result.token); - const claims = decodeJwt(result.token); - const verified = yield* Effect.promise(() => - jwtVerify(result.token, publicKey, { - algorithms: ['EdDSA'], +it.effect('memoises configuration within the refresh window and issues signed assertions', () => + Effect.gen(function* testProgram2() { + const { configuration, publicKey } = yield* makeConfiguration(); + let configurationLoads = 0; + const layer = makeGatewayIssuerLayer( + dependencies(configuration, { + loadConfig: Effect.sync(() => { + configurationLoads += 1; + return configuration; + }), + }), + ); + const [result] = yield* Effect.all( + [ + issueGatewayContextAssertion({ audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }) - ); + principal, + }), + issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }), + ], + { concurrency: 2 }, + ).pipe(Effect.provide(layer)); + const header = decodeProtectedHeader(result.token); + const claims = decodeJwt(result.token); + const verified = yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); - expect(result.expiresAt).toBe(1_700_000_300); - expect(header).toEqual({ - alg: 'EdDSA', - kid: 'current-2026-08', - typ: 'JWT', - }); - expect(claims).toEqual({ - aud: 'property-registry', - exp: 1_700_000_300, - iat: 1_700_000_000, - iss: issuer, - jti: '60000000-0000-4000-8000-000000000001', - principal, - sub: principal.principalId, - ver: 1, - }); - expect(verified.payload['principal']).toEqual(principal); - expect(JSON.stringify(claims)).not.toMatch( - /email|displayName|credential|cookie|sessionToken|actionKey|permission|policy|businessPayload/u - ); - expect(configurationLoads).toBe(1); - }) + expect(result.expiresAt).toBe(1_700_000_300); + expect(header).toEqual({ + alg: 'EdDSA', + kid: 'current-2026-08', + typ: 'JWT', + }); + expect(claims).toEqual({ + aud: 'property-registry', + exp: 1_700_000_300, + iat: 1_700_000_000, + iss: issuer, + jti: '60000000-0000-4000-8000-000000000001', + principal, + sub: principal.principalId, + ver: 1, + }); + expect(verified.payload['principal']).toEqual(principal); + expect(JSON.stringify(claims)).not.toMatch( + /email|displayName|credential|cookie|sessionToken|actionKey|permission|policy|businessPayload/u, + ); + expect(configurationLoads).toBe(1); + }), ); it.effect('shares cached configuration across concurrent valid issuances', () => @@ -166,12 +145,12 @@ it.effect('shares cached configuration across concurrent valid issuances', () => loadConfigCount += 1; return configuration; }), - }) + }), ); yield* Effect.addFinalizer(() => Effect.sync(() => { rs.restoreAllMocks(); - }) + }), ); const importKey = rs.spyOn(globalThis.crypto.subtle, 'importKey'); const results = yield* Effect.all( @@ -179,9 +158,9 @@ it.effect('shares cached configuration across concurrent valid issuances', () => issueGatewayContextAssertion({ audience: 'property-registry', principal, - }) + }), ), - { concurrency: 8 } + { concurrency: 8 }, ).pipe(Effect.provide(layer)); // The signing key is imported once and shared: the slot serialises concurrent first callers. expect(importKey).toHaveBeenCalledTimes(1); @@ -196,137 +175,115 @@ it.effect('shares cached configuration across concurrent valid issuances', () => audience: 'property-registry', currentDate: new Date(1_700_000_001_000), issuer, - }) + }), ); expect(verified.payload['principal']).toEqual(principal); - }) + }), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); - }) + }), ); -it.effect( - 'allows the next issuance after interrupting a pending key import', - () => - Effect.gen(function* testProgram5() { - const { configuration, publicKey } = yield* makeConfiguration(); - const started = Promise.withResolvers(); - const blocked = Promise.withResolvers(); - yield* Effect.addFinalizer(() => - Effect.sync(() => { - blocked.resolve(publicKey); - rs.restoreAllMocks(); - }) - ); - rs.spyOn(globalThis.crypto.subtle, 'importKey').mockImplementationOnce( - () => { - started.resolve(true); - return blocked.promise; - } - ); - const result = yield* Effect.gen(function* interruptedImport() { - const gatewayIssuer = yield* GatewayIssuer; - const first = yield* gatewayIssuer - .issue({ audience: 'property-registry', principal }) - .pipe(Effect.forkChild); - yield* Effect.promise(() => started.promise); - yield* Fiber.interrupt(first); - expect(Exit.isFailure(yield* Fiber.await(first))).toBe(true); - return yield* gatewayIssuer.issue({ - audience: 'property-registry', - principal, - }); - }).pipe( - Effect.provide(makeGatewayIssuerLayer(dependencies(configuration))) - ); - yield* Effect.promise(() => - jwtVerify(result.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }) - ); - }) +it.effect('allows the next issuance after interrupting a pending key import', () => + Effect.gen(function* testProgram5() { + const { configuration, publicKey } = yield* makeConfiguration(); + const started = Promise.withResolvers(); + const blocked = Promise.withResolvers(); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + blocked.resolve(publicKey); + rs.restoreAllMocks(); + }), + ); + rs.spyOn(globalThis.crypto.subtle, 'importKey').mockImplementationOnce(() => { + started.resolve(true); + return blocked.promise; + }); + const result = yield* Effect.gen(function* interruptedImport() { + const gatewayIssuer = yield* GatewayIssuer; + const first = yield* gatewayIssuer.issue({ audience: 'property-registry', principal }).pipe(Effect.forkChild); + yield* Effect.promise(() => started.promise); + yield* Fiber.interrupt(first); + expect(Exit.isFailure(yield* Fiber.await(first))).toBe(true); + return yield* gatewayIssuer.issue({ + audience: 'property-registry', + principal, + }); + }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))); + yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); + }), ); -it.effect( - 'refreshes configuration after 30 seconds and replaces the rotated signing key', - () => - Effect.gen(function* testProgram6() { - const { - configuration: initialConfiguration, - publicKey: initialPublicKey, - } = yield* makeConfiguration(); - const { - configuration: generatedRotatedConfiguration, - publicKey: rotatedPublicKey, - } = yield* makeConfiguration(); - const rotatedConfiguration = { - ...generatedRotatedConfiguration, - privateJwk: { - ...generatedRotatedConfiguration.privateJwk, - kid: 'rotated-2026-09', - }, - }; - let loadConfigCount = 0; - const layer = makeGatewayIssuerLayer( - dependencies(initialConfiguration, { - loadConfig: Effect.sync(() => { - loadConfigCount += 1; - return loadConfigCount === 1 - ? initialConfiguration - : rotatedConfiguration; - }), - }) - ); - const [initialResult, rotatedResult] = yield* Effect.gen( - function* gatewayRotationSequence() { - const initial = yield* issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }); - const cached = yield* issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }); - expect(loadConfigCount).toBe(1); - expect(decodeProtectedHeader(cached.token).kid).toBe( - initialConfiguration.privateJwk.kid - ); - yield* TestClock.adjust('31 seconds'); - const rotated = yield* issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }); - return [initial, rotated] as const; - } - ).pipe(Effect.provide(layer), Effect.provide(TestClock.layer())); - const initialHeader = decodeProtectedHeader(initialResult.token); - const rotatedHeader = decodeProtectedHeader(rotatedResult.token); +it.effect('refreshes configuration after 30 seconds and replaces the rotated signing key', () => + Effect.gen(function* testProgram6() { + const { configuration: initialConfiguration, publicKey: initialPublicKey } = yield* makeConfiguration(); + const { configuration: generatedRotatedConfiguration, publicKey: rotatedPublicKey } = yield* makeConfiguration(); + const rotatedConfiguration = { + ...generatedRotatedConfiguration, + privateJwk: { + ...generatedRotatedConfiguration.privateJwk, + kid: 'rotated-2026-09', + }, + }; + let loadConfigCount = 0; + const layer = makeGatewayIssuerLayer( + dependencies(initialConfiguration, { + loadConfig: Effect.sync(() => { + loadConfigCount += 1; + return loadConfigCount === 1 ? initialConfiguration : rotatedConfiguration; + }), + }), + ); + const [initialResult, rotatedResult] = yield* Effect.gen(function* gatewayRotationSequence() { + const initial = yield* issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }); + const cached = yield* issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }); + expect(loadConfigCount).toBe(1); + expect(decodeProtectedHeader(cached.token).kid).toBe(initialConfiguration.privateJwk.kid); + yield* TestClock.adjust('31 seconds'); + const rotated = yield* issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }); + return [initial, rotated] as const; + }).pipe(Effect.provide(layer), Effect.provide(TestClock.layer())); + const initialHeader = decodeProtectedHeader(initialResult.token); + const rotatedHeader = decodeProtectedHeader(rotatedResult.token); - yield* Effect.promise(() => - jwtVerify(initialResult.token, initialPublicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }) - ); - yield* Effect.promise(() => - jwtVerify(rotatedResult.token, rotatedPublicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }) - ); + yield* Effect.promise(() => + jwtVerify(initialResult.token, initialPublicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); + yield* Effect.promise(() => + jwtVerify(rotatedResult.token, rotatedPublicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); - expect(loadConfigCount).toBe(2); - expect(rotatedHeader.kid).toBe(rotatedConfiguration.privateJwk.kid); - expect(rotatedHeader.kid).not.toBe(initialHeader.kid); - }) + expect(loadConfigCount).toBe(2); + expect(rotatedHeader.kid).toBe(rotatedConfiguration.privateJwk.kid); + expect(rotatedHeader.kid).not.toBe(initialHeader.kid); + }), ); it.effect('does not cache configuration failures', () => @@ -337,31 +294,27 @@ it.effect('does not cache configuration failures', () => dependencies(configuration, { loadConfig: Effect.suspend(() => { loadConfigCount += 1; - return loadConfigCount === 1 - ? parseGatewayIssuerConfig({}) - : Effect.succeed(configuration); + return loadConfigCount === 1 ? parseGatewayIssuerConfig({}) : Effect.succeed(configuration); }), - }) + }), ); - const [configurationError, result] = yield* Effect.gen( - function* gatewayFailureSequence() { - const configurationFailure = yield* Effect.flip( - issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }) - ); - const issuedResult = yield* issueGatewayContextAssertion({ + const [configurationError, result] = yield* Effect.gen(function* gatewayFailureSequence() { + const configurationFailure = yield* Effect.flip( + issueGatewayContextAssertion({ audience: 'property-registry', principal, - }); - return [configurationFailure, issuedResult] as const; - } - ).pipe(Effect.provide(layer)); + }), + ); + const issuedResult = yield* issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }); + return [configurationFailure, issuedResult] as const; + }).pipe(Effect.provide(layer)); expect(configurationError.stage).toBe('configuration'); expect(result.token.length).toBeGreaterThan(0); expect(loadConfigCount).toBe(2); - }) + }), ); it.effect('retries a failed key import on the next issuance', () => @@ -370,26 +323,22 @@ it.effect('retries a failed key import on the next issuance', () => yield* Effect.addFinalizer(() => Effect.sync(() => { rs.restoreAllMocks(); - }) - ); - rs.spyOn(globalThis.crypto.subtle, 'importKey').mockRejectedValueOnce( - new Error('transient import failure') + }), ); + rs.spyOn(globalThis.crypto.subtle, 'importKey').mockRejectedValueOnce(new Error('transient import failure')); const [error, result] = yield* Effect.gen(function* retryImport() { const failed = yield* Effect.flip( issueGatewayContextAssertion({ audience: 'property-registry', principal, - }) + }), ); const issued = yield* issueGatewayContextAssertion({ audience: 'property-registry', principal, }); return [failed, issued] as const; - }).pipe( - Effect.provide(makeGatewayIssuerLayer(dependencies(configuration))) - ); + }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))); expect(error.stage).toBe('signing'); yield* Effect.promise(() => jwtVerify(result.token, publicKey, { @@ -397,167 +346,139 @@ it.effect('retries a failed key import on the next issuance', () => audience: 'property-registry', currentDate: new Date(1_700_000_001_000), issuer, - }) + }), ); - }) + }), ); -it.effect( - 'fails closed for unknown audiences and invalid Effect-managed time', - () => - Effect.gen(function* testProgram9() { - const { configuration } = yield* makeConfiguration(); - const audienceErrors = yield* Effect.all( - [ - Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'billing', principal }, - dependencies(configuration) - ) - ), - Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'property.registry', principal }, - dependencies(configuration) - ) - ), - ].map((effect) => - Effect.gen(function* testProgram10() { - return yield* effect; - }) +it.effect('fails closed for unknown audiences and invalid Effect-managed time', () => + Effect.gen(function* testProgram9() { + const { configuration } = yield* makeConfiguration(); + const audienceErrors = yield* Effect.all( + [ + Effect.flip(issueGatewayContextAssertionWith({ audience: 'billing', principal }, dependencies(configuration))), + Effect.flip( + issueGatewayContextAssertionWith({ audience: 'property.registry', principal }, dependencies(configuration)), ), - { concurrency: 'unbounded' } - ); - const timeError = yield* Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'property-registry', principal }, - dependencies(configuration, { - currentTimeSeconds: Effect.succeed(-1), - }) - ) - ); + ].map((effect) => + Effect.gen(function* testProgram10() { + return yield* effect; + }), + ), + { concurrency: 'unbounded' }, + ); + const timeError = yield* Effect.flip( + issueGatewayContextAssertionWith( + { audience: 'property-registry', principal }, + dependencies(configuration, { + currentTimeSeconds: Effect.succeed(-1), + }), + ), + ); - expect( - audienceErrors.every( - (error) => error.code === 'gateway_audience_invalid' - ) - ).toBe(true); - expect(audienceErrors.every((error) => error.stage === 'audience')).toBe( - true - ); - expect(timeError.code).toBe('gateway_issuer_unavailable'); - expect(timeError.stage).toBe('clock'); - }) + expect(audienceErrors.every((error) => error.code === 'gateway_audience_invalid')).toBe(true); + expect(audienceErrors.every((error) => error.stage === 'audience')).toBe(true); + expect(timeError.code).toBe('gateway_issuer_unavailable'); + expect(timeError.stage).toBe('clock'); + }), ); -it.effect( - 'rejects transport correlation or any other excess principal claim', - () => - Effect.gen(function* testProgram11() { - const { configuration } = yield* makeConfiguration(); - const error = yield* Effect.flip( - issueGatewayContextAssertionWith( - { - audience: 'property-registry', - principal: { ...principal, correlationId: 'must-remain-a-header' }, - }, - dependencies(configuration) - ) - ); - expect(error.code).toBe('gateway_issuer_unavailable'); - expect(error.stage).toBe('principal'); - }) +it.effect('rejects transport correlation or any other excess principal claim', () => + Effect.gen(function* testProgram11() { + const { configuration } = yield* makeConfiguration(); + const error = yield* Effect.flip( + issueGatewayContextAssertionWith( + { + audience: 'property-registry', + principal: { ...principal, correlationId: 'must-remain-a-header' }, + }, + dependencies(configuration), + ), + ); + expect(error.code).toBe('gateway_issuer_unavailable'); + expect(error.stage).toBe('principal'); + }), ); -it.effect( - 'identifies configuration and signing failures without exposing key material', - () => - Effect.gen(function* testProgram12() { - const { configuration } = yield* makeConfiguration(); - const configurationError = yield* Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'property-registry', principal }, - dependencies(configuration, { - loadConfig: parseGatewayIssuerConfig({}), - }) - ) - ); - const signingError = yield* Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'property-registry', principal }, - dependencies({ - ...configuration, - privateJwk: { ...configuration.privateJwk, d: 'invalid' }, - }) - ) - ); +it.effect('identifies configuration and signing failures without exposing key material', () => + Effect.gen(function* testProgram12() { + const { configuration } = yield* makeConfiguration(); + const configurationError = yield* Effect.flip( + issueGatewayContextAssertionWith( + { audience: 'property-registry', principal }, + dependencies(configuration, { + loadConfig: parseGatewayIssuerConfig({}), + }), + ), + ); + const signingError = yield* Effect.flip( + issueGatewayContextAssertionWith( + { audience: 'property-registry', principal }, + dependencies({ + ...configuration, + privateJwk: { ...configuration.privateJwk, d: 'invalid' }, + }), + ), + ); - expect(configurationError.stage).toBe('configuration'); - expect(signingError.stage).toBe('signing'); - expect(configurationError.reason).not.toContain( - 'ONTOS_GATEWAY_PRIVATE_JWK' - ); - expect(signingError.reason).not.toContain(configuration.privateJwk.d); - }) + expect(configurationError.stage).toBe('configuration'); + expect(signingError.stage).toBe('signing'); + expect(configurationError.reason).not.toContain('ONTOS_GATEWAY_PRIVATE_JWK'); + expect(signingError.reason).not.toContain(configuration.privateJwk.d); + }), ); -it.effect( - 'rejects missing configuration, HMAC keys, non-Ed25519 keys, and missing key IDs', - () => - Effect.gen(function* testProgram13() { - const invalidJwks = [ - undefined, - { - alg: 'HS256', - d: 'secret', - kid: 'hmac', - kty: 'oct', - use: 'sig', - x: 'secret', - }, - { - alg: 'EdDSA', - crv: 'X25519', - d: 'private', - kid: 'wrong-curve', - kty: 'OKP', - use: 'sig', - x: 'public', - }, - { - alg: 'EdDSA', - crv: 'Ed25519', - d: 'private', - kty: 'OKP', - use: 'sig', - x: 'public', - }, - ]; +it.effect('rejects missing configuration, HMAC keys, non-Ed25519 keys, and missing key IDs', () => + Effect.gen(function* testProgram13() { + const invalidJwks = [ + undefined, + { + alg: 'HS256', + d: 'secret', + kid: 'hmac', + kty: 'oct', + use: 'sig', + x: 'secret', + }, + { + alg: 'EdDSA', + crv: 'X25519', + d: 'private', + kid: 'wrong-curve', + kty: 'OKP', + use: 'sig', + x: 'public', + }, + { + alg: 'EdDSA', + crv: 'Ed25519', + d: 'private', + kty: 'OKP', + use: 'sig', + x: 'public', + }, + ]; - const errors = yield* Effect.all( - invalidJwks.map((privateJwk) => - Effect.gen(function* testProgram14() { - return yield* Effect.flip( - parseGatewayIssuerConfig( - withOptionalProperty( - { - ONTOS_GATEWAY_ISSUER: issuer, - }, - privateJwk !== undefined, - 'ONTOS_GATEWAY_PRIVATE_JWK', - JSON.stringify(privateJwk), - {} - ) - ) - ); - }) - ), - { concurrency: 'unbounded' } - ); - expect( - errors.every((error) => - Predicate.isTagged(error, 'GatewayIssuerConfigError') - ) - ).toBe(true); - }) + const errors = yield* Effect.all( + invalidJwks.map((privateJwk) => + Effect.gen(function* testProgram14() { + return yield* Effect.flip( + parseGatewayIssuerConfig( + withOptionalProperty( + { + ONTOS_GATEWAY_ISSUER: issuer, + }, + privateJwk !== undefined, + 'ONTOS_GATEWAY_PRIVATE_JWK', + JSON.stringify(privateJwk), + {}, + ), + ), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + expect(errors.every((error) => Predicate.isTagged(error, 'GatewayIssuerConfigError'))).toBe(true); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts b/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts index e4332a617..7e398cddc 100644 --- a/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts +++ b/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts @@ -1,8 +1,4 @@ -import { - ActionTransactionError, - IdentityTargetInvalidError, - PrincipalBindingMissingError, -} from '@app/core-runtime'; +import { ActionTransactionError, IdentityTargetInvalidError, PrincipalBindingMissingError } from '@app/core-runtime'; import { Effect, Redacted, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; @@ -19,10 +15,7 @@ import { actionSuccess, makeActionRuntimeDouble, } from '../support/action-runtime-double.ts'; -import { - makeApiKeyServiceDouble, - makePrincipalResolverDouble, -} from '../support/identity-service-doubles.ts'; +import { makeApiKeyServiceDouble, makePrincipalResolverDouble } from '../support/identity-service-doubles.ts'; const principal = { authBindingId: '00000000-0000-4000-8000-000000000002', @@ -54,7 +47,7 @@ const pendingMetadata = ( scope: { readonly issuerPrincipalId?: string; readonly tenantId?: string; - } = {} + } = {}, ) => JSON.stringify({ issuerPrincipalId: scope.issuerPrincipalId ?? principal.principalId, @@ -63,115 +56,101 @@ const pendingMetadata = ( tenantId: scope.tenantId ?? principal.tenantId, }); -it.effect( - 'compensates a failed Core bind and never exposes the provider key identifier', - () => - Effect.gen(function* testProgram1() { - const disabled: string[] = []; - const bindFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The requested binding target is invalid', - }); - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDomainFailure(bindFailure)]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId) => { - disabled.push(keyId); - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - resolver - ); - - const failure = yield* Effect.flip( - service.issue({ - correlationId: 'correlation-1', - idempotencyKey: 'issue-1', - principal, - requestHeaders: new Headers(), - }) - ); - expect(failure).toBe(bindFailure); - expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe( - true - ); - expect(disabled).toEqual(['private-provider-key-id']); - }) +it.effect('compensates a failed Core bind and never exposes the provider key identifier', () => + Effect.gen(function* testProgram1() { + const disabled: string[] = []; + const bindFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The requested binding target is invalid', + }); + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDomainFailure(bindFailure)]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId) => { + disabled.push(keyId); + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + resolver, + ); + + const failure = yield* Effect.flip( + service.issue({ + correlationId: 'correlation-1', + idempotencyKey: 'issue-1', + principal, + requestHeaders: new Headers(), + }), + ); + expect(failure).toBe(bindFailure); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); + expect(disabled).toEqual(['private-provider-key-id']); + }), ); -it.effect( - 'preserves resolver lifecycle failures instead of rewriting them as an outage', - () => - Effect.gen(function* testProgram2() { - const resolverFailure = new PrincipalBindingMissingError(); - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDefect('must not run')]).runtime, - makeApiKeyServiceDouble(), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => - Effect.fail(resolverFailure), - }) - ); - - const failure = yield* Effect.flip( - service.setStatus({ - authBindingId: '00000000-0000-4000-8000-000000000005', - correlationId: 'correlation-resolver-failure', - expectedStatus: 'active', - idempotencyKey: 'status-resolver-failure', - newStatus: 'disabled', - principal, - }) - ); - - expect(failure).toBe(resolverFailure); - expect(Predicate.isTagged(failure, 'PrincipalBindingMissingError')).toBe( - true - ); - }) +it.effect('preserves resolver lifecycle failures instead of rewriting them as an outage', () => + Effect.gen(function* testProgram2() { + const resolverFailure = new PrincipalBindingMissingError(); + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDefect('must not run')]).runtime, + makeApiKeyServiceDouble(), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => Effect.fail(resolverFailure), + }), + ); + + const failure = yield* Effect.flip( + service.setStatus({ + authBindingId: '00000000-0000-4000-8000-000000000005', + correlationId: 'correlation-resolver-failure', + expectedStatus: 'active', + idempotencyKey: 'status-resolver-failure', + newStatus: 'disabled', + principal, + }), + ); + + expect(failure).toBe(resolverFailure); + expect(Predicate.isTagged(failure, 'PrincipalBindingMissingError')).toBe(true); + }), ); -it.effect( - 'preserves a typed Core status-transition failure before touching provider state', - () => - Effect.gen(function* testProgram3() { - const actionFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The requested status transition is invalid', - }); - let providerCalls = 0; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDomainFailure(actionFailure)]).runtime, - makeApiKeyServiceDouble({ - setEnabled: () => { - providerCalls += 1; - return Effect.succeed(issued); - }, - }), - resolver - ); - - const failure = yield* Effect.flip( - service.setStatus({ - authBindingId: '00000000-0000-4000-8000-000000000005', - correlationId: 'correlation-core-failure', - expectedStatus: 'active', - idempotencyKey: 'status-core-failure', - newStatus: 'disabled', - principal, - }) - ); - - expect(failure).toBe(actionFailure); - expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe( - true - ); - expect(providerCalls).toBe(0); - }) +it.effect('preserves a typed Core status-transition failure before touching provider state', () => + Effect.gen(function* testProgram3() { + const actionFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The requested status transition is invalid', + }); + let providerCalls = 0; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDomainFailure(actionFailure)]).runtime, + makeApiKeyServiceDouble({ + setEnabled: () => { + providerCalls += 1; + return Effect.succeed(issued); + }, + }), + resolver, + ); + + const failure = yield* Effect.flip( + service.setStatus({ + authBindingId: '00000000-0000-4000-8000-000000000005', + correlationId: 'correlation-core-failure', + expectedStatus: 'active', + idempotencyKey: 'status-core-failure', + newStatus: 'disabled', + principal, + }), + ); + + expect(failure).toBe(actionFailure); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); + expect(providerCalls).toBe(0); + }), ); it('reconciles only expired pending leases in the trusted tenant and issuer scope', () => { @@ -213,429 +192,393 @@ it('reconciles only expired pending leases in the trusted tenant and issuer scop lifecycleOperationId: 'same-operation', nowEpochMillis, tenantId: principal.tenantId, - } + }, ); expect(selected).toEqual(['abandoned-key']); }); -it.effect( - 'returns a secret only after bind succeeds and strips the private provider key identifier', - () => - Effect.gen(function* testProgram4() { - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - ]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: () => Effect.succeed(issued), - }), - resolver - ); - - const result = yield* service.issue({ - correlationId: 'correlation-2', - idempotencyKey: 'issue-2', - principal, - requestHeaders: new Headers(), - }); - expect(result.secret).toBe('ontos-secret'); - expect(Object.hasOwn(result, 'providerKeyId')).toBe(false); - }) -); - -it.effect( - 'revokes the replacement before failing when closing the old Core binding fails', - () => - Effect.gen(function* testProgram5() { - const actionRuntime = makeActionRuntimeDouble([ +it.effect('returns a secret only after bind succeeds and strips the private provider key identifier', () => + Effect.gen(function* testProgram4() { + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([ actionSuccess({ authBindingId: '00000000-0000-4000-8000-000000000004', status: 'active', }), - actionCoreFailure(actionTransactionFailure('old binding unavailable')), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const disabled: string[] = []; - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - resolver - ); - - expect( - yield* Effect.flip( - service.rotate({ - correlationId: 'correlation-3', - idempotencyKey: 'rotate-1', - oldAuthBindingId: '00000000-0000-4000-8000-000000000005', - principal, - reason: 'Scheduled credential rotation', - requestHeaders: new Headers(), - }) - ) - ).toBeDefined(); - expect(actionRuntime.invocationCount()).toBe(3); - expect(disabled).toEqual(['old-provider-key-id']); - }) + ]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: () => Effect.succeed(issued), + }), + resolver, + ); + + const result = yield* service.issue({ + correlationId: 'correlation-2', + idempotencyKey: 'issue-2', + principal, + requestHeaders: new Headers(), + }); + expect(result.secret).toBe('ontos-secret'); + expect(Object.hasOwn(result, 'providerKeyId')).toBe(false); + }), ); -it.effect( - 'returns the replacement secret when both old closure and replacement rollback are unavailable', - () => - Effect.gen(function* testProgram6() { - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionCoreFailure(actionTransactionFailure('Core unavailable')), - actionCoreFailure(actionTransactionFailure('Core unavailable')), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), +it.effect('revokes the replacement before failing when closing the old Core binding fails', () => + Effect.gen(function* testProgram5() { + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionCoreFailure(actionTransactionFailure('old binding unavailable')), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const disabled: string[] = []; + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + resolver, + ); + + expect( + yield* Effect.flip( + service.rotate({ + correlationId: 'correlation-3', + idempotencyKey: 'rotate-1', + oldAuthBindingId: '00000000-0000-4000-8000-000000000005', + principal, + reason: 'Scheduled credential rotation', + requestHeaders: new Headers(), }), - resolver - ); + ), + ).toBeDefined(); + expect(actionRuntime.invocationCount()).toBe(3); + expect(disabled).toEqual(['old-provider-key-id']); + }), +); - const result = yield* service.rotate({ - correlationId: 'correlation-4', - idempotencyKey: 'rotate-2', - oldAuthBindingId: '00000000-0000-4000-8000-000000000005', - principal, - reason: 'Scheduled credential rotation', - requestHeaders: new Headers(), - }); - expect(result.secret).toBe('ontos-secret'); - expect(result.cleanupPending).toBe(true); - expect(actionRuntime.invocationCount()).toBe(3); - }) +it.effect('returns the replacement secret when both old closure and replacement rollback are unavailable', () => + Effect.gen(function* testProgram6() { + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionCoreFailure(actionTransactionFailure('Core unavailable')), + actionCoreFailure(actionTransactionFailure('Core unavailable')), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + }), + resolver, + ); + + const result = yield* service.rotate({ + correlationId: 'correlation-4', + idempotencyKey: 'rotate-2', + oldAuthBindingId: '00000000-0000-4000-8000-000000000005', + principal, + reason: 'Scheduled credential rotation', + requestHeaders: new Headers(), + }); + expect(result.secret).toBe('ontos-secret'); + expect(result.cleanupPending).toBe(true); + expect(actionRuntime.invocationCount()).toBe(3); + }), ); -it.effect( - 'returns the replacement secret when old Core closure committed but provider state is unavailable', - () => - Effect.gen(function* testProgram7() { - let resolverCalls = 0; - const providerUnavailable = new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.fail(providerUnavailable), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId, enabled) => - keyId === 'old-provider-key-id' && !enabled - ? Effect.fail(providerUnavailable) - : Effect.succeed({ ...issued, providerKeyId: keyId }), - }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => { - resolverCalls += 1; +it.effect('returns the replacement secret when old Core closure committed but provider state is unavailable', () => + Effect.gen(function* testProgram7() { + let resolverCalls = 0; + const providerUnavailable = new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', + }); + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.fail(providerUnavailable), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId, enabled) => + keyId === 'old-provider-key-id' && !enabled + ? Effect.fail(providerUnavailable) + : Effect.succeed({ ...issued, providerKeyId: keyId }), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => { + resolverCalls += 1; + return Effect.succeed({ + providerSubjectId: 'old-provider-key-id', + status: resolverCalls === 1 ? 'active' : 'revoked', + }); + }, + }), + ); + + const result = yield* service.rotate({ + correlationId: 'correlation-old-core-closed', + idempotencyKey: 'rotate-old-core-closed', + oldAuthBindingId: '00000000-0000-4000-8000-000000000005', + principal, + reason: 'Scheduled credential rotation', + requestHeaders: new Headers(), + }); + + expect(result.secret).toBe('ontos-secret'); + expect(result.cleanupPending).toBe(true); + expect(actionRuntime.invocationCount()).toBe(2); + expect(resolverCalls).toBe(2); + }), +); + +it.effect('does not return a replacement secret after rollback definitely revoked its Core binding', () => + Effect.gen(function* testProgram8() { + let replacementReads = 0; + const providerUnavailable = new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', + }); + const oldFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The old binding could not be closed', + }); + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionDomainFailure(oldFailure), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.fail(providerUnavailable), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: () => Effect.fail(providerUnavailable), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: (input) => { + if (input.authBindingId === '00000000-0000-4000-8000-000000000004') { + replacementReads += 1; return Effect.succeed({ - providerSubjectId: 'old-provider-key-id', - status: resolverCalls === 1 ? 'active' : 'revoked', + providerSubjectId: 'replacement-provider-key-id', + status: replacementReads === 1 ? 'active' : 'revoked', }); - }, - }) - ); - - const result = yield* service.rotate({ - correlationId: 'correlation-old-core-closed', - idempotencyKey: 'rotate-old-core-closed', + } + return Effect.succeed({ + providerSubjectId: 'old-provider-key-id', + status: 'active', + }); + }, + }), + ); + + const failure = yield* Effect.flip( + service.rotate({ + correlationId: 'correlation-definite-replacement-rollback', + idempotencyKey: 'definite-replacement-rollback', oldAuthBindingId: '00000000-0000-4000-8000-000000000005', principal, reason: 'Scheduled credential rotation', requestHeaders: new Headers(), - }); + }), + ); - expect(result.secret).toBe('ontos-secret'); - expect(result.cleanupPending).toBe(true); - expect(actionRuntime.invocationCount()).toBe(2); - expect(resolverCalls).toBe(2); - }) + expect(failure).toBe(oldFailure); + expect(actionRuntime.invocationCount()).toBe(3); + expect(replacementReads).toBe(2); + }), ); -it.effect( - 'does not return a replacement secret after rollback definitely revoked its Core binding', - () => - Effect.gen(function* testProgram8() { - let replacementReads = 0; - const providerUnavailable = new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }); - const oldFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The old binding could not be closed', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionDomainFailure(oldFailure), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.fail(providerUnavailable), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: () => Effect.fail(providerUnavailable), - }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: (input) => { - if ( - input.authBindingId === '00000000-0000-4000-8000-000000000004' - ) { - replacementReads += 1; - return Effect.succeed({ - providerSubjectId: 'replacement-provider-key-id', - status: replacementReads === 1 ? 'active' : 'revoked', - }); - } - return Effect.succeed({ - providerSubjectId: 'old-provider-key-id', - status: 'active', - }); - }, - }) - ); - - const failure = yield* Effect.flip( - service.rotate({ - correlationId: 'correlation-definite-replacement-rollback', - idempotencyKey: 'definite-replacement-rollback', - oldAuthBindingId: '00000000-0000-4000-8000-000000000005', - principal, - reason: 'Scheduled credential rotation', - requestHeaders: new Headers(), - }) - ); +it.effect('cleans one bounded pending batch and requires a retry before issuing another key', () => + Effect.gen(function* testProgram9() { + const disabled: string[] = []; + let issueCalls = 0; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDefect('must not bind')]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => { + issueCalls += 1; + return Effect.succeed(issued); + }, + pendingCleanup: () => + Effect.succeed({ + hasMore: true, + providerKeyIds: ['bounded-orphan'], + }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + makePrincipalResolverDouble({ + resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), + }), + ); + + const failure = yield* Effect.flip( + service.issue({ + correlationId: 'correlation-bounded-cleanup', + idempotencyKey: 'bounded-cleanup', + principal, + requestHeaders: new Headers(), + }), + ); - expect(failure).toBe(oldFailure); - expect(actionRuntime.invocationCount()).toBe(3); - expect(replacementReads).toBe(2); - }) + expect(Predicate.isTagged(failure, 'IdentityLifecycleOperationError')).toBe(true); + expect(disabled).toEqual(['bounded-orphan']); + expect(issueCalls).toBe(0); + }), ); -it.effect( - 'cleans one bounded pending batch and requires a retry before issuing another key', - () => - Effect.gen(function* testProgram9() { - const disabled: string[] = []; - let issueCalls = 0; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDefect('must not bind')]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => { - issueCalls += 1; - return Effect.succeed(issued); - }, - pendingCleanup: () => - Effect.succeed({ - hasMore: true, - providerKeyIds: ['bounded-orphan'], - }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - makePrincipalResolverDouble({ - resolveBetterAuthApiKey: () => - Effect.fail(new PrincipalBindingMissingError()), - }) - ); - - const failure = yield* Effect.flip( - service.issue({ - correlationId: 'correlation-bounded-cleanup', - idempotencyKey: 'bounded-cleanup', - principal, - requestHeaders: new Headers(), - }) - ); - - expect( - Predicate.isTagged(failure, 'IdentityLifecycleOperationError') - ).toBe(true); - expect(disabled).toEqual(['bounded-orphan']); - expect(issueCalls).toBe(0); - }) +it.effect('retries provider cleanup without repeating an already committed Core transition', () => + Effect.gen(function* testProgram10() { + const actionRuntime = makeActionRuntimeDouble([actionDefect(new Error('must not run'))]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + metadata: () => Effect.succeed({ ...issued, enabled: false }), + setEnabled: () => Effect.succeed({ ...issued, enabled: false }), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => + Effect.succeed({ + providerSubjectId: 'old-provider-key-id', + status: 'revoked', + }), + }), + ); + + const result = yield* service.setStatus({ + authBindingId: '00000000-0000-4000-8000-000000000005', + correlationId: 'correlation-5', + expectedStatus: 'active', + idempotencyKey: 'revoke-retry', + newStatus: 'revoked', + principal, + reason: 'Retry provider cleanup', + }); + expect(result.cleanupPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(0); + }), ); -it.effect( - 'retries provider cleanup without repeating an already committed Core transition', - () => - Effect.gen(function* testProgram10() { - const actionRuntime = makeActionRuntimeDouble([ - actionDefect(new Error('must not run')), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - metadata: () => Effect.succeed({ ...issued, enabled: false }), - setEnabled: () => Effect.succeed({ ...issued, enabled: false }), - }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => - Effect.succeed({ - providerSubjectId: 'old-provider-key-id', - status: 'revoked', +it.effect('preserves provider metadata failure after a safe Core disable instead of fabricating state', () => + Effect.gen(function* testProgram11() { + const metadataFailure = new ApiKeyStateInconsistentError({ + code: 'api_key_state_inconsistent', + reason: 'The provider key row is missing', + }); + const actionRuntime = makeActionRuntimeDouble([actionSuccess({ previousStatus: 'active', status: 'disabled' })]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + metadata: () => Effect.fail(metadataFailure), + setEnabled: () => + Effect.fail( + new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', }), - }) - ); + ), + }), + resolver, + ); - const result = yield* service.setStatus({ + const failure = yield* Effect.flip( + service.setStatus({ authBindingId: '00000000-0000-4000-8000-000000000005', - correlationId: 'correlation-5', + correlationId: 'correlation-provider-metadata-failure', expectedStatus: 'active', - idempotencyKey: 'revoke-retry', - newStatus: 'revoked', + idempotencyKey: 'disable-provider-metadata-failure', + newStatus: 'disabled', principal, - reason: 'Retry provider cleanup', - }); - expect(result.cleanupPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(0); - }) -); - -it.effect( - 'preserves provider metadata failure after a safe Core disable instead of fabricating state', - () => - Effect.gen(function* testProgram11() { - const metadataFailure = new ApiKeyStateInconsistentError({ - code: 'api_key_state_inconsistent', - reason: 'The provider key row is missing', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ previousStatus: 'active', status: 'disabled' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - metadata: () => Effect.fail(metadataFailure), - setEnabled: () => - Effect.fail( - new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }) - ), - }), - resolver - ); - - const failure = yield* Effect.flip( - service.setStatus({ - authBindingId: '00000000-0000-4000-8000-000000000005', - correlationId: 'correlation-provider-metadata-failure', - expectedStatus: 'active', - idempotencyKey: 'disable-provider-metadata-failure', - newStatus: 'disabled', - principal, - reason: 'Disable a missing provider key', - }) - ); + reason: 'Disable a missing provider key', + }), + ); - expect(failure).toBe(metadataFailure); - expect(actionRuntime.invocationCount()).toBe(1); - }) + expect(failure).toBe(metadataFailure); + expect(actionRuntime.invocationCount()).toBe(1); + }), ); -it.effect( - 'reconciles a provider key left pending by failed bind compensation before retrying issue', - () => - Effect.gen(function* testProgram12() { - const disabled: string[] = []; - const cleared: string[] = []; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - ]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: (keyId) => { - cleared.push(keyId); - return Effect.void; - }, - issue: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ - hasMore: false, - providerKeyIds: ['orphan-provider-key-id'], - }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, +it.effect('reconciles a provider key left pending by failed bind compensation before retrying issue', () => + Effect.gen(function* testProgram12() { + const disabled: string[] = []; + const cleared: string[] = []; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', }), - makePrincipalResolverDouble({ - resolveBetterAuthApiKey: () => - Effect.fail(new PrincipalBindingMissingError()), - }) - ); - - const result = yield* service.issue({ - correlationId: 'correlation-6', - idempotencyKey: 'issue-retry', - principal, - requestHeaders: new Headers(), - }); - - expect(result.secret).toBe('ontos-secret'); - expect(disabled).toEqual(['orphan-provider-key-id']); - expect(cleared).toEqual([ - 'orphan-provider-key-id', - 'private-provider-key-id', - ]); - }) + ]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: (keyId) => { + cleared.push(keyId); + return Effect.void; + }, + issue: () => Effect.succeed(issued), + pendingCleanup: () => + Effect.succeed({ + hasMore: false, + providerKeyIds: ['orphan-provider-key-id'], + }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + makePrincipalResolverDouble({ + resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), + }), + ); + + const result = yield* service.issue({ + correlationId: 'correlation-6', + idempotencyKey: 'issue-retry', + principal, + requestHeaders: new Headers(), + }); + + expect(result.secret).toBe('ontos-secret'); + expect(disabled).toEqual(['orphan-provider-key-id']); + expect(cleared).toEqual(['orphan-provider-key-id', 'private-provider-key-id']); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts b/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts index 2874bf246..672e445b2 100644 --- a/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts +++ b/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts @@ -68,24 +68,17 @@ const unconfiguredPrincipalManagement = (operation: string) => Effect.die(`${operation} is not configured in this test`); const principalManagementRepository: PrincipalManagementRepositoryService = { bindApiKey: () => unconfiguredPrincipalManagement('bindApiKey'), - changePrincipalStatus: () => - unconfiguredPrincipalManagement('changePrincipalStatus'), - createNonHumanPrincipal: () => - unconfiguredPrincipalManagement('createNonHumanPrincipal'), - setApiKeyBindingStatus: () => - unconfiguredPrincipalManagement('setApiKeyBindingStatus'), - validateSupportImpersonation: () => - unconfiguredPrincipalManagement('validateSupportImpersonation'), + changePrincipalStatus: () => unconfiguredPrincipalManagement('changePrincipalStatus'), + createNonHumanPrincipal: () => unconfiguredPrincipalManagement('createNonHumanPrincipal'), + setApiKeyBindingStatus: () => unconfiguredPrincipalManagement('setApiKeyBindingStatus'), + validateSupportImpersonation: () => unconfiguredPrincipalManagement('validateSupportImpersonation'), }; const providePrincipalManagementRepository = Effect.provideService( PrincipalManagementRepository, - principalManagementRepository + principalManagementRepository, ); const contextAccess = makeContextAccessDouble('allowed'); -const provideContextAccess = Effect.provideService( - ContextAccess, - contextAccess -); +const provideContextAccess = Effect.provideService(ContextAccess, contextAccess); const makeService = (options: { readonly actionRuntime: ActionRuntimeService; @@ -102,41 +95,33 @@ const makeService = (options: { Context.add(AuthenticationService, options.authentication), Context.add(AuthConfig, options.configuration), Context.add(PrincipalResolver, options.resolver), - Context.add( - SupportRecoveryPrincipalContextResolver, - options.supportRecoveryPrincipal - ), + Context.add(SupportRecoveryPrincipalContextResolver, options.supportRecoveryPrincipal), Context.add(SupportAuthProviderService, options.provider), - Context.add(SupportImpersonationStoreService, options.store) - ) + Context.add(SupportImpersonationStoreService, options.store), + ), ); return Object.freeze({ start: (input: Parameters[0]) => - service - .start(input) - .pipe(providePrincipalManagementRepository, provideContextAccess), + service.start(input).pipe(providePrincipalManagementRepository, provideContextAccess), stop: (input: Parameters[0]) => - service - .stop(input) - .pipe(providePrincipalManagementRepository, provideContextAccess), + service.stop(input).pipe(providePrincipalManagementRepository, provideContextAccess), }); }; -const supportRecoveryPrincipal: SupportRecoveryPrincipalContextResolverService = - { - resolveStoppedImpersonation: (input: { - readonly originalAuthBindingId: string; - readonly originalPrincipalId: string; - readonly originalSessionId: string; - readonly tenantId: string; - }) => - Effect.succeed({ - authBindingId: input.originalAuthBindingId, - authContextRef: `better-auth-session:${input.originalSessionId}`, - authMethod: 'session', - principalId: input.originalPrincipalId, - tenantId: input.tenantId, - }), - }; +const supportRecoveryPrincipal: SupportRecoveryPrincipalContextResolverService = { + resolveStoppedImpersonation: (input: { + readonly originalAuthBindingId: string; + readonly originalPrincipalId: string; + readonly originalSessionId: string; + readonly tenantId: string; + }) => + Effect.succeed({ + authBindingId: input.originalAuthBindingId, + authContextRef: `better-auth-session:${input.originalSessionId}`, + authMethod: 'session', + principalId: input.originalPrincipalId, + tenantId: input.tenantId, + }), +}; const provider = (impersonated: boolean): SupportAuthProvider => ({ api: { @@ -172,488 +157,427 @@ const provider = (impersonated: boolean): SupportAuthProvider => ({ }, }); -it.effect( - 'preserves definite requested-checkpoint errors for their declared HTTP mapping', - () => - Effect.gen(function* testProgram1() { - const failures = [ - new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'The Action permission was denied', - }), - new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The support target is invalid', - }), - new ActionAlreadyCommitted({ - code: 'action_already_committed', - invocationId: 'invocation-id', - reason: 'The requested checkpoint was already committed', - }), - ]; - yield* Effect.forEach( - failures, - (failure) => - Effect.gen(function* assertRequestedCheckpointFailure() { - let providerCalls = 0; - const outcome = Match.value(failure).pipe( - Match.tag('IdentityTargetInvalidError', actionDomainFailure), - Match.orElse(actionCoreFailure) - ); - const service = makeService({ - actionRuntime: makeActionRuntimeDouble([outcome]).runtime, - authentication: makeAuthenticationServiceDouble({ - resolveTenantContext: () => - Effect.succeed({ - identity: { - displayName: 'Original administrator', - email: 'original@example.test', - principalId: originalPrincipalId, - tenantId, - }, - principal: { - authBindingId: originalAuthBindingId, - authContextRef: `better-auth-session:${restoredSessionId}`, - authMethod: 'session', - principalId: originalPrincipalId, - tenantId, - }, - setCookieHeaders: [], - state: 'authenticated', - }), - }), - configuration, - provider: makeSupportAuthProviderDouble({ - impersonateUser: () => { - providerCalls += 1; - return Promise.reject(new Error('must not create a session')); - }, - }), - resolver: makePrincipalResolverDouble({ - resolveBetterAuthUserForPrincipal: () => - Effect.succeed('target-provider-user'), - }), - store: makeSupportImpersonationStoreDouble(), - supportRecoveryPrincipal, - }); - - const actual = yield* Effect.flip( - service - .start({ - idempotencyKey: `start-${failure._tag}`, - reason: 'Investigate a support incident', - requestHeaders: new Headers(), - targetPrincipalId, - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - `correlation-${failure._tag}` - ) - ) - ); - - expect(actual).toBe(failure); - expect(providerCalls).toBe(0); - }), - { concurrency: 1, discard: true } - ); - }) -); - -it.effect( - 'removes the provider session and recovery when started evidence cannot commit', - () => - Effect.gen(function* testProgram2() { - const startedFailure = new ActionPermissionDenied({ +it.effect('preserves definite requested-checkpoint errors for their declared HTTP mapping', () => + Effect.gen(function* testProgram1() { + const failures = [ + new ActionPermissionDenied({ code: 'action_permission_denied', - reason: 'The started checkpoint was denied', - }); - const deletedTables: string[] = []; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'requested', recorded: true }), - actionCoreFailure(startedFailure), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble({ - resolveTenantContext: () => - Effect.succeed({ - identity: { - displayName: 'Original administrator', - email: 'original@example.test', - principalId: originalPrincipalId, - tenantId, - }, - principal: { - authBindingId: originalAuthBindingId, - authContextRef: `better-auth-session:${restoredSessionId}`, - authMethod: 'session', - principalId: originalPrincipalId, - tenantId, - }, - setCookieHeaders: [], - state: 'authenticated', - }), - }), - configuration, - provider: makeSupportAuthProviderDouble({ - impersonateUser: () => - Promise.resolve({ - headers: new Headers(), - response: { session: { id: impersonationSessionId } }, + reason: 'The Action permission was denied', + }), + new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The support target is invalid', + }), + new ActionAlreadyCommitted({ + code: 'action_already_committed', + invocationId: 'invocation-id', + reason: 'The requested checkpoint was already committed', + }), + ]; + yield* Effect.forEach( + failures, + (failure) => + Effect.gen(function* assertRequestedCheckpointFailure() { + let providerCalls = 0; + const outcome = Match.value(failure).pipe( + Match.tag('IdentityTargetInvalidError', actionDomainFailure), + Match.orElse(actionCoreFailure), + ); + const service = makeService({ + actionRuntime: makeActionRuntimeDouble([outcome]).runtime, + authentication: makeAuthenticationServiceDouble({ + resolveTenantContext: () => + Effect.succeed({ + identity: { + displayName: 'Original administrator', + email: 'original@example.test', + principalId: originalPrincipalId, + tenantId, + }, + principal: { + authBindingId: originalAuthBindingId, + authContextRef: `better-auth-session:${restoredSessionId}`, + authMethod: 'session', + principalId: originalPrincipalId, + tenantId, + }, + setCookieHeaders: [], + state: 'authenticated', + }), }), - }), - resolver: makePrincipalResolverDouble({ - resolveBetterAuthUserForPrincipal: () => - Effect.succeed('target-provider-user'), - }), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deletedTables.push('deleted'); + configuration, + provider: makeSupportAuthProviderDouble({ + impersonateUser: () => { + providerCalls += 1; + return Promise.reject(new Error('must not create a session')); + }, }), - deleteSession: () => - Effect.sync(() => { - deletedTables.push('deleted'); + resolver: makePrincipalResolverDouble({ + resolveBetterAuthUserForPrincipal: () => Effect.succeed('target-provider-user'), }), - insertRecovery: () => Effect.void, - updateImpersonationSession: () => Effect.void, - }), - supportRecoveryPrincipal, - }); + store: makeSupportImpersonationStoreDouble(), + supportRecoveryPrincipal, + }); + + const actual = yield* Effect.flip( + service + .start({ + idempotencyKey: `start-${failure._tag}`, + reason: 'Investigate a support incident', + requestHeaders: new Headers(), + targetPrincipalId, + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, `correlation-${failure._tag}`)), + ); - const failure = yield* Effect.flip( - service - .start({ - idempotencyKey: 'started-compensation', - reason: 'Investigate a support incident', - requestHeaders: new Headers(), - targetPrincipalId, - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-started-compensation' - ) - ) - ); - - expect(failure).toBe(startedFailure); - expect(actionRuntime.invocationCount()).toBe(2); - expect(deletedTables).toHaveLength(2); - }) + expect(actual).toBe(failure); + expect(providerCalls).toBe(0); + }), + { concurrency: 1, discard: true }, + ); + }), ); -it.effect( - 'persists stop recovery before provider restoration and returns restored cookies on evidence failure', - () => - Effect.gen(function* testProgram3() { - let recovery: SupportRecoveryRecord | undefined; - let resolverCalled = false; - const transactionFailure = new ActionTransactionError({ - code: 'action_transaction_failed', - reason: 'The stopped checkpoint transaction failed', - }); - const service = makeService({ - actionRuntime: makeActionRuntimeDouble([ - actionCoreFailure(transactionFailure), - ]).runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(true), - resolver: makePrincipalResolverDouble({ - resolveBetterAuthUserForTenant: () => { - resolverCalled = true; - return Effect.die('disabled principal'); - }, - }), - store: makeSupportImpersonationStoreDouble({ - deleteSession: () => Effect.void, - insertRecovery: (value) => - Effect.sync(() => { - recovery = value; - }), - }), - supportRecoveryPrincipal, - }); +it.effect('removes the provider session and recovery when started evidence cannot commit', () => + Effect.gen(function* testProgram2() { + const startedFailure = new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'The started checkpoint was denied', + }); + const deletedTables: string[] = []; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'requested', recorded: true }), + actionCoreFailure(startedFailure), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble({ + resolveTenantContext: () => + Effect.succeed({ + identity: { + displayName: 'Original administrator', + email: 'original@example.test', + principalId: originalPrincipalId, + tenantId, + }, + principal: { + authBindingId: originalAuthBindingId, + authContextRef: `better-auth-session:${restoredSessionId}`, + authMethod: 'session', + principalId: originalPrincipalId, + tenantId, + }, + setCookieHeaders: [], + state: 'authenticated', + }), + }), + configuration, + provider: makeSupportAuthProviderDouble({ + impersonateUser: () => + Promise.resolve({ + headers: new Headers(), + response: { session: { id: impersonationSessionId } }, + }), + }), + resolver: makePrincipalResolverDouble({ + resolveBetterAuthUserForPrincipal: () => Effect.succeed('target-provider-user'), + }), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deletedTables.push('deleted'); + }), + deleteSession: () => + Effect.sync(() => { + deletedTables.push('deleted'); + }), + insertRecovery: () => Effect.void, + updateImpersonationSession: () => Effect.void, + }), + supportRecoveryPrincipal, + }); - const result = yield* service - .stop({ - idempotencyKey: 'stop-request-1', + const failure = yield* Effect.flip( + service + .start({ + idempotencyKey: 'started-compensation', + reason: 'Investigate a support incident', requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-1' - ) - ); - - expect(recovery).toEqual( - expect.objectContaining({ - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, targetPrincipalId, - tenantId, }) - ); - expect(result.checkpointPending).toBe(true); - expect(result.setCookieHeaders).toEqual([ - 'session=restored; Path=/; HttpOnly', - ]); - expect(resolverCalled).toBe(false); - }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-started-compensation')), + ); + + expect(failure).toBe(startedFailure); + expect(actionRuntime.invocationCount()).toBe(2); + expect(deletedTables).toHaveLength(2); + }), +); + +it.effect('persists stop recovery before provider restoration and returns restored cookies on evidence failure', () => + Effect.gen(function* testProgram3() { + let recovery: SupportRecoveryRecord | undefined; + let resolverCalled = false; + const transactionFailure = new ActionTransactionError({ + code: 'action_transaction_failed', + reason: 'The stopped checkpoint transaction failed', + }); + const service = makeService({ + actionRuntime: makeActionRuntimeDouble([actionCoreFailure(transactionFailure)]).runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(true), + resolver: makePrincipalResolverDouble({ + resolveBetterAuthUserForTenant: () => { + resolverCalled = true; + return Effect.die('disabled principal'); + }, + }), + store: makeSupportImpersonationStoreDouble({ + deleteSession: () => Effect.void, + insertRecovery: (value) => + Effect.sync(() => { + recovery = value; + }), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-request-1', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-1')); + + expect(recovery).toEqual( + expect.objectContaining({ + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + targetPrincipalId, + tenantId, + }), + ); + expect(result.checkpointPending).toBe(true); + expect(result.setCookieHeaders).toEqual(['session=restored; Path=/; HttpOnly']); + expect(resolverCalled).toBe(false); + }), +); + +it.effect('terminates the target session before retrying stopped evidence from the restored session', () => + Effect.gen(function* testProgram4() { + const recovery = { + actionId: 'impersonation-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + let recoveryDeleted = false; + let targetSessionActive = true; + const actionRuntime = makeActionRuntimeDouble([actionSuccess({ checkpoint: 'stopped', recorded: true })]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(false), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + recoveryDeleted = true; + }), + deleteSession: () => + Effect.sync(() => { + targetSessionActive = false; + }), + loadRecoveries: () => Effect.succeed([recovery]), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-request-2', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-2')); + + expect(actionRuntime.payloads[0]).toEqual({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigate support request', + sessionRef: `better-auth-session:${impersonationSessionId}`, + targetPrincipalId, + }); + expect(result.checkpointPending).toBe(false); + expect(targetSessionActive).toBe(false); + expect(recoveryDeleted).toBe(true); + }), ); -it.effect( - 'terminates the target session before retrying stopped evidence from the restored session', - () => - Effect.gen(function* testProgram4() { - const recovery = { - actionId: 'impersonation-action', +it.effect('completes every pending checkpoint correlated to the restored session', () => + Effect.gen(function* testProgram5() { + const secondImpersonationSessionId = 'second-impersonated-session-id'; + const recoveries = [ + { + actionId: 'impersonation-action-one', createdAt: new Date('2026-08-09T00:00:00.000Z'), impersonationSessionId, originalAuthBindingId, originalPrincipalId, originalSessionId: restoredSessionId, - reason: 'Investigate support request', + reason: 'First support request', targetPrincipalId, tenantId, - }; - let recoveryDeleted = false; - let targetSessionActive = true; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(false), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - recoveryDeleted = true; - }), - deleteSession: () => - Effect.sync(() => { - targetSessionActive = false; - }), - loadRecoveries: () => Effect.succeed([recovery]), - }), - supportRecoveryPrincipal, - }); - - const result = yield* service - .stop({ - idempotencyKey: 'stop-request-2', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-2' - ) - ); - - expect(actionRuntime.payloads[0]).toEqual({ - checkpoint: 'stopped', + }, + { + actionId: 'impersonation-action-two', + createdAt: new Date('2026-08-09T00:01:00.000Z'), + impersonationSessionId: secondImpersonationSessionId, + originalAuthBindingId, originalPrincipalId, - reason: 'Investigate support request', + originalSessionId: restoredSessionId, + reason: 'Second support request', + targetPrincipalId: '30000000-0000-4000-8000-000000000002', + tenantId, + }, + ]; + let deleteCount = 0; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'stopped', recorded: true }), + actionSuccess({ checkpoint: 'stopped', recorded: true }), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(false), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleteCount += 1; + }), + deleteSession: () => + Effect.sync(() => { + deleteCount += 1; + }), + loadRecoveries: () => Effect.succeed(recoveries), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-request-3', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-3')); + + expect(actionRuntime.payloads).toEqual([ + expect.objectContaining({ sessionRef: `better-auth-session:${impersonationSessionId}`, - targetPrincipalId, - }); - expect(result.checkpointPending).toBe(false); - expect(targetSessionActive).toBe(false); - expect(recoveryDeleted).toBe(true); - }) -); - -it.effect( - 'completes every pending checkpoint correlated to the restored session', - () => - Effect.gen(function* testProgram5() { - const secondImpersonationSessionId = 'second-impersonated-session-id'; - const recoveries = [ - { - actionId: 'impersonation-action-one', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'First support request', - targetPrincipalId, - tenantId, - }, - { - actionId: 'impersonation-action-two', - createdAt: new Date('2026-08-09T00:01:00.000Z'), - impersonationSessionId: secondImpersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Second support request', - targetPrincipalId: '30000000-0000-4000-8000-000000000002', - tenantId, - }, - ]; - let deleteCount = 0; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(false), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleteCount += 1; - }), - deleteSession: () => - Effect.sync(() => { - deleteCount += 1; - }), - loadRecoveries: () => Effect.succeed(recoveries), - }), - supportRecoveryPrincipal, - }); - - const result = yield* service - .stop({ - idempotencyKey: 'stop-request-3', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-3' - ) - ); - - expect(actionRuntime.payloads).toEqual([ - expect.objectContaining({ - sessionRef: `better-auth-session:${impersonationSessionId}`, - }), - expect.objectContaining({ - sessionRef: `better-auth-session:${secondImpersonationSessionId}`, - }), - ]); - expect(result.checkpointPending).toBe(false); - expect(deleteCount).toBe(4); - }) + }), + expect.objectContaining({ + sessionRef: `better-auth-session:${secondImpersonationSessionId}`, + }), + ]); + expect(result.checkpointPending).toBe(false); + expect(deleteCount).toBe(4); + }), ); -it.effect( - 'persists and completes stopped evidence on the first stop after impersonation expiry', - () => - Effect.gen(function* testProgram6() { - const expiredToken = 'expired-impersonation-token'; - const signedToken = encodeURIComponent( - `${expiredToken}.${yield* Effect.promise(() => makeSignature(expiredToken, configuration.secret))}` - ); - let persistedRecovery: SupportRecoveryRecord | undefined; - let deleteCalls = 0; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - getSession: () => - Promise.resolve({ headers: new Headers(), response: null }), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleteCalls += 1; - }), - deleteSession: () => - Effect.sync(() => { - deleteCalls += 1; - }), - insertRecovery: (value) => - Effect.sync(() => { - persistedRecovery = value; +it.effect('persists and completes stopped evidence on the first stop after impersonation expiry', () => + Effect.gen(function* testProgram6() { + const expiredToken = 'expired-impersonation-token'; + const signedToken = encodeURIComponent( + `${expiredToken}.${yield* Effect.promise(() => makeSignature(expiredToken, configuration.secret))}`, + ); + let persistedRecovery: SupportRecoveryRecord | undefined; + let deleteCalls = 0; + const actionRuntime = makeActionRuntimeDouble([actionSuccess({ checkpoint: 'stopped', recorded: true })]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + getSession: () => Promise.resolve({ headers: new Headers(), response: null }), + }), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleteCalls += 1; + }), + deleteSession: () => + Effect.sync(() => { + deleteCalls += 1; + }), + insertRecovery: (value) => + Effect.sync(() => { + persistedRecovery = value; + }), + loadExpiredRecovery: () => + Effect.succeed( + Option.some({ + actionId: 'expired-impersonation-action', + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, }), - loadExpiredRecovery: () => - Effect.succeed( - Option.some({ - actionId: 'expired-impersonation-action', - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }) - ), + ), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'first-expired-stop', + requestHeaders: new Headers({ + cookie: `better-auth.session_token=${signedToken}`, }), - supportRecoveryPrincipal, - }); + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-first-expired-stop')); - const result = yield* service - .stop({ - idempotencyKey: 'first-expired-stop', - requestHeaders: new Headers({ - cookie: `better-auth.session_token=${signedToken}`, - }), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-first-expired-stop' - ) - ); - - expect(persistedRecovery).toEqual( - expect.objectContaining({ - actionId: 'expired-impersonation-action', - impersonationSessionId, - originalSessionId: restoredSessionId, - }) - ); - expect(actionRuntime.payloads[0]).toEqual({ - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigate support request', - sessionRef: `better-auth-session:${impersonationSessionId}`, - targetPrincipalId, - }); - expect(result.checkpointPending).toBe(false); - expect( - result.setCookieHeaders.every((header) => header.includes('Max-Age=0')) - ).toBe(true); - expect(deleteCalls).toBe(2); - }) + expect(persistedRecovery).toEqual( + expect.objectContaining({ + actionId: 'expired-impersonation-action', + impersonationSessionId, + originalSessionId: restoredSessionId, + }), + ); + expect(actionRuntime.payloads[0]).toEqual({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigate support request', + sessionRef: `better-auth-session:${impersonationSessionId}`, + targetPrincipalId, + }); + expect(result.checkpointPending).toBe(false); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + expect(deleteCalls).toBe(2); + }), ); -const makeLostResponseRecoveryService = ( - recovery: SupportRecoveryRecord, - expiresAt: Date -) => { +const makeLostResponseRecoveryService = (recovery: SupportRecoveryRecord, expiresAt: Date) => { let deleted = false; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); + const actionRuntime = makeActionRuntimeDouble([actionSuccess({ checkpoint: 'stopped', recorded: true })]); const service = makeService({ actionRuntime: actionRuntime.runtime, authentication: makeAuthenticationServiceDouble(), configuration, provider: makeSupportAuthProviderDouble({ - getSession: () => - Promise.resolve({ headers: new Headers(), response: null }), + getSession: () => Promise.resolve({ headers: new Headers(), response: null }), }), resolver: makePrincipalResolverDouble(), store: makeSupportImpersonationStoreDouble({ @@ -667,7 +591,7 @@ const makeLostResponseRecoveryService = ( Option.some({ expiresAt, id: restoredSessionId, - }) + }), ), loadRecoveries: () => Effect.succeed([recovery]), }), @@ -676,231 +600,183 @@ const makeLostResponseRecoveryService = ( return { actionRuntime, deleted: () => deleted, service }; }; -it.effect( - 'restores the original session and stopped checkpoint after the provider response is lost', - () => - Effect.gen(function* testProgram7() { - const originalSessionToken = 'original-session-token'; - const adminValue = `${originalSessionToken}:true`; - const adminCookie = encodeURIComponent( - `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}` - ); - const requestHeaders = new Headers({ - cookie: `better-auth.admin_session=${adminCookie}; better-auth.session_token=deleted`, - }); - const recovery = { - actionId: 'impersonation-action', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }; - const { actionRuntime, deleted, service } = - makeLostResponseRecoveryService( - recovery, - new Date('2099-01-01T00:00:00.000Z') - ); - - const result = yield* service - .stop({ - idempotencyKey: 'stop-response-loss', - requestHeaders, - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-response-loss' - ) - ); - - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted()).toBe(true); - const restoredSessionCookie = result.setCookieHeaders.find((header) => - header.startsWith('better-auth.session_token=') - ); - expect(restoredSessionCookie).toBeDefined(); - expect(restoredSessionCookie?.includes('Max-Age=')).toBe(false); - const dontRememberCookie = result.setCookieHeaders.find((header) => - header.startsWith('better-auth.dont_remember=') - ); - expect(dontRememberCookie).toBeDefined(); - expect(dontRememberCookie?.includes('Max-Age=0')).toBe(false); - expect( - result.setCookieHeaders.some( - (header) => - header.startsWith('better-auth.admin_session=') && - header.includes('Max-Age=0') - ) - ).toBe(true); - }) -); - -it.effect( - 'completes stopped recovery when a lost response leaves only an expired original session', - () => - Effect.gen(function* testProgram8() { - yield* TestClock.setTime(new Date('2026-09-08T00:00:00.000Z').getTime()); - const originalSessionToken = 'expired-original-session-token'; - const adminValue = `${originalSessionToken}:`; - const adminCookie = encodeURIComponent( - `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}` - ); - const recovery = { - actionId: 'expired-original-action', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }; - const { actionRuntime, deleted, service } = - makeLostResponseRecoveryService( - recovery, - new Date('2000-01-01T00:00:00.000Z') - ); - - const result = yield* service - .stop({ - idempotencyKey: 'stop-expired-lost-response', - requestHeaders: new Headers({ - cookie: `better-auth.admin_session=${adminCookie}`, - }), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-expired-lost-response' - ) - ); - - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted()).toBe(true); - expect( - result.setCookieHeaders.every((header) => header.includes('Max-Age=0')) - ).toBe(true); - }) +it.effect('restores the original session and stopped checkpoint after the provider response is lost', () => + Effect.gen(function* testProgram7() { + const originalSessionToken = 'original-session-token'; + const adminValue = `${originalSessionToken}:true`; + const adminCookie = encodeURIComponent( + `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}`, + ); + const requestHeaders = new Headers({ + cookie: `better-auth.admin_session=${adminCookie}; better-auth.session_token=deleted`, + }); + const recovery = { + actionId: 'impersonation-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( + recovery, + new Date('2099-01-01T00:00:00.000Z'), + ); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-response-loss', + requestHeaders, + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-response-loss')); + + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted()).toBe(true); + const restoredSessionCookie = result.setCookieHeaders.find((header) => + header.startsWith('better-auth.session_token='), + ); + expect(restoredSessionCookie).toBeDefined(); + expect(restoredSessionCookie?.includes('Max-Age=')).toBe(false); + const dontRememberCookie = result.setCookieHeaders.find((header) => + header.startsWith('better-auth.dont_remember='), + ); + expect(dontRememberCookie).toBeDefined(); + expect(dontRememberCookie?.includes('Max-Age=0')).toBe(false); + expect( + result.setCookieHeaders.some( + (header) => header.startsWith('better-auth.admin_session=') && header.includes('Max-Age=0'), + ), + ).toBe(true); + }), ); -it.effect( - 'clears a mismatched restored session and completes recovery from the recorded original', - () => - Effect.gen(function* testProgram9() { - let deleted = false; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - ...provider(true).api, - stopImpersonating: () => { - const headers = new Headers(); - headers.append( - 'set-cookie', - 'better-auth.session_token=unexpected; Path=/; HttpOnly' - ); - return Promise.resolve({ - headers, - response: { session: { id: 'unexpected-restored-session' } }, - }); - }, +it.effect('completes stopped recovery when a lost response leaves only an expired original session', () => + Effect.gen(function* testProgram8() { + yield* TestClock.setTime(new Date('2026-09-08T00:00:00.000Z').getTime()); + const originalSessionToken = 'expired-original-session-token'; + const adminValue = `${originalSessionToken}:`; + const adminCookie = encodeURIComponent( + `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}`, + ); + const recovery = { + actionId: 'expired-original-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( + recovery, + new Date('2000-01-01T00:00:00.000Z'), + ); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-expired-lost-response', + requestHeaders: new Headers({ + cookie: `better-auth.admin_session=${adminCookie}`, }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleted = true; - }), - insertRecovery: () => Effect.void, - }), - supportRecoveryPrincipal, - }); - - const result = yield* service - .stop({ - idempotencyKey: 'stop-mismatched-restore', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-mismatched-restore' - ) - ); - - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted).toBe(true); - expect( - result.setCookieHeaders.every((header) => header.includes('Max-Age=0')) - ).toBe(true); - expect( - result.setCookieHeaders.some((header) => header.includes('unexpected')) - ).toBe(false); - }) + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-expired-lost-response')); + + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted()).toBe(true); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + }), ); -it.effect( - 'deletes the impersonation session and clears cookies when original restoration fails', - () => - Effect.gen(function* testProgram10() { - let deleteCalls = 0; - const checkpointFailure = new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'The stopped checkpoint was denied', - }); - const service = makeService({ - actionRuntime: makeActionRuntimeDouble([ - actionCoreFailure(checkpointFailure), - ]).runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - ...provider(true).api, - stopImpersonating: () => - Promise.reject(new Error('admin session expired')), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteSession: () => - Effect.sync(() => { - deleteCalls += 1; - }), - insertRecovery: () => Effect.void, - }), - supportRecoveryPrincipal, - }); +it.effect('clears a mismatched restored session and completes recovery from the recorded original', () => + Effect.gen(function* testProgram9() { + let deleted = false; + const actionRuntime = makeActionRuntimeDouble([actionSuccess({ checkpoint: 'stopped', recorded: true })]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + ...provider(true).api, + stopImpersonating: () => { + const headers = new Headers(); + headers.append('set-cookie', 'better-auth.session_token=unexpected; Path=/; HttpOnly'); + return Promise.resolve({ + headers, + response: { session: { id: 'unexpected-restored-session' } }, + }); + }, + }), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleted = true; + }), + insertRecovery: () => Effect.void, + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-mismatched-restore', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-mismatched-restore')); + + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted).toBe(true); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + expect(result.setCookieHeaders.some((header) => header.includes('unexpected'))).toBe(false); + }), +); - const result = yield* service - .stop({ - idempotencyKey: 'stop-expired-original', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-expired-original' - ) - ); - - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(true); - expect(deleteCalls).toBe(1); - expect( - result.setCookieHeaders.every((header) => header.includes('Max-Age=0')) - ).toBe(true); - }) +it.effect('deletes the impersonation session and clears cookies when original restoration fails', () => + Effect.gen(function* testProgram10() { + let deleteCalls = 0; + const checkpointFailure = new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'The stopped checkpoint was denied', + }); + const service = makeService({ + actionRuntime: makeActionRuntimeDouble([actionCoreFailure(checkpointFailure)]).runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + ...provider(true).api, + stopImpersonating: () => Promise.reject(new Error('admin session expired')), + }), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteSession: () => + Effect.sync(() => { + deleteCalls += 1; + }), + insertRecovery: () => Effect.void, + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-expired-original', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-expired-original')); + + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(true); + expect(deleteCalls).toBe(1); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts b/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts index abfebbd12..92e096dbc 100644 --- a/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts @@ -13,20 +13,10 @@ const contract = (appId: string, moduleId: string) => makeModuleContractFixture({ appId, moduleId, - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], }); -const allowlist = ( - entries: DeploymentAllowlist['entries'] -): DeploymentAllowlist => +const allowlist = (entries: DeploymentAllowlist['entries']): DeploymentAllowlist => Object.freeze({ entries: Object.freeze([...entries]), revision: JSON.stringify(entries), @@ -37,151 +27,104 @@ const response = (value: Value, init: ResponseInit = {}): Response => { 'content-type': 'application/json', ...Object.fromEntries(new Headers(init.headers)), }; - return new Response( - Predicate.isString(value) ? value : JSON.stringify(value), - { - ...init, - headers, - } - ); + return new Response(Predicate.isString(value) ? value : JSON.stringify(value), { + ...init, + headers, + }); }; -it.effect( - 'loads two independent deployment contracts once and preserves both identities', - () => - Effect.gen(function* verifyCase1() { - const requests: string[] = []; - const documents = new Map([ - [ - 'https://property.example.test/.well-known/ontos-module-manifest.json', - contract('property-registry', 'property.registry'), - ], - [ - 'https://documents.example.test/.well-known/ontos-module-manifest.json', - contract('documents-center', 'documents.center'), - ], - ]); - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { - appId: 'property-registry', - contractUrl: [...documents.keys()][0] ?? '', - }, - { - appId: 'documents-center', - contractUrl: [...documents.keys()][1] ?? '', - }, - ]), - (url, init) => { - const normalized = new Request(url).url; - requests.push(normalized); - expect(init?.redirect).toBe('manual'); - return Promise.resolve(response(documents.get(normalized))); - } - ); - const [first, concurrent, cached] = yield* Effect.all( - [loader, loader, loader], +it.effect('loads two independent deployment contracts once and preserves both identities', () => + Effect.gen(function* verifyCase1() { + const requests: string[] = []; + const documents = new Map([ + [ + 'https://property.example.test/.well-known/ontos-module-manifest.json', + contract('property-registry', 'property.registry'), + ], + [ + 'https://documents.example.test/.well-known/ontos-module-manifest.json', + contract('documents-center', 'documents.center'), + ], + ]); + const loader = makeInstalledModuleCatalogLoader( + allowlist([ { - concurrency: 'unbounded', - } - ); - expect(first).toBe(concurrent); - expect(first).toBe(cached); - expect(requests).toHaveLength(2); - expect(first.moduleIds).toEqual([ - 'documents.center', - 'property.registry', - ]); - expect( - first.getByDeploymentAppId('property-registry')?.manifest.module.id - ).toBe('property.registry'); - expect(first.getByModuleId('property.registry')?.deployment.appId).toBe( - 'property-registry' - ); - }) -); - -it.effect( - 'keeps a healthy deployment available on cold start when another is unreachable', - () => - Effect.gen(function* verifyCase2() { - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { - appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', - }, - { - appId: 'documents-center', - contractUrl: - 'https://documents.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - (url) => { - const appId = new Request(url).url.includes('property') - ? 'property-registry' - : 'documents-center'; - if (appId === 'property-registry') { - return Promise.reject(new Error('deployment unreachable')); - } - return Promise.resolve(response(contract(appId, 'documents.center'))); - } - ); - - const catalog = yield* loader; - - expect(catalog.moduleIds).toEqual(['documents.center']); - expect(catalog.deploymentStatuses).toEqual([ + appId: 'property-registry', + contractUrl: [...documents.keys()][0] ?? '', + }, { appId: 'documents-center', - moduleId: 'documents.center', - status: 'available', + contractUrl: [...documents.keys()][1] ?? '', }, + ]), + (url, init) => { + const normalized = new Request(url).url; + requests.push(normalized); + expect(init?.redirect).toBe('manual'); + return Promise.resolve(response(documents.get(normalized))); + }, + ); + const [first, concurrent, cached] = yield* Effect.all([loader, loader, loader], { + concurrency: 'unbounded', + }); + expect(first).toBe(concurrent); + expect(first).toBe(cached); + expect(requests).toHaveLength(2); + expect(first.moduleIds).toEqual(['documents.center', 'property.registry']); + expect(first.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe('property.registry'); + expect(first.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); + }), +); + +it.effect('keeps a healthy deployment available on cold start when another is unreachable', () => + Effect.gen(function* verifyCase2() { + const loader = makeInstalledModuleCatalogLoader( + allowlist([ { appId: 'property-registry', - reason: 'unavailable', - status: 'unavailable', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', }, - ]); - }) + { + appId: 'documents-center', + contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + (url) => { + const appId = new Request(url).url.includes('property') ? 'property-registry' : 'documents-center'; + if (appId === 'property-registry') { + return Promise.reject(new Error('deployment unreachable')); + } + return Promise.resolve(response(contract(appId, 'documents.center'))); + }, + ); + + const catalog = yield* loader; + + expect(catalog.moduleIds).toEqual(['documents.center']); + expect(catalog.deploymentStatuses).toEqual([ + { + appId: 'documents-center', + moduleId: 'documents.center', + status: 'available', + }, + { + appId: 'property-registry', + reason: 'unavailable', + status: 'unavailable', + }, + ]); + }), ); const unavailableResponses = [ - [ - 'unavailable', - () => Promise.reject(new Error('secret host failure')), - 'unavailable', - ], - [ - 'redirect', - () => Promise.resolve(response({}, { status: 302 })), - 'unavailable', - ], - [ - 'non-JSON', - () => - Promise.resolve( - response('{}', { headers: { 'content-type': 'text/html' } }) - ), - 'incompatible', - ], - [ - 'malformed JSON', - () => Promise.resolve(response('{broken')), - 'incompatible', - ], - [ - 'invalid schema', - () => Promise.resolve(response({ schemaVersion: '0' })), - 'incompatible', - ], + ['unavailable', () => Promise.reject(new Error('secret host failure')), 'unavailable'], + ['redirect', () => Promise.resolve(response({}, { status: 302 })), 'unavailable'], + ['non-JSON', () => Promise.resolve(response('{}', { headers: { 'content-type': 'text/html' } })), 'incompatible'], + ['malformed JSON', () => Promise.resolve(response('{broken')), 'incompatible'], + ['invalid schema', () => Promise.resolve(response({ schemaVersion: '0' })), 'incompatible'], [ 'mismatched app', - () => - Promise.resolve( - response(contract('documents-center', 'property.registry')) - ), + () => Promise.resolve(response(contract('documents-center', 'property.registry'))), 'incompatible', ], ] as const; @@ -192,11 +135,10 @@ for (const [label, fetcher, expectedReason] of unavailableResponses) { allowlist([ { appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', }, ]), - fetcher + fetcher, ); const catalog = yield* loader; expect(catalog.moduleIds).toEqual([]); @@ -207,182 +149,163 @@ for (const [label, fetcher, expectedReason] of unavailableResponses) { status: 'unavailable', }, ]); - }) + }), ); } -it.live( - 'classifies oversized, timed-out, and duplicate-module deployments without caching failures', - () => - Effect.gen(function* verifyCase4() { - let attempts = 0; - const one: DeploymentAllowlist['entries'][number] = { - appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', - }; - const oversized = makeInstalledModuleCatalogLoader( - allowlist([one]), - () => Promise.resolve(response('x'.repeat(64))), - { maxBytes: 32 } - ); - expect(yield* oversized).toMatchObject({ - deploymentStatuses: [ - { - appId: 'property-registry', - reason: 'unavailable', - status: 'unavailable', - }, - ], - }); - - const timedOut = makeInstalledModuleCatalogLoader( - allowlist([one]), - (_url, init) => { - const pending = Promise.withResolvers(); - init?.signal?.addEventListener( - 'abort', - () => pending.reject(new Error('aborted')), - { - once: true, - } - ); - return Promise.resolve(pending.promise); +it.live('classifies oversized, timed-out, and duplicate-module deployments without caching failures', () => + Effect.gen(function* verifyCase4() { + let attempts = 0; + const one: DeploymentAllowlist['entries'][number] = { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }; + const oversized = makeInstalledModuleCatalogLoader( + allowlist([one]), + () => Promise.resolve(response('x'.repeat(64))), + { maxBytes: 32 }, + ); + expect(yield* oversized).toMatchObject({ + deploymentStatuses: [ + { + appId: 'property-registry', + reason: 'unavailable', + status: 'unavailable', }, - { timeoutMs: 10 } - ); - expect(yield* timedOut).toMatchObject({ - deploymentStatuses: [ - { - appId: 'property-registry', - reason: 'timeout', - status: 'unavailable', - }, - ], - }); - - const duplicate = makeInstalledModuleCatalogLoader( - allowlist([ - one, - { - appId: 'documents-center', - contractUrl: - 'https://documents.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - (url) => { - attempts += 1; - return Promise.resolve( - new Request(url).url.includes('property') - ? response(contract('property-registry', 'shared.module')) - : response(contract('documents-center', 'shared.module')) - ); - } - ); - expect(yield* duplicate).toMatchObject({ - deploymentStatuses: [ - { - appId: 'documents-center', - reason: 'incompatible', - status: 'unavailable', - }, - { - appId: 'property-registry', - reason: 'incompatible', - status: 'unavailable', - }, - ], - }); - yield* duplicate; - expect(attempts).toBe(4); - }) -); - -it.effect( - 'recovers a deployment on a later read and caches only the fully healthy result', - () => - Effect.gen(function* verifyCase5() { - let requests = 0; - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { - appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - () => { - requests += 1; - if (requests === 1) { - return Promise.reject(new Error('temporarily unreachable')); - } - return Promise.resolve( - response(contract('property-registry', 'property.registry')) - ); - } - ); - - const degraded = yield* loader; - const recovered = yield* loader; - const cached = yield* loader; + ], + }); - expect(degraded.deploymentStatuses).toEqual([ + const timedOut = makeInstalledModuleCatalogLoader( + allowlist([one]), + (_url, init) => { + const pending = Promise.withResolvers(); + init?.signal?.addEventListener('abort', () => pending.reject(new Error('aborted')), { + once: true, + }); + return Promise.resolve(pending.promise); + }, + { timeoutMs: 10 }, + ); + expect(yield* timedOut).toMatchObject({ + deploymentStatuses: [ { appId: 'property-registry', - reason: 'unavailable', + reason: 'timeout', + status: 'unavailable', + }, + ], + }); + + const duplicate = makeInstalledModuleCatalogLoader( + allowlist([ + one, + { + appId: 'documents-center', + contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + (url) => { + attempts += 1; + return Promise.resolve( + new Request(url).url.includes('property') + ? response(contract('property-registry', 'shared.module')) + : response(contract('documents-center', 'shared.module')), + ); + }, + ); + expect(yield* duplicate).toMatchObject({ + deploymentStatuses: [ + { + appId: 'documents-center', + reason: 'incompatible', status: 'unavailable', }, - ]); - expect(recovered.deploymentStatuses).toEqual([ { appId: 'property-registry', - moduleId: 'property.registry', - status: 'available', + reason: 'incompatible', + status: 'unavailable', }, - ]); - expect(cached).toBe(recovered); - expect(requests).toBe(2); - }) + ], + }); + yield* duplicate; + expect(attempts).toBe(4); + }), ); -it.effect( - 'recreates the complete cache by constructing a new deployment-revision Layer', - () => - Effect.gen(function* verifyCase6() { - let requests = 0; - const fetcher = () => { +it.effect('recovers a deployment on a later read and caches only the fully healthy result', () => + Effect.gen(function* verifyCase5() { + let requests = 0; + const loader = makeInstalledModuleCatalogLoader( + allowlist([ + { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + () => { requests += 1; - return Promise.resolve( - response(contract('property-registry', 'property.registry')) - ); - }; - const firstRevision = makeInstalledModuleCatalogLayer( - allowlist([ + if (requests === 1) { + return Promise.reject(new Error('temporarily unreachable')); + } + return Promise.resolve(response(contract('property-registry', 'property.registry'))); + }, + ); + + const degraded = yield* loader; + const recovered = yield* loader; + const cached = yield* loader; + + expect(degraded.deploymentStatuses).toEqual([ + { + appId: 'property-registry', + reason: 'unavailable', + status: 'unavailable', + }, + ]); + expect(recovered.deploymentStatuses).toEqual([ + { + appId: 'property-registry', + moduleId: 'property.registry', + status: 'available', + }, + ]); + expect(cached).toBe(recovered); + expect(requests).toBe(2); + }), +); + +it.effect('recreates the complete cache by constructing a new deployment-revision Layer', () => + Effect.gen(function* verifyCase6() { + let requests = 0; + const fetcher = () => { + requests += 1; + return Promise.resolve(response(contract('property-registry', 'property.registry'))); + }; + const firstRevision = makeInstalledModuleCatalogLayer( + allowlist([ + { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + fetcher, + ); + const secondRevision = makeInstalledModuleCatalogLayer( + Object.freeze({ + ...allowlist([ { appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', }, ]), - fetcher - ); - const secondRevision = makeInstalledModuleCatalogLayer( - Object.freeze({ - ...allowlist([ - { - appId: 'property-registry', - contractUrl: - 'https://property.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - revision: 'revision-2', - }), - fetcher - ); + revision: 'revision-2', + }), + fetcher, + ); - yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); - yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); - yield* installedModuleCatalog.pipe(Effect.provide(secondRevision)); - expect(requests).toBe(2); - }) + yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); + yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); + yield* installedModuleCatalog.pipe(Effect.provide(secondRevision)); + expect(requests).toBe(2); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts b/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts index 6063b7ec4..a6bfc29ad 100644 --- a/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts @@ -5,48 +5,38 @@ import { expect, it } from 'effect-rstest'; import { makeModuleContractFixture } from '../../../../packages/core-runtime/src/testing/module-contract.ts'; import { matchInstalledOutboxMessagesOnce } from '../../api/modules/installed-outbox-matcher.ts'; -const contract = ( - appId: string, - moduleId: string, - outboxSubscriptions: readonly object[] = [] -) => makeModuleContractFixture({ appId, moduleId, outboxSubscriptions }); +const contract = (appId: string, moduleId: string, outboxSubscriptions: readonly object[] = []) => + makeModuleContractFixture({ appId, moduleId, outboxSubscriptions }); -it.effect( - 'passes a dormant subscription with an absent producer to Core matching', - () => - Effect.gen(function* verifyCase1() { - const subscription = { - consumerModuleKey: 'property.registry', - entrypoint: { - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'property.registry.document-projector', - moduleKey: 'property.registry', - role: 'worker', - scope: 'tenant', - }, - producerModuleKey: 'documents.center', - topic: 'documents.center.created', - workerKey: 'property.registry.document-projector', - } as const; - const catalog = buildInstalledModuleCatalog([ - { - contract: contract('property-registry', 'property.registry', [ - subscription, - ]), - expectedAppId: 'property-registry', - }, - ]); - let received: unknown; - const result = yield* matchInstalledOutboxMessagesOnce( - catalog, - (input) => { - received = input.subscriptions; - return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); - } - ); +it.effect('passes a dormant subscription with an absent producer to Core matching', () => + Effect.gen(function* verifyCase1() { + const subscription = { + consumerModuleKey: 'property.registry', + entrypoint: { + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'property.registry.document-projector', + moduleKey: 'property.registry', + role: 'worker', + scope: 'tenant', + }, + producerModuleKey: 'documents.center', + topic: 'documents.center.created', + workerKey: 'property.registry.document-projector', + } as const; + const catalog = buildInstalledModuleCatalog([ + { + contract: contract('property-registry', 'property.registry', [subscription]), + expectedAppId: 'property-registry', + }, + ]); + let received: unknown; + const result = yield* matchInstalledOutboxMessagesOnce(catalog, (input) => { + received = input.subscriptions; + return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); + }); - expect(received).toEqual([subscription]); - expect(result).toEqual({ deliveriesCreated: 1, messagesMatched: 1 }); - }) + expect(received).toEqual([subscription]); + expect(result).toEqual({ deliveriesCreated: 1, messagesMatched: 1 }); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts b/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts index accf804de..95555c36a 100644 --- a/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts @@ -10,68 +10,50 @@ import { installedVerticalIds, } from '../../api/verticals/installed-verticals.ts'; -it.effect( - 'derives installed vertical IDs from the injected topology without hardcoded registrations', - () => - Effect.gen(function* verifyCase1() { - const topology = Schema.decodeUnknownSync( - DeploymentAllowlistTopologySchema - )( - JSON.parse( - fs.readFileSync( - new URL( - '../../../../topology/reference-topology.json', - import.meta.url - ), - 'utf-8' - ) - ) - ); - const expectedInstalledIds = yield* deriveInstalledVerticalIds(topology); +it.effect('derives installed vertical IDs from the injected topology without hardcoded registrations', () => + Effect.gen(function* verifyCase1() { + const topology = Schema.decodeUnknownSync(DeploymentAllowlistTopologySchema)( + JSON.parse(fs.readFileSync(new URL('../../../../topology/reference-topology.json', import.meta.url), 'utf-8')), + ); + const expectedInstalledIds = yield* deriveInstalledVerticalIds(topology); - expect([...expectedInstalledIds]).toEqual(['party-registry']); - expect(expectedInstalledIds.has('party.registry')).toBe(false); - expect([...(yield* installedVerticalIds)]).toEqual([ - ...expectedInstalledIds, - ]); - const valid = yield* deriveInstalledVerticalIds({ - sharedPackages: [{ id: 'shared-contracts', kind: 'package' }], - shell: { id: 'shell-super-app', kind: 'shell' }, - verticals: [ - { id: 'property-registry', kind: 'vertical' }, - { id: 'future-generated', kind: 'vertical' }, - ], - }); - expect([...valid]).toEqual(['property-registry', 'future-generated']); - expect(valid.has('property.registry')).toBe(false); - }) + expect([...expectedInstalledIds]).toEqual(['party-registry']); + expect(expectedInstalledIds.has('party.registry')).toBe(false); + expect([...(yield* installedVerticalIds)]).toEqual([...expectedInstalledIds]); + const valid = yield* deriveInstalledVerticalIds({ + sharedPackages: [{ id: 'shared-contracts', kind: 'package' }], + shell: { id: 'shell-super-app', kind: 'shell' }, + verticals: [ + { id: 'property-registry', kind: 'vertical' }, + { id: 'future-generated', kind: 'vertical' }, + ], + }); + expect([...valid]).toEqual(['property-registry', 'future-generated']); + expect(valid.has('property.registry')).toBe(false); + }), ); -it.effect( - 'rejects malformed, non-vertical, invalid, and duplicate installed entries', - () => - Effect.gen(function* verifyCase2() { - const inputs = [ - {}, - { verticals: [{ id: 'shell-super-app', kind: 'shell' }] }, - { verticals: [{ id: '../inventory', kind: 'vertical' }] }, - { - verticals: [ - { id: 'inventory-stock', kind: 'vertical' }, - { id: 'inventory-stock', kind: 'vertical' }, - ], - }, - ]; - const errors = yield* Effect.all( - inputs.map((input) => - Effect.gen(function* verifyCase3() { - return yield* Effect.flip(deriveInstalledVerticalIds(input)); - }) - ), - { concurrency: 'unbounded' } - ); - expect(errors.every(Schema.is(InstalledVerticalTopologyError))).toBe( - true - ); - }) +it.effect('rejects malformed, non-vertical, invalid, and duplicate installed entries', () => + Effect.gen(function* verifyCase2() { + const inputs = [ + {}, + { verticals: [{ id: 'shell-super-app', kind: 'shell' }] }, + { verticals: [{ id: '../inventory', kind: 'vertical' }] }, + { + verticals: [ + { id: 'inventory-stock', kind: 'vertical' }, + { id: 'inventory-stock', kind: 'vertical' }, + ], + }, + ]; + const errors = yield* Effect.all( + inputs.map((input) => + Effect.gen(function* verifyCase3() { + return yield* Effect.flip(deriveInstalledVerticalIds(input)); + }), + ), + { concurrency: 'unbounded' }, + ); + expect(errors.every(Schema.is(InstalledVerticalTopologyError))).toBe(true); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/layout.test.tsx b/app/apps/shell-super-app/tests/unit/layout.test.tsx index 52d0c7b78..8652cca56 100644 --- a/app/apps/shell-super-app/tests/unit/layout.test.tsx +++ b/app/apps/shell-super-app/tests/unit/layout.test.tsx @@ -14,18 +14,14 @@ import { AppIdSchema } from '../../shared/api'; import Layout from '../../src/routes/layout'; import { AuthenticatedDashboardLayout } from '../../src/routes/shell-frame'; -const { accountMenuSelectHandlers, tenantValueChangeHandlers } = rstest.hoisted( - () => { - const accountHandlers: ComponentProps['onSelect'][] = []; - const tenantHandlers: ComponentProps< - typeof ActualSelect - >['onValueChange'][] = []; - return { - accountMenuSelectHandlers: accountHandlers, - tenantValueChangeHandlers: tenantHandlers, - }; - } -); +const { accountMenuSelectHandlers, tenantValueChangeHandlers } = rstest.hoisted(() => { + const accountHandlers: ComponentProps['onSelect'][] = []; + const tenantHandlers: ComponentProps['onValueChange'][] = []; + return { + accountMenuSelectHandlers: accountHandlers, + tenantValueChangeHandlers: tenantHandlers, + }; +}); rstest.mock('@modern-js/plugin-tanstack/runtime', () => ({ Outlet: () =>
Current route
, @@ -53,22 +49,18 @@ rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ 'shell.dashboard.brand': 'OntOS', 'shell.dashboard.header.label': 'Dashboard header', 'shell.dashboard.legalEntity.accessibleLabel': 'Current legal entity', - 'shell.dashboard.legalEntity.failed': - 'Legal entity switching failed. Try again.', + 'shell.dashboard.legalEntity.failed': 'Legal entity switching failed. Try again.', 'shell.dashboard.legalEntity.pending': 'Switching legal entity…', 'shell.dashboard.legalEntity.placeholder': 'Select a legal entity', - 'shell.dashboard.legalEntity.unavailable': - 'Legal entity choices are temporarily unavailable.', + 'shell.dashboard.legalEntity.unavailable': 'Legal entity choices are temporarily unavailable.', 'shell.dashboard.navigation.home': 'Home', 'shell.dashboard.navigation.label': 'Dashboard navigation', 'shell.dashboard.sidebar.label': 'Dashboard sidebar', 'shell.dashboard.tenant.accessibleLabel': 'Current tenant', 'shell.dashboard.tenant.failed': 'Tenant switching failed. Try again.', 'shell.dashboard.tenant.pending': 'Switching tenant…', - 'shell.dashboard.tenant.unavailable': - 'Tenant choices are temporarily unavailable.', - 'shell.modules.discovery.incompatible': - 'Incompatible module deployment', + 'shell.dashboard.tenant.unavailable': 'Tenant choices are temporarily unavailable.', + 'shell.modules.discovery.incompatible': 'Incompatible module deployment', 'shell.modules.discovery.revoked': 'Module revoked', 'shell.modules.discovery.timeout': 'Module deployment timed out', 'shell.modules.discovery.unavailable': 'Module deployment unavailable', @@ -129,9 +121,7 @@ const unavailableDeploymentAppIds = { const tenantProps = { currentLegalEntityId: 'legal-entity-1', currentTenantId: 'tenant-1', - legalEntityChoices: [ - { legalEntityId: 'legal-entity-1', legalName: 'Alpha entity' }, - ], + legalEntityChoices: [{ legalEntityId: 'legal-entity-1', legalName: 'Alpha entity' }], legalEntityState: 'available' as const, legalEntitySwitchFailed: false, legalEntitySwitchPending: false, @@ -174,16 +164,12 @@ test('renders the default Home dashboard contract and preserves page children', title={homeOverviewTitle} >
Page-specific content
- + , ); - expect( - screen.getByRole('complementary', { name: 'Dashboard sidebar' }) - ).toBeTruthy(); + expect(screen.getByRole('complementary', { name: 'Dashboard sidebar' })).toBeTruthy(); expect(screen.getByText('OntOS')).toBeTruthy(); - expect( - screen.getByRole('heading', { level: 1, name: 'Home overview' }) - ).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1, name: 'Home overview' })).toBeTruthy(); expect(screen.getByText('Page-specific content')).toBeTruthy(); const tenantSelect = screen.getByRole('combobox', { name: 'Current tenant' }); @@ -194,11 +180,7 @@ test('renders the default Home dashboard contract and preserves page children', name: 'Dashboard navigation', }); const links = [...navigationElement.querySelectorAll('a')]; - expect(links.map((link) => link.textContent)).toEqual([ - 'Home', - 'Future generated', - 'Testing one', - ]); + expect(links.map((link) => link.textContent)).toEqual(['Home', 'Future generated', 'Testing one']); expect(links.map((link) => link.getAttribute('href'))).toEqual([ '/en/', '/en/modules/future-generated', @@ -221,21 +203,13 @@ test('supports an alternate title and current MicroVertical without changing chi title={testingWorkspaceTitle} >

Stable child content

- + , ); - expect( - screen.getByRole('heading', { level: 1, name: 'Testing workspace' }) - ).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1, name: 'Testing workspace' })).toBeTruthy(); expect(screen.getByText('Stable child content')).toBeTruthy(); - expect( - screen.getByRole('link', { name: 'Home' }).hasAttribute('aria-current') - ).toBe(false); - expect( - screen - .getByRole('link', { name: 'Testing one' }) - .getAttribute('aria-current') - ).toBe('page'); + expect(screen.getByRole('link', { name: 'Home' }).hasAttribute('aria-current')).toBe(false); + expect(screen.getByRole('link', { name: 'Testing one' }).getAttribute('aria-current')).toBe('page'); }); test('supports module pages without a shell heading and keeps reduced horizontal content padding', () => { @@ -249,7 +223,7 @@ test('supports module pages without a shell heading and keeps reduced horizontal onLogout={noopLogout} >
Module content
- + , ); expect(screen.queryByRole('heading')).toBeNull(); @@ -269,14 +243,10 @@ test('keeps Home as the only navigation link when no active modules are supplied title={homeTitle} > Content - + , ); - expect( - screen - .getByRole('navigation', { name: 'Dashboard navigation' }) - .querySelectorAll('a') - ).toHaveLength(1); + expect(screen.getByRole('navigation', { name: 'Dashboard navigation' }).querySelectorAll('a')).toHaveLength(1); }); test('shows failed installed deployments as disabled identities with typed reasons', () => { @@ -303,7 +273,7 @@ test('shows failed installed deployments as disabled identities with typed reaso ]} > Content - + , ); expect(screen.getByText('property-registry')).toBeTruthy(); @@ -317,127 +287,103 @@ test('shows failed installed deployments as disabled identities with typed reaso expect(screen.queryByRole('link', { name: 'legacy-center' })).toBeNull(); }); -it.effect( - 'renders the account Menu last and dispatches only the logout command by keyboard', - () => - Effect.gen(function* accountKeyboardLogout() { - const onLogout = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - - ); - - const header = document.querySelector( - 'header[aria-label="Dashboard header"]' - ); - const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); - expect(header?.lastElementChild?.contains(trigger)).toBe(true); - const accountMenu = header?.querySelector( - ':scope > :last-child' - ); - expect(accountMenu?.dataset['position']).toBe('end'); - - trigger.focus(); - yield* Effect.promise(() => user.keyboard('{Enter}')); - const commands = yield* Effect.promise(() => - screen.findAllByRole('menuitem') - ); - expect(commands).toHaveLength(1); - expect(commands[0]?.textContent).toBe('Logout'); - yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); - expect(onLogout).toHaveBeenCalledTimes(1); - - accountMenuSelectHandlers.at(-1)?.({ value: 'unexpected' }); - expect(onLogout).toHaveBeenCalledTimes(1); - }) +it.effect('renders the account Menu last and dispatches only the logout command by keyboard', () => + Effect.gen(function* accountKeyboardLogout() { + const onLogout = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); + + const header = document.querySelector('header[aria-label="Dashboard header"]'); + const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); + expect(header?.lastElementChild?.contains(trigger)).toBe(true); + const accountMenu = header?.querySelector(':scope > :last-child'); + expect(accountMenu?.dataset['position']).toBe('end'); + + trigger.focus(); + yield* Effect.promise(() => user.keyboard('{Enter}')); + const commands = yield* Effect.promise(() => screen.findAllByRole('menuitem')); + expect(commands).toHaveLength(1); + expect(commands[0]?.textContent).toBe('Logout'); + yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); + expect(onLogout).toHaveBeenCalledTimes(1); + + accountMenuSelectHandlers.at(-1)?.({ value: 'unexpected' }); + expect(onLogout).toHaveBeenCalledTimes(1); + }), ); -it.effect( - 'retains the account trigger and disables the sole command while logout is pending', - () => - Effect.gen(function* pendingLogoutCommand() { - const onLogout = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - - ); - - const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); - yield* Effect.promise(() => user.click(trigger)); - const command = yield* Effect.promise(() => - screen.findByRole('menuitem', { name: 'Logging out…' }) - ); - expect(command.getAttribute('aria-disabled')).toBe('true'); - yield* Effect.promise(() => user.click(command)); - expect(onLogout).not.toHaveBeenCalled(); - }) +it.effect('retains the account trigger and disables the sole command while logout is pending', () => + Effect.gen(function* pendingLogoutCommand() { + const onLogout = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); + + const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); + yield* Effect.promise(() => user.click(trigger)); + const command = yield* Effect.promise(() => screen.findByRole('menuitem', { name: 'Logging out…' })); + expect(command.getAttribute('aria-disabled')).toBe('true'); + yield* Effect.promise(() => user.click(command)); + expect(onLogout).not.toHaveBeenCalled(); + }), ); -it.effect( - 'renders complete ordered tenant items and dispatches keyboard selection once', - () => - Effect.gen(function* tenantKeyboardSelection() { - const onTenantChange = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - - ); - - const trigger = screen.getByRole('combobox', { name: 'Current tenant' }); - yield* Effect.promise(() => user.click(trigger)); - const options = yield* Effect.promise(() => - screen.findAllByRole('option') - ); - expect(options.map((option) => option.textContent)).toEqual([ - 'Alpha tenant', - 'Zeta tenant', - ]); - expect(options.map((option) => option.dataset['value'])).toEqual([ - 'tenant-1', - 'tenant-2', - ]); - expect( - options.every((option) => option.querySelector('span') !== null) - ).toBe(true); - yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); - expect(onTenantChange).toHaveBeenCalledWith('tenant-2'); - expect(onTenantChange).toHaveBeenCalledTimes(1); - - tenantValueChangeHandlers.at(-1)?.({ - items: [tenantProps.tenantChoices[0]], - value: ['tenant-1'], - }); - tenantValueChangeHandlers.at(-1)?.({ items: [], value: [] }); - expect(onTenantChange).toHaveBeenCalledTimes(1); - }) +it.effect('renders complete ordered tenant items and dispatches keyboard selection once', () => + Effect.gen(function* tenantKeyboardSelection() { + const onTenantChange = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); + + const trigger = screen.getByRole('combobox', { name: 'Current tenant' }); + yield* Effect.promise(() => user.click(trigger)); + const options = yield* Effect.promise(() => screen.findAllByRole('option')); + expect(options.map((option) => option.textContent)).toEqual(['Alpha tenant', 'Zeta tenant']); + expect(options.map((option) => option.dataset['value'])).toEqual(['tenant-1', 'tenant-2']); + expect(options.every((option) => option.querySelector('span') !== null)).toBe(true); + yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); + expect(onTenantChange).toHaveBeenCalledWith('tenant-2'); + expect(onTenantChange).toHaveBeenCalledTimes(1); + + tenantValueChangeHandlers.at(-1)?.({ + items: [tenantProps.tenantChoices[0]], + value: ['tenant-1'], + }); + tenantValueChangeHandlers.at(-1)?.({ items: [], value: [] }); + expect(onTenantChange).toHaveBeenCalledTimes(1); + }), ); test('disables unavailable, one-choice, and pending tenant states with associated feedback', () => { @@ -452,13 +398,9 @@ test('disables unavailable, one-choice, and pending tenant states with associate title={homeTitle} > Content - + , ); - expect( - screen - .getByRole('combobox', { name: 'Current tenant' }) - .hasAttribute('disabled') - ).toBe(true); + expect(screen.getByRole('combobox', { name: 'Current tenant' }).hasAttribute('disabled')).toBe(true); rerender( Content - + , ); - expect( - screen - .getByRole('combobox', { name: 'Current tenant' }) - .hasAttribute('disabled') - ).toBe(true); + expect(screen.getByRole('combobox', { name: 'Current tenant' }).hasAttribute('disabled')).toBe(true); rerender( Content - + , ); const unavailable = screen.getByRole('combobox', { name: 'Current tenant' }); expect(unavailable.hasAttribute('disabled')).toBe(true); - expect(unavailable.getAttribute('aria-describedby')).toBe( - 'tenant-switch-status' - ); - expect( - screen.getByText('Tenant choices are temporarily unavailable.') - ).toBeTruthy(); + expect(unavailable.getAttribute('aria-describedby')).toBe('tenant-switch-status'); + expect(screen.getByText('Tenant choices are temporarily unavailable.')).toBeTruthy(); rerender( Content - + , ); - expect( - screen - .getByRole('combobox', { name: 'Current tenant' }) - .hasAttribute('disabled') - ).toBe(true); + expect(screen.getByRole('combobox', { name: 'Current tenant' }).hasAttribute('disabled')).toBe(true); expect(screen.getByText('Switching tenant…')).toBeTruthy(); }); @@ -534,7 +464,7 @@ test('associates failed tenant feedback and keeps multiple choices operable', () title={homeTitle} > Content - + , ); const trigger = screen.getByRole('combobox', { name: 'Current tenant' }); expect(trigger.hasAttribute('disabled')).toBe(false); @@ -554,7 +484,7 @@ test('names the legal-entity selector by its own label and keeps a sole choice o title={homeTitle} > Content - + , ); const legalEntity = screen.getByRole('combobox', { @@ -563,17 +493,10 @@ test('names the legal-entity selector by its own label and keeps a sole choice o expect(legalEntity.hasAttribute('aria-label')).toBe(false); expect(legalEntity.hasAttribute('aria-describedby')).toBe(false); expect(legalEntity.hasAttribute('disabled')).toBe(false); - expect( - screen - .getByRole('combobox', { name: 'Current tenant' }) - .getAttribute('aria-label') - ).toBe('Current tenant'); + expect(screen.getByRole('combobox', { name: 'Current tenant' }).getAttribute('aria-label')).toBe('Current tenant'); expect(screen.queryByText('Select a legal entity')).toBeNull(); - const { - currentLegalEntityId: _selectedLegalEntityId, - ...unselectedLegalEntityProps - } = tenantProps; + const { currentLegalEntityId: _selectedLegalEntityId, ...unselectedLegalEntityProps } = tenantProps; rerender( Content - + , ); expect(screen.getByText('Select a legal entity')).toBeTruthy(); @@ -596,9 +519,7 @@ interface LegalEntitySelectorStateCase { readonly overrides: Partial< Pick< ComponentProps, - | 'legalEntityState' - | 'legalEntitySwitchFailed' - | 'legalEntitySwitchPending' + 'legalEntityState' | 'legalEntitySwitchFailed' | 'legalEntitySwitchPending' > >; readonly statusText: string; @@ -639,21 +560,15 @@ test.each(legalEntitySelectorStateCases)( title={homeTitle} > Content - + , ); const legalEntity = screen.getByRole('combobox', { name: 'Current legal entity', }); expect(legalEntity.hasAttribute('disabled')).toBe(disabled); - expect(legalEntity.getAttribute('aria-describedby')).toBe( - 'legal-entity-switch-status' - ); + expect(legalEntity.getAttribute('aria-describedby')).toBe('legal-entity-switch-status'); expect(screen.getByText(statusText)).toBeTruthy(); - expect( - screen - .getByRole('combobox', { name: 'Current tenant' }) - .getAttribute('aria-describedby') - ).toBeNull(); - } + expect(screen.getByRole('combobox', { name: 'Current tenant' }).getAttribute('aria-describedby')).toBeNull(); + }, ); diff --git a/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts b/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts index 1462921a1..2217d577c 100644 --- a/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts +++ b/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts @@ -1,8 +1,5 @@ import { ContextAccess, LegalEntityContext } from '@app/core-runtime'; -import type { - ContextAccessService, - LegalEntityContextService, -} from '@app/core-runtime'; +import type { ContextAccessService, LegalEntityContextService } from '@app/core-runtime'; import { Effect, Layer, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; @@ -22,137 +19,111 @@ const beta = { legalName: 'Beta', }; -const context = ( - entities = [alpha, beta] as const -): LegalEntityContextService => ({ +const context = (entities = [alpha, beta] as const): LegalEntityContextService => ({ listActiveForTenant: () => Effect.succeed(entities), validateSelection: (_tenantId, legalEntityId) => { - const selected = entities.find( - (entity) => entity.legalEntityId === legalEntityId - ); - return selected === undefined - ? Effect.die('missing fixture entity') - : Effect.succeed(selected); + const selected = entities.find((entity) => entity.legalEntityId === legalEntityId); + return selected === undefined ? Effect.die('missing fixture entity') : Effect.succeed(selected); }, }); -const access = ( - decisions: Readonly> -): ContextAccessService => ({ +const access = (decisions: Readonly>): ContextAccessService => ({ legalEntities: ({ legalEntityIds }) => Effect.succeed( legalEntityIds.map((key) => ({ decision: decisions[key] ?? 'denied', key, - })) - ), - modules: ({ moduleIds }) => - Effect.succeed( - moduleIds.map((key) => ({ decision: 'denied' as const, key })) + })), ), + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision: 'denied' as const, key }))), resources: () => Effect.succeed([]), - tenants: ({ tenantIds }) => - Effect.succeed( - tenantIds.map((key) => ({ decision: 'denied' as const, key })) - ), + tenants: ({ tenantIds }) => Effect.succeed(tenantIds.map((key) => ({ decision: 'denied' as const, key }))), }); const provideSelectionServices = ( effect: Effect.Effect, legalEntityContext: LegalEntityContextService, - contextAccess: ContextAccessService + contextAccess: ContextAccessService, ): Effect.Effect => effect.pipe( Effect.provide( Layer.mergeAll( Layer.succeed(LegalEntityContext, legalEntityContext), - Layer.succeed(ContextAccess, contextAccess) - ) - ) + Layer.succeed(ContextAccess, contextAccess), + ), + ), ); -it.effect( - 'auto-selects the only authorized entity and preserves an exact saved choice', - () => - Effect.gen(function* verifyCase1() { - const only = yield* provideSelectionServices( +it.effect('auto-selects the only authorized entity and preserves an exact saved choice', () => + Effect.gen(function* verifyCase1() { + const only = yield* provideSelectionServices( + resolveAuthorizedLegalEntities({ principalId, tenantId }), + context(), + access({ [alpha.legalEntityId]: 'allowed' }), + ); + expect(only).toEqual({ + available: [alpha], + selected: alpha, + state: 'selected', + }); + const saved = yield* provideSelectionServices( + resolveAuthorizedLegalEntities({ + principalId, + savedLegalEntityId: beta.legalEntityId, + tenantId, + }), + context(), + access({ + [alpha.legalEntityId]: 'allowed', + [beta.legalEntityId]: 'allowed', + }), + ); + expect(saved).toEqual({ + available: [alpha, beta], + selected: beta, + state: 'selected', + }); + }), +); + +it.effect('requires a choice for several entities and blocks zero definite grants', () => + Effect.gen(function* verifyCase2() { + expect( + yield* provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), - access({ [alpha.legalEntityId]: 'allowed' }) - ); - expect(only).toEqual({ - available: [alpha], - selected: alpha, - state: 'selected', - }); - const saved = yield* provideSelectionServices( - resolveAuthorizedLegalEntities({ - principalId, - savedLegalEntityId: beta.legalEntityId, - tenantId, - }), - context(), access({ [alpha.legalEntityId]: 'allowed', [beta.legalEntityId]: 'allowed', - }) - ); - expect(saved).toEqual({ - available: [alpha, beta], - selected: beta, - state: 'selected', - }); - }) -); - -it.effect( - 'requires a choice for several entities and blocks zero definite grants', - () => - Effect.gen(function* verifyCase2() { - expect( - yield* provideSelectionServices( - resolveAuthorizedLegalEntities({ principalId, tenantId }), - context(), - access({ - [alpha.legalEntityId]: 'allowed', - [beta.legalEntityId]: 'allowed', - }) - ) - ).toEqual({ available: [alpha, beta], state: 'selection_required' }); - expect( - yield* provideSelectionServices( - resolveAuthorizedLegalEntities({ principalId, tenantId }), - context(), - access({}) - ) - ).toEqual({ available: [], state: 'access_blocked' }); - }) + }), + ), + ).toEqual({ available: [alpha, beta], state: 'selection_required' }); + expect( + yield* provideSelectionServices(resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), access({})), + ).toEqual({ available: [], state: 'access_blocked' }); + }), ); -it.effect( - 'fails closed for authorization uncertainty and validates a switch independently', - () => - Effect.gen(function* verifyCase3() { - const unavailable = yield* Effect.flip( - provideSelectionServices( - resolveAuthorizedLegalEntities({ principalId, tenantId }), - context(), - access({ [alpha.legalEntityId]: 'unavailable' }) - ) - ); - expect( - Predicate.isTagged(unavailable, 'LegalEntitySelectionUnavailableError') - ).toBe(true); - expect( - yield* provideSelectionServices( - validateAuthorizedLegalEntity({ - legalEntityId: beta.legalEntityId, - principalId, - tenantId, - }), - context(), - access({ [beta.legalEntityId]: 'allowed' }) - ) - ).toEqual(beta); - }) +it.effect('fails closed for authorization uncertainty and validates a switch independently', () => + Effect.gen(function* verifyCase3() { + const unavailable = yield* Effect.flip( + provideSelectionServices( + resolveAuthorizedLegalEntities({ principalId, tenantId }), + context(), + access({ [alpha.legalEntityId]: 'unavailable' }), + ), + ); + expect(Predicate.isTagged(unavailable, 'LegalEntitySelectionUnavailableError')).toBe(true); + expect( + yield* provideSelectionServices( + validateAuthorizedLegalEntity({ + legalEntityId: beta.legalEntityId, + principalId, + tenantId, + }), + context(), + access({ [beta.legalEntityId]: 'allowed' }), + ), + ).toEqual(beta); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts b/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts index f404103fc..90b258cb6 100644 --- a/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts @@ -11,15 +11,7 @@ import type { ModuleStateSnapshot, TrustedPrincipalContext, } from '@app/core-runtime'; -import { - Clock, - Effect, - Fiber, - Function as Fn, - Match, - Predicate, - Schema, -} from 'effect'; +import { Clock, Effect, Fiber, Function as Fn, Match, Predicate, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { TestClock } from 'effect/testing'; @@ -41,57 +33,42 @@ const trustedContext: TrustedPrincipalContext = { interface FakeGatewayOptions { readonly deniedEntrypointKeys?: ReadonlySet; - readonly onPrepare?: ( - entrypoints: readonly ModuleEntrypointDescriptor[] - ) => void; + readonly onPrepare?: (entrypoints: readonly ModuleEntrypointDescriptor[]) => void; readonly unavailable?: boolean; } -const makeFakeGateway = ( - options: FakeGatewayOptions = {} -): ModuleEntrypointGatewayService => { - const check: ModuleEntrypointGatewayService['check'] = ( - snapshot, - entrypoint - ) => { +const makeFakeGateway = (options: FakeGatewayOptions = {}): ModuleEntrypointGatewayService => { + const check: ModuleEntrypointGatewayService['check'] = (snapshot, entrypoint) => { if (!snapshot.entrypointKeys.includes(entrypoint.entrypointKey)) { return Effect.fail( new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', reason: 'Module state could not be checked safely', - }) + }), ); } return options.deniedEntrypointKeys?.has(entrypoint.entrypointKey) === true ? Effect.fail( new ModuleStateDeniedError({ code: 'module_state_denied', - reason: - 'The module entrypoint is unavailable in the current module state', - }) + reason: 'The module entrypoint is unavailable in the current module state', + }), ) : Effect.void; }; - const prepareSnapshot: ModuleEntrypointGatewayService['prepareSnapshot'] = ( - context, - entrypoints - ) => { + const prepareSnapshot: ModuleEntrypointGatewayService['prepareSnapshot'] = (context, entrypoints) => { options.onPrepare?.(entrypoints); if (options.unavailable === true || context.tenantId.length === 0) { return Effect.fail( new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', reason: 'Module state could not be checked safely', - }) + }), ); } const snapshot: ModuleStateSnapshot = Object.freeze({ - entrypointKeys: Object.freeze( - entrypoints.map(({ entrypointKey }) => entrypointKey) - ), - moduleKeys: Object.freeze( - [...new Set(entrypoints.map(({ moduleKey }) => moduleKey))].toSorted() - ), + entrypointKeys: Object.freeze(entrypoints.map(({ entrypointKey }) => entrypointKey)), + moduleKeys: Object.freeze([...new Set(entrypoints.map(({ moduleKey }) => moduleKey))].toSorted()), tenantId: context.tenantId, }); return Effect.succeed(snapshot); @@ -106,15 +83,12 @@ const makeFakeGateway = ( new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', reason: 'Module state could not be checked safely', - }) + }), ), - Effect.flatMap((trusted) => prepareSnapshot(trusted, entrypoints)) + Effect.flatMap((trusted) => prepareSnapshot(trusted, entrypoints)), ), run: (input) => - check(input.snapshot, input.entrypoint).pipe( - Effect.andThen(input.authorize), - Effect.andThen(input.load) - ), + check(input.snapshot, input.entrypoint).pipe(Effect.andThen(input.authorize), Effect.andThen(input.load)), }; return gateway; }; @@ -134,323 +108,286 @@ const component = defineTenantModuleEntrypoint({ role: 'public_component', }); -const compatibleRemoteModule = (value: { readonly default: unknown }) => - Predicate.isFunction(value.default); - -it.effect( - 'prepares one complete trusted composition and invokes allowed lazy loaders', - () => - Effect.gen(function* verifyCompleteComposition() { - let batches = 0; - let loads = 0; - const gateway = makeFakeGateway({ - onPrepare: (entrypoints) => { - batches += 1; - expect(entrypoints).toEqual([page, component]); - }, - }); - const result = yield* loadModuleEntrypointComposition( +const compatibleRemoteModule = (value: { readonly default: unknown }) => Predicate.isFunction(value.default); + +it.effect('prepares one complete trusted composition and invokes allowed lazy loaders', () => + Effect.gen(function* verifyCompleteComposition() { + let batches = 0; + let loads = 0; + const gateway = makeFakeGateway({ + onPrepare: (entrypoints) => { + batches += 1; + expect(entrypoints).toEqual([page, component]); + }, + }); + const result = yield* loadModuleEntrypointComposition( + gateway, + trustedContext, + [page, component].map((entrypoint) => ({ + authorize: Effect.void, + entrypoint, + load: Effect.sync(() => { + loads += 1; + return `loaded-${loads}`; + }), + })), + ); + expect(result).toEqual(['loaded-1', 'loaded-2']); + expect(batches).toBe(1); + }), +); + +it.effect('checks the complete composition before authorizing or invoking any loader', () => + Effect.gen(function* verifyDeniedComposition() { + let authorizations = 0; + let loads = 0; + const gateway = makeFakeGateway({ + deniedEntrypointKeys: new Set([component.entrypointKey]), + }); + const error = yield* Effect.flip( + loadModuleEntrypointComposition( gateway, trustedContext, [page, component].map((entrypoint) => ({ - authorize: Effect.void, + authorize: Effect.sync(() => { + authorizations += 1; + }), entrypoint, load: Effect.sync(() => { loads += 1; - return `loaded-${loads}`; + return loads; }), - })) - ); - expect(result).toEqual(['loaded-1', 'loaded-2']); - expect(batches).toBe(1); - }) -); - -it.effect( - 'checks the complete composition before authorizing or invoking any loader', - () => - Effect.gen(function* verifyDeniedComposition() { - let authorizations = 0; - let loads = 0; - const gateway = makeFakeGateway({ - deniedEntrypointKeys: new Set([component.entrypointKey]), - }); - const error = yield* Effect.flip( - loadModuleEntrypointComposition( - gateway, - trustedContext, - [page, component].map((entrypoint) => ({ - authorize: Effect.sync(() => { - authorizations += 1; - }), - entrypoint, - load: Effect.sync(() => { - loads += 1; - return loads; - }), - })) - ) - ); - expect(Schema.is(ModuleStateDeniedError)(error)).toBe(true); - expect(authorizations).toBe(0); - expect(loads).toBe(0); - }) + })), + ), + ); + expect(Schema.is(ModuleStateDeniedError)(error)).toBe(true); + expect(authorizations).toBe(0); + expect(loads).toBe(0); + }), ); -class RemoteLoadUnavailable extends Schema.TaggedError()( - 'RemoteLoadUnavailable', - {} -) {} -const FakeUnavailableUiStateSchema = Schema.Literals([ - 'forbidden', - 'unavailable', -]); +class RemoteLoadUnavailable extends Schema.TaggedError()('RemoteLoadUnavailable', {}) {} +const FakeUnavailableUiStateSchema = Schema.Literals(['forbidden', 'unavailable']); type FakeUnavailableUiState = typeof FakeUnavailableUiStateSchema.Type; -const mapFakeUnavailableUiState = ( - error: ModuleStateGateError | RemoteLoadUnavailable -): FakeUnavailableUiState => +const mapFakeUnavailableUiState = (error: ModuleStateGateError | RemoteLoadUnavailable): FakeUnavailableUiState => Match.value(error).pipe( Match.tag('ModuleStateDeniedError', () => 'forbidden' as const), - Match.tag( - 'ModuleStateCheckUnavailableError', - 'RemoteLoadUnavailable', - () => 'unavailable' as const - ), - Match.exhaustive + Match.tag('ModuleStateCheckUnavailableError', 'RemoteLoadUnavailable', () => 'unavailable' as const), + Match.exhaustive, ); -it.effect( - 'preserves typed gate and remote-load failures for exhaustive UI mapping', - () => - Effect.gen(function* verifyTypedFailures() { - const gateFailure = yield* Effect.flip( - loadModuleEntrypointComposition( - makeFakeGateway({ unavailable: true }), - trustedContext, - [ - { - authorize: Effect.void, - entrypoint: page, - load: Effect.succeed('unreachable'), - }, - ] - ) - ); - expect(mapFakeUnavailableUiState(gateFailure)).toBe('unavailable'); - - const remoteFailure = yield* Effect.flip( - loadModuleEntrypointComposition(makeFakeGateway(), trustedContext, [ - { - authorize: Effect.void, - entrypoint: page, - load: Effect.fail(new RemoteLoadUnavailable()), - }, - ]) - ); - expect(remoteFailure).toEqual(new RemoteLoadUnavailable()); - expect(mapFakeUnavailableUiState(remoteFailure)).toBe('unavailable'); - }) +it.effect('preserves typed gate and remote-load failures for exhaustive UI mapping', () => + Effect.gen(function* verifyTypedFailures() { + const gateFailure = yield* Effect.flip( + loadModuleEntrypointComposition(makeFakeGateway({ unavailable: true }), trustedContext, [ + { + authorize: Effect.void, + entrypoint: page, + load: Effect.succeed('unreachable'), + }, + ]), + ); + expect(mapFakeUnavailableUiState(gateFailure)).toBe('unavailable'); + + const remoteFailure = yield* Effect.flip( + loadModuleEntrypointComposition(makeFakeGateway(), trustedContext, [ + { + authorize: Effect.void, + entrypoint: page, + load: Effect.fail(new RemoteLoadUnavailable()), + }, + ]), + ); + expect(remoteFailure).toEqual(new RemoteLoadUnavailable()); + expect(mapFakeUnavailableUiState(remoteFailure)).toBe('unavailable'); + }), ); -it.live( - 'settles browser entrypoint success, rejection, incompatibility, and timeout independently', - () => - Effect.gen(function* verifySettledLoads() { - const pending = Promise.withResolvers<{ readonly default: () => null }>(); - const [ready, unavailable, incompatible, timedOut] = yield* Effect.all( - [ - settleModuleEntrypointLoad( - Fn.constant(Promise.resolve({ default: remoteDefault })), - compatibleRemoteModule, - 50 - ), - settleModuleEntrypointLoad( - Fn.constant(Promise.reject(new Error('remote unavailable'))), - compatibleRemoteModule, - 50 - ), - settleModuleEntrypointLoad( - Fn.constant(Promise.resolve({ default: 'not a component' })), - compatibleRemoteModule, - 50 - ), - settleModuleEntrypointLoad( - Fn.constant(pending.promise), - compatibleRemoteModule, - 1 - ), - ], - { concurrency: 'unbounded' } - ); +it.live('settles browser entrypoint success, rejection, incompatibility, and timeout independently', () => + Effect.gen(function* verifySettledLoads() { + const pending = Promise.withResolvers<{ readonly default: () => null }>(); + const [ready, unavailable, incompatible, timedOut] = yield* Effect.all( + [ + settleModuleEntrypointLoad( + Fn.constant(Promise.resolve({ default: remoteDefault })), + compatibleRemoteModule, + 50, + ), + settleModuleEntrypointLoad( + Fn.constant(Promise.reject(new Error('remote unavailable'))), + compatibleRemoteModule, + 50, + ), + settleModuleEntrypointLoad( + Fn.constant(Promise.resolve({ default: 'not a component' })), + compatibleRemoteModule, + 50, + ), + settleModuleEntrypointLoad(Fn.constant(pending.promise), compatibleRemoteModule, 1), + ], + { concurrency: 'unbounded' }, + ); - expect(ready.state).toBe('ready'); - expect(unavailable).toEqual({ - reason: 'unavailable', - state: 'unavailable', - }); - expect(incompatible).toEqual({ - reason: 'incompatible', - state: 'unavailable', - }); - expect(timedOut).toEqual({ reason: 'timeout', state: 'unavailable' }); - }) + expect(ready.state).toBe('ready'); + expect(unavailable).toEqual({ + reason: 'unavailable', + state: 'unavailable', + }); + expect(incompatible).toEqual({ + reason: 'incompatible', + state: 'unavailable', + }); + expect(timedOut).toEqual({ reason: 'timeout', state: 'unavailable' }); + }), ); -it.effect( - 'settles several browser entrypoints without one failure hiding healthy loads', - () => - Effect.gen(function* verifySettledLoadCollection() { - const results = yield* settleModuleEntrypointLoads([ - { - identity: 'documents-center/page', - isCompatible: compatibleRemoteModule, - load: Fn.constant(Promise.resolve({ default: remoteDefault })), - timeoutMs: 50, - }, - { - identity: 'property-registry/page', - isCompatible: compatibleRemoteModule, +it.effect('settles several browser entrypoints without one failure hiding healthy loads', () => + Effect.gen(function* verifySettledLoadCollection() { + const results = yield* settleModuleEntrypointLoads([ + { + identity: 'documents-center/page', + isCompatible: compatibleRemoteModule, + load: Fn.constant(Promise.resolve({ default: remoteDefault })), + timeoutMs: 50, + }, + { + identity: 'property-registry/page', + isCompatible: compatibleRemoteModule, - load: () => Promise.reject(new Error('remote unavailable')), - timeoutMs: 50, - }, - { - identity: 'throwing-validator/page', - isCompatible: () => { - throw new TypeError('malformed runtime value'); - }, - load: Fn.constant(Promise.resolve({ default: remoteDefault })), - timeoutMs: 50, + load: () => Promise.reject(new Error('remote unavailable')), + timeoutMs: 50, + }, + { + identity: 'throwing-validator/page', + isCompatible: () => { + throw new TypeError('malformed runtime value'); }, - ]); + load: Fn.constant(Promise.resolve({ default: remoteDefault })), + timeoutMs: 50, + }, + ]); - expect(results).toEqual([ - { - identity: 'documents-center/page', - state: 'ready', - value: expect.objectContaining({ default: expect.any(Function) }), - }, - { - identity: 'property-registry/page', - reason: 'unavailable', - state: 'unavailable', - }, - { - identity: 'throwing-validator/page', - reason: 'incompatible', - state: 'unavailable', - }, - ]); - }) + expect(results).toEqual([ + { + identity: 'documents-center/page', + state: 'ready', + value: expect.objectContaining({ default: expect.any(Function) }), + }, + { + identity: 'property-registry/page', + reason: 'unavailable', + state: 'unavailable', + }, + { + identity: 'throwing-validator/page', + reason: 'incompatible', + state: 'unavailable', + }, + ]); + }), ); interface RemoteModule { readonly default: () => null; } -it.live( - 'never starts a queued load whose deadline expired before a permit became available', - () => - Effect.gen(function* verifyCase1() { - const pendingLoads: PromiseWithResolvers[] = []; - const firstWindowStarted = Promise.withResolvers(); - let loadCount = 0; - const resultsFiber = yield* Effect.forkChild( - settleModuleEntrypointLoads( - Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ - identity: `module-${index}/page`, - isCompatible: compatibleRemoteModule, - - load: () => { - const pending = Promise.withResolvers(); - pendingLoads.push(pending); - loadCount += 1; - if (loadCount === MODULE_LOAD_CONCURRENCY) { - firstWindowStarted.resolve(null); - } - return Promise.resolve(pending.promise); - }, - // The first window must outlive runner jitter so every slot is really held; only the - // queued load carries the short deadline that expires before any permit frees up. - timeoutMs: index < MODULE_LOAD_CONCURRENCY ? 500 : 10, - })) - ) - ); +it.live('never starts a queued load whose deadline expired before a permit became available', () => + Effect.gen(function* verifyCase1() { + const pendingLoads: PromiseWithResolvers[] = []; + const firstWindowStarted = Promise.withResolvers(); + let loadCount = 0; + const resultsFiber = yield* Effect.forkChild( + settleModuleEntrypointLoads( + Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ + identity: `module-${index}/page`, + isCompatible: compatibleRemoteModule, + + load: () => { + const pending = Promise.withResolvers(); + pendingLoads.push(pending); + loadCount += 1; + if (loadCount === MODULE_LOAD_CONCURRENCY) { + firstWindowStarted.resolve(null); + } + return Promise.resolve(pending.promise); + }, + // The first window must outlive runner jitter so every slot is really held; only the + // queued load carries the short deadline that expires before any permit frees up. + timeoutMs: index < MODULE_LOAD_CONCURRENCY ? 500 : 10, + })), + ), + ); + + yield* Effect.promise(() => firstWindowStarted.promise); + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); + expect(pendingLoads).toHaveLength(MODULE_LOAD_CONCURRENCY); - yield* Effect.promise(() => firstWindowStarted.promise); - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - expect(pendingLoads).toHaveLength(MODULE_LOAD_CONCURRENCY); + const results = yield* Fiber.join(resultsFiber); + expect(results).toEqual( + Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ + identity: `module-${index}/page`, + reason: 'timeout', + state: 'unavailable', + })), + ); + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - const results = yield* Fiber.join(resultsFiber); - expect(results).toEqual( + for (const pending of pendingLoads) { + pending.resolve({ default: () => null }); + } + yield* Effect.all( + pendingLoads.map(({ promise }) => + Effect.gen(function* verifyCase2() { + return yield* Effect.promise(() => promise); + }), + ), + { concurrency: 'unbounded' }, + ); + yield* Effect.yieldNow; + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); + }), +); + +it.live('never starts an expired queued load when synchronous work delays deadline timers', () => + Effect.gen(function* verifyCase3() { + const started: number[] = []; + // Use the live clock: TestClock would not advance while the JavaScript thread is blocked. + const results = yield* Clock.clockWith((clock) => + settleModuleEntrypointLoads( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, - reason: 'timeout', - state: 'unavailable', - })) - ); - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - - for (const pending of pendingLoads) { - pending.resolve({ default: () => null }); - } - yield* Effect.all( - pendingLoads.map(({ promise }) => - Effect.gen(function* verifyCase2() { - return yield* Effect.promise(() => promise); - }) - ), - { concurrency: 'unbounded' } - ); - yield* Effect.yieldNow; - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - }) -); + isCompatible: compatibleRemoteModule, -it.live( - 'never starts an expired queued load when synchronous work delays deadline timers', - () => - Effect.gen(function* verifyCase3() { - const started: number[] = []; - // Use the live clock: TestClock would not advance while the JavaScript thread is blocked. - const results = yield* Clock.clockWith((clock) => - settleModuleEntrypointLoads( - Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ - identity: `module-${index}/page`, - isCompatible: compatibleRemoteModule, - - load: () => { - started.push(index); - if (index === 0) { - const unblockAt = clock.currentTimeMillisUnsafe() + 40; - // Busy-wait past the queued deadline without letting its timer callback run. - while (clock.currentTimeMillisUnsafe() < unblockAt) { - // Intentionally keep the event loop blocked. - } + load: () => { + started.push(index); + if (index === 0) { + const unblockAt = clock.currentTimeMillisUnsafe() + 40; + // Busy-wait past the queued deadline without letting its timer callback run. + while (clock.currentTimeMillisUnsafe() < unblockAt) { + // Intentionally keep the event loop blocked. } - return Promise.resolve({ default: remoteDefault }); - }, - timeoutMs: index === MODULE_LOAD_CONCURRENCY ? 10 : 5000, - })) - ) - ); + } + return Promise.resolve({ default: remoteDefault }); + }, + timeoutMs: index === MODULE_LOAD_CONCURRENCY ? 10 : 5000, + })), + ), + ); - expect(started).toContain(0); - expect(started).not.toContain(MODULE_LOAD_CONCURRENCY); - expect(results[MODULE_LOAD_CONCURRENCY]).toEqual({ - identity: `module-${MODULE_LOAD_CONCURRENCY}/page`, - reason: 'timeout', - state: 'unavailable', - }); - }) + expect(started).toContain(0); + expect(started).not.toContain(MODULE_LOAD_CONCURRENCY); + expect(results[MODULE_LOAD_CONCURRENCY]).toEqual({ + identity: `module-${MODULE_LOAD_CONCURRENCY}/page`, + reason: 'timeout', + state: 'unavailable', + }); + }), ); it.effect('abandons queued loads when the caller is interrupted', () => Effect.gen(function* verifyInterruptedCaller() { - const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => - Promise.withResolvers() - ); + const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => Promise.withResolvers()); const firstWindowStarted = Promise.withResolvers(); const started: number[] = []; const caller = yield* Effect.forkChild( @@ -464,13 +401,10 @@ it.effect('abandons queued loads when the caller is interrupted', () => if (started.length === MODULE_LOAD_CONCURRENCY) { firstWindowStarted.resolve(null); } - return ( - pendingLoads[index]?.promise ?? - Promise.resolve({ default: remoteDefault }) - ); + return pendingLoads[index]?.promise ?? Promise.resolve({ default: remoteDefault }); }, - })) - ) + })), + ), ); yield* Effect.promise(() => firstWindowStarted.promise); @@ -479,81 +413,65 @@ it.effect('abandons queued loads when the caller is interrupted', () => for (const pending of pendingLoads) { pending.resolve({ default: () => null }); } - yield* Effect.promise(() => - Promise.all(pendingLoads.map(({ promise }) => promise)) - ); + yield* Effect.promise(() => Promise.all(pendingLoads.map(({ promise }) => promise))); yield* TestClock.adjust('1 millis'); expect(started).toHaveLength(8); expect(started).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); - }).pipe(Effect.provide(TestClock.layer())) + }).pipe(Effect.provide(TestClock.layer())), ); -it.effect( - 'holds a running timed-out load permit until settlement then releases it to a live queued load', - () => - Effect.gen(function* verifyPermitRelease() { - const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => - Promise.withResolvers() - ); - const firstWindowStarted = Promise.withResolvers(); - const queuedLoadStarted = Promise.withResolvers(); - const events: string[] = []; - const resultsFiber = yield* Effect.forkChild( - settleModuleEntrypointLoads( - Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ - identity: `module-${index}/page`, - isCompatible: compatibleRemoteModule, - - load: () => { - events.push(`started-${index}`); - if (index === MODULE_LOAD_CONCURRENCY - 1) { - firstWindowStarted.resolve(null); - } - const pending = pendingLoads[index]; - if (pending === undefined) { - queuedLoadStarted.resolve(null); - return Promise.resolve({ default: remoteDefault }); - } - return Promise.resolve( - pending.promise.then((value) => { - events.push(`settled-${index}`); - return value; - }) - ); - }, - timeoutMs: index === 0 ? 10 : 1000, - })) - ) - ); +it.effect('holds a running timed-out load permit until settlement then releases it to a live queued load', () => + Effect.gen(function* verifyPermitRelease() { + const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => Promise.withResolvers()); + const firstWindowStarted = Promise.withResolvers(); + const queuedLoadStarted = Promise.withResolvers(); + const events: string[] = []; + const resultsFiber = yield* Effect.forkChild( + settleModuleEntrypointLoads( + Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ + identity: `module-${index}/page`, + isCompatible: compatibleRemoteModule, - yield* Effect.promise(() => firstWindowStarted.promise); - yield* TestClock.adjust('20 millis'); - expect(events).toEqual( - Array.from( - { length: MODULE_LOAD_CONCURRENCY }, - (_, index) => `started-${index}` - ) - ); + load: () => { + events.push(`started-${index}`); + if (index === MODULE_LOAD_CONCURRENCY - 1) { + firstWindowStarted.resolve(null); + } + const pending = pendingLoads[index]; + if (pending === undefined) { + queuedLoadStarted.resolve(null); + return Promise.resolve({ default: remoteDefault }); + } + return Promise.resolve( + pending.promise.then((value) => { + events.push(`settled-${index}`); + return value; + }), + ); + }, + timeoutMs: index === 0 ? 10 : 1000, + })), + ), + ); - pendingLoads[0]?.resolve({ default: () => null }); - yield* Effect.promise(() => queuedLoadStarted.promise); - expect(events.slice(-2)).toEqual([ - 'settled-0', - `started-${MODULE_LOAD_CONCURRENCY}`, - ]); - for (const pending of pendingLoads) { - pending.resolve({ default: () => null }); - } - const results = yield* Fiber.join(resultsFiber); - expect(results[0]).toEqual({ - identity: 'module-0/page', - reason: 'timeout', - state: 'unavailable', - }); - expect(results.slice(1).every(({ state }) => state === 'ready')).toBe( - true - ); - }).pipe(Effect.provide(TestClock.layer())) + yield* Effect.promise(() => firstWindowStarted.promise); + yield* TestClock.adjust('20 millis'); + expect(events).toEqual(Array.from({ length: MODULE_LOAD_CONCURRENCY }, (_, index) => `started-${index}`)); + + pendingLoads[0]?.resolve({ default: () => null }); + yield* Effect.promise(() => queuedLoadStarted.promise); + expect(events.slice(-2)).toEqual(['settled-0', `started-${MODULE_LOAD_CONCURRENCY}`]); + for (const pending of pendingLoads) { + pending.resolve({ default: () => null }); + } + const results = yield* Fiber.join(resultsFiber); + expect(results[0]).toEqual({ + identity: 'module-0/page', + reason: 'timeout', + state: 'unavailable', + }); + expect(results.slice(1).every(({ state }) => state === 'ready')).toBe(true); + }).pipe(Effect.provide(TestClock.layer())), ); it.effect('does not surface a late remote rejection after a timeout', () => @@ -572,12 +490,12 @@ it.effect('does not surface a late remote rejection after a timeout', () => return Promise.resolve( pending.promise.finally(() => { loadSettled.resolve(null); - }) + }), ); }, timeoutMs: 10, }, - ]) + ]), ); yield* Effect.promise(() => loadStarted.promise); yield* TestClock.adjust('10 millis'); @@ -598,15 +516,10 @@ it.effect('does not surface a late remote rejection after a timeout', () => state: 'unavailable', }, ]); - }).pipe(Effect.provide(TestClock.layer())) + }).pipe(Effect.provide(TestClock.layer())), ); -it.effect.each([ - 'selection_required', - 'not_found', - 'forbidden', - 'unavailable', -] as const)( +it.effect.each(['selection_required', 'not_found', 'forbidden', 'unavailable'] as const)( 'never invokes a remote loader after a %s target resolution', (outcome) => { let loads = 0; @@ -616,33 +529,29 @@ it.effect.each([ Effect.sync(() => { loads += 1; return 'unreachable'; - }) - ) + }), + ), ); expect(failure).toEqual({ outcome }); expect(loads).toBe(0); }); - } + }, ); -it.effect( - 'invokes the lazy registry only after receiving an approved target', - () => - Effect.gen(function* verifyApprovedTargetResolution() { - let loads = 0; - const target = { - appId: 'inventory-app', - componentKey: 'inventory.stock.page', - }; - const result = yield* resolveThenLoadModuleTarget( - Effect.succeed(target), - (approved) => - Effect.sync(() => { - loads += 1; - return approved.componentKey; - }) - ); - expect(result).toBe('inventory.stock.page'); - expect(loads).toBe(1); - }) +it.effect('invokes the lazy registry only after receiving an approved target', () => + Effect.gen(function* verifyApprovedTargetResolution() { + let loads = 0; + const target = { + appId: 'inventory-app', + componentKey: 'inventory.stock.page', + }; + const result = yield* resolveThenLoadModuleTarget(Effect.succeed(target), (approved) => + Effect.sync(() => { + loads += 1; + return approved.componentKey; + }), + ); + expect(result).toBe('inventory.stock.page'); + expect(loads).toBe(1); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts b/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts index 0d27ea658..3ac7f987f 100644 --- a/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts +++ b/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts @@ -1,9 +1,6 @@ import { Schema } from 'effect'; -import { - LegalEntityIdSchema, - SafeTenantIdentitySchema, -} from '../../../shared/api.ts'; +import { LegalEntityIdSchema, SafeTenantIdentitySchema } from '../../../shared/api.ts'; import type { AuthenticatedHomePageModel } from '../../../src/routes/[lang]/page.data.ts'; /** The authenticated shell every route page test renders its own model on top of. */ @@ -17,9 +14,7 @@ export const authenticatedShellFixture = (): AuthenticatedHomePageModel => ({ }), legalEntities: { items: [], state: 'available' }, navigation: { items: [], state: 'available', unavailableDeployments: [] }, - selectedLegalEntityId: Schema.decodeUnknownSync(LegalEntityIdSchema)( - '20000000-0000-4000-8000-000000000001' - ), + selectedLegalEntityId: Schema.decodeUnknownSync(LegalEntityIdSchema)('20000000-0000-4000-8000-000000000001'), state: 'authenticated', tenants: { items: [], state: 'available' }, }); diff --git a/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts index 3fa401658..d383bcba4 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts @@ -15,8 +15,7 @@ const { } = rstest.hoisted(() => ({ availableLegalEntitiesMock: rstest.fn(), availableTenantsMock: rstest.fn(), - browserConfigValuesMock: - rstest.fn<() => { readonly BETTER_AUTH_URL?: string }>(), + browserConfigValuesMock: rstest.fn<() => { readonly BETTER_AUTH_URL?: string }>(), currentSessionMock: rstest.fn(), shellCompositionMock: rstest.fn(), })); @@ -59,16 +58,10 @@ const request = () => const loadModel = ({ request: input }: { readonly request: Request }) => Effect.suspend(() => loadHomePageModel(input).pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown(browserConfigValuesMock()) - ) - ) + Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown(browserConfigValuesMock())), + ), ); -const withBetterAuthUrl = ( - baseUrl: string, - operation: () => Effect.Effect -) => +const withBetterAuthUrl = (baseUrl: string, operation: () => Effect.Effect) => Effect.suspend(() => { browserConfigValuesMock.mockReturnValueOnce({ BETTER_AUTH_URL: baseUrl }); return operation(); @@ -76,22 +69,20 @@ const withBetterAuthUrl = ( beforeEach(() => { browserConfigValuesMock.mockReturnValue({}); - currentSessionMock.mockReturnValue( - Effect.succeed({ identity, state: 'authenticated' as const }) - ); + currentSessionMock.mockReturnValue(Effect.succeed({ identity, state: 'authenticated' as const })); availableLegalEntitiesMock.mockReturnValue( Effect.succeed({ legalEntities: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], selectedLegalEntityId: 'legal-1', state: 'authenticated', - }) + }), ); shellCompositionMock.mockReturnValue( Effect.succeed({ navigation, state: 'available' as const, unavailableDeployments: [], - }) + }), ); availableTenantsMock.mockReturnValue( Effect.succeed({ @@ -99,204 +90,167 @@ beforeEach(() => { { name: 'Alpha tenant', tenantId: 'tenant-1' }, { name: 'Zeta tenant', tenantId: 'tenant-2' }, ], - }) + }), ); }); -it.effect( - 'resolves trusted context before returning one serializable composition', - () => - Effect.gen(function* verifyCase1() { - expect(yield* loadModel({ request: request() })).toEqual({ - contextState: 'authenticated', - identity, - legalEntities: { - items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], - state: 'available', - }, - navigation: { - items: navigation, - state: 'available', - unavailableDeployments: [], - }, - selectedLegalEntityId: 'legal-1', - state: 'authenticated', - tenants: { - items: [ - { name: 'Alpha tenant', tenantId: 'tenant-1' }, - { name: 'Zeta tenant', tenantId: 'tenant-2' }, - ], - state: 'available', - }, - }); - expect(currentSessionMock.mock.invocationCallOrder[0]).toBeLessThan( - shellCompositionMock.mock.invocationCallOrder[0] ?? - Number.POSITIVE_INFINITY - ); - }) +it.effect('resolves trusted context before returning one serializable composition', () => + Effect.gen(function* verifyCase1() { + expect(yield* loadModel({ request: request() })).toEqual({ + contextState: 'authenticated', + identity, + legalEntities: { + items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], + state: 'available', + }, + navigation: { + items: navigation, + state: 'available', + unavailableDeployments: [], + }, + selectedLegalEntityId: 'legal-1', + state: 'authenticated', + tenants: { + items: [ + { name: 'Alpha tenant', tenantId: 'tenant-1' }, + { name: 'Zeta tenant', tenantId: 'tenant-2' }, + ], + state: 'available', + }, + }); + expect(currentSessionMock.mock.invocationCallOrder[0]).toBeLessThan( + shellCompositionMock.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, + ); + }), ); it.effect('does not request composition for an anonymous session', () => Effect.gen(function* verifyCase2() { - currentSessionMock.mockReturnValueOnce( - Effect.succeed({ state: 'anonymous' as const }) - ); + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); expect(yield* loadModel({ request: request() })).toEqual({ state: 'anonymous', }); expect(shellCompositionMock).not.toHaveBeenCalled(); expect(availableTenantsMock).not.toHaveBeenCalled(); - }) + }), ); -it.effect( - 'does not invent a selected legal entity while a tenant session requires selection', - () => - Effect.gen(function* verifyCase3() { - const tenantIdentity = { - displayName: identity.displayName, - email: identity.email, - principalId: identity.principalId, - tenantId: identity.tenantId, - }; - currentSessionMock.mockReturnValueOnce( - Effect.succeed({ - availableLegalEntities: [ - { legalEntityId: 'legal-1', legalName: 'Alpha company' }, - ], - identity: tenantIdentity, - state: 'selection_required' as const, - }) - ); - const model = yield* loadModel({ request: request() }); - expect(model).toMatchObject({ - contextState: 'selection_required', +it.effect('does not invent a selected legal entity while a tenant session requires selection', () => + Effect.gen(function* verifyCase3() { + const tenantIdentity = { + displayName: identity.displayName, + email: identity.email, + principalId: identity.principalId, + tenantId: identity.tenantId, + }; + currentSessionMock.mockReturnValueOnce( + Effect.succeed({ + availableLegalEntities: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], identity: tenantIdentity, - legalEntities: { - items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], - state: 'available', - }, - state: 'authenticated', - }); - expect(model).not.toHaveProperty('selectedLegalEntityId'); - expect(shellCompositionMock).not.toHaveBeenCalled(); - expect(availableLegalEntitiesMock).not.toHaveBeenCalled(); - }) + state: 'selection_required' as const, + }), + ); + const model = yield* loadModel({ request: request() }); + expect(model).toMatchObject({ + contextState: 'selection_required', + identity: tenantIdentity, + legalEntities: { + items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], + state: 'available', + }, + state: 'authenticated', + }); + expect(model).not.toHaveProperty('selectedLegalEntityId'); + expect(shellCompositionMock).not.toHaveBeenCalled(); + expect(availableLegalEntitiesMock).not.toHaveBeenCalled(); + }), ); -it.effect( - 'uses the configured HTTPS origin for the server-side session request', - () => - Effect.gen(function* verifyCase4() { - currentSessionMock.mockReturnValueOnce( - Effect.succeed({ state: 'anonymous' as const }) - ); +it.effect('uses the configured HTTPS origin for the server-side session request', () => + Effect.gen(function* verifyCase4() { + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); - yield* withBetterAuthUrl('https://shell.stage.example.test', () => - Effect.gen(function* verifyCase5() { - return yield* loadModel({ - request: new Request('http://shell.stage.example.test/en'), - }); - }) - ); + yield* withBetterAuthUrl('https://shell.stage.example.test', () => + Effect.gen(function* verifyCase5() { + return yield* loadModel({ + request: new Request('http://shell.stage.example.test/en'), + }); + }), + ); - expect( - currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString() - ).toBe('https://shell.stage.example.test/shell-super-app-api'); - }) + expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( + 'https://shell.stage.example.test/shell-super-app-api', + ); + }), ); -it.effect( - 'keeps the configured local HTTP origin for the server-side session request', - () => - Effect.gen(function* verifyCase6() { - currentSessionMock.mockReturnValueOnce( - Effect.succeed({ state: 'anonymous' as const }) - ); +it.effect('keeps the configured local HTTP origin for the server-side session request', () => + Effect.gen(function* verifyCase6() { + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); - yield* withBetterAuthUrl('http://localhost:3020', () => - Effect.gen(function* verifyCase7() { - return yield* loadModel({ - request: new Request('http://localhost:3020/en'), - }); - }) - ); + yield* withBetterAuthUrl('http://localhost:3020', () => + Effect.gen(function* verifyCase7() { + return yield* loadModel({ + request: new Request('http://localhost:3020/en'), + }); + }), + ); - expect( - currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString() - ).toBe('http://localhost:3020/shell-super-app-api'); - }) + expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( + 'http://localhost:3020/shell-super-app-api', + ); + }), ); -it.effect( - 'maps composition failure to unavailable without discarding verified context', - () => - Effect.gen(function* verifyCase8() { - shellCompositionMock.mockReturnValueOnce( - Effect.fail({ _tag: 'ShellCapabilityUnavailableProblem' }) - ); - expect(yield* loadModel({ request: request() })).toMatchObject({ - contextState: 'authenticated', - identity, - navigation: { items: [], state: 'unavailable' }, - state: 'authenticated', - }); - }) +it.effect('maps composition failure to unavailable without discarding verified context', () => + Effect.gen(function* verifyCase8() { + shellCompositionMock.mockReturnValueOnce(Effect.fail({ _tag: 'ShellCapabilityUnavailableProblem' })); + expect(yield* loadModel({ request: request() })).toMatchObject({ + contextState: 'authenticated', + identity, + navigation: { items: [], state: 'unavailable' }, + state: 'authenticated', + }); + }), ); -it.effect( - 'maps tenant failure to the current-tenant fallback without discarding composition', - () => - Effect.gen(function* verifyCase9() { - availableTenantsMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }) - ); - expect(yield* loadModel({ request: request() })).toMatchObject({ - navigation: { items: navigation, state: 'available' }, - tenants: { - items: [{ name: 'tenant-1', tenantId: 'tenant-1' }], - state: 'unavailable', - }, - }); - }) +it.effect('maps tenant failure to the current-tenant fallback without discarding composition', () => + Effect.gen(function* verifyCase9() { + availableTenantsMock.mockReturnValueOnce(Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' })); + expect(yield* loadModel({ request: request() })).toMatchObject({ + navigation: { items: navigation, state: 'available' }, + tenants: { + items: [{ name: 'tenant-1', tenantId: 'tenant-1' }], + state: 'unavailable', + }, + }); + }), ); -it.effect( - 'keeps legal-entity acquisition failure explicit without claiming choices are available', - () => - Effect.gen(function* verifyCase10() { - availableLegalEntitiesMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }) - ); - expect(yield* loadModel({ request: request() })).toMatchObject({ - legalEntities: { items: [], state: 'unavailable' }, - state: 'authenticated', - }); - }) +it.effect('keeps legal-entity acquisition failure explicit without claiming choices are available', () => + Effect.gen(function* verifyCase10() { + availableLegalEntitiesMock.mockReturnValueOnce(Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' })); + expect(yield* loadModel({ request: request() })).toMatchObject({ + legalEntities: { items: [], state: 'unavailable' }, + state: 'authenticated', + }); + }), ); -it.effect( - 'does not collapse an authentication infrastructure failure into an anonymous session', - () => - Effect.gen(function* verifyCase11() { - currentSessionMock.mockReturnValueOnce( - Effect.fail({ _tag: 'AuthenticationUnavailableProblem' }) - ); - expect(yield* loadModel({ request: request() })).toEqual({ - state: 'unavailable', - }); - }) +it.effect('does not collapse an authentication infrastructure failure into an anonymous session', () => + Effect.gen(function* verifyCase11() { + currentSessionMock.mockReturnValueOnce(Effect.fail({ _tag: 'AuthenticationUnavailableProblem' })); + expect(yield* loadModel({ request: request() })).toEqual({ + state: 'unavailable', + }); + }), ); -it.effect( - 'tears down stale authenticated data when tenant context requires authentication', - () => - Effect.gen(function* verifyCase12() { - availableTenantsMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantAuthenticationRequiredProblem' }) - ); - expect(yield* loadModel({ request: request() })).toEqual({ - state: 'anonymous', - }); - }) +it.effect('tears down stale authenticated data when tenant context requires authentication', () => + Effect.gen(function* verifyCase12() { + availableTenantsMock.mockReturnValueOnce(Effect.fail({ _tag: 'TenantAuthenticationRequiredProblem' })); + expect(yield* loadModel({ request: request() })).toEqual({ + state: 'anonymous', + }); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx index e0071ef43..0a04c1e19 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx @@ -19,10 +19,7 @@ import { HomeView } from '../../../../src/routes/[lang]/page.tsx'; import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; -import type { - LocalizedLinkCall, - LocalizedLinkDoubleProps, -} from '../../../support/localized-link-double.tsx'; +import type { LocalizedLinkCall, LocalizedLinkDoubleProps } from '../../../support/localized-link-double.tsx'; import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; const { @@ -78,7 +75,7 @@ const translations = new Map( 'shell.modules.unavailable': 'Module access unavailable', 'shell.search.label': 'Search this legal entity', 'shell.search.submit': 'Search', - }) + }), ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ @@ -112,31 +109,16 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ browserRuntime: { runPromise: browserRunPromiseMock }, })); -const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( - '00000000-0000-4000-8000-000000000001' -); -const tenantId1 = Schema.decodeUnknownSync(TenantIdSchema)( - '00000000-0000-4000-8000-000000000101' -); -const tenantId2 = Schema.decodeUnknownSync(TenantIdSchema)( - '00000000-0000-4000-8000-000000000102' -); -const legalEntityId1 = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000201' -); -const legalEntityId2 = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000202' -); +const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)('00000000-0000-4000-8000-000000000001'); +const tenantId1 = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101'); +const tenantId2 = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000102'); +const legalEntityId1 = Schema.decodeUnknownSync(LegalEntityIdSchema)('00000000-0000-4000-8000-000000000201'); +const legalEntityId2 = Schema.decodeUnknownSync(LegalEntityIdSchema)('00000000-0000-4000-8000-000000000202'); const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); -const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)( - 'shell.navigation.modules' -); -const inventoryModuleId = - Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); +const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); +const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); -const authenticatedModel = (options?: { - readonly moduleEnabled?: boolean; -}): HomePageModel => ({ +const authenticatedModel = (options?: { readonly moduleEnabled?: boolean }): HomePageModel => ({ contextState: 'authenticated', identity: { displayName: 'Ada Lovelace', @@ -184,12 +166,8 @@ beforeEach(() => { navigateMock.mockResolvedValue(undefined); browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); - switchTenantMock.mockReturnValue( - Effect.succeed({ selectedTenantId: tenantId2 }) - ); - switchLegalEntityMock.mockReturnValue( - Effect.succeed({ selectedLegalEntityId: legalEntityId2 }) - ); + switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId2 })); + switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId2 })); }); afterEach(() => { @@ -201,9 +179,7 @@ afterEach(() => { it('anonymous home exposes only the localized login action', () => { render(); - expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe( - '/en/login' - ); + expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/en/login'); expect(screen.queryByRole('banner')).toBeNull(); }); @@ -219,9 +195,7 @@ it('the anonymous login action resolves Czech from the same canonical target', ( languageState.current = 'cs'; render(); expect(localizedLinkCalls.map((call) => call.to)).toContain('/login'); - expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe( - '/cs/login' - ); + expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/cs/login'); }); it('the unavailable dashboard exposes no navigable affordance', () => { @@ -231,21 +205,15 @@ it('the unavailable dashboard exposes no navigable affordance', () => { }); it('a disabled module affordance stays non-interactive text', () => { - render( - - ); + render(); expect(screen.queryByRole('link', { name: 'Inventory' })).toBeNull(); expect(screen.getByText('Inventory')).toBeTruthy(); - expect(localizedLinkCalls.map((call) => call.to)).not.toContain( - '/modules/inventory.stock' - ); + expect(localizedLinkCalls.map((call) => call.to)).not.toContain('/modules/inventory.stock'); }); it('authenticated home renders server-composed navigation and selected legal context', () => { render(); - expect( - screen.getByRole('link', { name: 'Inventory' }).getAttribute('href') - ).toBe('/en/modules/inventory.stock'); + expect(screen.getByRole('link', { name: 'Inventory' }).getAttribute('href')).toBe('/en/modules/inventory.stock'); expect(screen.getByText('Read only')).toBeTruthy(); expect(screen.getByText(legalEntityId1)).toBeTruthy(); expect(screen.queryByText('inventory.stock')).toBeNull(); @@ -255,127 +223,93 @@ it.live('successful tenant switch performs a full document reload', () => Effect.gen(function* successfulTenantSwitchPerformsAFull() { const user = userEvent.setup(); render(); - yield* Effect.promise(() => - user.click(screen.getByRole('combobox', { name: 'Current tenant' })) - ); - const tenantOption = yield* Effect.promise(() => - screen.findByRole('option', { name: 'Zeta tenant' }) - ); + yield* Effect.promise(() => user.click(screen.getByRole('combobox', { name: 'Current tenant' }))); + const tenantOption = yield* Effect.promise(() => screen.findByRole('option', { name: 'Zeta tenant' })); yield* Effect.promise(() => user.click(tenantOption)); yield* Effect.promise(() => - waitFor(() => - expect(switchTenantMock).toHaveBeenCalledWith( - { tenantId: tenantId2 }, - { locale: 'en' } - ) - ) + waitFor(() => expect(switchTenantMock).toHaveBeenCalledWith({ tenantId: tenantId2 }, { locale: 'en' })), ); yield* Effect.promise(() => waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.', - }) - ) + }), + ), ); - }) + }), ); it.live('successful legal-entity switch performs a full document reload', () => Effect.gen(function* successfulLegalEntitySwitchPerformsA() { const user = userEvent.setup(); render(); - yield* Effect.promise(() => - user.click(screen.getByRole('combobox', { name: 'Current legal entity' })) - ); - const legalEntityOption = yield* Effect.promise(() => - screen.findByRole('option', { name: 'Beta company' }) - ); + yield* Effect.promise(() => user.click(screen.getByRole('combobox', { name: 'Current legal entity' }))); + const legalEntityOption = yield* Effect.promise(() => screen.findByRole('option', { name: 'Beta company' })); yield* Effect.promise(() => user.click(legalEntityOption)); yield* Effect.promise(() => waitFor(() => - expect(switchLegalEntityMock).toHaveBeenCalledWith( - { legalEntityId: legalEntityId2 }, - { locale: 'en' } - ) - ) + expect(switchLegalEntityMock).toHaveBeenCalledWith({ legalEntityId: legalEntityId2 }, { locale: 'en' }), + ), ); yield* Effect.promise(() => waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.', - }) - ) + }), + ), ); - }) + }), ); it.live('search submission navigates to the localized Shell search route', () => Effect.gen(function* searchSubmissionNavigatesToTheLocalized() { const user = userEvent.setup(); render(); - yield* Effect.promise(() => - user.type(screen.getByLabelText('Search this legal entity'), 'Unit 1') - ); - yield* Effect.promise(() => - user.click(screen.getByRole('button', { name: 'Search' })) - ); + yield* Effect.promise(() => user.type(screen.getByLabelText('Search this legal entity'), 'Unit 1')); + yield* Effect.promise(() => user.click(screen.getByRole('button', { name: 'Search' }))); expect(navigateMock).toHaveBeenCalledWith({ to: '/en/search?q=Unit%201' }); - }) + }), ); it.live('logout clears the authenticated composition together', () => Effect.gen(function* logoutClearsTheAuthenticatedCompositionTogether() { const user = userEvent.setup(); render(); - yield* Effect.promise(() => - user.click(screen.getByRole('button', { name: 'Ada Lovelace' })) - ); - const logoutItem = yield* Effect.promise(() => - screen.findByRole('menuitem', { name: 'Logout' }) - ); + yield* Effect.promise(() => user.click(screen.getByRole('button', { name: 'Ada Lovelace' }))); + const logoutItem = yield* Effect.promise(() => screen.findByRole('menuitem', { name: 'Logout' })); // Happy DOM has no layout. Give pointer movement distinct coordinates so // the menu can distinguish it from virtual focus after a prior selection. - yield* Effect.promise(() => - user.pointer({ coords: { x: 10, y: 10 }, target: logoutItem }) - ); + yield* Effect.promise(() => user.pointer({ coords: { x: 10, y: 10 }, target: logoutItem })); yield* Effect.promise(() => user.click(logoutItem)); - yield* Effect.promise(() => - waitFor(() => expect(signOutMock).toHaveBeenCalledWith({ locale: 'en' })) - ); + yield* Effect.promise(() => waitFor(() => expect(signOutMock).toHaveBeenCalledWith({ locale: 'en' }))); yield* Effect.promise(() => waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '/en/login', - }) - ) + }), + ), ); - }) + }), ); -const tenantAuthenticationRequired = Schema.decodeUnknownSync( - TenantAuthenticationRequiredProblemSchema -)({ +const tenantAuthenticationRequired = Schema.decodeUnknownSync(TenantAuthenticationRequiredProblemSchema)({ _tag: 'TenantAuthenticationRequiredProblem', detail: 'The tenant session expired.', status: 401, title: 'Tenant authentication required', type: 'https://ontos.dev/problems/tenant-authentication-required', }); -const tenantAccessForbidden = Schema.decodeUnknownSync( - TenantAccessForbiddenProblemSchema -)({ +const tenantAccessForbidden = Schema.decodeUnknownSync(TenantAccessForbiddenProblemSchema)({ _tag: 'TenantAccessForbiddenProblem', detail: 'The principal cannot use this tenant.', status: 403, title: 'Tenant access forbidden', type: 'https://ontos.dev/problems/tenant-access-forbidden', }); -const legalEntityAccessForbidden = Schema.decodeUnknownSync( - LegalEntityAccessForbiddenProblemSchema -)({ +const legalEntityAccessForbidden = Schema.decodeUnknownSync(LegalEntityAccessForbiddenProblemSchema)({ _tag: 'LegalEntityAccessForbiddenProblem', detail: 'The principal cannot use this legal entity.', status: 403, @@ -442,30 +376,16 @@ const switchFailureCases: SwitchFailureCase[] = [ it.live.each(switchFailureCases)( 'settles $name into its own selector without leaving it pending', - ({ - comboboxName, - failedText, - failure, - optionName, - pendingText, - reloads, - switchMock, - }) => + ({ comboboxName, failedText, failure, optionName, pendingText, reloads, switchMock }) => Effect.gen(function* settlesTheSwitchFailureIntoItsOwnSelector() { switchMock.mockReturnValue(Effect.fail(failure)); const user = userEvent.setup(); render(); - yield* Effect.promise(() => - user.click(screen.getByRole('combobox', { name: comboboxName })) - ); - const option = yield* Effect.promise(() => - screen.findByRole('option', { name: optionName }) - ); + yield* Effect.promise(() => user.click(screen.getByRole('combobox', { name: comboboxName }))); + const option = yield* Effect.promise(() => screen.findByRole('option', { name: optionName })); yield* Effect.promise(() => user.click(option)); - yield* Effect.promise(() => - waitFor(() => expect(switchMock).toHaveBeenCalledTimes(1)) - ); + yield* Effect.promise(() => waitFor(() => expect(switchMock).toHaveBeenCalledTimes(1))); if (reloads) { yield* Effect.promise(() => @@ -473,25 +393,17 @@ it.live.each(switchFailureCases)( expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.', - }) - ) - ); - yield* Effect.promise(() => - waitFor(() => expect(screen.queryByText(pendingText)).toBeNull()) + }), + ), ); + yield* Effect.promise(() => waitFor(() => expect(screen.queryByText(pendingText)).toBeNull())); expect(screen.queryByText(failedText)).toBeNull(); } else { - yield* Effect.promise(() => - waitFor(() => expect(screen.getByText(failedText)).toBeTruthy()) - ); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText(failedText)).toBeTruthy())); expect(navigateMock).not.toHaveBeenCalled(); expect(screen.queryByText(pendingText)).toBeNull(); } - expect( - screen - .getByRole('combobox', { name: comboboxName }) - .hasAttribute('disabled') - ).toBe(false); - }) + expect(screen.getByRole('combobox', { name: comboboxName }).hasAttribute('disabled')).toBe(false); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts b/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts index 729d20ddb..b201fda93 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts @@ -16,17 +16,11 @@ test.each([ ['dashboard.account', cs.shell.dashboard.account, en.shell.dashboard.account], ['dashboard.header', cs.shell.dashboard.header, en.shell.dashboard.header], ['dashboard.home', cs.shell.dashboard.home, en.shell.dashboard.home], - [ - 'dashboard.navigation', - cs.shell.dashboard.navigation, - en.shell.dashboard.navigation, - ], + ['dashboard.navigation', cs.shell.dashboard.navigation, en.shell.dashboard.navigation], ['dashboard.sidebar', cs.shell.dashboard.sidebar, en.shell.dashboard.sidebar], ['dashboard.tenant', cs.shell.dashboard.tenant, en.shell.dashboard.tenant], ])('aligns Czech and English %s translation keys', (_name, czech, english) => { - expect(Object.keys(czech).toSorted()).toEqual( - Object.keys(english).toSorted() - ); + expect(Object.keys(czech).toSorted()).toEqual(Object.keys(english).toSorted()); }); test('keeps the Czech and English login translation contracts aligned', () => { @@ -40,7 +34,7 @@ test('includes the login route in the generated metadata manifest', () => { descriptionKey: 'shell.login.seo.description', id: 'shell-login', titleKey: 'shell.login.title', - }) + }), ); }); diff --git a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx index 25186a8d5..6cfec708d 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx @@ -8,30 +8,21 @@ import LoginPage from '../../../../src/routes/[lang]/login/page'; import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; -import type { - LocalizedLinkCall, - LocalizedLinkDoubleProps, -} from '../../../support/localized-link-double.tsx'; +import type { LocalizedLinkCall, LocalizedLinkDoubleProps } from '../../../support/localized-link-double.tsx'; import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; -const { - browserRunPromiseMock, - invalidateMock, - languageState, - localizedLinkCalls, - navigateMock, - signInMock, -} = rstest.hoisted(() => { - const recordedLinkCalls: LocalizedLinkCall[] = []; - return { - browserRunPromiseMock: rstest.fn(), - invalidateMock: rstest.fn(), - languageState: { current: 'en' }, - localizedLinkCalls: recordedLinkCalls, - navigateMock: rstest.fn(), - signInMock: rstest.fn(), - }; -}); +const { browserRunPromiseMock, invalidateMock, languageState, localizedLinkCalls, navigateMock, signInMock } = + rstest.hoisted(() => { + const recordedLinkCalls: LocalizedLinkCall[] = []; + return { + browserRunPromiseMock: rstest.fn(), + invalidateMock: rstest.fn(), + languageState: { current: 'en' }, + localizedLinkCalls: recordedLinkCalls, + navigateMock: rstest.fn(), + signInMock: rstest.fn(), + }; + }); beforeEach(() => { invalidateMock.mockImplementation(() => Promise.resolve()); @@ -45,7 +36,7 @@ beforeEach(() => { principalId: 'principal-1', tenantId: 'tenant-1', }, - }) + }), ); }); @@ -60,7 +51,7 @@ const translations = new Map( 'shell.login.title': 'Login', 'shell.login.toast.description': 'Fill in both required fields.', 'shell.login.toast.title': 'Login details are incomplete', - }) + }), ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ @@ -96,8 +87,7 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ })); const getLogin = () => screen.getByRole('textbox', { name: 'Login *' }); -const getPassword = () => - screen.getByLabelText(/^Password/u, { selector: 'input' }); +const getPassword = () => screen.getByLabelText(/^Password/u, { selector: 'input' }); const getSubmit = () => screen.getByRole('button', { name: 'Login' }); const renderLogin = () => render(); @@ -126,11 +116,7 @@ it('shows the required login controls through the UI kit', () => { expect(password.getAttribute('autocomplete')).toBe('current-password'); expect(password.hasAttribute('required')).toBe(true); expect(submit.getAttribute('type')).toBe('submit'); - expect( - screen - .getByRole('link', { name: '← Back to the home page' }) - .getAttribute('href') - ).toBe('/en'); + expect(screen.getByRole('link', { name: '← Back to the home page' }).getAttribute('href')).toBe('/en'); }); it('the back link hands the canonical home target to the framework link', () => { @@ -147,11 +133,7 @@ it('the back link resolves Czech from the same canonical target', () => { renderLogin(); expect(localizedLinkCalls.map((call) => call.to)).toContain('/'); - expect( - screen - .getByRole('link', { name: '← Back to the home page' }) - .getAttribute('href') - ).toBe('/cs'); + expect(screen.getByRole('link', { name: '← Back to the home page' }).getAttribute('href')).toBe('/cs'); }); const submitLogin = (login: string, password: string) => @@ -233,26 +215,14 @@ it.effect.each(validationCases)( yield* submitLogin(login, password); const incompleteToasts = loginInvalid || passwordInvalid ? 1 : 0; - expect(getLogin().getAttribute('aria-invalid')).toBe( - loginInvalid ? 'true' : null - ); - expect(getPassword().getAttribute('aria-invalid')).toBe( - passwordInvalid ? 'true' : null - ); - expect(screen.queryAllByText('Enter your login.')).toHaveLength( - loginInvalid ? 1 : 0 - ); - expect(screen.queryAllByText('Enter your password.')).toHaveLength( - passwordInvalid ? 1 : 0 - ); - expect( - screen.queryAllByText('Login details are incomplete') - ).toHaveLength(incompleteToasts); - expect( - screen.queryAllByText('Fill in both required fields.') - ).toHaveLength(incompleteToasts); + expect(getLogin().getAttribute('aria-invalid')).toBe(loginInvalid ? 'true' : null); + expect(getPassword().getAttribute('aria-invalid')).toBe(passwordInvalid ? 'true' : null); + expect(screen.queryAllByText('Enter your login.')).toHaveLength(loginInvalid ? 1 : 0); + expect(screen.queryAllByText('Enter your password.')).toHaveLength(passwordInvalid ? 1 : 0); + expect(screen.queryAllByText('Login details are incomplete')).toHaveLength(incompleteToasts); + expect(screen.queryAllByText('Fill in both required fields.')).toHaveLength(incompleteToasts); expect(document.activeElement).toBe(focusTargets[focus]()); - }) + }), ); it.effect('creates one Toast per repeated invalid submission', () => @@ -261,10 +231,8 @@ it.effect('creates one Toast per repeated invalid submission', () => yield* Effect.promise(() => user.click(getSubmit())); expect(screen.getAllByText('Login details are incomplete')).toHaveLength(2); - expect(screen.getAllByText('Fill in both required fields.')).toHaveLength( - 2 - ); - }) + expect(screen.getAllByText('Fill in both required fields.')).toHaveLength(2); + }), ); it.effect('clears stale errors after both fields are corrected', () => @@ -278,7 +246,7 @@ it.effect('clears stale errors after both fields are corrected', () => expect(getPassword().getAttribute('aria-invalid')).toBeNull(); expect(screen.queryByText('Enter your login.')).toBeNull(); expect(screen.queryByText('Enter your password.')).toBeNull(); - }) + }), ); it.effect('runs the same validation when submitted with Enter', () => @@ -292,66 +260,60 @@ it.effect('runs the same validation when submitted with Enter', () => expect(getPassword().getAttribute('aria-invalid')).toBe('true'); expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); expect(document.activeElement).toBe(getLogin()); - }) + }), ); -it.effect( - 'submits valid values through the Shell authentication client and navigates home', - () => - Effect.gen(function* submitsValidValuesThroughShellAuthClient() { - yield* submitLogin('admin', 'secret'); - - yield* Effect.promise(() => - waitFor(() => { - expect(signInMock).toHaveBeenCalledWith( - { - email: 'admin', - password: Redacted.make('secret'), - }, - { locale: 'en' } - ); - expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); - expect(invalidateMock).toHaveBeenCalledWith({ sync: true }); - expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); - expect(getSubmit().hasAttribute('disabled')).toBe(false); - expect(screen.queryByText('shell.login.error.internal')).toBeNull(); - expect(screen.queryByText('Login details are incomplete')).toBeNull(); - }) - ); - }) +it.effect('submits valid values through the Shell authentication client and navigates home', () => + Effect.gen(function* submitsValidValuesThroughShellAuthClient() { + yield* submitLogin('admin', 'secret'); + + yield* Effect.promise(() => + waitFor(() => { + expect(signInMock).toHaveBeenCalledWith( + { + email: 'admin', + password: Redacted.make('secret'), + }, + { locale: 'en' }, + ); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); + expect(invalidateMock).toHaveBeenCalledWith({ sync: true }); + expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); + expect(getSubmit().hasAttribute('disabled')).toBe(false); + expect(screen.queryByText('shell.login.error.internal')).toBeNull(); + expect(screen.queryByText('Login details are incomplete')).toBeNull(); + }), + ); + }), ); -it.effect( - 'reports navigation failure and restores the login form after authentication', - () => - Effect.gen(function* reportsNavigationFailure() { - navigateMock.mockRejectedValueOnce('Navigation failed'); - yield* submitLogin('admin', 'secret'); - - yield* Effect.promise(() => - waitFor(() => { - expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); - expect(screen.getByText('shell.login.error.internal')).toBeDefined(); - expect(getSubmit().hasAttribute('disabled')).toBe(false); - expect(document.activeElement).toBe(getLogin()); - }) - ); - }) +it.effect('reports navigation failure and restores the login form after authentication', () => + Effect.gen(function* reportsNavigationFailure() { + navigateMock.mockRejectedValueOnce('Navigation failed'); + yield* submitLogin('admin', 'secret'); + + yield* Effect.promise(() => + waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); + expect(screen.getByText('shell.login.error.internal')).toBeDefined(); + expect(getSubmit().hasAttribute('disabled')).toBe(false); + expect(document.activeElement).toBe(getLogin()); + }), + ); + }), ); -it.effect( - 'keeps navigation on the login route when auth cache refresh fails', - () => - Effect.gen(function* reportsAuthenticationRefreshFailure() { - invalidateMock.mockRejectedValueOnce('Route refresh failed'); - yield* submitLogin('admin', 'secret'); - yield* Effect.promise(() => - waitFor(() => { - expect(invalidateMock).toHaveBeenCalledWith({ sync: true }); - expect(navigateMock).not.toHaveBeenCalled(); - expect(screen.getByText('shell.login.error.internal')).toBeDefined(); - expect(getSubmit().hasAttribute('disabled')).toBe(false); - }) - ); - }) +it.effect('keeps navigation on the login route when auth cache refresh fails', () => + Effect.gen(function* reportsAuthenticationRefreshFailure() { + invalidateMock.mockRejectedValueOnce('Route refresh failed'); + yield* submitLogin('admin', 'secret'); + yield* Effect.promise(() => + waitFor(() => { + expect(invalidateMock).toHaveBeenCalledWith({ sync: true }); + expect(navigateMock).not.toHaveBeenCalled(); + expect(screen.getByText('shell.login.error.internal')).toBeDefined(); + expect(getSubmit().hasAttribute('disabled')).toBe(false); + }), + ); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts index 13d2c8387..fd7817d46 100644 --- a/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts @@ -5,17 +5,12 @@ import { TestClock } from 'effect/testing'; import * as actualAuthClient from '../../../../src/api/auth-client.ts' with { rstest: 'importActual', }; -import { - loadModulePageModel, - selectRouteParams, -} from '../../../../src/routes/[lang]/modules/[moduleId]/page.data.ts'; - -const { loadHomePageModelMock, resolveModuleTargetMock } = rstest.hoisted( - () => ({ - loadHomePageModelMock: rstest.fn(), - resolveModuleTargetMock: rstest.fn(), - }) -); +import { loadModulePageModel, selectRouteParams } from '../../../../src/routes/[lang]/modules/[moduleId]/page.data.ts'; + +const { loadHomePageModelMock, resolveModuleTargetMock } = rstest.hoisted(() => ({ + loadHomePageModelMock: rstest.fn(), + resolveModuleTargetMock: rstest.fn(), +})); rstest.mock('../../../../src/api/auth-client.ts', () => ({ ...actualAuthClient, @@ -53,12 +48,7 @@ const request = () => { /** The module program reads its origin from config; pin an empty provider so every case is identical. */ const moduleModel = (input: Parameters[0]) => - loadModulePageModel(input).pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown({}) - ) - ); + loadModulePageModel(input).pipe(Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({}))); beforeEach(() => { loadHomePageModelMock.mockReturnValue(Effect.succeed(authenticatedShell)); @@ -69,7 +59,7 @@ beforeEach(() => { entrypointKey: 'party.registry.page.contacts', moduleId: 'party.registry', writable: false, - }) + }), ); }); @@ -82,79 +72,68 @@ it('selects only declared safe route parameters and omits overlong values', () = moduleId: 'attacker.module', overlong: 'x'.repeat(201), }, - ['id', 'overlong'] - ) + ['id', 'overlong'], + ), ).toEqual({ id: 'party-1' }); }); -it.effect( - 'retains only declared bounded route parameters outside the resolved target identity', - () => - Effect.gen(function* retainsOnlyDeclaredBoundedRouteParameters() { - expect( - yield* moduleModel({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - routeParams: { id: 'party-1' }, - }) - ).toMatchObject({ - routeParams: { id: 'party-1' }, - state: 'resolved', - target: { - appId: 'party-registry', - componentKey: 'party.registry.page-contacts', +it.effect('retains only declared bounded route parameters outside the resolved target identity', () => + Effect.gen(function* retainsOnlyDeclaredBoundedRouteParameters() { + expect( + yield* moduleModel({ + params: { entrypointKey: 'party.registry.page.contacts', moduleId: 'party.registry', }, - }); - expect(resolveModuleTargetMock).toHaveBeenCalledWith( - { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - expect.any(Object) - ); - }) + request: request(), + routeParams: { id: 'party-1' }, + }), + ).toMatchObject({ + routeParams: { id: 'party-1' }, + state: 'resolved', + target: { + appId: 'party-registry', + componentKey: 'party.registry.page-contacts', + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + }); + expect(resolveModuleTargetMock).toHaveBeenCalledWith( + { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + expect.any(Object), + ); + }), ); -it.effect( - 'retains module landing behavior when no exact page entrypoint is supplied', - () => - Effect.gen(function* retainsModuleLandingBehaviorWhenNo() { - expect( - yield* moduleModel({ - params: { moduleId: 'party.registry' }, - request: request(), - }) - ).toMatchObject({ routeParams: {} }); - expect(resolveModuleTargetMock).toHaveBeenCalledWith( - { moduleId: 'party.registry' }, - expect.any(Object) - ); - }) +it.effect('retains module landing behavior when no exact page entrypoint is supplied', () => + Effect.gen(function* retainsModuleLandingBehaviorWhenNo() { + expect( + yield* moduleModel({ + params: { moduleId: 'party.registry' }, + request: request(), + }), + ).toMatchObject({ routeParams: {} }); + expect(resolveModuleTargetMock).toHaveBeenCalledWith({ moduleId: 'party.registry' }, expect.any(Object)); + }), ); -it.effect( - 'does not request or load a private target before authentication', - () => - Effect.gen(function* doesNotRequestOrLoadA() { - loadHomePageModelMock.mockReturnValueOnce( - Effect.succeed({ state: 'anonymous' }) - ); - expect( - yield* moduleModel({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }) - ).toMatchObject({ state: 'selection_required' }); - expect(resolveModuleTargetMock).not.toHaveBeenCalled(); - }) +it.effect('does not request or load a private target before authentication', () => + Effect.gen(function* doesNotRequestOrLoadA() { + loadHomePageModelMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' })); + expect( + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ).toMatchObject({ state: 'selection_required' }); + expect(resolveModuleTargetMock).not.toHaveBeenCalled(); + }), ); it.effect.each([ @@ -162,44 +141,36 @@ it.effect.each([ ['ShellTargetForbiddenProblem', 'forbidden'], ['ShellTargetNotFoundProblem', 'not_found'], ['ShellCapabilityUnavailableProblem', 'unavailable'], -] as const)( - 'maps %s without returning a resolved private target', - ([_tag, state]) => - Effect.gen(function* ShellSelectionRequiredProblem() { - resolveModuleTargetMock.mockReturnValueOnce(Effect.fail({ _tag })); - expect( - yield* moduleModel({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }) - ).toMatchObject({ state }); - }) +] as const)('maps %s without returning a resolved private target', ([_tag, state]) => + Effect.gen(function* ShellSelectionRequiredProblem() { + resolveModuleTargetMock.mockReturnValueOnce(Effect.fail({ _tag })); + expect( + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ).toMatchObject({ state }); + }), ); -it.effect( - 'fails with the typed timeout instead of hanging on an unresponsive shell read', - () => - Effect.gen(function* failsWithTheTypedTimeout() { - const entered = yield* Deferred.make<'entered'>(); - loadHomePageModelMock.mockReturnValueOnce( - Effect.andThen(Deferred.succeed(entered, 'entered'), Effect.never) - ); - const fiber = yield* Effect.forkChild( - moduleModel({ - params: { moduleId: 'party.registry' }, - request: request(), - }) - ); - yield* Deferred.await(entered); - - yield* TestClock.adjust('30 seconds'); - - expect(yield* Effect.flip(Fiber.join(fiber))).toBeInstanceOf( - Cause.TimeoutError - ); - expect(resolveModuleTargetMock).not.toHaveBeenCalled(); - }) +it.effect('fails with the typed timeout instead of hanging on an unresponsive shell read', () => + Effect.gen(function* failsWithTheTypedTimeout() { + const entered = yield* Deferred.make<'entered'>(); + loadHomePageModelMock.mockReturnValueOnce(Effect.andThen(Deferred.succeed(entered, 'entered'), Effect.never)); + const fiber = yield* Effect.forkChild( + moduleModel({ + params: { moduleId: 'party.registry' }, + request: request(), + }), + ); + yield* Deferred.await(entered); + + yield* TestClock.adjust('30 seconds'); + + expect(yield* Effect.flip(Fiber.join(fiber))).toBeInstanceOf(Cause.TimeoutError); + expect(resolveModuleTargetMock).not.toHaveBeenCalled(); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx index d5f9661dd..9e584d843 100644 --- a/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx @@ -9,22 +9,16 @@ import type { ModuleTargetPageModel } from '../../../../src/routes/[lang]/module import ModuleTargetPage from '../../../../src/routes/[lang]/modules/[moduleId]/page.tsx'; import { authenticatedShellFixture } from '../authenticated-shell-fixture.ts'; -type ResolvedPageModel = Extract< - ModuleTargetPageModel, - { readonly state: 'resolved' } ->; +type ResolvedPageModel = Extract; -const { - findApprovedVerticalPageClientMock, - loadRemotePageMock, - remotePropsMock, - useLoaderDataMock, -} = rstest.hoisted(() => ({ - findApprovedVerticalPageClientMock: rstest.fn(), - loadRemotePageMock: rstest.fn(), - remotePropsMock: rstest.fn(), - useLoaderDataMock: rstest.fn(), -})); +const { findApprovedVerticalPageClientMock, loadRemotePageMock, remotePropsMock, useLoaderDataMock } = rstest.hoisted( + () => ({ + findApprovedVerticalPageClientMock: rstest.fn(), + loadRemotePageMock: rstest.fn(), + remotePropsMock: rstest.fn(), + useLoaderDataMock: rstest.fn(), + }), +); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ useModernI18n: () => ({ t: (key: string) => key }), @@ -35,9 +29,7 @@ rstest.mock('@modern-js/plugin-tanstack/runtime', () => ({ })); rstest.mock('@techsio/ui-kit/atoms/status-text', () => ({ - StatusText: ({ children }: { readonly children: ReactNode }) => ( - {children} - ), + StatusText: ({ children }: { readonly children: ReactNode }) => {children}, })); rstest.mock('../../../../src/api/vertical-clients.ts', () => ({ @@ -45,11 +37,7 @@ rstest.mock('../../../../src/api/vertical-clients.ts', () => ({ })); rstest.mock('../../../../src/routes/shell-frame.tsx', () => ({ - AuthenticatedDashboardLayout: ({ - children, - }: { - readonly children: ReactNode; - }) =>
{children}
, + AuthenticatedDashboardLayout: ({ children }: { readonly children: ReactNode }) =>
{children}
, })); rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({ @@ -69,11 +57,7 @@ rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({ const shell: ResolvedPageModel['shell'] = authenticatedShellFixture(); -const targetFixture = ( - componentKey: string, - entrypointKey: string, - writable = true -) => +const targetFixture = (componentKey: string, entrypointKey: string, writable = true) => Schema.decodeUnknownSync(ResolvedModuleTargetSchema)({ appId: 'contacts', componentKey, @@ -86,10 +70,7 @@ const resolvedModel: ResolvedPageModel = { routeParams: { id: 'customer-1' }, shell, state: 'resolved', - target: targetFixture( - 'contacts.core.page-customers', - 'contacts.core.page.customers' - ), + target: targetFixture('contacts.core.page-customers', 'contacts.core.page.customers'), }; interface ExactPageCase { @@ -111,8 +92,7 @@ const exactPageCases: ExactPageCase[] = [ { componentKey: 'contacts.core.page-customer-detail', entrypointKey: 'contacts.core.page.customer-detail', - renderedText: - 'contacts.core.page-customer-detail:11111111-1111-4111-8111-111111111111', + renderedText: 'contacts.core.page-customer-detail:11111111-1111-4111-8111-111111111111', routeParams: { id: '11111111-1111-4111-8111-111111111111' }, writable: true, }, @@ -133,8 +113,7 @@ const exactPageCases: ExactPageCase[] = [ { componentKey: 'contacts.core.page-contact-detail', entrypointKey: 'contacts.core.page.contact-detail', - renderedText: - 'contacts.core.page-contact-detail:11111111-1111-4111-8111-111111111111', + renderedText: 'contacts.core.page-contact-detail:11111111-1111-4111-8111-111111111111', routeParams: { contactId: '33333333-3333-4333-8333-333333333333', id: '11111111-1111-4111-8111-111111111111', @@ -144,8 +123,7 @@ const exactPageCases: ExactPageCase[] = [ { componentKey: 'contacts.core.page-contact-edit', entrypointKey: 'contacts.core.page.contact-edit', - renderedText: - 'contacts.core.page-contact-edit:11111111-1111-4111-8111-111111111111', + renderedText: 'contacts.core.page-contact-edit:11111111-1111-4111-8111-111111111111', routeParams: { contactId: '33333333-3333-4333-8333-333333333333', id: '11111111-1111-4111-8111-111111111111', @@ -155,8 +133,7 @@ const exactPageCases: ExactPageCase[] = [ { componentKey: 'contacts.core.page-contact-create', entrypointKey: 'contacts.core.page.contact-create', - renderedText: - 'contacts.core.page-contact-create:11111111-1111-4111-8111-111111111111', + renderedText: 'contacts.core.page-contact-create:11111111-1111-4111-8111-111111111111', routeParams: { id: '11111111-1111-4111-8111-111111111111' }, writable: false, }, @@ -175,9 +152,7 @@ beforeEach(() => { }; }) => { remotePropsMock({ routeParams, target }); - return ( -
{`${target.componentKey}:${routeParams['id'] ?? 'static'}`}
- ); + return
{`${target.componentKey}:${routeParams['id'] ?? 'static'}`}
; }, }); findApprovedVerticalPageClientMock.mockReturnValue({ @@ -190,12 +165,7 @@ afterEach(() => { rstest.clearAllMocks(); }); -it.each([ - 'selection_required', - 'forbidden', - 'not_found', - 'unavailable', -] as const)( +it.each(['selection_required', 'forbidden', 'not_found', 'unavailable'] as const)( 'does not consult or invoke the private registry for a %s exact-page response', (state) => { useLoaderDataMock.mockReturnValue({ @@ -205,40 +175,26 @@ it.each([ render(); expect(findApprovedVerticalPageClientMock).not.toHaveBeenCalled(); expect(loadRemotePageMock).not.toHaveBeenCalled(); - } + }, ); -it.live( - 'invokes the exact private page loader only after a resolved authenticated response', - () => - Effect.gen(function* invokesTheExactPrivatePageLoader() { - useLoaderDataMock.mockReturnValue(resolvedModel); - render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith( - resolvedModel.target - ); - yield* Effect.promise(() => - waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)) - ); - expect( - yield* Effect.promise(() => - screen.findByText('contacts.core.page-customers:customer-1') - ) - ).toBeTruthy(); - }) +it.live('invokes the exact private page loader only after a resolved authenticated response', () => + Effect.gen(function* invokesTheExactPrivatePageLoader() { + useLoaderDataMock.mockReturnValue(resolvedModel); + render(); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(resolvedModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:customer-1'))).toBeTruthy(); + }), ); it('reads loader data from the active Party Registry owner route', () => { - useLoaderDataMock.mockImplementation( - ({ from }: { readonly from: string }) => { - if (from !== '/$lang/contacts') { - throw new Error( - `Invariant failed: Could not find an active match from "${from}"` - ); - } - return resolvedModel; + useLoaderDataMock.mockImplementation(({ from }: { readonly from: string }) => { + if (from !== '/$lang/contacts') { + throw new Error(`Invariant failed: Could not find an active match from "${from}"`); } - ); + return resolvedModel; + }); expect(() => render()).not.toThrow(); expect(useLoaderDataMock).toHaveBeenCalledWith({ @@ -247,23 +203,15 @@ it('reads loader data from the active Party Registry owner route', () => { }); }); -it.live( - 'maps an unreachable approved remote to its safe local diagnostic', - () => - Effect.gen(function* mapsAnUnreachableApprovedRemoteTo() { - loadRemotePageMock.mockRejectedValueOnce( - new Error('private remote error') - ); - useLoaderDataMock.mockReturnValue(resolvedModel); +it.live('maps an unreachable approved remote to its safe local diagnostic', () => + Effect.gen(function* mapsAnUnreachableApprovedRemoteTo() { + loadRemotePageMock.mockRejectedValueOnce(new Error('private remote error')); + useLoaderDataMock.mockReturnValue(resolvedModel); - render(); + render(); - expect( - yield* Effect.promise(() => - screen.findByText('shell.moduleTarget.unavailable') - ) - ).toBeTruthy(); - }) + expect(yield* Effect.promise(() => screen.findByText('shell.moduleTarget.unavailable'))).toBeTruthy(); + }), ); it.live('rejects a malformed remote module before React receives it', () => @@ -273,30 +221,18 @@ it.live('rejects a malformed remote module before React receives it', () => render(); - expect( - yield* Effect.promise(() => - screen.findByText('shell.moduleTarget.incompatible') - ) - ).toBeTruthy(); + expect(yield* Effect.promise(() => screen.findByText('shell.moduleTarget.incompatible'))).toBeTruthy(); expect(remotePropsMock).not.toHaveBeenCalled(); - }) + }), ); -it.live( - 'passes an empty route-parameter record to a resolved static page', - () => - Effect.gen(function* passesAnEmptyRouteParameterRecord() { - useLoaderDataMock.mockReturnValue({ ...resolvedModel, routeParams: {} }); - render(); - yield* Effect.promise(() => - waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)) - ); - expect( - yield* Effect.promise(() => - screen.findByText('contacts.core.page-customers:static') - ) - ).toBeTruthy(); - }) +it.live('passes an empty route-parameter record to a resolved static page', () => + Effect.gen(function* passesAnEmptyRouteParameterRecord() { + useLoaderDataMock.mockReturnValue({ ...resolvedModel, routeParams: {} }); + render(); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:static'))).toBeTruthy(); + }), ); it.live.each(exactPageCases)( @@ -312,18 +248,12 @@ it.live.each(exactPageCases)( render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith( - exactModel.target - ); - yield* Effect.promise(() => - waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)) - ); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(exactModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); expect(remotePropsMock).toHaveBeenCalledWith({ routeParams, target: exactModel.target, }); - expect( - yield* Effect.promise(() => screen.findByText(renderedText)) - ).toBeTruthy(); - }) + expect(yield* Effect.promise(() => screen.findByText(renderedText))).toBeTruthy(); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx index da92add5a..7c831cc98 100644 --- a/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx @@ -4,14 +4,8 @@ import { Deferred, Effect, Schema } from 'effect'; import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; import type { ReactNode } from 'react'; -import { - ShellResourceResponseSchema, - ShellTargetForbiddenProblemSchema, -} from '../../../../shared/api.ts'; -import type { - MediaAttachmentResponse, - ShellResourceResponse, -} from '../../../../shared/api.ts'; +import { ShellResourceResponseSchema, ShellTargetForbiddenProblemSchema } from '../../../../shared/api.ts'; +import type { MediaAttachmentResponse, ShellResourceResponse } from '../../../../shared/api.ts'; import type { ResourcePageModel } from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts'; import ResourcePage from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.tsx'; import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { @@ -27,22 +21,17 @@ interface DashboardPageProps { readonly title: string; } -const { - attachResourceMediaMock, - browserRunPromiseMock, - dashboardRenders, - shellControlsMock, - useLoaderDataMock, -} = rstest.hoisted(() => { - const renders: DashboardPageProps[] = []; - return { - attachResourceMediaMock: rstest.fn(), - browserRunPromiseMock: rstest.fn(), - dashboardRenders: renders, - shellControlsMock: rstest.fn(), - useLoaderDataMock: rstest.fn(), - }; -}); +const { attachResourceMediaMock, browserRunPromiseMock, dashboardRenders, shellControlsMock, useLoaderDataMock } = + rstest.hoisted(() => { + const renders: DashboardPageProps[] = []; + return { + attachResourceMediaMock: rstest.fn(), + browserRunPromiseMock: rstest.fn(), + dashboardRenders: renders, + shellControlsMock: rstest.fn(), + useLoaderDataMock: rstest.fn(), + }; + }); const translations = new Map( Object.entries({ @@ -65,7 +54,7 @@ const translations = new Map( 'shell.resource.timeline.title': 'Timeline', 'shell.resource.title': 'Resource', 'shell.resource.unavailable': 'This resource is unavailable', - }) + }), ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ @@ -92,10 +81,7 @@ rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({ })); rstest.mock('../../../../src/routes/shell-frame.tsx', () => ({ - AuthenticatedDashboardLayout: ({ - children, - ...props - }: DashboardPageProps & { readonly children: ReactNode }) => { + AuthenticatedDashboardLayout: ({ children, ...props }: DashboardPageProps & { readonly children: ReactNode }) => { dashboardRenders.push(props); return (
@@ -111,9 +97,7 @@ const shell: ReadyModel['shell'] = authenticatedShellFixture(); const attachedResponse: MediaAttachmentResponse = { attached: true }; const resourceFixture = ( - overrides: Partial< - Schema.Codec.Encoded - > = {} + overrides: Partial> = {}, ): ShellResourceResponse => Schema.decodeUnknownSync(ShellResourceResponseSchema)({ detail: { @@ -140,17 +124,13 @@ const resourceFixture = ( ...overrides, }); -const readyModel = ( - resource: ShellResourceResponse = resourceFixture() -): ReadyModel => ({ +const readyModel = (resource: ShellResourceResponse = resourceFixture()): ReadyModel => ({ resource, shell, state: 'ready', }); -const forbiddenProblem = Schema.decodeUnknownSync( - ShellTargetForbiddenProblemSchema -)({ +const forbiddenProblem = Schema.decodeUnknownSync(ShellTargetForbiddenProblemSchema)({ _tag: 'ShellTargetForbiddenProblem', detail: 'The principal cannot attach media to this resource.', status: 403, @@ -217,7 +197,7 @@ it.each([ expect(shellControlsMock).toHaveBeenCalledWith(undefined); expect(attachResourceMediaMock).not.toHaveBeenCalled(); expect(browserRunPromiseMock).not.toHaveBeenCalled(); - } + }, ); const closedStates: readonly { @@ -244,7 +224,7 @@ it.each(closedStates)( expect(shellControlsMock).toHaveBeenCalledWith(shell); expect(attachResourceMediaMock).not.toHaveBeenCalled(); expect(browserRunPromiseMock).not.toHaveBeenCalled(); - } + }, ); it('titles the dashboard from the resource and scopes it to the owning module', () => { @@ -252,9 +232,7 @@ it('titles the dashboard from the resource and scopes it to the owning module', expect(lastDashboardProps().title).toBe('Invoice 42'); expect(lastDashboardProps().currentModuleId).toBe('billing.invoices'); - expect( - screen.getByRole('heading', { level: 2, name: 'Invoice 42' }) - ).toBeTruthy(); + expect(screen.getByRole('heading', { level: 2, name: 'Invoice 42' })).toBeTruthy(); expect(screen.getByText('Status')).toBeTruthy(); expect(screen.getByText('1 250,00 CZK')).toBeTruthy(); }); @@ -269,9 +247,7 @@ it('renders one timeline entry per projection row with its ISO instant', () => { }); it('announces an empty and a lagging timeline projection', () => { - renderResourcePage( - readyModel(resourceFixture({ projectionLagging: true, timeline: [] })) - ); + renderResourcePage(readyModel(resourceFixture({ projectionLagging: true, timeline: [] }))); expect(screen.getByText('No timeline entries yet')).toBeTruthy(); expect(screen.getByText('The timeline is catching up')).toBeTruthy(); @@ -292,9 +268,7 @@ it.live.each(disabledMediaCases)( ({ blockedText, reason }) => Effect.gen(function* refusesADisabledAttachSeam() { const user = userEvent.setup(); - renderResourcePage( - readyModel(resourceFixture({ media: { enabled: false, reason } })) - ); + renderResourcePage(readyModel(resourceFixture({ media: { enabled: false, reason } }))); expect(screen.getByText(blockedText)).toBeTruthy(); expect(attachButton().hasAttribute('disabled')).toBe(true); @@ -305,7 +279,7 @@ it.live.each(disabledMediaCases)( expect(browserRunPromiseMock).not.toHaveBeenCalled(); expect(screen.queryByText('Media attached')).toBeNull(); expect(screen.queryByText('Attaching the media failed')).toBeNull(); - }) + }), ); it('marks the attachment pending inside the very click that starts it', () => { @@ -337,99 +311,73 @@ it('starts one attachment for a double click, because the first click already di expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); }); -it.live( - 'holds the attach seam disabled for the whole in-flight attachment', - () => - Effect.gen(function* holdsTheAttachSeamDisabled() { - const gate = yield* Deferred.make(); - attachResourceMediaMock.mockReturnValue(Deferred.await(gate)); - const user = userEvent.setup(); - renderResourcePage(readyModel()); +it.live('holds the attach seam disabled for the whole in-flight attachment', () => + Effect.gen(function* holdsTheAttachSeamDisabled() { + const gate = yield* Deferred.make(); + attachResourceMediaMock.mockReturnValue(Deferred.await(gate)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); - yield* Effect.promise(() => user.click(attachButton())); - yield* Effect.promise(() => - waitFor(() => expect(screen.getByText('Attaching media…')).toBeTruthy()) - ); - expect(attachButton().hasAttribute('disabled')).toBe(true); + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching media…')).toBeTruthy())); + expect(attachButton().hasAttribute('disabled')).toBe(true); - yield* Effect.promise(() => user.click(attachButton())); - expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); - expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); - - yield* Deferred.succeed(gate, attachedResponse); - yield* Effect.promise(() => - waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()) - ); - expect(attachButton().hasAttribute('disabled')).toBe(false); - }) + yield* Effect.promise(() => user.click(attachButton())); + expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); + + yield* Deferred.succeed(gate, attachedResponse); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy())); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), ); -it.live( - 'attaches media for the loaded resource reference and reports success once', - () => - Effect.gen(function* attachesMediaForTheLoadedResource() { - const user = userEvent.setup(); - const model = readyModel(); - renderResourcePage(model); +it.live('attaches media for the loaded resource reference and reports success once', () => + Effect.gen(function* attachesMediaForTheLoadedResource() { + const user = userEvent.setup(); + const model = readyModel(); + renderResourcePage(model); - yield* Effect.promise(() => user.click(attachButton())); - yield* Effect.promise(() => - waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()) - ); + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy())); - expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); - expect(attachResourceMediaMock).toHaveBeenCalledWith(model.resource.ref); - expect(screen.queryByText('Attaching media…')).toBeNull(); - expect(screen.queryByText('Attaching the media failed')).toBeNull(); - expect(attachButton().hasAttribute('disabled')).toBe(false); - }) + expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); + expect(attachResourceMediaMock).toHaveBeenCalledWith(model.resource.ref); + expect(screen.queryByText('Attaching media…')).toBeNull(); + expect(screen.queryByText('Attaching the media failed')).toBeNull(); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), ); -it.live( - 'settles a typed attachment failure into its own status without a defect', - () => - Effect.gen(function* settlesATypedAttachmentFailure() { - attachResourceMediaMock.mockReturnValue(Effect.fail(forbiddenProblem)); - const user = userEvent.setup(); - renderResourcePage(readyModel()); +it.live('settles a typed attachment failure into its own status without a defect', () => + Effect.gen(function* settlesATypedAttachmentFailure() { + attachResourceMediaMock.mockReturnValue(Effect.fail(forbiddenProblem)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); - yield* Effect.promise(() => user.click(attachButton())); - yield* Effect.promise(() => - waitFor(() => - expect(screen.getByText('Attaching the media failed')).toBeTruthy() - ) - ); + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy())); - expect(screen.queryByText('Media attached')).toBeNull(); - expect(screen.queryByText('Attaching media…')).toBeNull(); - expect(attachButton().hasAttribute('disabled')).toBe(false); - }) + expect(screen.queryByText('Media attached')).toBeNull(); + expect(screen.queryByText('Attaching media…')).toBeNull(); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), ); -it.live( - 'retries after a failure and replaces the failure status with success', - () => - Effect.gen(function* retriesAfterAFailure() { - attachResourceMediaMock.mockReturnValueOnce( - Effect.fail(forbiddenProblem) - ); - const user = userEvent.setup(); - renderResourcePage(readyModel()); +it.live('retries after a failure and replaces the failure status with success', () => + Effect.gen(function* retriesAfterAFailure() { + attachResourceMediaMock.mockReturnValueOnce(Effect.fail(forbiddenProblem)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); - yield* Effect.promise(() => user.click(attachButton())); - yield* Effect.promise(() => - waitFor(() => - expect(screen.getByText('Attaching the media failed')).toBeTruthy() - ) - ); + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy())); - yield* Effect.promise(() => user.click(attachButton())); - yield* Effect.promise(() => - waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()) - ); + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy())); - expect(attachResourceMediaMock).toHaveBeenCalledTimes(2); - expect(browserRunPromiseMock).toHaveBeenCalledTimes(2); - expect(screen.queryByText('Attaching the media failed')).toBeNull(); - }) + expect(attachResourceMediaMock).toHaveBeenCalledTimes(2); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Attaching the media failed')).toBeNull(); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx index cb8f9dd85..737e755a7 100644 --- a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx @@ -17,10 +17,7 @@ import SearchPage from '../../../../src/routes/[lang]/search/page.tsx'; import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; -import type { - LocalizedLinkCall, - LocalizedLinkDoubleProps, -} from '../../../support/localized-link-double.tsx'; +import type { LocalizedLinkCall, LocalizedLinkDoubleProps } from '../../../support/localized-link-double.tsx'; import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx'; const { @@ -67,7 +64,7 @@ const translations = new Map( 'shell.search.submit': 'Search', 'shell.search.title': 'Search', 'shell.search.unavailable': 'Search unavailable', - }) + }), ); rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ @@ -100,24 +97,14 @@ rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ browserRuntime: { runPromise: browserRunPromiseMock }, })); -const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( - '00000000-0000-4000-8000-000000000001' -); -const tenantId = Schema.decodeUnknownSync(TenantIdSchema)( - '00000000-0000-4000-8000-000000000101' -); -const legalEntityId = Schema.decodeUnknownSync(LegalEntityIdSchema)( - '00000000-0000-4000-8000-000000000201' -); +const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)('00000000-0000-4000-8000-000000000001'); +const tenantId = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101'); +const legalEntityId = Schema.decodeUnknownSync(LegalEntityIdSchema)('00000000-0000-4000-8000-000000000201'); const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app'); -const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)( - 'shell.navigation.modules' -); -const inventoryModuleId = - Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); +const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules'); +const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock'); const plainResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit-1'); -const awkwardResourceId = - Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2'); +const awkwardResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2'); const authenticatedShell = (): HomePageModel => ({ contextState: 'authenticated', @@ -157,10 +144,7 @@ const authenticatedShell = (): HomePageModel => ({ }, }); -const readyModel = ( - resourceType: string, - resourceId: typeof plainResourceId -): SearchPageModel => ({ +const readyModel = (resourceType: string, resourceId: typeof plainResourceId): SearchPageModel => ({ query: 'unit', response: { partial: false, @@ -176,18 +160,13 @@ const readyModel = ( state: 'ready', }); -const resourceLinkCalls = () => - localizedLinkCalls.filter((call) => call.to.startsWith('/resources')); +const resourceLinkCalls = () => localizedLinkCalls.filter((call) => call.to.startsWith('/resources')); beforeEach(() => { browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); - switchTenantMock.mockReturnValue( - Effect.succeed({ selectedTenantId: tenantId }) - ); - switchLegalEntityMock.mockReturnValue( - Effect.succeed({ selectedLegalEntityId: legalEntityId }) - ); + switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId })); + switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId })); useLoaderDataMock.mockReturnValue(readyModel('stock-item', plainResourceId)); }); @@ -210,27 +189,23 @@ test('a search result hands the canonical resource route to the framework link', resourceType: 'stock-item', }); expect(resultCall?.href).toBeUndefined(); - expect( - screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') - ).toBe('/en/resources/inventory.stock/stock-item/unit-1'); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/en/resources/inventory.stock/stock-item/unit-1', + ); }); test('a search result resolves the Czech resource route from the same canonical target', () => { languageState.current = 'cs'; render(); - expect(resourceLinkCalls()[0]?.to).toBe( - '/resources/$moduleId/$resourceType/$resourceId' + expect(resourceLinkCalls()[0]?.to).toBe('/resources/$moduleId/$resourceType/$resourceId'); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/cs/zdroje/inventory.stock/stock-item/unit-1', ); - expect( - screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') - ).toBe('/cs/zdroje/inventory.stock/stock-item/unit-1'); }); test('resource path segments stay percent-encoded per segment', () => { - useLoaderDataMock.mockReturnValue( - readyModel('stock item', awkwardResourceId) - ); + useLoaderDataMock.mockReturnValue(readyModel('stock item', awkwardResourceId)); render(); expect(resourceLinkCalls()[0]?.params).toEqual({ @@ -238,9 +213,9 @@ test('resource path segments stay percent-encoded per segment', () => { resourceId: 'unit #1/2', resourceType: 'stock item', }); - expect( - screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href') - ).toBe('/en/resources/inventory.stock/stock%20item/unit%20%231%2F2'); + expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe( + '/en/resources/inventory.stock/stock%20item/unit%20%231%2F2', + ); }); test('an empty result set exposes no resource affordance', () => { diff --git a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts index 1bf1be134..d553e4ecd 100644 --- a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts @@ -1,7 +1,4 @@ -import { - buildInstalledModuleCatalog, - resolveInstalledModuleCatalog, -} from '@app/core-runtime'; +import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog } from '@app/core-runtime'; import type { ContextAccessDecision, ContextAccessService, @@ -18,27 +15,14 @@ const tenantId = '10000000-0000-4000-8000-000000000001'; const legalEntityId = '20000000-0000-4000-8000-000000000001'; const principalId = '30000000-0000-4000-8000-000000000001'; -const deployment = ( - appId: string, - moduleId: string, - displayName: string, - order: number -) => ({ +const deployment = (appId: string, moduleId: string, displayName: string, order: number) => ({ deployment: { appId, buildMarker: `build-${appId}` }, manifest: { activation: { defaultState: 'inactive', preservesHistoryWhenInactive: true, scope: 'tenant', - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], }, module: { description: `${displayName} capability.`, @@ -115,21 +99,11 @@ const deployment = ( const catalog = (): InstalledModuleCatalog => buildInstalledModuleCatalog([ { - contract: deployment( - 'property-registry', - 'property.registry', - 'Property', - 20 - ), + contract: deployment('property-registry', 'property.registry', 'Property', 20), expectedAppId: 'property-registry', }, { - contract: deployment( - 'documents-center', - 'documents.center', - 'Documents', - 10 - ), + contract: deployment('documents-center', 'documents.center', 'Documents', 10), expectedAppId: 'documents-center', }, ]); @@ -152,13 +126,10 @@ const catalogWithNumberLikeOrder = (): InstalledModuleCatalog => { ...contract.manifest.publicSurface, shellContributions: { ...contract.manifest.publicSurface.shellContributions, - navigation: - contract.manifest.publicSurface.shellContributions.navigation.map( - (contribution) => ({ - ...contribution, - order: numberLikeOrder(contribution.order), - }) - ), + navigation: contract.manifest.publicSurface.shellContributions.navigation.map((contribution) => ({ + ...contribution, + order: numberLikeOrder(contribution.order), + })), }, }, }, @@ -167,12 +138,7 @@ const catalogWithNumberLikeOrder = (): InstalledModuleCatalog => { }; const catalogWithSecondPropertyPage = (): InstalledModuleCatalog => { - const property = deployment( - 'property-registry', - 'property.registry', - 'Property', - 20 - ); + const property = deployment('property-registry', 'property.registry', 'Property', 20); property.manifest.publicSurface.components.push({ expose: './PageCustomers', key: 'property.registry.page-customers', @@ -197,12 +163,7 @@ const catalogWithSecondPropertyPage = (): InstalledModuleCatalog => { return buildInstalledModuleCatalog([ { contract: property, expectedAppId: 'property-registry' }, { - contract: deployment( - 'documents-center', - 'documents.center', - 'Documents', - 10 - ), + contract: deployment('documents-center', 'documents.center', 'Documents', 10), expectedAppId: 'documents-center', }, ]); @@ -210,14 +171,12 @@ const catalogWithSecondPropertyPage = (): InstalledModuleCatalog => { const contextAccess = ( decisions: Readonly>, - onBatch?: (moduleIds: readonly string[]) => void + onBatch?: (moduleIds: readonly string[]) => void, ): ContextAccessService => ({ legalEntities: () => Effect.succeed([]), modules: ({ moduleIds }) => { onBatch?.(moduleIds); - return Effect.succeed( - moduleIds.map((key) => ({ decision: decisions[key] ?? 'denied', key })) - ); + return Effect.succeed(moduleIds.map((key) => ({ decision: decisions[key] ?? 'denied', key }))); }, resources: () => Effect.succeed([]), tenants: () => Effect.succeed([]), @@ -225,149 +184,123 @@ const contextAccess = ( const context = { legalEntityId, principalId, tenantId } as const; -it.effect( - 'composes one deterministic state and permission batch with lifecycle affordances', - () => - Effect.gen(function* composesOneDeterministicStateAndPermission() { - let stateBatches = 0; - let permissionBatches = 0; - const composition = makeShellComposition({ - catalog: Effect.succeed(catalog()), - contextAccess: contextAccess( - { 'documents.center': 'allowed', 'property.registry': 'allowed' }, - () => (permissionBatches += 1) - ), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => { - stateBatches += 1; - return Effect.succeed( - moduleIds.map((moduleKey) => ({ - moduleKey, - state: - moduleKey === 'documents.center' - ? ('read_only' as const) - : ('deprecated' as const), - })) - ); - }, - }, - }); - const result = yield* composition.compose(context); - expect(result).toEqual({ - navigation: [ - { - appId: 'documents-center', - enabled: true, - groupKey: 'shell.navigation.modules', - href: '/documents-center', - label: 'Documents', - moduleId: 'documents.center', - order: 10, - state: 'read_only', - unavailable: false, - writable: false, - }, - { - appId: 'property-registry', - enabled: true, - groupKey: 'shell.navigation.modules', - href: '/property-registry', - label: 'Property', - moduleId: 'property.registry', - order: 20, - state: 'deprecated', - unavailable: false, - writable: false, - }, - ], - state: 'available', - unavailableDeployments: [], - }); - expect({ permissionBatches, stateBatches }).toEqual({ - permissionBatches: 1, - stateBatches: 1, - }); - }) -); - -it.effect( - 'keeps healthy navigation and exposes failed installed deployments separately', - () => - Effect.gen(function* keepsHealthyNavigationAndExposesFailed() { - const degradedCatalog = resolveInstalledModuleCatalog([ - { - contract: deployment( - 'documents-center', - 'documents.center', - 'Documents', - 10 - ), - expectedAppId: 'documents-center', - outcome: 'fetched', +it.effect('composes one deterministic state and permission batch with lifecycle affordances', () => + Effect.gen(function* composesOneDeterministicStateAndPermission() { + let stateBatches = 0; + let permissionBatches = 0; + const composition = makeShellComposition({ + catalog: Effect.succeed(catalog()), + contextAccess: contextAccess( + { 'documents.center': 'allowed', 'property.registry': 'allowed' }, + () => (permissionBatches += 1), + ), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => { + stateBatches += 1; + return Effect.succeed( + moduleIds.map((moduleKey) => ({ + moduleKey, + state: moduleKey === 'documents.center' ? ('read_only' as const) : ('deprecated' as const), + })), + ); }, + }, + }); + const result = yield* composition.compose(context); + expect(result).toEqual({ + navigation: [ { - expectedAppId: 'property-registry', - outcome: 'failed', - reason: 'timeout', - }, - ]); - const result = yield* makeShellComposition({ - catalog: Effect.succeed(degradedCatalog), - contextAccess: contextAccess({ 'documents.center': 'allowed' }), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' })) - ), + appId: 'documents-center', + enabled: true, + groupKey: 'shell.navigation.modules', + href: '/documents-center', + label: 'Documents', + moduleId: 'documents.center', + order: 10, + state: 'read_only', + unavailable: false, + writable: false, }, - }).compose(context); - - expect(result.state).toBe('available'); - if (result.state !== 'available') { - throw new Error('expected an available degraded composition'); - } - expect(result.navigation.map(({ moduleId }) => moduleId)).toEqual([ - 'documents.center', - ]); - expect(result.unavailableDeployments).toEqual([ { appId: 'property-registry', - reason: 'timeout', - status: 'unavailable', + enabled: true, + groupKey: 'shell.navigation.modules', + href: '/property-registry', + label: 'Property', + moduleId: 'property.registry', + order: 20, + state: 'deprecated', + unavailable: false, + writable: false, }, - ]); - expect(() => - Schema.decodeUnknownSync(ShellCompositionSchema)(result) - ).not.toThrow(); - }) + ], + state: 'available', + unavailableDeployments: [], + }); + expect({ permissionBatches, stateBatches }).toEqual({ + permissionBatches: 1, + stateBatches: 1, + }); + }), ); -it.effect( - 'normalizes number-like module order before returning the public composition', - () => - Effect.gen(function* normalizesNumberLikeModuleOrderBefore() { - const result = yield* makeShellComposition({ - catalog: Effect.succeed(catalogWithNumberLikeOrder()), - contextAccess: contextAccess({ - 'documents.center': 'allowed', - 'property.registry': 'allowed', - }), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' })) - ), - }, - }).compose(context); +it.effect('keeps healthy navigation and exposes failed installed deployments separately', () => + Effect.gen(function* keepsHealthyNavigationAndExposesFailed() { + const degradedCatalog = resolveInstalledModuleCatalog([ + { + contract: deployment('documents-center', 'documents.center', 'Documents', 10), + expectedAppId: 'documents-center', + outcome: 'fetched', + }, + { + expectedAppId: 'property-registry', + outcome: 'failed', + reason: 'timeout', + }, + ]); + const result = yield* makeShellComposition({ + catalog: Effect.succeed(degradedCatalog), + contextAccess: contextAccess({ 'documents.center': 'allowed' }), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), + }, + }).compose(context); - expect(result.navigation.map(({ order }) => order)).toEqual([10, 20]); - expect( - result.navigation.every(({ order }) => Object.is(order, Number(order))) - ).toBe(true); - expect(() => - Schema.decodeUnknownSync(ShellCompositionSchema)(result) - ).not.toThrow(); - }) + expect(result.state).toBe('available'); + if (result.state !== 'available') { + throw new Error('expected an available degraded composition'); + } + expect(result.navigation.map(({ moduleId }) => moduleId)).toEqual(['documents.center']); + expect(result.unavailableDeployments).toEqual([ + { + appId: 'property-registry', + reason: 'timeout', + status: 'unavailable', + }, + ]); + expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); + }), +); + +it.effect('normalizes number-like module order before returning the public composition', () => + Effect.gen(function* normalizesNumberLikeModuleOrderBefore() { + const result = yield* makeShellComposition({ + catalog: Effect.succeed(catalogWithNumberLikeOrder()), + contextAccess: contextAccess({ + 'documents.center': 'allowed', + 'property.registry': 'allowed', + }), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), + }, + }).compose(context); + + expect(result.navigation.map(({ order }) => order)).toEqual([10, 20]); + expect(result.navigation.every(({ order }) => Object.is(order, Number(order)))).toBe(true); + expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); + }), ); it.effect.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( @@ -382,9 +315,7 @@ it.effect.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( }), moduleStates: { getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state })) - ), + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), }, }).compose(context); expect(result).toEqual({ @@ -392,99 +323,90 @@ it.effect.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( state: 'available', unavailableDeployments: [], }); - }) + }), ); -it.effect( - 'omits definite denial while preserving unavailable authorization as disabled', - () => - Effect.gen(function* omitsDefiniteDenialWhilePreservingUnavailable() { - const result = yield* makeShellComposition({ - catalog: Effect.succeed(catalog()), - contextAccess: contextAccess({ - 'documents.center': 'denied', - 'property.registry': 'unavailable', - }), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' })) - ), - }, - }).compose(context); - expect(result.state).toBe('available'); - expect(result.navigation).toEqual([ - { - appId: 'property-registry', - enabled: false, - groupKey: 'shell.navigation.modules', - label: 'Property', - moduleId: 'property.registry', - order: 20, - state: 'active', - unavailable: true, - writable: true, - }, - ]); - }) +it.effect('omits definite denial while preserving unavailable authorization as disabled', () => + Effect.gen(function* omitsDefiniteDenialWhilePreservingUnavailable() { + const result = yield* makeShellComposition({ + catalog: Effect.succeed(catalog()), + contextAccess: contextAccess({ + 'documents.center': 'denied', + 'property.registry': 'unavailable', + }), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), + }, + }).compose(context); + expect(result.state).toBe('available'); + expect(result.navigation).toEqual([ + { + appId: 'property-registry', + enabled: false, + groupKey: 'shell.navigation.modules', + label: 'Property', + moduleId: 'property.registry', + order: 20, + state: 'active', + unavailable: true, + writable: true, + }, + ]); + }), ); -it.effect( - 'resolves direct targets independently with exhaustive safe outcomes and historical reads', - () => - Effect.gen(function* resolvesDirectTargetsIndependentlyWithExhaustive() { - let state: TenantModuleState = 'active'; - let decision: ContextAccessDecision = 'allowed'; - const mutableAccess = contextAccess({}); - const composition = makeShellComposition({ - catalog: Effect.succeed(catalog()), - contextAccess: { - ...mutableAccess, - modules: ({ moduleIds }) => - Effect.succeed(moduleIds.map((key) => ({ decision, key }))), - }, - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state })) - ), - }, - }); - const resolved = yield* composition.resolveModuleTarget(context, { - moduleId: 'property.registry', - }); - expect(resolved.outcome).toBe('resolved'); - decision = 'denied'; - const forbidden = yield* composition.resolveModuleTarget(context, { - moduleId: 'property.registry', - }); - expect(forbidden.outcome).toBe('forbidden'); - decision = 'unavailable'; - const unavailable = yield* composition.resolveModuleTarget(context, { - moduleId: 'property.registry', - }); - expect(unavailable.outcome).toBe('unavailable'); - decision = 'allowed'; - state = 'archived'; - const archived = yield* composition.resolveModuleTarget(context, { - moduleId: 'property.registry', - }); - expect(archived.outcome).toBe('not_found'); - const historical = yield* composition.resolveModuleTarget(context, { - access: 'historical_read', - moduleId: 'property.registry', - }); - expect(historical.outcome).toBe('resolved'); - const selectionRequired = yield* composition.resolveModuleTarget( - { principalId, tenantId }, - { moduleId: 'property.registry' } - ); - expect(selectionRequired.outcome).toBe('selection_required'); - const missing = yield* composition.resolveModuleTarget(context, { - moduleId: 'missing.module', - }); - expect(missing.outcome).toBe('not_found'); - }) +it.effect('resolves direct targets independently with exhaustive safe outcomes and historical reads', () => + Effect.gen(function* resolvesDirectTargetsIndependentlyWithExhaustive() { + let state: TenantModuleState = 'active'; + let decision: ContextAccessDecision = 'allowed'; + const mutableAccess = contextAccess({}); + const composition = makeShellComposition({ + catalog: Effect.succeed(catalog()), + contextAccess: { + ...mutableAccess, + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))), + }, + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), + }, + }); + const resolved = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(resolved.outcome).toBe('resolved'); + decision = 'denied'; + const forbidden = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(forbidden.outcome).toBe('forbidden'); + decision = 'unavailable'; + const unavailable = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(unavailable.outcome).toBe('unavailable'); + decision = 'allowed'; + state = 'archived'; + const archived = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(archived.outcome).toBe('not_found'); + const historical = yield* composition.resolveModuleTarget(context, { + access: 'historical_read', + moduleId: 'property.registry', + }); + expect(historical.outcome).toBe('resolved'); + const selectionRequired = yield* composition.resolveModuleTarget( + { principalId, tenantId }, + { moduleId: 'property.registry' }, + ); + expect(selectionRequired.outcome).toBe('selection_required'); + const missing = yield* composition.resolveModuleTarget(context, { + moduleId: 'missing.module', + }); + expect(missing.outcome).toBe('not_found'); + }), ); it.effect.each(['active', 'read_only', 'deprecated'] as const)( @@ -499,9 +421,7 @@ it.effect.each(['active', 'read_only', 'deprecated'] as const)( }), moduleStates: { getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed( - moduleIds.map((moduleKey) => ({ moduleKey, state })) - ), + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), }, }); const landing = yield* composition.resolveModuleTarget(context, { @@ -530,5 +450,5 @@ it.effect.each(['active', 'read_only', 'deprecated'] as const)( moduleId: 'property.registry', }); expect(crossOwnedPage.outcome).toBe('not_found'); - }) + }), ); diff --git a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts index 5e2d13e16..3abd282d8 100644 --- a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts @@ -20,20 +20,12 @@ describe('Shell governed module-target schemas', () => { writable: true, }; - const decodedInput = Schema.decodeUnknownSync( - GovernedResolveModuleTargetPayloadSchema - )(input); - const decodedResult = Schema.decodeUnknownSync( - GovernedResolvedModuleTargetSchema - )(result); + const decodedInput = Schema.decodeUnknownSync(GovernedResolveModuleTargetPayloadSchema)(input); + const decodedResult = Schema.decodeUnknownSync(GovernedResolvedModuleTargetSchema)(result); expect(decodedInput).toEqual(input); expect(decodedResult).toEqual(result); - expect( - Schema.encodeSync(GovernedResolveModuleTargetPayloadSchema)(decodedInput) - ).toEqual(input); - expect( - Schema.encodeSync(GovernedResolvedModuleTargetSchema)(decodedResult) - ).toEqual(result); + expect(Schema.encodeSync(GovernedResolveModuleTargetPayloadSchema)(decodedInput)).toEqual(input); + expect(Schema.encodeSync(GovernedResolvedModuleTargetSchema)(decodedResult)).toEqual(result); }); }); diff --git a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts index a7bfde77f..ef4287506 100644 --- a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts @@ -40,10 +40,7 @@ const ref = Schema.decodeUnknownSync(ResourceRefSchema)({ resourceId: 'unit-1', resourceType, }); -const entrypoint = ( - role: 'api' | 'search', - access: 'read' | 'write' = 'read' -) => ({ +const entrypoint = (role: 'api' | 'search', access: 'read' | 'write' = 'read') => ({ access, authorization: { kind: 'context_permission' as const, @@ -65,15 +62,7 @@ const catalog = (): InstalledModuleCatalog => defaultState: 'inactive', preservesHistoryWhenInactive: true, scope: 'tenant', - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], }, module: { description: 'Property capability.', @@ -186,47 +175,38 @@ const catalog = (): InstalledModuleCatalog => const access = ( moduleDecision: ContextAccessDecision = 'allowed', resourceDecision: ContextAccessDecision = 'allowed', - resourceWriteDecision: ContextAccessDecision = resourceDecision + resourceWriteDecision: ContextAccessDecision = resourceDecision, ): ContextAccessService => ({ legalEntities: () => Effect.succeed([]), - modules: ({ moduleIds }) => - Effect.succeed(moduleIds.map((key) => ({ decision: moduleDecision, key }))), + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision: moduleDecision, key }))), resources: ({ permission = 'read', resources }) => Effect.succeed( resources.map(({ moduleId: owner, resourceId, resourceType: type }) => ({ - decision: - permission === 'write' ? resourceWriteDecision : resourceDecision, + decision: permission === 'write' ? resourceWriteDecision : resourceDecision, key: `${owner}:${type}:${resourceId}`, - })) + })), ), - tenants: ({ tenantIds }) => - Effect.succeed(tenantIds.map((key) => ({ decision: moduleDecision, key }))), + tenants: ({ tenantIds }) => Effect.succeed(tenantIds.map((key) => ({ decision: moduleDecision, key }))), }); const dependencies = ( state: TenantModuleState = 'active', moduleDecision: ContextAccessDecision = 'allowed', resourceDecision: ContextAccessDecision = 'allowed', - resourceWriteDecision: ContextAccessDecision = resourceDecision + resourceWriteDecision: ContextAccessDecision = resourceDecision, ) => { let assertion = 0; return { catalog: Effect.succeed(catalog()), - contextAccess: access( - moduleDecision, - resourceDecision, - resourceWriteDecision - ), + contextAccess: access(moduleDecision, resourceDecision, resourceWriteDecision), issueAssertion: () => { const authorization = `Bearer test-${assertion}`; assertion += 1; return Effect.succeed(authorization); }, moduleStates: { - getTenantModuleStates: ( - _tenantId: string, - moduleIds: readonly string[] - ) => Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), + getTenantModuleStates: (_tenantId: string, moduleIds: readonly string[]) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), }, }; }; @@ -245,106 +225,93 @@ it.effect('search treats empty input as empty without touching providers', () => results: [], }); expect(calls).toBe(0); - }) + }), ); -it.effect( - 'search keeps an eligible provider with zero candidates as a successful empty result', - () => - Effect.gen(function* searchKeepsAnEligibleProviderWith() { - const baseline = dependencies(); - const result = yield* makeShellSearch( - { - ...baseline, - contextAccess: { - ...baseline.contextAccess, - resources: () => - Effect.die( - 'empty results must not authorize an empty resource batch' - ), - }, +it.effect('search keeps an eligible provider with zero candidates as a successful empty result', () => + Effect.gen(function* searchKeepsAnEligibleProviderWith() { + const baseline = dependencies(); + const result = yield* makeShellSearch( + { + ...baseline, + contextAccess: { + ...baseline.contextAccess, + resources: () => Effect.die('empty results must not authorize an empty resource batch'), }, - { search: () => Effect.succeed([]) } - ).search(context, 'unit'); - expect(result).toEqual({ partial: false, results: [] }); - }) + }, + { search: () => Effect.succeed([]) }, + ).search(context, 'unit'); + expect(result).toEqual({ partial: false, results: [] }); + }), ); -it.effect( - 'search filters resource denials and reports partial provider failure', - () => - Effect.gen(function* searchFiltersResourceDenialsAndReports() { - const result = yield* makeShellSearch( - dependencies('active', 'allowed', 'denied'), - { - search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - } - ).search(context, ' unit '); - expect(result).toEqual({ partial: false, results: [] }); +it.effect('search filters resource denials and reports partial provider failure', () => + Effect.gen(function* searchFiltersResourceDenialsAndReports() { + const result = yield* makeShellSearch(dependencies('active', 'allowed', 'denied'), { + search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), + }).search(context, ' unit '); + expect(result).toEqual({ partial: false, results: [] }); - const installed = catalog(); - const [contract] = installed.contracts; - if (contract === undefined) { - throw new TypeError( - 'The search fixture must install its module contract' - ); - } - const backupSearchKey = 'property.registry.backup-unit-search'; - const catalogWithBackupSearch = buildInstalledModuleCatalog([ - { - contract: { - ...contract, - manifest: { - ...contract.manifest, - publicSurface: { - ...contract.manifest.publicSurface, + const installed = catalog(); + const [contract] = installed.contracts; + if (contract === undefined) { + throw new TypeError('The search fixture must install its module contract'); + } + const backupSearchKey = 'property.registry.backup-unit-search'; + const catalogWithBackupSearch = buildInstalledModuleCatalog([ + { + contract: { + ...contract, + manifest: { + ...contract.manifest, + publicSurface: { + ...contract.manifest.publicSurface, + search: [ + ...contract.manifest.publicSurface.search, + { + accessFiltering: 'resource_permission' as const, + key: backupSearchKey, + owningModuleId: moduleId, + resourceType, + }, + ], + shellContributions: { + ...contract.manifest.publicSurface.shellContributions, search: [ - ...contract.manifest.publicSurface.search, + ...contract.manifest.publicSurface.shellContributions.search, { - accessFiltering: 'resource_permission' as const, - key: backupSearchKey, - owningModuleId: moduleId, - resourceType, + contributionKey: 'property.registry.search.backup-unit', + entrypoint: { + ...entrypoint('search'), + entrypointKey: 'property.registry.search.backup', + }, + searchKey: backupSearchKey, }, ], - shellContributions: { - ...contract.manifest.publicSurface.shellContributions, - search: [ - ...contract.manifest.publicSurface.shellContributions - .search, - { - contributionKey: 'property.registry.search.backup-unit', - entrypoint: { - ...entrypoint('search'), - entrypointKey: 'property.registry.search.backup', - }, - searchKey: backupSearchKey, - }, - ], - }, }, }, }, - expectedAppId: 'property-registry', }, - ]); - const partial = makeShellSearch( - { - ...dependencies(), - catalog: Effect.succeed(catalogWithBackupSearch), - }, - { - search: ({ searchKey }) => - searchKey === backupSearchKey - ? Effect.fail(new ShellProviderUnavailableError()) - : Effect.succeed([{ ref, title: 'Unit 1' }]), - } - ); - expect(yield* partial.search(context, 'unit')).toEqual({ - partial: true, - results: [{ kind: 'resource', ref, title: 'Unit 1' }], - }); - }) + expectedAppId: 'property-registry', + }, + ]); + const partial = makeShellSearch( + { + ...dependencies(), + catalog: Effect.succeed(catalogWithBackupSearch), + }, + { + search: ({ searchKey }) => + searchKey === backupSearchKey + ? Effect.fail(new ShellProviderUnavailableError()) + : Effect.succeed([{ ref, title: 'Unit 1' }]), + }, + ); + expect(yield* partial.search(context, 'unit')).toEqual({ + partial: true, + results: [{ kind: 'resource', ref, title: 'Unit 1' }], + }); + }), ); it.effect( @@ -356,8 +323,7 @@ it.effect( if (contract === undefined) { throw new Error('The test catalog must include one installed contract'); } - const [partyResourceDescriptor] = - contract.manifest.publicSurface.resourceTypes; + const [partyResourceDescriptor] = contract.manifest.publicSurface.resourceTypes; if (partyResourceDescriptor === undefined) { throw new Error('The test catalog must include one resource type'); } @@ -420,9 +386,7 @@ it.effect( }, }, }; - const partyCatalog = buildInstalledModuleCatalog([ - { contract: partyContract, expectedAppId: 'party-registry' }, - ]); + const partyCatalog = buildInstalledModuleCatalog([{ contract: partyContract, expectedAppId: 'party-registry' }]); const calls: unknown[] = []; const baseline = dependencies(); const result = yield* makeShellSearch( @@ -431,12 +395,8 @@ it.effect( catalog: Effect.succeed(partyCatalog), contextAccess: { ...baseline.contextAccess, - modules: () => - Effect.die('tenant-scoped search must not require module access'), - resources: () => - Effect.die( - 'tenant-scoped search must not require resource access' - ), + modules: () => Effect.die('tenant-scoped search must not require module access'), + resources: () => Effect.die('tenant-scoped search must not require resource access'), tenants: ({ permission, tenantIds }) => { calls.push({ permission, tenantIds }); return Effect.succeed([{ decision: 'allowed', key: tenantId }]); @@ -460,7 +420,7 @@ it.effect( }, ]); }, - } + }, ).search(tenantContext, { includeArchived: true, query: ' party ', @@ -490,7 +450,7 @@ it.effect( }, ], }); - }) + }), ); it.effect('search fails only when every eligible provider fails', () => @@ -498,309 +458,262 @@ it.effect('search fails only when every eligible provider fails', () => const effect = makeShellSearch(dependencies(), { search: () => Effect.fail(new ShellProviderUnavailableError()), }).search(context, 'unit'); - expect( - Schema.is(ShellProviderUnavailableError)(yield* Effect.flip(effect)) - ).toBe(true); - }) + expect(Schema.is(ShellProviderUnavailableError)(yield* Effect.flip(effect))).toBe(true); + }), ); -it.effect( - 'Counterparty search preserves both identities, selected scope, roles and collision metadata', - () => - Effect.gen(function* CounterpartySearchPreservesBothIdentitiesSelected() { - const [contract] = catalog().contracts; - if (contract === undefined) { - throw new Error('The test catalog must include one installed contract'); - } - const filteredCatalog = buildInstalledModuleCatalog([ - { - contract: { - ...contract, - manifest: { - ...contract.manifest, - publicSurface: { - ...contract.manifest.publicSurface, - search: contract.manifest.publicSurface.search.map( - (descriptor) => ({ - ...descriptor, - requestFilters: ['includeArchived', 'role'] as const, - }) - ), - }, +it.effect('Counterparty search preserves both identities, selected scope, roles and collision metadata', () => + Effect.gen(function* CounterpartySearchPreservesBothIdentitiesSelected() { + const [contract] = catalog().contracts; + if (contract === undefined) { + throw new Error('The test catalog must include one installed contract'); + } + const filteredCatalog = buildInstalledModuleCatalog([ + { + contract: { + ...contract, + manifest: { + ...contract.manifest, + publicSurface: { + ...contract.manifest.publicSurface, + search: contract.manifest.publicSurface.search.map((descriptor) => ({ + ...descriptor, + requestFilters: ['includeArchived', 'role'] as const, + })), }, }, - expectedAppId: 'property-registry', }, - ]); - const counterpartyRef = { ...ref, tenantId }; - const canonicalPartyRef = { - ...ref, - resourceId: 'party-1', - resourceType: 'property.registry.party', - tenantId, - }; - const collision = { - counterpartyRefs: [ - counterpartyRef, - { ...counterpartyRef, resourceId: 'unit-2' }, - ], - kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION', - }; - const value = { - collision, - currentRoles: ['CUSTOMER', 'SUPPLIER'], - legalEntity: { legalEntityId, tenantId }, - party: { - archived: true, - matchedViaAlias: true, - ref: canonicalPartyRef, - title: 'Canonical Party', + expectedAppId: 'property-registry', + }, + ]); + const counterpartyRef = { ...ref, tenantId }; + const canonicalPartyRef = { + ...ref, + resourceId: 'party-1', + resourceType: 'property.registry.party', + tenantId, + }; + const collision = { + counterpartyRefs: [counterpartyRef, { ...counterpartyRef, resourceId: 'unit-2' }], + kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION', + }; + const value = { + collision, + currentRoles: ['CUSTOMER', 'SUPPLIER'], + legalEntity: { legalEntityId, tenantId }, + party: { + archived: true, + matchedViaAlias: true, + ref: canonicalPartyRef, + title: 'Canonical Party', + }, + ref: counterpartyRef, + }; + const calls: unknown[] = []; + const search = makeShellSearch( + { ...dependencies(), catalog: Effect.succeed(filteredCatalog) }, + { + search: (input) => { + calls.push(input); + return Effect.succeed([value]); }, - ref: counterpartyRef, - }; - const calls: unknown[] = []; - const search = makeShellSearch( - { ...dependencies(), catalog: Effect.succeed(filteredCatalog) }, - { - search: (input) => { - calls.push(input); - return Effect.succeed([value]); - }, - } - ); - const result = yield* search.search(context, { - includeArchived: true, - query: 'canonical', - role: 'CUSTOMER', - }); - expect(calls[0]).toMatchObject({ - includeArchived: true, - role: 'CUSTOMER', - }); - expect(result).toEqual({ - partial: false, - results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }], - }); - expect(yield* search.search(tenantContext, 'canonical')).toEqual({ - partial: false, - results: [], - }); - expect(calls).toHaveLength(1); - const baseline = dependencies(); - const redacted = yield* makeShellSearch( - { - ...baseline, - catalog: Effect.succeed(filteredCatalog), - contextAccess: { - ...baseline.contextAccess, - resources: ({ resources }) => - Effect.succeed( - resources.map((resource) => ({ - decision: - resource.resourceId === 'unit-2' - ? ('denied' as const) - : ('allowed' as const), - key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, - })) - ), - }, + }, + ); + const result = yield* search.search(context, { + includeArchived: true, + query: 'canonical', + role: 'CUSTOMER', + }); + expect(calls[0]).toMatchObject({ + includeArchived: true, + role: 'CUSTOMER', + }); + expect(result).toEqual({ + partial: false, + results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }], + }); + expect(yield* search.search(tenantContext, 'canonical')).toEqual({ + partial: false, + results: [], + }); + expect(calls).toHaveLength(1); + const baseline = dependencies(); + const redacted = yield* makeShellSearch( + { + ...baseline, + catalog: Effect.succeed(filteredCatalog), + contextAccess: { + ...baseline.contextAccess, + resources: ({ resources }) => + Effect.succeed( + resources.map((resource) => ({ + decision: resource.resourceId === 'unit-2' ? ('denied' as const) : ('allowed' as const), + key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, + })), + ), }, - { search: () => Effect.succeed([value]) } - ).search(context, 'canonical'); - expect(JSON.stringify(redacted)).not.toContain('unit-2'); - expect(redacted.results[0]).not.toHaveProperty('collision'); - }) + }, + { search: () => Effect.succeed([value]) }, + ).search(context, 'canonical'); + expect(JSON.stringify(redacted)).not.toContain('unit-2'); + expect(redacted.results[0]).not.toHaveProperty('collision'); + }), ); -it.effect( - 'treats a missing tenant module-state record as hidden rather than authorization uncertainty', - () => - Effect.gen(function* treatsAMissingTenantModuleState() { - let calls = 0; - const hiddenDependencies = { - ...dependencies(), - moduleStates: { getTenantModuleStates: () => Effect.succeed([]) }, - }; - expect( - yield* makeShellSearch(hiddenDependencies, { - search: () => { - calls += 1; - return Effect.succeed([{ ref, title: 'Unit 1' }]); - }, - }).search(context, 'unit') - ).toEqual({ partial: false, results: [] }); - const gateway = { - detail: () => { +it.effect('treats a missing tenant module-state record as hidden rather than authorization uncertainty', () => + Effect.gen(function* treatsAMissingTenantModuleState() { + let calls = 0; + const hiddenDependencies = { + ...dependencies(), + moduleStates: { getTenantModuleStates: () => Effect.succeed([]) }, + }; + expect( + yield* makeShellSearch(hiddenDependencies, { + search: () => { calls += 1; - return Effect.succeed({ fields: [], title: 'Unit 1' }); + return Effect.succeed([{ ref, title: 'Unit 1' }]); }, - timeline: () => - Effect.succeed({ entries: [], projectionLagging: false }), - }; - expect( - yield* makeShellResourceDetail(hiddenDependencies, gateway).resolve( - context, - ref - ) - ).toEqual({ outcome: 'not_found' }); - expect(yield* attachShellMedia(context, ref)).toEqual({ - outcome: 'unavailable', - }); - expect(calls).toBe(0); - }) + }).search(context, 'unit'), + ).toEqual({ partial: false, results: [] }); + const gateway = { + detail: () => { + calls += 1; + return Effect.succeed({ fields: [], title: 'Unit 1' }); + }, + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect(yield* makeShellResourceDetail(hiddenDependencies, gateway).resolve(context, ref)).toEqual({ + outcome: 'not_found', + }); + expect(yield* attachShellMedia(context, ref)).toEqual({ + outcome: 'unavailable', + }); + expect(calls).toBe(0); + }), ); -it.effect( - 'search fails closed for module or resource authorization uncertainty', - () => - Effect.gen(function* searchFailsClosedForModuleOr() { - expect( - Schema.is(ShellProviderUnavailableError)( - yield* Effect.flip( - makeShellSearch(dependencies('active', 'unavailable'), { - search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - }).search(context, 'unit') - ) - ) - ).toBe(true); - expect( - Schema.is(ShellProviderUnavailableError)( - yield* Effect.flip( - makeShellSearch(dependencies('active', 'allowed', 'unavailable'), { - search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - }).search(context, 'unit') - ) - ) - ).toBe(true); - }) +it.effect('search fails closed for module or resource authorization uncertainty', () => + Effect.gen(function* searchFailsClosedForModuleOr() { + expect( + Schema.is(ShellProviderUnavailableError)( + yield* Effect.flip( + makeShellSearch(dependencies('active', 'unavailable'), { + search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), + }).search(context, 'unit'), + ), + ), + ).toBe(true); + expect( + Schema.is(ShellProviderUnavailableError)( + yield* Effect.flip( + makeShellSearch(dependencies('active', 'allowed', 'unavailable'), { + search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), + }).search(context, 'unit'), + ), + ), + ).toBe(true); + }), ); -it.effect( - 'resource detail applies catalog, state, module and resource gates before providers', - () => - Effect.gen(function* resourceDetailAppliesCatalogStateModule() { - let calls = 0; - const provider = { - detail: () => { - calls += 1; - return Effect.succeed({ fields: [], title: 'Unit 1' }); - }, - timeline: () => - Effect.succeed({ entries: [], projectionLagging: false }), - }; - expect( - yield* makeShellResourceDetail( - dependencies('inactive'), - provider - ).resolve(context, ref) - ).toEqual({ outcome: 'not_found' }); - expect( - yield* makeShellResourceDetail( - dependencies('active', 'denied'), - provider - ).resolve(context, ref) - ).toEqual({ outcome: 'forbidden' }); - expect( - yield* makeShellResourceDetail( - dependencies('active', 'allowed', 'unavailable'), - provider - ).resolve(context, ref) - ).toEqual({ outcome: 'unavailable' }); - expect(calls).toBe(0); - }) +it.effect('resource detail applies catalog, state, module and resource gates before providers', () => + Effect.gen(function* resourceDetailAppliesCatalogStateModule() { + let calls = 0; + const provider = { + detail: () => { + calls += 1; + return Effect.succeed({ fields: [], title: 'Unit 1' }); + }, + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect(yield* makeShellResourceDetail(dependencies('inactive'), provider).resolve(context, ref)).toEqual({ + outcome: 'not_found', + }); + expect(yield* makeShellResourceDetail(dependencies('active', 'denied'), provider).resolve(context, ref)).toEqual({ + outcome: 'forbidden', + }); + expect( + yield* makeShellResourceDetail(dependencies('active', 'allowed', 'unavailable'), provider).resolve(context, ref), + ).toEqual({ outcome: 'unavailable' }); + expect(calls).toBe(0); + }), ); -it.effect( - 'resource detail sorts an authorized timeline and exposes projection lag', - () => - Effect.gen(function* resourceDetailSortsAnAuthorizedTimeline() { - const result = yield* makeShellResourceDetail(dependencies(), { - detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), - timeline: () => - Effect.succeed({ - entries: [ - { - occurredAt: '2026-01-01T00:00:00Z', - summary: 'Created', - timelineEntryId: '1', - }, - { - occurredAt: '2026-02-01T00:00:00Z', - summary: 'Updated', - timelineEntryId: '2', - }, - ], - projectionLagging: true, - }), - }).resolve(context, ref); - expect(result).toEqual({ - detail: { fields: [], title: 'Unit 1' }, - media: { enabled: false, reason: 'unavailable' }, - outcome: 'resolved', - projectionLagging: true, - timeline: [ - { - occurredAt: DateTime.makeUnsafe('2026-02-01T00:00:00Z'), - summary: 'Updated', - timelineEntryId: '2', - }, - { - occurredAt: DateTime.makeUnsafe('2026-01-01T00:00:00Z'), - summary: 'Created', - timelineEntryId: '1', - }, - ], - }); - if (result.outcome !== 'resolved') { - throw new TypeError('The authorized resource fixture must resolve'); - } - expect( - yield* Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))( - result.timeline - ) - ).toEqual([ +it.effect('resource detail sorts an authorized timeline and exposes projection lag', () => + Effect.gen(function* resourceDetailSortsAnAuthorizedTimeline() { + const result = yield* makeShellResourceDetail(dependencies(), { + detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), + timeline: () => + Effect.succeed({ + entries: [ + { + occurredAt: '2026-01-01T00:00:00Z', + summary: 'Created', + timelineEntryId: '1', + }, + { + occurredAt: '2026-02-01T00:00:00Z', + summary: 'Updated', + timelineEntryId: '2', + }, + ], + projectionLagging: true, + }), + }).resolve(context, ref); + expect(result).toEqual({ + detail: { fields: [], title: 'Unit 1' }, + media: { enabled: false, reason: 'unavailable' }, + outcome: 'resolved', + projectionLagging: true, + timeline: [ { - occurredAt: '2026-02-01T00:00:00.000Z', + occurredAt: DateTime.makeUnsafe('2026-02-01T00:00:00Z'), summary: 'Updated', timelineEntryId: '2', }, { - occurredAt: '2026-01-01T00:00:00.000Z', + occurredAt: DateTime.makeUnsafe('2026-01-01T00:00:00Z'), summary: 'Created', timelineEntryId: '1', }, - ]); - }) + ], + }); + if (result.outcome !== 'resolved') { + throw new TypeError('The authorized resource fixture must resolve'); + } + expect(yield* Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))(result.timeline)).toEqual([ + { + occurredAt: '2026-02-01T00:00:00.000Z', + summary: 'Updated', + timelineEntryId: '2', + }, + { + occurredAt: '2026-01-01T00:00:00.000Z', + summary: 'Created', + timelineEntryId: '1', + }, + ]); + }), ); -it.effect( - 'media affordance remains unavailable until a generated Action exists', - () => - Effect.gen(function* mediaAffordanceRemainsUnavailableUntilA() { - const provider = { - detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), - timeline: () => - Effect.succeed({ entries: [], projectionLagging: false }), - }; - expect( - yield* makeShellResourceDetail( - dependencies('read_only'), - provider - ).resolve(context, ref) - ).toMatchObject({ media: { enabled: false, reason: 'read_only' } }); - expect( - yield* makeShellResourceDetail( - dependencies('active', 'allowed', 'allowed', 'denied'), - provider - ).resolve(context, ref) - ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); - expect( - yield* makeShellResourceDetail(dependencies(), provider).resolve( - context, - ref - ) - ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); - }) +it.effect('media affordance remains unavailable until a generated Action exists', () => + Effect.gen(function* mediaAffordanceRemainsUnavailableUntilA() { + const provider = { + detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect(yield* makeShellResourceDetail(dependencies('read_only'), provider).resolve(context, ref)).toMatchObject({ + media: { enabled: false, reason: 'read_only' }, + }); + expect( + yield* makeShellResourceDetail(dependencies('active', 'allowed', 'allowed', 'denied'), provider).resolve( + context, + ref, + ), + ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); + expect(yield* makeShellResourceDetail(dependencies(), provider).resolve(context, ref)).toMatchObject({ + media: { enabled: false, reason: 'unavailable' }, + }); + }), ); it.effect('media endpoint cannot invoke a provider mutation', () => @@ -808,25 +721,23 @@ it.effect('media endpoint cannot invoke a provider mutation', () => expect(yield* attachShellMedia(context, ref)).toEqual({ outcome: 'unavailable', }); - }) + }), ); -it.effect( - 'acquires a fresh audience-scoped assertion for each provider attempt', - () => - Effect.gen(function* acquiresAFreshAudienceScopedAssertion() { - const authorizations: string[] = []; - const result = yield* makeShellResourceDetail(dependencies(), { - detail: ({ authorization }) => { - authorizations.push(authorization); - return Effect.succeed({ fields: [], title: 'Unit 1' }); - }, - timeline: ({ authorization }) => { - authorizations.push(authorization); - return Effect.succeed({ entries: [], projectionLagging: false }); - }, - }).resolve(context, ref); - expect(result.outcome).toBe('resolved'); - expect(authorizations).toEqual(['Bearer test-0', 'Bearer test-1']); - }) +it.effect('acquires a fresh audience-scoped assertion for each provider attempt', () => + Effect.gen(function* acquiresAFreshAudienceScopedAssertion() { + const authorizations: string[] = []; + const result = yield* makeShellResourceDetail(dependencies(), { + detail: ({ authorization }) => { + authorizations.push(authorization); + return Effect.succeed({ fields: [], title: 'Unit 1' }); + }, + timeline: ({ authorization }) => { + authorizations.push(authorization); + return Effect.succeed({ entries: [], projectionLagging: false }); + }, + }).resolve(context, ref); + expect(result.outcome).toBe('resolved'); + expect(authorizations).toEqual(['Bearer test-0', 'Bearer test-1']); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts index 93f0af05b..41363ad5d 100644 --- a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts +++ b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts @@ -39,138 +39,102 @@ it.effect('accepts the complete stage-only demo bootstrap configuration', () => authSecret: 'stage-auth-secret-with-at-least-32-characters', databaseAdminUrl: 'postgresql://db:password@db:5432/db', }); - }) + }), ); -it.effect( - 'defines both exact stage accounts without storing their passwords', - () => - Effect.sync(() => { - expect(STAGE_DEMO_ACCOUNTS).toEqual([ - { - email: 'demo@test.com', - passwordEnvironmentKey: 'STAGE_DEMO_PASSWORD', - principalDisplayName: 'Techsio Demo', - }, - { - email: 'siampark01@test.com', - passwordEnvironmentKey: 'STAGE_SIAMPARK_PASSWORD', - principalDisplayName: 'Siampark 01', - }, - ]); - }) +it.effect('defines both exact stage accounts without storing their passwords', () => + Effect.sync(() => { + expect(STAGE_DEMO_ACCOUNTS).toEqual([ + { + email: 'demo@test.com', + passwordEnvironmentKey: 'STAGE_DEMO_PASSWORD', + principalDisplayName: 'Techsio Demo', + }, + { + email: 'siampark01@test.com', + passwordEnvironmentKey: 'STAGE_SIAMPARK_PASSWORD', + principalDisplayName: 'Siampark 01', + }, + ]); + }), ); -it.effect( - 'refuses to provision outside stage or without an operator-supplied password', - () => - Effect.gen(function* refusesToProvisionOutsideStage() { - expect( - yield* Effect.flip( - parseStageDemoBootstrapConfig({ - ...validEnvironment, - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', - }) - ) - ).toMatchObject({ reason: expect.stringMatching(/stage environment/u) }); - expect( - yield* Effect.flip( - parseStageDemoBootstrapConfig({ - ...validEnvironment, - STAGE_DEMO_PASSWORD: undefined, - }) - ) - ).toMatchObject({ - reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u), - }); - expect( - yield* Effect.flip( - parseStageDemoBootstrapConfig({ - ...validEnvironment, - STAGE_SIAMPARK_PASSWORD: undefined, - }) - ) - ).toMatchObject({ - reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u), - }); - }) +it.effect('refuses to provision outside stage or without an operator-supplied password', () => + Effect.gen(function* refusesToProvisionOutsideStage() { + expect( + yield* Effect.flip( + parseStageDemoBootstrapConfig({ + ...validEnvironment, + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', + }), + ), + ).toMatchObject({ reason: expect.stringMatching(/stage environment/u) }); + expect( + yield* Effect.flip( + parseStageDemoBootstrapConfig({ + ...validEnvironment, + STAGE_DEMO_PASSWORD: undefined, + }), + ), + ).toMatchObject({ + reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u), + }); + expect( + yield* Effect.flip( + parseStageDemoBootstrapConfig({ + ...validEnvironment, + STAGE_SIAMPARK_PASSWORD: undefined, + }), + ), + ).toMatchObject({ + reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u), + }); + }), ); -it.effect( - 'treats an exact record as idempotent and rejects conflicting state', - () => - Effect.gen(function* treatsAnExactRecordAsIdempotent() { - const expected = { - name: 'Techsio', - slug: 'techsio', - status: 'active', - } as const; - expect( - yield* classifyExactStageDemoRecord('tenant', undefined, expected) - ).toBe('create'); - expect( - yield* classifyExactStageDemoRecord('tenant', expected, expected) - ).toBe('existing'); - expect( - yield* Effect.flip( - classifyExactStageDemoRecord( - 'tenant', - { ...expected, name: 'Other tenant' }, - expected - ) - ) - ).toMatchObject({ reason: expect.stringMatching(/conflicts/u) }); - }) +it.effect('treats an exact record as idempotent and rejects conflicting state', () => + Effect.gen(function* treatsAnExactRecordAsIdempotent() { + const expected = { + name: 'Techsio', + slug: 'techsio', + status: 'active', + } as const; + expect(yield* classifyExactStageDemoRecord('tenant', undefined, expected)).toBe('create'); + expect(yield* classifyExactStageDemoRecord('tenant', expected, expected)).toBe('existing'); + expect( + yield* Effect.flip(classifyExactStageDemoRecord('tenant', { ...expected, name: 'Other tenant' }, expected)), + ).toMatchObject({ reason: expect.stringMatching(/conflicts/u) }); + }), ); -it.live( - 'keeps the demo bootstrap operator-invoked and excludes its password from source', - () => - Effect.gen(function* keepsTheDemoBootstrapOperatorinvoked() { - const rootPackage = yield* Effect.promise(() => - readFile(new URL('../../../../package.json', import.meta.url), 'utf-8') - ); - const shellPackage = yield* Effect.promise(() => - readFile(new URL('../../package.json', import.meta.url), 'utf-8') - ); - const bootstrapCommand = yield* Effect.promise(() => - readFile( - new URL('../../scripts/bootstrap-stage-demo.sh', import.meta.url), - 'utf-8' - ) - ); - const zerops = yield* Effect.promise(() => - readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8') - ); - const coreBootstrap = yield* Effect.promise(() => - readFile( - new URL( - '../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts', - import.meta.url - ), - 'utf-8' - ) - ); - const shellBootstrap = yield* Effect.promise(() => - readFile( - new URL( - '../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', - import.meta.url - ), - 'utf-8' - ) - ); - expect(JSON.parse(rootPackage).scripts['stage:bootstrap-demo']).toBe( - 'pnpm --filter @app/shell-super-app stage:bootstrap-demo' - ); - expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe( - 'sh scripts/bootstrap-stage-demo.sh' - ); - expect(bootstrapCommand).toMatch(/stty -echo/u); - expect(bootstrapCommand).toMatch(/STAGE_DEMO_PASSWORD/u); - expect(bootstrapCommand).toMatch(/STAGE_SIAMPARK_PASSWORD/u); - expect(zerops).not.toMatch(/start:.*stage:bootstrap-demo/u); - expect(zerops).not.toMatch(/^\s*STAGE_DEMO_PASSWORD:/mu); - expect(zerops).not.toMatch(/^\s*STAGE_SIAMPARK_PASSWORD:/mu); - expect(coreBootstrap).toMatch(/ULTRAMODERN_DEPLOYMENT_ENVIRONMENT/u); - expect(coreBootstrap).toMatch(/buildRelationships/u); - expect(shellBootstrap).toMatch(/reconcileStageContextBootstraps/u); - expect(shellBootstrap).not.toMatch(/contextKey/u); - }) +it.live('keeps the demo bootstrap operator-invoked and excludes its password from source', () => + Effect.gen(function* keepsTheDemoBootstrapOperatorinvoked() { + const rootPackage = yield* Effect.promise(() => + readFile(new URL('../../../../package.json', import.meta.url), 'utf-8'), + ); + const shellPackage = yield* Effect.promise(() => readFile(new URL('../../package.json', import.meta.url), 'utf-8')); + const bootstrapCommand = yield* Effect.promise(() => + readFile(new URL('../../scripts/bootstrap-stage-demo.sh', import.meta.url), 'utf-8'), + ); + const zerops = yield* Effect.promise(() => readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8')); + const coreBootstrap = yield* Effect.promise(() => + readFile( + new URL('../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts', import.meta.url), + 'utf-8', + ), + ); + const shellBootstrap = yield* Effect.promise(() => + readFile(new URL('../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', import.meta.url), 'utf-8'), + ); + expect(JSON.parse(rootPackage).scripts['stage:bootstrap-demo']).toBe( + 'pnpm --filter @app/shell-super-app stage:bootstrap-demo', + ); + expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe('sh scripts/bootstrap-stage-demo.sh'); + expect(bootstrapCommand).toMatch(/stty -echo/u); + expect(bootstrapCommand).toMatch(/STAGE_DEMO_PASSWORD/u); + expect(bootstrapCommand).toMatch(/STAGE_SIAMPARK_PASSWORD/u); + expect(zerops).not.toMatch(/start:.*stage:bootstrap-demo/u); + expect(zerops).not.toMatch(/^\s*STAGE_DEMO_PASSWORD:/mu); + expect(zerops).not.toMatch(/^\s*STAGE_SIAMPARK_PASSWORD:/mu); + expect(coreBootstrap).toMatch(/ULTRAMODERN_DEPLOYMENT_ENVIRONMENT/u); + expect(coreBootstrap).toMatch(/buildRelationships/u); + expect(shellBootstrap).toMatch(/reconcileStageContextBootstraps/u); + expect(shellBootstrap).not.toMatch(/contextKey/u); + }), ); diff --git a/app/docs/architecture/COMMERCE_APPLICATIONS.md b/app/docs/architecture/COMMERCE_APPLICATIONS.md index 0ac4ca122..70706d581 100644 --- a/app/docs/architecture/COMMERCE_APPLICATIONS.md +++ b/app/docs/architecture/COMMERCE_APPLICATIONS.md @@ -6,13 +6,13 @@ The accepted product decision is [ADR-0017](../../../docs/adr/0017-commerce-appl ## Application inventory -| Application/edge | Deployment and ownership | -| --- | --- | -| **Storefront Application** | External to the standard OntOS Shell deployment. Owns framework, routes, rendering, layout, interaction, branding, assets, and SEO. A customer may have separate B2C/B2B storefronts. | -| **Storefront-local BFF/proxy** | Deployed with one storefront. Holds its Storefront Client credential server-side, provides a same-origin browser edge, and performs presentation-oriented request shaping/aggregation. | -| **Commerce Storefront API** | Thin OntOS channel edge over public Commerce module contracts. Authenticates, resolves trusted Commerce Purchasing Context, authorizes, translates, aggregates bounded reads, and invokes governed Actions. | -| **Commerce Operations** | Purpose-built staff application over published MicroVertical clients and governed entrypoints. Uses staff authentication; owns no canonical commerce facts. | -| **Agentic Shopping Adapter** | Future peer channel adapter over native Commerce contracts, for example MCP or UCP. It is not implemented by this decision. | +| Application/edge | Deployment and ownership | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Storefront Application** | External to the standard OntOS Shell deployment. Owns framework, routes, rendering, layout, interaction, branding, assets, and SEO. A customer may have separate B2C/B2B storefronts. | +| **Storefront-local BFF/proxy** | Deployed with one storefront. Holds its Storefront Client credential server-side, provides a same-origin browser edge, and performs presentation-oriented request shaping/aggregation. | +| **Commerce Storefront API** | Thin OntOS channel edge over public Commerce module contracts. Authenticates, resolves trusted Commerce Purchasing Context, authorizes, translates, aggregates bounded reads, and invokes governed Actions. | +| **Commerce Operations** | Purpose-built staff application over published MicroVertical clients and governed entrypoints. Uses staff authentication; owns no canonical commerce facts. | +| **Agentic Shopping Adapter** | Future peer channel adapter over native Commerce contracts, for example MCP or UCP. It is not implemented by this decision. | Shell/Core remains business-neutral. Do not add commerce orchestration, Commerce Portal Account lifecycle, Storefront rendering, provider mapping, or Commerce Operations workflows to Shell/Core merely because several modules or channels need them. diff --git a/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md b/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md index ba02e2822..f35cc4dfe 100644 --- a/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md +++ b/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md @@ -26,15 +26,15 @@ This is not a general PostgreSQL reachability analyzer. Indirect execution throu **VERIFIED** against a freshly migrated local database: -| Surface | `ontos_runtime` authority | -| --- | --- | -| Cluster and roles | Login only; no superuser, `BYPASSRLS`, database/role creation, replication, inheritance, memberships, or parameter grants | -| Database and schemas | `CONNECT` and temporary objects; `USAGE` on application schemas; no database or schema `CREATE` | -| Relations | DML across 27 owner tables; no migration-journal access or relation-control privileges | -| Sequences | `USAGE` and `SELECT` on one application sequence; no `UPDATE` | -| Ownership and privileged execution | No application-object ownership and no executable `SECURITY DEFINER` path | -| RLS | Two tables have enabled and forced RLS | -| Trusted settings | Can set both `ontos.tenant_id` and `ontos.legal_entity_id`; local values disappear after rollback | +| Surface | `ontos_runtime` authority | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Cluster and roles | Login only; no superuser, `BYPASSRLS`, database/role creation, replication, inheritance, memberships, or parameter grants | +| Database and schemas | `CONNECT` and temporary objects; `USAGE` on application schemas; no database or schema `CREATE` | +| Relations | DML across 27 owner tables; no migration-journal access or relation-control privileges | +| Sequences | `USAGE` and `SELECT` on one application sequence; no `UPDATE` | +| Ownership and privileged execution | No application-object ownership and no executable `SECURITY DEFINER` path | +| RLS | Two tables have enabled and forced RLS | +| Trusted settings | Can set both `ontos.tenant_id` and `ontos.legal_entity_id`; local values disappear after rollback | Exact counts describe this local database, not production. Re-run the audit against each target environment. @@ -47,15 +47,15 @@ The first is a blast-radius problem. The second is a trust-root problem; splitti ## Process-to-identity map -| Boundary | Current evidence | Status | -| --- | --- | --- | -| Migration/bootstrap | Drizzle and role bootstrap use `DATABASE_ADMIN_URL`. | Repository wiring **VERIFIED**; deployed secret and rotation **UNKNOWN**. | -| Shell requests | Auth and Core persistence use the shared `DATABASE_URL`. | Code **VERIFIED**; deployed login **UNKNOWN**. | -| Contacts requests | Contacts persistence and assertion redemption use the shared `DATABASE_URL`. | Code **VERIFIED**; deployed login **UNKNOWN**. | -| Core | Package/schema boundary composed into its caller; no independent process or credential. | **VERIFIED**. | -| Workers | Generated workers compose the shared database layers; no production worker is installed. | Current code **VERIFIED**; future identity **INFERRED**. | -| SpiceDB | Separate datastore login; applications use gRPC plus a pre-shared key. | Bootstrap **VERIFIED**; deployed distribution **UNKNOWN**. | -| Browser/remotes | Boundary checks reject database imports outside server owners. | Static boundary **VERIFIED**; not protection from a compromised server process. | +| Boundary | Current evidence | Status | +| ------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Migration/bootstrap | Drizzle and role bootstrap use `DATABASE_ADMIN_URL`. | Repository wiring **VERIFIED**; deployed secret and rotation **UNKNOWN**. | +| Shell requests | Auth and Core persistence use the shared `DATABASE_URL`. | Code **VERIFIED**; deployed login **UNKNOWN**. | +| Contacts requests | Contacts persistence and assertion redemption use the shared `DATABASE_URL`. | Code **VERIFIED**; deployed login **UNKNOWN**. | +| Core | Package/schema boundary composed into its caller; no independent process or credential. | **VERIFIED**. | +| Workers | Generated workers compose the shared database layers; no production worker is installed. | Current code **VERIFIED**; future identity **INFERRED**. | +| SpiceDB | Separate datastore login; applications use gRPC plus a pre-shared key. | Bootstrap **VERIFIED**; deployed distribution **UNKNOWN**. | +| Browser/remotes | Boundary checks reject database imports outside server owners. | Static boundary **VERIFIED**; not protection from a compromised server process. | External service configuration may supply production credentials, so their absence from `zerops.yaml` proves nothing about deployed identity distribution. @@ -77,13 +77,13 @@ validated request context ## Negative evidence and remaining gaps -| Threat | Current evidence | Gap | -| --- | --- | --- | -| Cross-tenant SQL | RLS integration tests prove filtering for selected context. | They do not prove the context is authentic. | -| Raw database access | Static boundaries reject imports from non-owner surfaces. | Arbitrary code inside an allowed server process retains the credential. | -| DDL and role escalation | The audit checks effective privileges, ownership, grant authority, and reachable roles; local baseline has none. | Production needs its own audit and pilot denial probes. | -| Privileged execution | The audit checks directly executable `SECURITY DEFINER` routines and privileged owner-context views; local baseline has none. | Every future privileged path needs a narrow contract and review. | -| Unrelated-schema DML | The audit enumerates effective relation and sequence access. | Denial is impossible today because the shared role intentionally spans three schemas. | +| Threat | Current evidence | Gap | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Cross-tenant SQL | RLS integration tests prove filtering for selected context. | They do not prove the context is authentic. | +| Raw database access | Static boundaries reject imports from non-owner surfaces. | Arbitrary code inside an allowed server process retains the credential. | +| DDL and role escalation | The audit checks effective privileges, ownership, grant authority, and reachable roles; local baseline has none. | Production needs its own audit and pilot denial probes. | +| Privileged execution | The audit checks directly executable `SECURITY DEFINER` routines and privileged owner-context views; local baseline has none. | Every future privileged path needs a narrow contract and review. | +| Unrelated-schema DML | The audit enumerates effective relation and sequence access. | Denial is impossible today because the shared role intentionally spans three schemas. | ## Pilot options diff --git a/app/docs/architecture/DRIZZLE_V1_UPGRADE.md b/app/docs/architecture/DRIZZLE_V1_UPGRADE.md index c6d6adb94..ffb4c427f 100644 --- a/app/docs/architecture/DRIZZLE_V1_UPGRADE.md +++ b/app/docs/architecture/DRIZZLE_V1_UPGRADE.md @@ -21,14 +21,14 @@ The initial upgrade adopted tagged `rc.4`. The native Effect migration in [PR #4 ### Initial rc.4 dependencies -| Package | Before | After | Owners | -| --- | --- | --- | --- | -| `drizzle-orm` | 0.45.2 | 1.0.0-rc.4 | root, `core-runtime`, Shell, `contacts` | -| `drizzle-kit` | 0.31.10 | 1.0.0-rc.4 | `core-runtime`, Shell, `contacts` | -| `better-auth` | 1.6.23 | 1.7.2 | root, Shell | -| `@better-auth/api-key` | 1.6.23 | 1.7.2 | Shell | +| Package | Before | After | Owners | +| ------------------------------ | -------- | ------------ | ---------------------------------------- | +| `drizzle-orm` | 0.45.2 | 1.0.0-rc.4 | root, `core-runtime`, Shell, `contacts` | +| `drizzle-kit` | 0.31.10 | 1.0.0-rc.4 | `core-runtime`, Shell, `contacts` | +| `better-auth` | 1.6.23 | 1.7.2 | root, Shell | +| `@better-auth/api-key` | 1.6.23 | 1.7.2 | Shell | | `@better-auth/drizzle-adapter` | indirect | 1.7.2 direct | root, Shell (`/relations-v2` entrypoint) | -| `auth` (Better Auth CLI) | 1.6.23 | 1.7.2 | Shell | +| `auth` (Better Auth CLI) | 1.6.23 | 1.7.2 | Shell | This table records the original upgrade, when Party was named Contacts. The current cohort above supersedes its Drizzle versions. Every owner pins the identical Drizzle pair. @@ -81,10 +81,7 @@ for (const root of roots) { const qualifier = `"${entity.schema}"."${entity.table}".`; const strip = (text) => text.split(qualifier).join(''); for (const field of fields[entity.entityType] ?? []) { - if ( - typeof entity[field] === 'string' && - entity[field].includes(qualifier) - ) { + if (typeof entity[field] === 'string' && entity[field].includes(qualifier)) { entity[field] = strip(entity[field]); fragments++; touched = true; @@ -149,18 +146,18 @@ The v1 migrator applies every migration folder missing from the table, not only Environment: Darwin arm64, Node `26.5.0` and pnpm `11.25.0` through `mise exec --`, PostgreSQL 17 in the local Compose container on port 5433. -| Proof | Result | -| --- | --- | -| `drizzle-kit up` for Core, Auth, Contacts | 9 + 6 + 4 folders; every `migration.sql` byte-identical to its predecessor | -| Auth `add-account-issuer` on the populated copy | exit 0, one credential row backfilled to `local:credential`, unique index present | -| `db:generate` for each owner after normalization | `No schema changes, nothing to migrate` for all three | -| `db:check` for each owner | `Everything's fine` | +| Proof | Result | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `drizzle-kit up` for Core, Auth, Contacts | 9 + 6 + 4 folders; every `migration.sql` byte-identical to its predecessor | +| Auth `add-account-issuer` on the populated copy | exit 0, one credential row backfilled to `local:credential`, unique index present | +| `db:generate` for each owner after normalization | `No schema changes, nothing to migrate` for all three | +| `db:check` for each owner | `Everything's fine` | | `db:migrate` on a `TEMPLATE ontos` copy with v0 journals | exit 0, 9/7/4 rows, `name` backfilled, catalog column hash identical after a rerun | -| `db:migrate` on an empty database | exit 0, 9/7/4 rows, schemas `core`/`auth`/`contacts`/`drizzle` present | -| `db:verify` on both databases | exact schemas, journals, and 18/6/3 typed tables verified | -| `pnpm typecheck`, `pnpm lint` | clean | -| `pnpm action:test:unit`, `pnpm outbox:test` | 64/64 and all outbox tests passing | -| `pnpm db:test` (Core, Auth integration, Contacts) | see the pull request for the final run | +| `db:migrate` on an empty database | exit 0, 9/7/4 rows, schemas `core`/`auth`/`contacts`/`drizzle` present | +| `db:verify` on both databases | exact schemas, journals, and 18/6/3 typed tables verified | +| `pnpm typecheck`, `pnpm lint` | clean | +| `pnpm action:test:unit`, `pnpm outbox:test` | 64/64 and all outbox tests passing | +| `pnpm db:test` (Core, Auth integration, Contacts) | see the pull request for the final run | ## Re-proof checklist diff --git a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md index 444bc626a..afa504e2f 100644 --- a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md +++ b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md @@ -38,79 +38,79 @@ pnpm lint The audit column is the **primary** section, not an exclusive mapping. Cross-cutting findings (for example B2 time control, A1 reusable clients, A4 ADTs) can also motivate these rules. Follow each rule link for its exact detection policy, defaults, exemptions, and limitations. -| Rule | Audit | Total | Source | Tests | Scripts | -| --- | --- | --: | --: | --: | --: | -| [`no-ad-hoc-argv-in-scripts`](../../tools/oxlint/effect-native/rules/no-ad-hoc-argv-in-scripts.ts) | B3 | 24 | 0 | 0 | 24 | -| [`no-ambient-date`](../../tools/oxlint/effect-native/rules/no-ambient-date.ts) | B5 | 77 | 43 | 16 | 18 | -| [`no-ambient-process-env`](../../tools/oxlint/effect-native/rules/no-ambient-process-env.ts) | A3 | 127 | 20 | 32 | 75 | -| [`no-async-script-program`](../../tools/oxlint/effect-native/rules/no-async-script-program.ts) | B3 | 182 | 0 | 0 | 182 | -| [`no-bare-effect-run`](../../tools/oxlint/effect-native/rules/no-bare-effect-run.ts) | A1 | 3 | 3 | 0 | 0 | -| [`no-console-in-scripts`](../../tools/oxlint/effect-native/rules/no-console-in-scripts.ts) | B3 | 26 | 0 | 0 | 26 | -| [`no-dependency-parameters`](../../tools/oxlint/effect-native/rules/no-dependency-parameters.ts) | B4 | 96 | 96 | 0 | 0 | -| [`no-direct-node-io-in-scripts`](../../tools/oxlint/effect-native/rules/no-direct-node-io-in-scripts.ts) | B3 | 54 | 0 | 0 | 54 | -| [`no-dotenv-loading`](../../tools/oxlint/effect-native/rules/no-dotenv-loading.ts) | A3 | 28 | 20 | 2 | 6 | -| [`no-driver-failure-inspection`](../../tools/oxlint/effect-native/rules/no-driver-failure-inspection.ts) | A5 | 43 | 43 | 0 | 0 | -| [`no-duplicate-literal-vocabulary`](../../tools/oxlint/effect-native/rules/no-duplicate-literal-vocabulary.ts) | B5 | 9 | 9 | 0 | 0 | -| [`no-effect-provide-in-library`](../../tools/oxlint/effect-native/rules/no-effect-provide-in-library.ts) | A1 | 5 | 5 | 0 | 0 | -| [`no-effect-run-in-scripts`](../../tools/oxlint/effect-native/rules/no-effect-run-in-scripts.ts) | B3 | 2 | 0 | 0 | 2 | -| [`no-effect-run-in-tests`](../../tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts) | B2 | 970 | 0 | 970 | 0 | -| [`no-environment-record-type`](../../tools/oxlint/effect-native/rules/no-environment-record-type.ts) | A3 | 37 | 33 | 0 | 4 | -| [`no-failure-discarding-error-callback`](../../tools/oxlint/effect-native/rules/no-failure-discarding-error-callback.ts) | A4 | 234 | 234 | 0 | 0 | -| [`no-hand-built-http-server-in-tests`](../../tools/oxlint/effect-native/rules/no-hand-built-http-server-in-tests.ts) | B2 | 9 | 0 | 9 | 0 | -| [`no-hand-built-problem-details`](../../tools/oxlint/effect-native/rules/no-hand-built-problem-details.ts) | A4 | 194 | 194 | 0 | 0 | -| [`no-hand-parsed-environment-value`](../../tools/oxlint/effect-native/rules/no-hand-parsed-environment-value.ts) | A3 | 113 | 95 | 1 | 17 | -| [`no-hand-rolled-tagged-union`](../../tools/oxlint/effect-native/rules/no-hand-rolled-tagged-union.ts) | B5 | 66 | 64 | 2 | 0 | -| [`no-imperative-loop-in-effect-gen`](../../tools/oxlint/effect-native/rules/no-imperative-loop-in-effect-gen.ts) | B1 | 56 | 56 | 0 | 0 | -| [`no-interface-first-codec`](../../tools/oxlint/effect-native/rules/no-interface-first-codec.ts) | A2 | 36 | 36 | 0 | 0 | -| [`no-json-schema-as-document-contract`](../../tools/oxlint/effect-native/rules/no-json-schema-as-document-contract.ts) | A7 | 15 | 15 | 0 | 0 | -| [`no-layer-fresh`](../../tools/oxlint/effect-native/rules/no-layer-fresh.ts) | A1 | 1 | 1 | 0 | 0 | -| [`no-layer-or-die-outside-root`](../../tools/oxlint/effect-native/rules/no-layer-or-die-outside-root.ts) | A1 | 12 | 12 | 0 | 0 | -| [`no-layer-provide-in-library`](../../tools/oxlint/effect-native/rules/no-layer-provide-in-library.ts) | A1 | 31 | 31 | 0 | 0 | -| [`no-literal-union-type-alias`](../../tools/oxlint/effect-native/rules/no-literal-union-type-alias.ts) | B5 | 34 | 19 | 4 | 11 | -| [`no-local-defect-seam`](../../tools/oxlint/effect-native/rules/no-local-defect-seam.ts) | A4 | 50 | 50 | 0 | 0 | -| [`no-manual-config-in-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-manual-config-in-scaffold-templates.ts) | A8 | 3 | 0 | 0 | 3 | -| [`no-manual-cookie-serialization`](../../tools/oxlint/effect-native/rules/no-manual-cookie-serialization.ts) | C1 | 7 | 7 | 0 | 0 | -| [`no-manual-error-handling-in-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-manual-error-handling-in-scaffold-templates.ts) | A8 | 4 | 0 | 0 | 4 | -| [`no-manual-identity-annotations`](../../tools/oxlint/effect-native/rules/no-manual-identity-annotations.ts) | A6 | 47 | 47 | 0 | 0 | -| [`no-manual-route-param-parsing`](../../tools/oxlint/effect-native/rules/no-manual-route-param-parsing.ts) | A9 | 3 | 3 | 0 | 0 | -| [`no-manual-tag-comparison`](../../tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts) | C2 | 271 | 195 | 76 | 0 | -| [`no-native-error-construction`](../../tools/oxlint/effect-native/rules/no-native-error-construction.ts) | A4 | 143 | 143 | 0 | 0 | -| [`no-native-json-parse`](../../tools/oxlint/effect-native/rules/no-native-json-parse.ts) | C1 | 44 | 8 | 0 | 36 | -| [`no-native-json-stringify`](../../tools/oxlint/effect-native/rules/no-native-json-stringify.ts) | C1 | 100 | 36 | 0 | 64 | -| [`no-native-timers`](../../tools/oxlint/effect-native/rules/no-native-timers.ts) | B1 | 14 | 2 | 12 | 0 | -| [`no-nested-effect-run`](../../tools/oxlint/effect-native/rules/no-nested-effect-run.ts) | S1 | 19 | 19 | 0 | 0 | -| [`no-nullable-schema-field`](../../tools/oxlint/effect-native/rules/no-nullable-schema-field.ts) | B5 | 121 | 121 | 0 | 0 | -| [`no-nullable-service-outcome`](../../tools/oxlint/effect-native/rules/no-nullable-service-outcome.ts) | B5 | 25 | 24 | 0 | 1 | -| [`no-per-operation-http-api-client`](../../tools/oxlint/effect-native/rules/no-per-operation-http-api-client.ts) | B1 | 74 | 74 | 0 | 0 | -| [`no-per-request-key-material`](../../tools/oxlint/effect-native/rules/no-per-request-key-material.ts) | B1 | 4 | 3 | 0 | 1 | -| [`no-process-exit-outside-script-entry`](../../tools/oxlint/effect-native/rules/no-process-exit-outside-script-entry.ts) | B3 | 15 | 0 | 0 | 15 | -| [`no-promise-first-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-promise-first-scaffold-templates.ts) | A8 | 4 | 0 | 0 | 4 | -| [`no-promise-shaped-port`](../../tools/oxlint/effect-native/rules/no-promise-shaped-port.ts) | A5 | 53 | 53 | 0 | 0 | -| [`no-raw-effect-adt-tag-check`](../../tools/oxlint/effect-native/rules/no-raw-effect-adt-tag-check.ts) | C2 | 21 | 16 | 5 | 0 | -| [`no-refinement-outside-schema`](../../tools/oxlint/effect-native/rules/no-refinement-outside-schema.ts) | A2 | 46 | 35 | 1 | 10 | -| [`no-route-local-error-classifier`](../../tools/oxlint/effect-native/rules/no-route-local-error-classifier.ts) | A4 | 9 | 9 | 0 | 0 | -| [`no-runtime-construction-outside-root`](../../tools/oxlint/effect-native/rules/no-runtime-construction-outside-root.ts) | A1 | 0 | 0 | 0 | 0 | -| [`no-scattered-browser-effect-run`](../../tools/oxlint/effect-native/rules/no-scattered-browser-effect-run.ts) | A9 | 13 | 13 | 0 | 0 | -| [`no-sequential-independent-yields`](../../tools/oxlint/effect-native/rules/no-sequential-independent-yields.ts) | B1 | 13 | 13 | 0 | 0 | -| [`no-string-timestamp-schema`](../../tools/oxlint/effect-native/rules/no-string-timestamp-schema.ts) | B5 | 36 | 27 | 1 | 8 | -| [`no-structural-document-walking`](../../tools/oxlint/effect-native/rules/no-structural-document-walking.ts) | A7 | 60 | 32 | 0 | 28 | -| [`no-symbol-slotted-operation-record`](../../tools/oxlint/effect-native/rules/no-symbol-slotted-operation-record.ts) | B4 | 24 | 24 | 0 | 0 | -| [`no-sync-schema-codec`](../../tools/oxlint/effect-native/rules/no-sync-schema-codec.ts) | C1 | 34 | 29 | 0 | 5 | -| [`no-threaded-correlation-parameter`](../../tools/oxlint/effect-native/rules/no-threaded-correlation-parameter.ts) | A6 | 86 | 86 | 0 | 0 | -| [`no-throw-in-configuration-parser`](../../tools/oxlint/effect-native/rules/no-throw-in-configuration-parser.ts) | A3 | 28 | 28 | 0 | 0 | -| [`no-throw-in-effect-callback`](../../tools/oxlint/effect-native/rules/no-throw-in-effect-callback.ts) | A4 | 69 | 69 | 0 | 0 | -| [`no-throw-in-scripts`](../../tools/oxlint/effect-native/rules/no-throw-in-scripts.ts) | B3 | 317 | 0 | 0 | 317 | -| [`no-unbranded-identifier-schema`](../../tools/oxlint/effect-native/rules/no-unbranded-identifier-schema.ts) | A2 | 230 | 199 | 30 | 1 | -| [`no-unjustified-file-wide-lint-suppression`](../../tools/oxlint/effect-native/rules/no-unjustified-file-wide-lint-suppression.ts) | A8 | 299 | 153 | 133 | 13 | -| [`no-unmanaged-mutable-state`](../../tools/oxlint/effect-native/rules/no-unmanaged-mutable-state.ts) | C3 | 10 | 10 | 0 | 0 | -| [`no-unredacted-secret-field`](../../tools/oxlint/effect-native/rules/no-unredacted-secret-field.ts) | A3 | 96 | 89 | 0 | 7 | -| [`no-wide-factory-signature`](../../tools/oxlint/effect-native/rules/no-wide-factory-signature.ts) | B4 | 33 | 33 | 0 | 0 | -| [`prefer-effect-fn-for-operations`](../../tools/oxlint/effect-native/rules/prefer-effect-fn-for-operations.ts) | B4 | 189 | 189 | 0 | 0 | -| [`prefer-match-over-tag-switch`](../../tools/oxlint/effect-native/rules/prefer-match-over-tag-switch.ts) | C2 | 39 | 37 | 2 | 0 | -| [`require-concurrency-option`](../../tools/oxlint/effect-native/rules/require-concurrency-option.ts) | B1 | 21 | 21 | 0 | 0 | -| [`require-context-service-for-service-interface`](../../tools/oxlint/effect-native/rules/require-context-service-for-service-interface.ts) | B4 | 13 | 13 | 0 | 0 | -| [`require-observability-layers-at-runtime-root`](../../tools/oxlint/effect-native/rules/require-observability-layers-at-runtime-root.ts) | A6 | 12 | 12 | 0 | 0 | -| [`require-timeout-on-external-effect`](../../tools/oxlint/effect-native/rules/require-timeout-on-external-effect.ts) | B1 | 103 | 103 | 0 | 0 | +| Rule | Audit | Total | Source | Tests | Scripts | +| -------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ----: | -----: | ----: | ------: | +| [`no-ad-hoc-argv-in-scripts`](../../tools/oxlint/effect-native/rules/no-ad-hoc-argv-in-scripts.ts) | B3 | 24 | 0 | 0 | 24 | +| [`no-ambient-date`](../../tools/oxlint/effect-native/rules/no-ambient-date.ts) | B5 | 77 | 43 | 16 | 18 | +| [`no-ambient-process-env`](../../tools/oxlint/effect-native/rules/no-ambient-process-env.ts) | A3 | 127 | 20 | 32 | 75 | +| [`no-async-script-program`](../../tools/oxlint/effect-native/rules/no-async-script-program.ts) | B3 | 182 | 0 | 0 | 182 | +| [`no-bare-effect-run`](../../tools/oxlint/effect-native/rules/no-bare-effect-run.ts) | A1 | 3 | 3 | 0 | 0 | +| [`no-console-in-scripts`](../../tools/oxlint/effect-native/rules/no-console-in-scripts.ts) | B3 | 26 | 0 | 0 | 26 | +| [`no-dependency-parameters`](../../tools/oxlint/effect-native/rules/no-dependency-parameters.ts) | B4 | 96 | 96 | 0 | 0 | +| [`no-direct-node-io-in-scripts`](../../tools/oxlint/effect-native/rules/no-direct-node-io-in-scripts.ts) | B3 | 54 | 0 | 0 | 54 | +| [`no-dotenv-loading`](../../tools/oxlint/effect-native/rules/no-dotenv-loading.ts) | A3 | 28 | 20 | 2 | 6 | +| [`no-driver-failure-inspection`](../../tools/oxlint/effect-native/rules/no-driver-failure-inspection.ts) | A5 | 43 | 43 | 0 | 0 | +| [`no-duplicate-literal-vocabulary`](../../tools/oxlint/effect-native/rules/no-duplicate-literal-vocabulary.ts) | B5 | 9 | 9 | 0 | 0 | +| [`no-effect-provide-in-library`](../../tools/oxlint/effect-native/rules/no-effect-provide-in-library.ts) | A1 | 5 | 5 | 0 | 0 | +| [`no-effect-run-in-scripts`](../../tools/oxlint/effect-native/rules/no-effect-run-in-scripts.ts) | B3 | 2 | 0 | 0 | 2 | +| [`no-effect-run-in-tests`](../../tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts) | B2 | 970 | 0 | 970 | 0 | +| [`no-environment-record-type`](../../tools/oxlint/effect-native/rules/no-environment-record-type.ts) | A3 | 37 | 33 | 0 | 4 | +| [`no-failure-discarding-error-callback`](../../tools/oxlint/effect-native/rules/no-failure-discarding-error-callback.ts) | A4 | 234 | 234 | 0 | 0 | +| [`no-hand-built-http-server-in-tests`](../../tools/oxlint/effect-native/rules/no-hand-built-http-server-in-tests.ts) | B2 | 9 | 0 | 9 | 0 | +| [`no-hand-built-problem-details`](../../tools/oxlint/effect-native/rules/no-hand-built-problem-details.ts) | A4 | 194 | 194 | 0 | 0 | +| [`no-hand-parsed-environment-value`](../../tools/oxlint/effect-native/rules/no-hand-parsed-environment-value.ts) | A3 | 113 | 95 | 1 | 17 | +| [`no-hand-rolled-tagged-union`](../../tools/oxlint/effect-native/rules/no-hand-rolled-tagged-union.ts) | B5 | 66 | 64 | 2 | 0 | +| [`no-imperative-loop-in-effect-gen`](../../tools/oxlint/effect-native/rules/no-imperative-loop-in-effect-gen.ts) | B1 | 56 | 56 | 0 | 0 | +| [`no-interface-first-codec`](../../tools/oxlint/effect-native/rules/no-interface-first-codec.ts) | A2 | 36 | 36 | 0 | 0 | +| [`no-json-schema-as-document-contract`](../../tools/oxlint/effect-native/rules/no-json-schema-as-document-contract.ts) | A7 | 15 | 15 | 0 | 0 | +| [`no-layer-fresh`](../../tools/oxlint/effect-native/rules/no-layer-fresh.ts) | A1 | 1 | 1 | 0 | 0 | +| [`no-layer-or-die-outside-root`](../../tools/oxlint/effect-native/rules/no-layer-or-die-outside-root.ts) | A1 | 12 | 12 | 0 | 0 | +| [`no-layer-provide-in-library`](../../tools/oxlint/effect-native/rules/no-layer-provide-in-library.ts) | A1 | 31 | 31 | 0 | 0 | +| [`no-literal-union-type-alias`](../../tools/oxlint/effect-native/rules/no-literal-union-type-alias.ts) | B5 | 34 | 19 | 4 | 11 | +| [`no-local-defect-seam`](../../tools/oxlint/effect-native/rules/no-local-defect-seam.ts) | A4 | 50 | 50 | 0 | 0 | +| [`no-manual-config-in-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-manual-config-in-scaffold-templates.ts) | A8 | 3 | 0 | 0 | 3 | +| [`no-manual-cookie-serialization`](../../tools/oxlint/effect-native/rules/no-manual-cookie-serialization.ts) | C1 | 7 | 7 | 0 | 0 | +| [`no-manual-error-handling-in-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-manual-error-handling-in-scaffold-templates.ts) | A8 | 4 | 0 | 0 | 4 | +| [`no-manual-identity-annotations`](../../tools/oxlint/effect-native/rules/no-manual-identity-annotations.ts) | A6 | 47 | 47 | 0 | 0 | +| [`no-manual-route-param-parsing`](../../tools/oxlint/effect-native/rules/no-manual-route-param-parsing.ts) | A9 | 3 | 3 | 0 | 0 | +| [`no-manual-tag-comparison`](../../tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts) | C2 | 271 | 195 | 76 | 0 | +| [`no-native-error-construction`](../../tools/oxlint/effect-native/rules/no-native-error-construction.ts) | A4 | 143 | 143 | 0 | 0 | +| [`no-native-json-parse`](../../tools/oxlint/effect-native/rules/no-native-json-parse.ts) | C1 | 44 | 8 | 0 | 36 | +| [`no-native-json-stringify`](../../tools/oxlint/effect-native/rules/no-native-json-stringify.ts) | C1 | 100 | 36 | 0 | 64 | +| [`no-native-timers`](../../tools/oxlint/effect-native/rules/no-native-timers.ts) | B1 | 14 | 2 | 12 | 0 | +| [`no-nested-effect-run`](../../tools/oxlint/effect-native/rules/no-nested-effect-run.ts) | S1 | 19 | 19 | 0 | 0 | +| [`no-nullable-schema-field`](../../tools/oxlint/effect-native/rules/no-nullable-schema-field.ts) | B5 | 121 | 121 | 0 | 0 | +| [`no-nullable-service-outcome`](../../tools/oxlint/effect-native/rules/no-nullable-service-outcome.ts) | B5 | 25 | 24 | 0 | 1 | +| [`no-per-operation-http-api-client`](../../tools/oxlint/effect-native/rules/no-per-operation-http-api-client.ts) | B1 | 74 | 74 | 0 | 0 | +| [`no-per-request-key-material`](../../tools/oxlint/effect-native/rules/no-per-request-key-material.ts) | B1 | 4 | 3 | 0 | 1 | +| [`no-process-exit-outside-script-entry`](../../tools/oxlint/effect-native/rules/no-process-exit-outside-script-entry.ts) | B3 | 15 | 0 | 0 | 15 | +| [`no-promise-first-scaffold-templates`](../../tools/oxlint/effect-native/rules/no-promise-first-scaffold-templates.ts) | A8 | 4 | 0 | 0 | 4 | +| [`no-promise-shaped-port`](../../tools/oxlint/effect-native/rules/no-promise-shaped-port.ts) | A5 | 53 | 53 | 0 | 0 | +| [`no-raw-effect-adt-tag-check`](../../tools/oxlint/effect-native/rules/no-raw-effect-adt-tag-check.ts) | C2 | 21 | 16 | 5 | 0 | +| [`no-refinement-outside-schema`](../../tools/oxlint/effect-native/rules/no-refinement-outside-schema.ts) | A2 | 46 | 35 | 1 | 10 | +| [`no-route-local-error-classifier`](../../tools/oxlint/effect-native/rules/no-route-local-error-classifier.ts) | A4 | 9 | 9 | 0 | 0 | +| [`no-runtime-construction-outside-root`](../../tools/oxlint/effect-native/rules/no-runtime-construction-outside-root.ts) | A1 | 0 | 0 | 0 | 0 | +| [`no-scattered-browser-effect-run`](../../tools/oxlint/effect-native/rules/no-scattered-browser-effect-run.ts) | A9 | 13 | 13 | 0 | 0 | +| [`no-sequential-independent-yields`](../../tools/oxlint/effect-native/rules/no-sequential-independent-yields.ts) | B1 | 13 | 13 | 0 | 0 | +| [`no-string-timestamp-schema`](../../tools/oxlint/effect-native/rules/no-string-timestamp-schema.ts) | B5 | 36 | 27 | 1 | 8 | +| [`no-structural-document-walking`](../../tools/oxlint/effect-native/rules/no-structural-document-walking.ts) | A7 | 60 | 32 | 0 | 28 | +| [`no-symbol-slotted-operation-record`](../../tools/oxlint/effect-native/rules/no-symbol-slotted-operation-record.ts) | B4 | 24 | 24 | 0 | 0 | +| [`no-sync-schema-codec`](../../tools/oxlint/effect-native/rules/no-sync-schema-codec.ts) | C1 | 34 | 29 | 0 | 5 | +| [`no-threaded-correlation-parameter`](../../tools/oxlint/effect-native/rules/no-threaded-correlation-parameter.ts) | A6 | 86 | 86 | 0 | 0 | +| [`no-throw-in-configuration-parser`](../../tools/oxlint/effect-native/rules/no-throw-in-configuration-parser.ts) | A3 | 28 | 28 | 0 | 0 | +| [`no-throw-in-effect-callback`](../../tools/oxlint/effect-native/rules/no-throw-in-effect-callback.ts) | A4 | 69 | 69 | 0 | 0 | +| [`no-throw-in-scripts`](../../tools/oxlint/effect-native/rules/no-throw-in-scripts.ts) | B3 | 317 | 0 | 0 | 317 | +| [`no-unbranded-identifier-schema`](../../tools/oxlint/effect-native/rules/no-unbranded-identifier-schema.ts) | A2 | 230 | 199 | 30 | 1 | +| [`no-unjustified-file-wide-lint-suppression`](../../tools/oxlint/effect-native/rules/no-unjustified-file-wide-lint-suppression.ts) | A8 | 299 | 153 | 133 | 13 | +| [`no-unmanaged-mutable-state`](../../tools/oxlint/effect-native/rules/no-unmanaged-mutable-state.ts) | C3 | 10 | 10 | 0 | 0 | +| [`no-unredacted-secret-field`](../../tools/oxlint/effect-native/rules/no-unredacted-secret-field.ts) | A3 | 96 | 89 | 0 | 7 | +| [`no-wide-factory-signature`](../../tools/oxlint/effect-native/rules/no-wide-factory-signature.ts) | B4 | 33 | 33 | 0 | 0 | +| [`prefer-effect-fn-for-operations`](../../tools/oxlint/effect-native/rules/prefer-effect-fn-for-operations.ts) | B4 | 189 | 189 | 0 | 0 | +| [`prefer-match-over-tag-switch`](../../tools/oxlint/effect-native/rules/prefer-match-over-tag-switch.ts) | C2 | 39 | 37 | 2 | 0 | +| [`require-concurrency-option`](../../tools/oxlint/effect-native/rules/require-concurrency-option.ts) | B1 | 21 | 21 | 0 | 0 | +| [`require-context-service-for-service-interface`](../../tools/oxlint/effect-native/rules/require-context-service-for-service-interface.ts) | B4 | 13 | 13 | 0 | 0 | +| [`require-observability-layers-at-runtime-root`](../../tools/oxlint/effect-native/rules/require-observability-layers-at-runtime-root.ts) | A6 | 12 | 12 | 0 | 0 | +| [`require-timeout-on-external-effect`](../../tools/oxlint/effect-native/rules/require-timeout-on-external-effect.ts) | B1 | 103 | 103 | 0 | 0 | ## Boundaries that remain review work diff --git a/app/docs/architecture/ERRORS.md b/app/docs/architecture/ERRORS.md index f1781fde7..2d157bba4 100644 --- a/app/docs/architecture/ERRORS.md +++ b/app/docs/architecture/ERRORS.md @@ -48,18 +48,18 @@ For the Shell-user MicroVertical Action identity boundary, the generated verifie Choose the status from the meaning of the failure, not from a generic domain-error default. -| Status | Use when | -| --- | --- | -| `400` | The request cannot be decoded or structurally validated against its schema. | -| `401` | Credentials are missing, invalid, expired, revoked, or otherwise unusable. Include a `WWW-Authenticate` challenge. | -| `403` | Authentication succeeded, but the principal is not permitted to perform the operation. | -| `404` | The requested resource is absent and revealing that fact is allowed. | -| `409` | The operation conflicts with the current mutable state or a concurrency invariant and may succeed after resolution. | -| `422` | The request is structurally valid but semantically ineligible, and the failure is not authorization or conflict. | -| `429` | The caller exceeded a rate or quota limit. | -| `500` | An unexpected internal defect was caught at the outer HTTP seam. | -| `503` | A required capability is temporarily unavailable and retry may succeed later. | -| `504` | A required upstream operation did not complete before its deadline. | +| Status | Use when | +| ------ | ------------------------------------------------------------------------------------------------------------------- | +| `400` | The request cannot be decoded or structurally validated against its schema. | +| `401` | Credentials are missing, invalid, expired, revoked, or otherwise unusable. Include a `WWW-Authenticate` challenge. | +| `403` | Authentication succeeded, but the principal is not permitted to perform the operation. | +| `404` | The requested resource is absent and revealing that fact is allowed. | +| `409` | The operation conflicts with the current mutable state or a concurrency invariant and may succeed after resolution. | +| `422` | The request is structurally valid but semantically ineligible, and the failure is not authorization or conflict. | +| `429` | The caller exceeded a rate or quota limit. | +| `500` | An unexpected internal defect was caught at the outer HTTP seam. | +| `503` | A required capability is temporarily unavailable and retry may succeed later. | +| `504` | A required upstream operation did not complete before its deadline. | Identity endpoints apply the same meanings exhaustively. Missing or unusable Shell credentials use `401` with a Bearer challenge; the API-key exchange uses an API-key challenge. A definite permission denial or active credential bound to a forbidden tenant/principal/legal entity is `403`; lifecycle state races are `409`; missing runtime records are `404`; ineligible targets are `422`; a missing required idempotency key is `428`; provider throttling is `429`. Structurally invalid operation payloads are `400`. Database, SpiceDB, resolver, evidence, or provider uncertainty is retryable `503`, while only caught defects at the outer handler seam become sanitized `500`. Problem Details never include keys, hashes, cookies, provider diagnostics, identifiers, or signature details. diff --git a/app/docs/architecture/MICROVERTICALS.md b/app/docs/architecture/MICROVERTICALS.md index a3dc227fd..93baa5b6f 100644 --- a/app/docs/architecture/MICROVERTICALS.md +++ b/app/docs/architecture/MICROVERTICALS.md @@ -12,9 +12,9 @@ The current generated manifest/catalog does not yet implement `implementationId` OntOS has two different kinds of seams. Do not treat them as equivalent. -| Seam | Location | Meaning | -| --- | --- | --- | -| Vertical | Between MicroVerticals | A strict physical deployment seam that must always be preserved. | +| Seam | Location | Meaning | +| ---------- | -------------------------------------------- | --------------------------------------------------------------------------------------- | +| Vertical | Between MicroVerticals | A strict physical deployment seam that must always be preserved. | | Horizontal | Between frontend and backend in one vertical | A virtual seam represented by the generated, Effect-based BFF client—not a domain seam. | ## Vertical Seams: Strict and Independently Deployable diff --git a/app/docs/architecture/MODULE_ENTRYPOINTS.md b/app/docs/architecture/MODULE_ENTRYPOINTS.md index bded534a9..3af42af4e 100644 --- a/app/docs/architecture/MODULE_ENTRYPOINTS.md +++ b/app/docs/architecture/MODULE_ENTRYPOINTS.md @@ -8,12 +8,12 @@ This document defines the Core-owned invariant for loading or dispatching OntOS Every entrypoint is an immutable Effect Schema-backed value containing a stable entrypoint key, owning module key, role, access class, and explicit scope. -| Role | Permitted access | -| --- | --- | -| `action` | `write` | -| `worker` | `background` | -| `page`, `public_component`, `search` | `read` or an explicit `historical_read` | -| `api`, `report` | an explicitly selected `read`, `historical_read`, or `write` | +| Role | Permitted access | +| ------------------------------------ | ------------------------------------------------------------ | +| `action` | `write` | +| `worker` | `background` | +| `page`, `public_component`, `search` | `read` or an explicit `historical_read` | +| `api`, `report` | an explicitly selected `read`, `historical_read`, or `write` | Tenant entrypoints are created only with the tenant constructor. Core capabilities use the system constructor explicitly; a `core.*` prefix does not imply a bypass. A system entrypoint bypasses tenant module-state acquisition only and still passes every applicable authentication, permission, Policy, transaction, and evidence control. @@ -40,14 +40,14 @@ Missing state is a definite denial. An unavailable database read, malformed pers At a trusted Shell, SSR, route, or BFF boundary, collect every descriptor the request may use, deduplicate and sort its tenant module keys, read them in one indexed query, decode each state once, and create an immutable request snapshot covering the exact key set. Every later decision is pure in-memory evaluation. Undeclared keys fail closed without an implicit lookup. Empty and system-only compositions perform no state query. -| Runtime composition | Module-state database work | -| --- | --- | -| One Shell/SSR/page composition with any number of declared entrypoints | At most one batch query for all distinct tenant module keys | -| Repeated decisions from one request snapshot | Zero additional queries | -| Explicit Core system entrypoints | Zero tenant-module-state queries | -| One independently deployed BFF request | At most one batch query for that request composition | -| One business Action attempt | One early indexed read plus one transaction-aware recheck | -| One Outbox Worker claim cycle | Zero additional queries beyond the existing claim query/join | +| Runtime composition | Module-state database work | +| ---------------------------------------------------------------------- | ------------------------------------------------------------ | +| One Shell/SSR/page composition with any number of declared entrypoints | At most one batch query for all distinct tenant module keys | +| Repeated decisions from one request snapshot | Zero additional queries | +| Explicit Core system entrypoints | Zero tenant-module-state queries | +| One independently deployed BFF request | At most one batch query for that request composition | +| One business Action attempt | One early indexed read plus one transaction-aware recheck | +| One Outbox Worker claim cycle | Zero additional queries beyond the existing claim query/join | Snapshots are request-scoped, never process-global, browser-authoritative, TTL-based, or distributed caches. The next independent request observes state again. A Shell decision does not replace the independent BFF or Action check at the next trust boundary. Telemetry may contain batch size, acquisition duration, snapshot reuse, scope, access, and outcome, but not payloads, credentials, raw persistence causes, or private implementation identifiers. diff --git a/app/docs/architecture/PARTY_REGISTRY.md b/app/docs/architecture/PARTY_REGISTRY.md index 8005670ad..b02799333 100644 --- a/app/docs/architecture/PARTY_REGISTRY.md +++ b/app/docs/architecture/PARTY_REGISTRY.md @@ -62,17 +62,17 @@ Do not add this union to a contract that only needs one side. Counterparty alway All state changes use declared Actions and require idempotency unless the general Action rules explicitly justify otherwise. -| Capability | Legal Entity scope | Permission target | Required authority | -| --- | --- | --- | --- | -| Party create/update/archive/unarchive | optional | Tenant | manage Party identity | -| Official Identifier add/end/correct | optional | Tenant | manage Party identity | -| Contact Point add/end/correct | optional | Tenant | manage Party identity/contact data | -| Party Relationship create/end/correct | optional | Tenant | manage Party relationships | -| Party Matching review | optional | Tenant | review Party identity | -| Party Merge | optional | Tenant | merge Party identity | -| Party Read/Search | optional | Tenant | read Party identity/contact data | -| Counterparty create/read/search | required | Legal Entity or Counterparty | read/manage that commercial context | -| Counterparty Role add/end | required | Counterparty | manage that commercial context | +| Capability | Legal Entity scope | Permission target | Required authority | +| ------------------------------------- | ------------------ | ---------------------------- | ----------------------------------- | +| Party create/update/archive/unarchive | optional | Tenant | manage Party identity | +| Official Identifier add/end/correct | optional | Tenant | manage Party identity | +| Contact Point add/end/correct | optional | Tenant | manage Party identity/contact data | +| Party Relationship create/end/correct | optional | Tenant | manage Party relationships | +| Party Matching review | optional | Tenant | review Party identity | +| Party Merge | optional | Tenant | merge Party identity | +| Party Read/Search | optional | Tenant | read Party identity/contact data | +| Counterparty create/read/search | required | Legal Entity or Counterparty | read/manage that commercial context | +| Counterparty Role add/end | required | Counterparty | manage that commercial context | `legalEntityScope: optional` means trusted session context may contain a selected Legal Entity. It does not scope the Party fact or grant authority. The Action payload never supplies or overrides trusted Tenant or Legal Entity context. diff --git a/app/docs/quality-audit.md b/app/docs/quality-audit.md index ec5f61704..bf06a7209 100644 --- a/app/docs/quality-audit.md +++ b/app/docs/quality-audit.md @@ -29,10 +29,10 @@ The gate requires all six unique expected analyzer results, reported statuses, e ## What each report answers -| Tool | Report | Interpretation | -| --- | --- | --- | -| Knip | Unused files, exports, types, dependencies, and import/dependency problems | No consumer was found in the configured model. Framework roots and external consumers need review before removal. | -| JSCPD | Substantial repeated token sequences | Candidate shared implementation, including copies of unchanged files. | +| Tool | Report | Interpretation | +| ------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Knip | Unused files, exports, types, dependencies, and import/dependency problems | No consumer was found in the configured model. Framework roots and external consumers need review before removal. | +| JSCPD | Substantial repeated token sequences | Candidate shared implementation, including copies of unchanged files. | | Fallow | Strict clones, separate semantic similarities, and control-flow complexity above 10/15 | Strict matches are primary clone observations; semantic matches are advisory. Complexity separates React heuristics from control flow. | Use the checked-in analyzer configs and runner as the command and scope authority. They account for runtime source, tooling, tests, and framework consumers. Intentionally invalid custom-rule fixtures, dependencies, and generated build output require explicit handling; editable Codesmith starter files remain source. Discovery totals and clone-eligible file totals differ because clone detectors omit files shorter than their token/line minimums. Do not compare duplication percentages between tools as if they used the same denominator. diff --git a/app/module-federation.shared.ts b/app/module-federation.shared.ts index 73e74d355..ec7011ea1 100644 --- a/app/module-federation.shared.ts +++ b/app/module-federation.shared.ts @@ -1,10 +1,6 @@ type SharedRuntimeVersions = Readonly< Record< - | '@modern-js/plugin-i18n/runtime' - | '@modern-js/runtime' - | '@tanstack/react-router' - | 'react' - | 'react-dom', + '@modern-js/plugin-i18n/runtime' | '@modern-js/runtime' | '@tanstack/react-router' | 'react' | 'react-dom', string > >; diff --git a/app/oxfmt.config.ts b/app/oxfmt.config.ts index fe413499c..a31cca208 100644 --- a/app/oxfmt.config.ts +++ b/app/oxfmt.config.ts @@ -1,8 +1,8 @@ import { defineConfig } from 'oxfmt'; -import ultracite from 'ultracite/oxfmt'; export default defineConfig({ - ...ultracite, + printWidth: 120, + trailingComma: 'all', ignorePatterns: [ '.agents', '.codex/skills', diff --git a/app/oxlint.config.ts b/app/oxlint.config.ts index 01487fbb9..d1e782a6e 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -5,19 +5,13 @@ import react from 'ultracite/oxlint/react'; import { testRestrictedImports } from './tools/oxlint/effect-native/shared/test-restricted-imports.ts'; -const selectedJsPlugins = selectJsPlugins([ - 'github', - 'sonarjs', - 'react-doctor', -]); +const selectedJsPlugins = selectJsPlugins(['github', 'sonarjs', 'react-doctor']); const jsPlugins = { ...selectedJsPlugins, // Load GitHub's published rule-only entrypoint, not its ESLint configuration aggregator. // The aggregator eagerly imports eslint-plugin-import and the ESLint runner; the rules do not. jsPlugins: selectedJsPlugins.jsPlugins.map((plugin) => - plugin.name === 'github' - ? { ...plugin, specifier: 'eslint-plugin-github/lib/plugin.js' } - : plugin + plugin.name === 'github' ? { ...plugin, specifier: 'eslint-plugin-github/lib/plugin.js' } : plugin, ), }; @@ -45,9 +39,7 @@ const antiSlopEffectRules = { // Effect-native architecture rules derived from docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md. // Each rule cites the audit finding it enforces; see tools/oxlint/effect-native/README.md. -const effectNativeRules: NonNullable< - Parameters[0]['rules'] -> = { +const effectNativeRules: NonNullable[0]['rules']> = { 'effect-native/no-ad-hoc-argv-in-scripts': 'error', 'effect-native/no-ambient-date': 'error', 'effect-native/no-ambient-process-env': 'error', @@ -246,10 +238,7 @@ export default defineConfig({ { // This guarded test-only entrypoint composes real services with boundary fakes. // database-access:check rejects imports of it from production source. - files: [ - 'packages/core-runtime/src/testing/**/*.ts', - 'apps/shell-super-app/tests/e2e/auth-fixture.ts', - ], + files: ['packages/core-runtime/src/testing/**/*.ts', 'apps/shell-super-app/tests/e2e/auth-fixture.ts'], rules: { 'anti-slop-effect/no-service-constructor-imports': 'off', }, @@ -315,10 +304,7 @@ export default defineConfig({ }, { // Test registration deliberately returns an ignored promise, and test synchronization may use `.then`. - files: [ - '**/*.{test,spec,test-d,spec-d}.{ts,tsx,js,jsx}', - '**/__tests__/**/*.{ts,tsx,js,jsx}', - ], + files: ['**/*.{test,spec,test-d,spec-d}.{ts,tsx,js,jsx}', '**/__tests__/**/*.{ts,tsx,js,jsx}'], rules: { 'github/no-then': 'off', // Ultracite's JS-plugin preset applies the same test-data exception; repeat it because @@ -447,10 +433,7 @@ export default defineConfig({ }, { // React component names are intentionally PascalCase, contrary to SonarJS's function-name default. - files: [ - '**/*.tsx', - 'apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts', - ], + files: ['**/*.tsx', 'apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts'], rules: { 'sonarjs/function-name': 'off', }, @@ -486,10 +469,7 @@ export default defineConfig({ { // Dynamic Modern.js cache paths contain `.js-${appId}` but are filesystem paths, // not CSS class names; the GitHub rule cannot distinguish those string domains. - files: [ - 'apps/shell-super-app/modern.config.ts', - 'verticals/party-registry/modern.config.ts', - ], + files: ['apps/shell-super-app/modern.config.ts', 'verticals/party-registry/modern.config.ts'], rules: { 'github/js-class-name': 'off', }, @@ -497,9 +477,7 @@ export default defineConfig({ { // React Doctor currently emits its internal computed-property lowering TODO for this // typed form-error update; the code is valid and the dedicated compiler rules stay active. - files: [ - 'verticals/party-registry/src/features/customers/customer-form.tsx', - ], + files: ['verticals/party-registry/src/features/customers/customer-form.tsx'], rules: { 'react/todo': 'off', }, @@ -515,10 +493,7 @@ export default defineConfig({ { // These aliases name stable domain boundaries even when their current representation is // identical to another type; removing the names would couple public/runtime APIs to storage. - files: [ - 'apps/shell-super-app/src/api/auth-client.ts', - 'packages/core-runtime/src/actions/runtime.ts', - ], + files: ['apps/shell-super-app/src/api/auth-client.ts', 'packages/core-runtime/src/actions/runtime.ts'], rules: { 'sonarjs/redundant-type-aliases': 'off', }, @@ -690,9 +665,7 @@ export default defineConfig({ { // The edit page keeps one cohesive mutation/detail workflow; splitting it would move // authorization and retry state across component boundaries during this lint-only migration. - files: [ - 'verticals/party-registry/src/routes/**/contacts/customers/**/contacts/**/edit/page.tsx', - ], + files: ['verticals/party-registry/src/routes/**/contacts/customers/**/contacts/**/edit/page.tsx'], rules: { 'react-doctor/no-giant-component': 'off', }, @@ -787,9 +760,7 @@ export default defineConfig({ { // This Proxy preserves the real Drizzle executor type while replacing two methods in a live // integration fixture. Reflect.get is required to preserve the original receiver. - files: [ - 'verticals/party-registry/tests/integration/customer-ares-lookup-bff.test.ts', - ], + files: ['verticals/party-registry/tests/integration/customer-ares-lookup-bff.test.ts'], rules: { 'anti-slop/no-reflect-get': 'off', }, @@ -879,10 +850,7 @@ export default defineConfig({ 'import/export': 'error', 'import/no-namespace': ['error', { ignore: ['effect/*'] }], 'no-console': 'error', - 'perfectionist/sort-enums': [ - 'error', - { partitionByComment: true, sortByValue: 'always' }, - ], + 'perfectionist/sort-enums': ['error', { partitionByComment: true, sortByValue: 'always' }], 'perfectionist/sort-heritage-clauses': 'error', 'perfectionist/sort-interfaces': 'error', 'perfectionist/sort-jsx-props': 'error', @@ -901,15 +869,9 @@ export default defineConfig({ 'sonarjs/no-nested-conditional': 'off', 'sonarjs/no-redundant-jump': 'off', 'sonarjs/no-unused-vars': 'off', - 'typescript/no-require-imports': [ - 'error', - { allow: [String.raw`/package\.json$`] }, - ], + 'typescript/no-require-imports': ['error', { allow: [String.raw`/package\.json$`] }], // Terse void callbacks are idiomatic for framework and test APIs; confusing assignments remain errors. - 'typescript/no-confusing-void-expression': [ - 'error', - { ignoreArrowShorthand: true }, - ], + 'typescript/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], // Single-use generics preserve inferred return predicates and object value types throughout Effect APIs. 'typescript/no-unnecessary-type-parameters': 'off', // Annotating rejected-promise callbacks as unknown bypasses OntOS's named-error boundary policy. diff --git a/app/packages/core-runtime/MIGRATIONS.md b/app/packages/core-runtime/MIGRATIONS.md index 26e92d0f7..bace823b0 100644 --- a/app/packages/core-runtime/MIGRATIONS.md +++ b/app/packages/core-runtime/MIGRATIONS.md @@ -4,17 +4,17 @@ Anti-slop's `no-shape-in-symbol-names` rule rejects identifiers containing `Shape`, so the former public aliases cannot remain as deprecated compatibility exports. Import these replacements from `@app/core-runtime`: -| Removed type | Replacement | -| --- | --- | -| `PrincipalResolverShape` | `PrincipalResolverService` | +| Removed type | Replacement | +| ---------------------------------------------- | ------------------------------------------------ | +| `PrincipalResolverShape` | `PrincipalResolverService` | | `SupportRecoveryPrincipalContextResolverShape` | `SupportRecoveryPrincipalContextResolverService` | -| `LegalEntityContextShape` | `LegalEntityContextService` | -| `ContextAccessShape` | `ContextAccessService` | -| `OperationalScopeResolverShape` | `OperationalScopeResolverService` | -| `ModuleStateGateShape` | `ModuleStateGateService` | -| `ModuleEntrypointGatewayShape` | `ModuleEntrypointGatewayService` | -| `TenantModuleStateServiceShape` | `TenantModuleStateServiceContract` | -| `InstalledModuleCatalogServiceShape` | `InstalledModuleCatalogServiceContract` | +| `LegalEntityContextShape` | `LegalEntityContextService` | +| `ContextAccessShape` | `ContextAccessService` | +| `OperationalScopeResolverShape` | `OperationalScopeResolverService` | +| `ModuleStateGateShape` | `ModuleStateGateService` | +| `ModuleEntrypointGatewayShape` | `ModuleEntrypointGatewayService` | +| `TenantModuleStateServiceShape` | `TenantModuleStateServiceContract` | +| `InstalledModuleCatalogServiceShape` | `InstalledModuleCatalogServiceContract` | This is an intentional source-level migration: update type-only imports to the replacement name. Runtime service tags and behavior are unchanged. diff --git a/app/packages/core-runtime/scripts/verify-db-schema.mts b/app/packages/core-runtime/scripts/verify-db-schema.mts index c71364a92..be160af83 100644 --- a/app/packages/core-runtime/scripts/verify-db-schema.mts +++ b/app/packages/core-runtime/scripts/verify-db-schema.mts @@ -31,12 +31,9 @@ import { workerCheckpoints, } from '../src/db/schema.ts'; -class DatabaseVerificationError extends Schema.TaggedError()( - 'DatabaseVerificationError', - { - reason: Schema.String, - } -) {} +class DatabaseVerificationError extends Schema.TaggedError()('DatabaseVerificationError', { + reason: Schema.String, +}) {} const CatalogRowSchema = Schema.Struct({ kind: Schema.Literals(['migration', 'table']), @@ -56,16 +53,16 @@ const isUnsafeRuntimeRole = (role: RuntimeRoleRow | undefined): boolean => const verifyTypedQuery = ( tableName: string, - query: () => Effect.Effect + query: () => Effect.Effect, ): Effect.Effect => query().pipe( Effect.mapError( () => new DatabaseVerificationError({ reason: `Typed verification failed for ${CORE_SCHEMA_NAME}.${tableName}`, - }) + }), ), - Effect.asVoid + Effect.asVoid, ); const verifyRuntimeRole = Effect.gen(function* verifyRuntimeRoleEffect() { @@ -77,41 +74,39 @@ const verifyRuntimeRole = Effect.gen(function* verifyRuntimeRoleEffect() { from pg_catalog.pg_roles as role where role.rolname = current_user `, - 'objects' + 'objects', ) .pipe( Effect.mapError( () => new DatabaseVerificationError({ reason: 'Unable to verify the PostgreSQL runtime role', - }) - ) + }), + ), ); const [role] = runtimeRole; if (isUnsafeRuntimeRole(role)) { return yield* new DatabaseVerificationError({ - reason: - 'The application runtime role must be non-superuser and must not bypass RLS', + reason: 'The application runtime role must be non-superuser and must not bypass RLS', }); } return yield* Effect.void; }); -const verifySearchIsolation = Effect.gen( - function* verifySearchIsolationEffect() { - const database = yield* CoreDatabase; - for (const [tableName, operations] of [ - ['search_index_entries', ['delete', 'insert', 'select', 'update']], - ['search_projection_generations', ['insert', 'select', 'update']], - ['search_projection_rebuilds', ['insert', 'select', 'update']], - ] as const) { - const searchIsolation = yield* database.executor - .execute<{ - policy_names: string[]; - relforcerowsecurity: boolean; - relrowsecurity: boolean; - }>( - sql` +const verifySearchIsolation = Effect.gen(function* verifySearchIsolationEffect() { + const database = yield* CoreDatabase; + for (const [tableName, operations] of [ + ['search_index_entries', ['delete', 'insert', 'select', 'update']], + ['search_projection_generations', ['insert', 'select', 'update']], + ['search_projection_rebuilds', ['insert', 'select', 'update']], + ] as const) { + const searchIsolation = yield* database.executor + .execute<{ + policy_names: string[]; + relforcerowsecurity: boolean; + relrowsecurity: boolean; + }>( + sql` select relation.relrowsecurity, relation.relforcerowsecurity, @@ -125,39 +120,32 @@ const verifySearchIsolation = Effect.gen( and relation.relname = ${tableName} group by relation.relrowsecurity, relation.relforcerowsecurity `, - 'objects' - ) - .pipe( - Effect.mapError( - () => - new DatabaseVerificationError({ - reason: 'Unable to verify Core Search tenant isolation', - }) - ) - ); - const [searchIsolationRow] = searchIsolation; - const expectedSearchPolicies = operations.map( - (operation) => `core_${tableName}_tenant_${operation}` + 'objects', + ) + .pipe( + Effect.mapError( + () => + new DatabaseVerificationError({ + reason: 'Unable to verify Core Search tenant isolation', + }), + ), ); - if ( - searchIsolationRow === undefined || - !searchIsolationRow.relrowsecurity || - !searchIsolationRow.relforcerowsecurity || - searchIsolationRow.policy_names.length !== - expectedSearchPolicies.length || - searchIsolationRow.policy_names.some( - (policy, index) => policy !== expectedSearchPolicies[index] - ) - ) { - return yield* new DatabaseVerificationError({ - reason: - 'Core Search must enforce forced tenant RLS with complete owner-operation policies', - }); - } + const [searchIsolationRow] = searchIsolation; + const expectedSearchPolicies = operations.map((operation) => `core_${tableName}_tenant_${operation}`); + if ( + searchIsolationRow === undefined || + !searchIsolationRow.relrowsecurity || + !searchIsolationRow.relforcerowsecurity || + searchIsolationRow.policy_names.length !== expectedSearchPolicies.length || + searchIsolationRow.policy_names.some((policy, index) => policy !== expectedSearchPolicies[index]) + ) { + return yield* new DatabaseVerificationError({ + reason: 'Core Search must enforce forced tenant RLS with complete owner-operation policies', + }); } - return yield* Effect.void; } -); + return yield* Effect.void; +}); const verifyCatalog = Effect.gen(function* verifyCatalogEffect() { const database = yield* CoreDatabase; @@ -195,15 +183,15 @@ const verifyCatalog = Effect.gen(function* verifyCatalogEffect() { select kind, schema_name, table_name from migration_bookkeeping order by kind, schema_name, table_name `, - 'objects' + 'objects', ) .pipe( Effect.mapError( () => new DatabaseVerificationError({ reason: 'Unable to compare the PostgreSQL application catalog', - }) - ) + }), + ), ); const entries: CatalogEntry[] = []; @@ -234,12 +222,8 @@ const verifyCatalog = Effect.gen(function* verifyCatalogEffect() { migrationBookkeepingTables.sort(); if ( - migrationBookkeepingTables.length !== - expectedMigrationBookkeepingTables.length || - migrationBookkeepingTables.some( - (tableName, index) => - tableName !== expectedMigrationBookkeepingTables[index] - ) + migrationBookkeepingTables.length !== expectedMigrationBookkeepingTables.length || + migrationBookkeepingTables.some((tableName, index) => tableName !== expectedMigrationBookkeepingTables[index]) ) { return yield* new DatabaseVerificationError({ reason: `Expected Drizzle migration bookkeeping tables [${expectedMigrationBookkeepingTables.join(', ')}], found [${migrationBookkeepingTables.join(', ')}]`, @@ -304,26 +288,23 @@ const verifyDatabase = Effect.gen(function* verifyDatabaseEffect() { where namespace.nspname = ${CORE_SCHEMA_NAME} order by constraint_record.conname `, - 'objects' + 'objects', ) .pipe( Effect.mapError( () => new DatabaseVerificationError({ reason: 'Unable to verify same-tenant constraints', - }) - ) + }), + ), ); const presentCompositeConstraints = constraintRows .map((row) => row.conname) .filter((name) => requiredCompositeConstraints.includes(name)) .toSorted(); if ( - presentCompositeConstraints.length !== - requiredCompositeConstraints.length || - presentCompositeConstraints.some( - (name, index) => name !== requiredCompositeConstraints[index] - ) + presentCompositeConstraints.length !== requiredCompositeConstraints.length || + presentCompositeConstraints.some((name, index) => name !== requiredCompositeConstraints[index]) ) { return yield* new DatabaseVerificationError({ reason: 'Required composite same-tenant constraints are missing', @@ -350,11 +331,7 @@ const verifyDatabase = Effect.gen(function* verifyDatabaseEffect() { searchProjectionGenerations, searchProjectionRebuilds, workerCheckpoints, - ].map((table) => - verifyTypedQuery(getTableName(table), () => - database.executor.select().from(table).limit(0) - ) - ); + ].map((table) => verifyTypedQuery(getTableName(table), () => database.executor.select().from(table).limit(0))); for (const query of typedQueries) { yield* query; @@ -367,13 +344,7 @@ const verifyDatabase = Effect.gen(function* verifyDatabaseEffect() { }; }); -const DatabaseRuntimeLive = CoreDatabaseLive.pipe( - Layer.provide(DatabaseConfigLive) -); -const result = await Effect.runPromise( - Effect.provide(verifyDatabase, DatabaseRuntimeLive) -); +const DatabaseRuntimeLive = CoreDatabaseLive.pipe(Layer.provide(DatabaseConfigLive)); +const result = await Effect.runPromise(Effect.provide(verifyDatabase, DatabaseRuntimeLive)); -console.log( - `Verified ${result.tableCount} typed tables in PostgreSQL schema ${CORE_SCHEMA_NAME}` -); +console.log(`Verified ${result.tableCount} typed tables in PostgreSQL schema ${CORE_SCHEMA_NAME}`); diff --git a/app/packages/core-runtime/src/actions/collector.ts b/app/packages/core-runtime/src/actions/collector.ts index 960f4c9d5..b7498965c 100644 --- a/app/packages/core-runtime/src/actions/collector.ts +++ b/app/packages/core-runtime/src/actions/collector.ts @@ -1,12 +1,7 @@ import { Effect, Match, Schema, Predicate } from 'effect'; import { ActionCollectorError } from './errors.ts'; -import { - DataAccessEventSchema, - DomainEventSchema, - OutboxMessageSchema, - createDomainEventReference, -} from './events.ts'; +import { DataAccessEventSchema, DomainEventSchema, OutboxMessageSchema, createDomainEventReference } from './events.ts'; import type { ActionAccessEvidencePolicy, ActionEvidenceSnapshot, @@ -20,19 +15,13 @@ import type { OutboxMessage, } from './events.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const invalidCollectorInput = (reason: string) => new ActionCollectorError({ @@ -50,8 +39,7 @@ const freezeJson = (value: Value): Value => { return value; }; -const cloneAndFreeze = (value: Value): Value => - freezeJson(structuredClone(value)); +const cloneAndFreeze = (value: Value): Value => freezeJson(structuredClone(value)); const JsonObjectSchema = Schema.Record(Schema.String, Schema.Json); const JsonObjectJsonStringSchema = Schema.fromJsonString(JsonObjectSchema); @@ -64,10 +52,7 @@ const RuntimeResultHashEvidenceSchema = Schema.Struct({ }); const metadataOnlyPolicyFields = ( - policy: Extract< - ActionAccessEvidencePolicy, - { readonly captureMode: 'metadata_only' } - > + policy: Extract, ) => ({ evidenceCaptureMode: policy.captureMode, @@ -75,10 +60,7 @@ const metadataOnlyPolicyFields = ( }) as const; const redactedPayloadPolicyFields = ( - policy: Extract< - ActionAccessEvidencePolicy, - { readonly captureMode: 'redacted_payload' } - > + policy: Extract, ) => ({ evidenceCaptureMode: policy.captureMode, @@ -88,12 +70,10 @@ const redactedPayloadPolicyFields = ( const hasIncompleteRedactedEvidence = (event: DataAccessEvent): boolean => event.evidenceCaptureMode === 'redacted_payload' && - (event.redactionProfile === undefined || - event.evidencePayloadJson === undefined); + (event.redactionProfile === undefined || event.evidencePayloadJson === undefined); const hasUnexpectedRedactionProfile = (event: DataAccessEvent): boolean => - event.evidenceCaptureMode !== 'redacted_payload' && - event.redactionProfile !== undefined; + event.evidenceCaptureMode !== 'redacted_payload' && event.redactionProfile !== undefined; const hasMetadataResultEvidence = (event: DataAccessEvent): boolean => event.evidenceCaptureMode === 'metadata_only' && @@ -104,70 +84,44 @@ const hasMetadataResultEvidence = (event: DataAccessEvent): boolean => const hasInvalidHashEvidence = (event: DataAccessEvent): boolean => event.evidenceCaptureMode === 'hash_only' && (event.evidencePayloadJson !== undefined || - (event.resultFingerprintHash === undefined) !== - (event.resultFingerprintSchema === undefined)); + (event.resultFingerprintHash === undefined) !== (event.resultFingerprintSchema === undefined)); const hasUnsupportedResultEvidence = (event: DataAccessEvent): boolean => event.evidenceCaptureMode === 'stored_artifact' || (event.evidenceCaptureMode === 'redacted_payload' && - (event.resultFingerprintHash !== undefined || - event.resultFingerprintSchema !== undefined)); + (event.resultFingerprintHash !== undefined || event.resultFingerprintSchema !== undefined)); -const validateDataAccessInvariant = ( - event: DataAccessEvent -): Effect.Effect => { +const validateDataAccessInvariant = (event: DataAccessEvent): Effect.Effect => { if (hasIncompleteRedactedEvidence(event)) { return Effect.fail( - invalidCollectorInput( - 'A redacted Data Access Event requires a redaction profile and evidence payload' - ) + invalidCollectorInput('A redacted Data Access Event requires a redaction profile and evidence payload'), ); } if (hasUnexpectedRedactionProfile(event)) { - return Effect.fail( - invalidCollectorInput( - 'A redaction profile is allowed only for redacted Data Access Events' - ) - ); + return Effect.fail(invalidCollectorInput('A redaction profile is allowed only for redacted Data Access Events')); } if (hasMetadataResultEvidence(event)) { - return Effect.fail( - invalidCollectorInput( - 'Metadata-only Data Access evidence cannot contain result evidence' - ) - ); + return Effect.fail(invalidCollectorInput('Metadata-only Data Access evidence cannot contain result evidence')); } if (hasInvalidHashEvidence(event)) { return Effect.fail( - invalidCollectorInput( - 'Hash-only Data Access evidence requires a paired result fingerprint and schema' - ) + invalidCollectorInput('Hash-only Data Access evidence requires a paired result fingerprint and schema'), ); } if (hasUnsupportedResultEvidence(event)) { - return Effect.fail( - invalidCollectorInput( - 'The Action runtime does not accept this result evidence shape' - ) - ); + return Effect.fail(invalidCollectorInput('The Action runtime does not accept this result evidence shape')); } - const targetParts = [ - event.targetModuleKey, - event.targetResourceType, - event.targetResourceId, - ].filter((part) => part !== undefined); + const targetParts = [event.targetModuleKey, event.targetResourceType, event.targetResourceId].filter( + (part) => part !== undefined, + ); if (targetParts.length !== 0 && targetParts.length !== 3) { - return Effect.fail( - invalidCollectorInput( - 'A Data Access Event target must be fully specified or absent' - ) - ); + return Effect.fail(invalidCollectorInput('A Data Access Event target must be fully specified or absent')); } return Effect.succeed(event); @@ -175,43 +129,28 @@ const validateDataAccessInvariant = ( export interface ActionCollector { readonly addDomainEvent: ( - event: DeclaredDomainEvent - ) => Effect.Effect; - readonly addDomainEventInput: ( - event: Input + event: DeclaredDomainEvent, ) => Effect.Effect; + readonly addDomainEventInput: (event: Input) => Effect.Effect; readonly addOutboxMessage: ( domainEvent: DomainEventReference, - message: OutboxMessage + message: OutboxMessage, ) => Effect.Effect; readonly addOutboxMessageInput: ( domainEvent: Reference, - message: Message + message: Message, ) => Effect.Effect; readonly recordAuditEvidence: ( - evidence: Readonly>> - ) => Effect.Effect; - readonly recordAuditEvidenceInput: ( - evidence: Input - ) => Effect.Effect; - readonly recordDataAccess: ( - event: DataAccessEventInput - ) => Effect.Effect; - readonly recordDataAccessInput: ( - event: Input + evidence: Readonly>>, ) => Effect.Effect; + readonly recordAuditEvidenceInput: (evidence: Input) => Effect.Effect; + readonly recordDataAccess: (event: DataAccessEventInput) => Effect.Effect; + readonly recordDataAccessInput: (event: Input) => Effect.Effect; readonly snapshot: () => ActionEvidenceSnapshot; } -export const createActionCollector = < - DomainEvents extends DomainEventContractMap, ->( - ...[ - domainEventContracts, - owningModuleKey, - accessEvidencePolicy, - auditEvidenceSchema, - ]: readonly [ +export const createActionCollector = ( + ...[domainEventContracts, owningModuleKey, accessEvidencePolicy, auditEvidenceSchema]: readonly [ domainEventContracts: DomainEvents, owningModuleKey: string, accessEvidencePolicy: ActionAccessEvidencePolicy, @@ -219,120 +158,72 @@ export const createActionCollector = < ] ): ActionCollector => { const dataAccessEvents: DataAccessEvent[] = []; - let auditEvidence: Readonly< - Record> - > = {}; + let auditEvidence: Readonly>> = {}; let hasAuditEvidence = false; const domainEvents: DomainEvent[] = []; const outboxMessages: CollectedOutboxMessage[] = []; const references = new Map(); - const recordAuditEvidenceInput = ( - evidence: Input - ): Effect.Effect => + const recordAuditEvidenceInput = (evidence: Input): Effect.Effect => Schema.decodeUnknownEffect(UnknownRecordSchema)(evidence).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput('Action audit evidence must be a JSON object') - ) + Effect.fail(invalidCollectorInput('Action audit evidence must be a JSON object')), ), Effect.flatMap((evidenceRecord) => { if (hasAuditEvidence) { - return Effect.fail( - invalidCollectorInput( - 'Action audit evidence may be recorded only once' - ) - ); + return Effect.fail(invalidCollectorInput('Action audit evidence may be recorded only once')); } if ( Schema.is(RuntimeActionKeyEvidenceSchema)(evidenceRecord) || Schema.is(RuntimeResultHashEvidenceSchema)(evidenceRecord) ) { - return Effect.fail( - invalidCollectorInput( - 'Action audit evidence cannot replace runtime-owned fields' - ) - ); + return Effect.fail(invalidCollectorInput('Action audit evidence cannot replace runtime-owned fields')); } if (auditEvidenceSchema === undefined) { - return Effect.fail( - invalidCollectorInput( - 'This Action does not declare custom audit evidence' - ) - ); + return Effect.fail(invalidCollectorInput('This Action does not declare custom audit evidence')); } const inputKeys = Object.keys(evidenceRecord).toSorted(); return Schema.decodeUnknownEffect(auditEvidenceSchema)(evidence).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput( - 'The Action audit evidence does not match its declared schema' - ) - ) + Effect.fail(invalidCollectorInput('The Action audit evidence does not match its declared schema')), ), - Effect.map((declared) => ({ declared, inputKeys })) + Effect.map((declared) => ({ declared, inputKeys })), ); }), Effect.flatMap(({ declared, inputKeys }) => - Schema.decodeUnknownEffect(Schema.Json)(declared).pipe( - Effect.map((decoded) => ({ decoded, inputKeys })) - ) + Schema.decodeUnknownEffect(Schema.Json)(declared).pipe(Effect.map((decoded) => ({ decoded, inputKeys }))), ), Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput('The Action audit evidence is not valid JSON') - ) + Effect.fail(invalidCollectorInput('The Action audit evidence is not valid JSON')), ), Effect.flatMap(({ decoded, inputKeys }) => { if (!Schema.is(JsonObjectSchema)(decoded)) { - return Effect.fail( - invalidCollectorInput('Action audit evidence must be a JSON object') - ); + return Effect.fail(invalidCollectorInput('Action audit evidence must be a JSON object')); } const decodedKeys = Object.keys(decoded).toSorted(); - if ( - inputKeys.length !== decodedKeys.length || - inputKeys.some((key, index) => key !== decodedKeys[index]) - ) { - return Effect.fail( - invalidCollectorInput( - 'Action audit evidence contains undeclared fields' - ) - ); + if (inputKeys.length !== decodedKeys.length || inputKeys.some((key, index) => key !== decodedKeys[index])) { + return Effect.fail(invalidCollectorInput('Action audit evidence contains undeclared fields')); } return Schema.encodeEffect(JsonObjectJsonStringSchema)(decoded).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput( - 'The Action audit evidence is not valid JSON' - ) - ) + Effect.fail(invalidCollectorInput('The Action audit evidence is not valid JSON')), ), Effect.flatMap((encoded) => { if (Buffer.byteLength(encoded, 'utf-8') > 4096) { - return Effect.fail( - invalidCollectorInput( - 'Action audit evidence exceeds its size limit' - ) - ); + return Effect.fail(invalidCollectorInput('Action audit evidence exceeds its size limit')); } return Effect.sync(() => { auditEvidence = cloneAndFreeze(decoded); hasAuditEvidence = true; }); - }) + }), ); - }) + }), ); - const recordAuditEvidence: ActionCollector['recordAuditEvidence'] = - recordAuditEvidenceInput; + const recordAuditEvidence: ActionCollector['recordAuditEvidence'] = recordAuditEvidenceInput; - const recordDataAccessInput = ( - event: Input - ): Effect.Effect => { - const eventRecord = Schema.is(UnknownRecordSchema)(event) - ? event - : undefined; + const recordDataAccessInput = (event: Input): Effect.Effect => { + const eventRecord = Schema.is(UnknownRecordSchema)(event) ? event : undefined; const resultFingerprintHash = eventRecord?.['resultFingerprintHash']; const policyFields = Match.value(accessEvidencePolicy).pipe( Match.when({ captureMode: 'hash_only' }, (policy) => @@ -344,91 +235,58 @@ export const createActionCollector = < resultFingerprintHash !== undefined, 'resultFingerprintSchema', policy.resultFingerprintSchema, - {} - ) + {}, + ), ), Match.when({ captureMode: 'metadata_only' }, metadataOnlyPolicyFields), - Match.when( - { captureMode: 'redacted_payload' }, - redactedPayloadPolicyFields - ), - Match.exhaustive + Match.when({ captureMode: 'redacted_payload' }, redactedPayloadPolicyFields), + Match.exhaustive, ); - const materializedEvent = - eventRecord === undefined ? event : { ...eventRecord, ...policyFields }; + const materializedEvent = eventRecord === undefined ? event : { ...eventRecord, ...policyFields }; - return Schema.decodeUnknownEffect(DataAccessEventSchema)( - materializedEvent - ).pipe( + return Schema.decodeUnknownEffect(DataAccessEventSchema)(materializedEvent).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput('The Data Access Event is structurally invalid') - ) + Effect.fail(invalidCollectorInput('The Data Access Event is structurally invalid')), ), Effect.flatMap(validateDataAccessInvariant), Effect.tap((decoded) => Effect.sync(() => { dataAccessEvents.push(cloneAndFreeze(decoded)); - }) + }), ), - Effect.asVoid + Effect.asVoid, ); }; - const recordDataAccess: ActionCollector['recordDataAccess'] = - recordDataAccessInput; + const recordDataAccess: ActionCollector['recordDataAccess'] = recordDataAccessInput; - const addDomainEventInput = ( - event: Input - ): Effect.Effect => + const addDomainEventInput = (event: Input): Effect.Effect => Schema.decodeUnknownEffect(DomainEventSchema)(event).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput('The Domain Event is structurally invalid') - ) + Effect.fail(invalidCollectorInput('The Domain Event is structurally invalid')), ), Effect.flatMap((decoded) => { if (decoded.producerModuleKey !== owningModuleKey) { - return Effect.fail( - invalidCollectorInput( - 'A Domain Event producer must match the owning Action module' - ) - ); + return Effect.fail(invalidCollectorInput('A Domain Event producer must match the owning Action module')); } if (!Object.hasOwn(domainEventContracts, decoded.eventType)) { - return Effect.fail( - invalidCollectorInput( - 'The Domain Event is not declared by this Action' - ) - ); + return Effect.fail(invalidCollectorInput('The Domain Event is not declared by this Action')); } const payloadSchema = domainEventContracts[decoded.eventType]; if (payloadSchema === undefined) { - return Effect.fail( - invalidCollectorInput( - 'The Domain Event declaration has no payload schema' - ) - ); + return Effect.fail(invalidCollectorInput('The Domain Event declaration has no payload schema')); } return Schema.decodeEffect(payloadSchema)(decoded.payloadJson).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput( - 'The Domain Event payload violates its declared contract' - ) - ) + Effect.fail(invalidCollectorInput('The Domain Event payload violates its declared contract')), ), Effect.flatMap((payload) => Schema.decodeUnknownEffect(Schema.Json)(payload).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput( - 'The decoded Domain Event payload is not JSON' - ) - ) - ) - ) + Effect.fail(invalidCollectorInput('The decoded Domain Event payload is not JSON')), + ), + ), ), - Effect.map((payloadJson) => ({ ...decoded, payloadJson })) + Effect.map((payloadJson) => ({ ...decoded, payloadJson })), ); }), Effect.map((decoded) => { @@ -437,14 +295,11 @@ export const createActionCollector = < domainEvents.push(cloneAndFreeze(decoded)); references.set(reference, index); return reference; - }) + }), ); - const addDomainEvent: ActionCollector['addDomainEvent'] = - addDomainEventInput; + const addDomainEvent: ActionCollector['addDomainEvent'] = addDomainEventInput; - const findReferenceIndex = ( - candidate: Reference - ): number | undefined => { + const findReferenceIndex = (candidate: Reference): number | undefined => { for (const [reference, index] of references) { if (Object.is(reference, candidate)) { return index; @@ -454,23 +309,19 @@ export const createActionCollector = < }; const addOutboxMessageInput = ( domainEvent: Reference, - message: Message + message: Message, ): Effect.Effect => { const domainEventIndex = findReferenceIndex(domainEvent); if (domainEventIndex === undefined) { return Effect.fail( - invalidCollectorInput( - 'An Outbox Message must reference a Domain Event from the same Action execution' - ) + invalidCollectorInput('An Outbox Message must reference a Domain Event from the same Action execution'), ); } return Schema.decodeUnknownEffect(OutboxMessageSchema)(message).pipe( Effect.catchTag('SchemaError', () => - Effect.fail( - invalidCollectorInput('The Outbox Message is structurally invalid') - ) + Effect.fail(invalidCollectorInput('The Outbox Message is structurally invalid')), ), Effect.flatMap((decoded) => { const registeredDomainEvent = domainEvents[domainEventIndex]; @@ -480,9 +331,7 @@ export const createActionCollector = < decoded.producerModuleKey !== registeredDomainEvent.producerModuleKey ) { return Effect.fail( - invalidCollectorInput( - 'An Outbox Message producer must match its registered Domain Event producer' - ) + invalidCollectorInput('An Outbox Message producer must match its registered Domain Event producer'), ); } return Effect.succeed(decoded); @@ -493,15 +342,14 @@ export const createActionCollector = < Object.freeze({ domainEventIndex, message: cloneAndFreeze(decoded), - }) + }), ); - }) + }), ), - Effect.asVoid + Effect.asVoid, ); }; - const addOutboxMessage: ActionCollector['addOutboxMessage'] = - addOutboxMessageInput; + const addOutboxMessage: ActionCollector['addOutboxMessage'] = addOutboxMessageInput; const snapshot = (): ActionEvidenceSnapshot => Object.freeze({ diff --git a/app/packages/core-runtime/src/actions/context.ts b/app/packages/core-runtime/src/actions/context.ts index fa002cff3..f815f668d 100644 --- a/app/packages/core-runtime/src/actions/context.ts +++ b/app/packages/core-runtime/src/actions/context.ts @@ -10,21 +10,13 @@ import type { DomainEventReference, OutboxMessage, } from './events.ts'; -import { - decodedStringBrand, - nonEmptyString, - TargetModuleKeySchema, - TargetResourceIdSchema, -} from './string-schemas.ts'; +import { decodedStringBrand, nonEmptyString, TargetModuleKeySchema, TargetResourceIdSchema } from './string-schemas.ts'; export { TrustedPrincipalContextSchema } from './principal-context.ts'; export type { TrustedPrincipalContext } from './principal-context.ts'; const CorrelationIdSchema = decodedStringBrand(nonEmptyString, 'CorrelationId'); -const IdempotencyKeySchema = decodedStringBrand( - nonEmptyString, - 'IdempotencyKey' -); +const IdempotencyKeySchema = decodedStringBrand(nonEmptyString, 'IdempotencyKey'); const TraceIdSchema = decodedStringBrand(nonEmptyString, 'TraceId'); export const ActionTransportMetadataSchema = Schema.Struct({ @@ -36,26 +28,20 @@ export const ActionTransportMetadataSchema = Schema.Struct({ traceId: Schema.optionalKey(TraceIdSchema), }); -export type ActionTransportMetadata = Schema.Schema.Type< - typeof ActionTransportMetadataSchema ->; +export type ActionTransportMetadata = Schema.Schema.Type; -export interface ActionCollectorMethods< - DomainEvents extends DomainEventContractMap, -> { +export interface ActionCollectorMethods { readonly addDomainEvent: ( - event: DeclaredDomainEvent + event: DeclaredDomainEvent, ) => Effect.Effect; readonly addOutboxMessage: ( domainEvent: DomainEventReference, - message: OutboxMessage + message: OutboxMessage, ) => Effect.Effect; readonly recordAuditEvidence: ( - evidence: Readonly>> - ) => Effect.Effect; - readonly recordDataAccess: ( - event: DataAccessEventInput + evidence: Readonly>>, ) => Effect.Effect; + readonly recordDataAccess: (event: DataAccessEventInput) => Effect.Effect; } export interface ActionHandlerContext< diff --git a/app/packages/core-runtime/src/actions/definition.ts b/app/packages/core-runtime/src/actions/definition.ts index 6effdb7ef..94a71e38b 100644 --- a/app/packages/core-runtime/src/actions/definition.ts +++ b/app/packages/core-runtime/src/actions/definition.ts @@ -3,34 +3,18 @@ import { Effect, Schema, Predicate } from 'effect'; import type { ScopedTransactionExecutor } from '../db/scoped-transaction.ts'; import type { ModuleEntrypointDescriptor } from '../modules/module-entrypoint.ts'; import { LEGAL_ENTITY_SCOPES } from '../operations/context.ts'; -import type { - OperationalScope, - LegalEntityScope, -} from '../operations/context.ts'; +import type { OperationalScope, LegalEntityScope } from '../operations/context.ts'; import type { OperationContextUnavailable } from '../operations/errors.ts'; -import type { - ResourceAccessTarget, - TenantPermissionKey, -} from '../permissions/context-access.ts'; +import type { ResourceAccessTarget, TenantPermissionKey } from '../permissions/context-access.ts'; import type { ActionHandlerContext } from './context.ts'; -import { - ActionPayloadValidationError, - ActionResultValidationError, -} from './errors.ts'; +import { ActionPayloadValidationError, ActionResultValidationError } from './errors.ts'; import type { ActionCollectorError } from './errors.ts'; -import type { - ActionAccessEvidencePolicy, - DomainEventContractMap, -} from './events.ts'; +import type { ActionAccessEvidencePolicy, DomainEventContractMap } from './events.ts'; import { isActionPolicy } from './policy.ts'; import type { ActionPolicy } from './policy.ts'; -const actionRegistration: unique symbol = Symbol( - '@app/core-runtime/actions/registration' -); -const actionResourcePermissionDeclaration: unique symbol = Symbol( - '@app/core-runtime/actions/resource-permission' -); +const actionRegistration: unique symbol = Symbol('@app/core-runtime/actions/registration'); +const actionResourcePermissionDeclaration: unique symbol = Symbol('@app/core-runtime/actions/resource-permission'); class ActionPrivateStorage { declare readonly [actionRegistration]?: true; @@ -45,12 +29,9 @@ class ActionPrivateStorage { static create( value: Value, - publicFields: PublicFields + publicFields: PublicFields, ): ActionPrivateStorage & Readonly { - const storage = Object.assign( - new ActionPrivateStorage(value), - publicFields - ); + const storage = Object.assign(new ActionPrivateStorage(value), publicFields); Object.freeze(storage); return storage; } @@ -62,27 +43,19 @@ class ActionPrivateStorage { const ActionIdempotencyRuleSchema = Schema.Literals(['optional', 'required']); export type ActionIdempotencyRule = typeof ActionIdempotencyRuleSchema.Type; -const ActionAuditProfileSchema = Schema.Literals([ - 'minimal', - 'sensitive', - 'standard', -]); +const ActionAuditProfileSchema = Schema.Literals(['minimal', 'sensitive', 'standard']); export type ActionAuditProfile = typeof ActionAuditProfileSchema.Type; -export type ActionTenantPermission = Exclude< - TenantPermissionKey, - 'access' | 'read_party_identity' ->; +export type ActionTenantPermission = Exclude; export type ActionLegalEntityPermission = 'manage_counterparty'; const ActionResourcePermissionSchema = Schema.Literals(['read', 'write']); -export type ActionResourcePermission = - typeof ActionResourcePermissionSchema.Type; +export type ActionResourcePermission = typeof ActionResourcePermissionSchema.Type; export interface ActionResourcePermissionTarget { readonly permission: ActionResourcePermission; readonly resource: ResourceAccessTarget; } export type ActionResourcePermissionTargetResolver = ( payload: Payload, - scope: OperationalScope + scope: OperationalScope, ) => ActionResourcePermissionTarget; export type ActionResourcePermissionDeclaration = ActionPrivateStorage< ActionResourcePermissionTargetResolver @@ -91,10 +64,9 @@ export type ActionResourcePermissionDeclaration = ActionPrivateStorage< readonly kind: 'resource'; }; -const ActionDefinitionInvariantError = Schema.TaggedError()( - 'ActionDefinitionInvariantError', - { message: Schema.String } -); +const ActionDefinitionInvariantError = Schema.TaggedError()('ActionDefinitionInvariantError', { + message: Schema.String, +}); const failActionDefinition = (message: string): never => { throw new ActionDefinitionInvariantError({ message }); @@ -102,12 +74,10 @@ const failActionDefinition = (message: string): never => { /** Declares a private resolver for an additional Resource permission check. */ export const defineActionResourcePermission = ( - resolver: ActionResourcePermissionTargetResolver + resolver: ActionResourcePermissionTargetResolver, ): ActionResourcePermissionDeclaration => { if (!Predicate.isFunction(resolver)) { - return failActionDefinition( - 'Action Resource permission resolver must be a function' - ); + return failActionDefinition('Action Resource permission resolver must be a function'); } return ActionPrivateStorage.create(resolver, { [actionResourcePermissionDeclaration]: true as const, @@ -115,16 +85,14 @@ export const defineActionResourcePermission = ( }); }; -const ActionResourcePermissionDeclarationSchema = Schema.instanceOf( - ActionPrivateStorage -).check( +const ActionResourcePermissionDeclarationSchema = Schema.instanceOf(ActionPrivateStorage).check( Schema.makeFilter((declaration) => declaration[actionResourcePermissionDeclaration] === true && declaration.kind === 'resource' && Object.isFrozen(declaration) ? undefined - : 'Expected an immutable Action Resource permission declaration' - ) + : 'Expected an immutable Action Resource permission declaration', + ), ); export interface ActionDescriptor< @@ -153,23 +121,16 @@ export interface ActionDescriptor< readonly legalEntityScope: LegalEntityScope; readonly owningModuleKey: Owner; readonly payloadSchema: PayloadSchema; - readonly policies: readonly ActionPolicy< - PayloadSchema['Type'], - NoInfer - >[]; + readonly policies: readonly ActionPolicy>[]; /** Declares an additional Resource permission resolved from decoded input and trusted scope. */ - readonly resourcePermission?: ActionResourcePermissionDeclaration< - PayloadSchema['Type'] - >; + readonly resourcePermission?: ActionResourcePermissionDeclaration; readonly resultSchema: ResultSchema; readonly schemaVersion: string; /** * Declares an additional tenant-role permission required for the decoded payload. * Returning undefined means the Action executor relation is sufficient for that payload. */ - readonly tenantPermission?: ( - payload: PayloadSchema['Type'] - ) => ActionTenantPermission | undefined; + readonly tenantPermission?: (payload: PayloadSchema['Type']) => ActionTenantPermission | undefined; } export type ActionHandler< @@ -181,23 +142,17 @@ export type ActionHandler< Requirements = never, > = ( payload: PayloadSchema['Type'], - context: ActionHandlerContext -) => Effect.Effect< - ResultSchema['Type'], - ActionCollectorError | DomainErrorSchema['Type'], - Requirements ->; + context: ActionHandlerContext, +) => Effect.Effect; export type ActionServiceFactory = ( transaction: ScopedTransactionExecutor, - scope: OperationalScope + scope: OperationalScope, ) => Effect.Effect; type EmptyActionServices = Readonly>; const emptyActionServices: EmptyActionServices = Object.freeze({}); -const emptyActionServiceFactory: ActionServiceFactory< - EmptyActionServices -> = () => Effect.succeed(emptyActionServices); +const emptyActionServiceFactory: ActionServiceFactory = () => Effect.succeed(emptyActionServices); type ActionRegistrationPrivateValue< PayloadSchema extends Schema.ConstraintDecoder, @@ -207,14 +162,7 @@ type ActionRegistrationPrivateValue< Services = Readonly>, HandlerRequirements = never, > = readonly [ - handler: ActionHandler< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Services, - HandlerRequirements - >, + handler: ActionHandler, serviceFactory: ActionServiceFactory, ]; @@ -239,15 +187,7 @@ export type ActionRegistration< readonly _handlerRequirements?: HandlerRequirements; readonly _services?: Services; readonly [actionRegistration]: true; - readonly descriptor: Readonly< - ActionDescriptor< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Owner - > - >; + readonly descriptor: Readonly>; }; /** @@ -299,87 +239,59 @@ export interface ActionDescriptorValidationInput { readonly tenantPermission?: unknown; } -const validateActionEntrypoint = ( - descriptor: ActionDescriptorValidationInput -): void => { +const validateActionEntrypoint = (descriptor: ActionDescriptorValidationInput): void => { if ( descriptor.entrypoint.role !== 'action' || descriptor.entrypoint.access !== 'write' || descriptor.entrypoint.moduleKey !== descriptor.owningModuleKey || - descriptor.entrypoint.scope !== - (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') || + descriptor.entrypoint.scope !== (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') || !Object.isFrozen(descriptor.entrypoint) ) { return failActionDefinition( - 'Action entrypoint must be an immutable action/write descriptor with the required owner scope' + 'Action entrypoint must be an immutable action/write descriptor with the required owner scope', ); } }; -const validateActionLegalEntityScope = ( - descriptor: ActionDescriptorValidationInput -): void => { - if ( - !LEGAL_ENTITY_SCOPES.some((scope) => scope === descriptor.legalEntityScope) - ) { - return failActionDefinition( - 'Action legal-entity scope must be required, optional, or forbidden' - ); +const validateActionLegalEntityScope = (descriptor: ActionDescriptorValidationInput): void => { + if (!LEGAL_ENTITY_SCOPES.some((scope) => scope === descriptor.legalEntityScope)) { + return failActionDefinition('Action legal-entity scope must be required, optional, or forbidden'); } if ( descriptor.legalEntityPermission !== undefined && - (descriptor.legalEntityPermission !== 'manage_counterparty' || - descriptor.legalEntityScope !== 'required') + (descriptor.legalEntityPermission !== 'manage_counterparty' || descriptor.legalEntityScope !== 'required') ) { return failActionDefinition( - 'Action Legal Entity permission must be supported and require trusted Legal Entity scope' + 'Action Legal Entity permission must be supported and require trusted Legal Entity scope', ); } }; -const validateActionPermissions = ( - descriptor: ActionDescriptorValidationInput -): void => { +const validateActionPermissions = (descriptor: ActionDescriptorValidationInput): void => { if ( (descriptor.resourcePermission !== undefined && - !Schema.is(ActionResourcePermissionDeclarationSchema)( - descriptor.resourcePermission - )) || - (descriptor.tenantPermission !== undefined && - !Predicate.isFunction(descriptor.tenantPermission)) + !Schema.is(ActionResourcePermissionDeclarationSchema)(descriptor.resourcePermission)) || + (descriptor.tenantPermission !== undefined && !Predicate.isFunction(descriptor.tenantPermission)) ) { - return failActionDefinition( - 'Action permission declarations and resolvers must be valid' - ); + return failActionDefinition('Action permission declarations and resolvers must be valid'); } }; const validateActionPolicies = ( descriptor: ActionDescriptorValidationInput, - policies: readonly Policy[] | undefined + policies: readonly Policy[] | undefined, ): void => { if (!Array.isArray(policies)) { - return failActionDefinition( - 'Action policies must be an explicit readonly array of Policy references' - ); + return failActionDefinition('Action policies must be an explicit readonly array of Policy references'); } validateActionPermissions(descriptor); for (const policy of policies) { if (!isActionPolicy(policy)) { - return failActionDefinition( - 'Action policies must contain direct Policy object references' - ); + return failActionDefinition('Action policies must contain direct Policy object references'); } - if ( - policy.scope === 'microvertical' && - policy.owningModuleKey !== descriptor.owningModuleKey - ) { - return failActionDefinition( - 'A MicroVertical Policy must be owned by the Action owning module' - ); + if (policy.scope === 'microvertical' && policy.owningModuleKey !== descriptor.owningModuleKey) { + return failActionDefinition('A MicroVertical Policy must be owned by the Action owning module'); } } }; -export const validateActionDescriptorInput = ( - descriptor: ActionDescriptorValidationInput -): void => { +export const validateActionDescriptorInput = (descriptor: ActionDescriptorValidationInput): void => { validateActionEntrypoint(descriptor); validateActionLegalEntityScope(descriptor); validateActionPolicies(descriptor, descriptor.policies); @@ -393,13 +305,7 @@ export function defineAction< const Owner extends string, HandlerRequirements, >( - descriptor: ActionDescriptor< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Owner - >, + descriptor: ActionDescriptor, handler: ActionHandler< PayloadSchema, ResultSchema, @@ -407,7 +313,7 @@ export function defineAction< DomainEvents, EmptyActionServices, HandlerRequirements - > + >, ): ActionRegistration< PayloadSchema, ResultSchema, @@ -426,22 +332,9 @@ export function defineAction< Services, HandlerRequirements, >( - descriptor: ActionDescriptor< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Owner - >, + descriptor: ActionDescriptor, ...definition: readonly [ - handler: ActionHandler< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Services, - HandlerRequirements - >, + handler: ActionHandler, serviceFactory: ActionServiceFactory, ] ): ActionRegistration< @@ -462,13 +355,7 @@ export function defineAction< Services, HandlerRequirements, >( - descriptor: ActionDescriptor< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Owner - >, + descriptor: ActionDescriptor, ...definition: | readonly [ handler: ActionHandler< @@ -503,13 +390,10 @@ export function defineAction< }); if (definition.length === 1) { const [handler] = definition; - return ActionPrivateStorage.create( - [handler, emptyActionServiceFactory] as const, - { - [actionRegistration]: true as const, - descriptor: frozenDescriptor, - } - ); + return ActionPrivateStorage.create([handler, emptyActionServiceFactory] as const, { + [actionRegistration]: true as const, + descriptor: frozenDescriptor, + }); } const [handler, serviceFactory] = definition; return ActionPrivateStorage.create([handler, serviceFactory] as const, { @@ -518,22 +402,16 @@ export function defineAction< }); } -const AnyActionRegistrationSchema = Schema.instanceOf( - ActionPrivateStorage -).check( +const AnyActionRegistrationSchema = Schema.instanceOf(ActionPrivateStorage).check( Schema.makeFilter((registration) => - registration[actionRegistration] === true && - registration.descriptor !== undefined && - Object.isFrozen(registration) + registration[actionRegistration] === true && registration.descriptor !== undefined && Object.isFrozen(registration) ? undefined - : 'Expected an immutable Action registration' - ) + : 'Expected an immutable Action registration', + ), ); /** Runtime guard for the opaque value created by defineAction. */ -export const isActionRegistration = ( - value: Value -): value is Value & AnyActionRegistration => +export const isActionRegistration = (value: Value): value is Value & AnyActionRegistration => Schema.is(AnyActionRegistrationSchema)(value); /** Internal Core runtime seam. Action handlers are intentionally absent from the public registration. */ @@ -554,15 +432,9 @@ export const getActionHandler = < Owner, Services, HandlerRequirements - > -): ActionHandler< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Services, - HandlerRequirements -> => ActionPrivateStorage.getValue(registration)[0]; + >, +): ActionHandler => + ActionPrivateStorage.getValue(registration)[0]; export const getActionServiceFactory = < PayloadSchema extends Schema.ConstraintDecoder, @@ -581,9 +453,8 @@ export const getActionServiceFactory = < Owner, Services, HandlerRequirements - > -): ActionServiceFactory => - ActionPrivateStorage.getValue(registration)[1]; + >, +): ActionServiceFactory => ActionPrivateStorage.getValue(registration)[1]; export const getActionResourcePermissionTargetResolver = < PayloadSchema extends Schema.ConstraintDecoder, @@ -602,16 +473,13 @@ export const getActionResourcePermissionTargetResolver = < Owner, Services, HandlerRequirements - > + >, ): ActionResourcePermissionTargetResolver | undefined => registration.descriptor.resourcePermission === undefined ? undefined : ActionPrivateStorage.getValue(registration.descriptor.resourcePermission); -const preserveFailureCause = ( - failure: Failure, - cause: unknown -): Failure => { +const preserveFailureCause = (failure: Failure, cause: unknown): Failure => { Object.defineProperty(failure, 'cause', { configurable: false, enumerable: false, @@ -621,19 +489,16 @@ const preserveFailureCause = ( return failure; }; -export const decodeActionPayload = < - PayloadSchema extends Schema.ConstraintDecoder, - Payload, ->( +export const decodeActionPayload = , Payload>( schema: PayloadSchema, - payload: Payload + payload: Payload, ): Effect.Effect => { if (Object.is(schema, Schema.Void) && payload !== undefined) { return Effect.fail( new ActionPayloadValidationError({ code: 'action_payload_invalid', reason: 'This Action does not accept a business payload', - }) + }), ); } @@ -644,22 +509,17 @@ export const decodeActionPayload = < code: 'action_payload_invalid', reason: 'The Action payload does not match its declared schema', }), - cause - ) - ) + cause, + ), + ), ); }; -export const decodeActionResult = < - ResultSchema extends Schema.ConstraintDecoder, - Result, ->( +export const decodeActionResult = , Result>( schema: ResultSchema, - result: Result + result: Result, ): Effect.Effect => - Schema.encodeUnknownEffect( - Schema.make>(schema.ast) - )(result).pipe( + Schema.encodeUnknownEffect(Schema.make>(schema.ast))(result).pipe( Effect.flatMap(Schema.decodeUnknownEffect(schema)), Effect.mapError((cause) => preserveFailureCause( @@ -667,7 +527,7 @@ export const decodeActionResult = < code: 'action_result_invalid', reason: 'The Action result does not match its declared schema', }), - cause - ) - ) + cause, + ), + ), ); diff --git a/app/packages/core-runtime/src/actions/error-schema.ts b/app/packages/core-runtime/src/actions/error-schema.ts index 74833e032..91bf06ce0 100644 --- a/app/packages/core-runtime/src/actions/error-schema.ts +++ b/app/packages/core-runtime/src/actions/error-schema.ts @@ -1,16 +1,10 @@ import type { Cause } from 'effect'; import { Schema } from 'effect'; -export const actionErrorSchema = < - const Tag extends string, - const Fields extends Schema.Struct.Fields, ->( +export const actionErrorSchema = ( tag: Tag, - fields: Fields + fields: Fields, ) => { type Contract = Schema.TaggedStruct; - return Schema.TaggedError()( - tag, - fields - ); + return Schema.TaggedError()(tag, fields); }; diff --git a/app/packages/core-runtime/src/actions/errors.ts b/app/packages/core-runtime/src/actions/errors.ts index 1ba3720ad..bd8e8d422 100644 --- a/app/packages/core-runtime/src/actions/errors.ts +++ b/app/packages/core-runtime/src/actions/errors.ts @@ -1,9 +1,6 @@ import { Cause, Schema } from 'effect'; -import type { - ModuleStateCheckUnavailableError, - ModuleStateDeniedError, -} from '../modules/module-state-gate-errors.ts'; +import type { ModuleStateCheckUnavailableError, ModuleStateDeniedError } from '../modules/module-state-gate-errors.ts'; import type { OperationContextError } from '../operations/errors.ts'; import { actionErrorSchema } from './error-schema.ts'; import type { ActionTransactionError } from './transaction-error.ts'; @@ -14,128 +11,76 @@ const safeReason = { reason: Schema.String, } as const; -const ActionInvocationIdSchema = Schema.String.pipe( - Schema.brand('ActionInvocationId'), - Schema.decodeTo(Schema.String) -); +const ActionInvocationIdSchema = Schema.String.pipe(Schema.brand('ActionInvocationId'), Schema.decodeTo(Schema.String)); -const ActionPayloadValidationErrorValue = actionErrorSchema( - 'ActionPayloadValidationError', - { - code: Schema.Literal('action_payload_invalid'), - ...safeReason, - } -); -export type ActionPayloadValidationError = InstanceType< - typeof ActionPayloadValidationErrorValue ->; +const ActionPayloadValidationErrorValue = actionErrorSchema('ActionPayloadValidationError', { + code: Schema.Literal('action_payload_invalid'), + ...safeReason, +}); +export type ActionPayloadValidationError = InstanceType; export { ActionPayloadValidationErrorValue as ActionPayloadValidationError }; -const ActionResultValidationErrorValue = actionErrorSchema( - 'ActionResultValidationError', - { - code: Schema.Literal('action_result_invalid'), - ...safeReason, - } -); -export type ActionResultValidationError = InstanceType< - typeof ActionResultValidationErrorValue ->; +const ActionResultValidationErrorValue = actionErrorSchema('ActionResultValidationError', { + code: Schema.Literal('action_result_invalid'), + ...safeReason, +}); +export type ActionResultValidationError = InstanceType; export { ActionResultValidationErrorValue as ActionResultValidationError }; -const ActionTrustedContextValidationErrorValue = actionErrorSchema( - 'ActionTrustedContextValidationError', - { - code: Schema.Literal('action_trusted_context_invalid'), - ...safeReason, - } -); -export type ActionTrustedContextValidationError = InstanceType< - typeof ActionTrustedContextValidationErrorValue ->; +const ActionTrustedContextValidationErrorValue = actionErrorSchema('ActionTrustedContextValidationError', { + code: Schema.Literal('action_trusted_context_invalid'), + ...safeReason, +}); +export type ActionTrustedContextValidationError = InstanceType; export { ActionTrustedContextValidationErrorValue as ActionTrustedContextValidationError }; -const ActionIdempotencyKeyRequiredValue = actionErrorSchema( - 'ActionIdempotencyKeyRequired', - { - code: Schema.Literal('action_idempotency_key_required'), - ...safeReason, - } -); -export type ActionIdempotencyKeyRequired = InstanceType< - typeof ActionIdempotencyKeyRequiredValue ->; +const ActionIdempotencyKeyRequiredValue = actionErrorSchema('ActionIdempotencyKeyRequired', { + code: Schema.Literal('action_idempotency_key_required'), + ...safeReason, +}); +export type ActionIdempotencyKeyRequired = InstanceType; export { ActionIdempotencyKeyRequiredValue as ActionIdempotencyKeyRequired }; -const ActionPermissionDeniedValue = actionErrorSchema( - 'ActionPermissionDenied', - { - code: Schema.Literal('action_permission_denied'), - ...safeReason, - } -); -export type ActionPermissionDenied = InstanceType< - typeof ActionPermissionDeniedValue ->; +const ActionPermissionDeniedValue = actionErrorSchema('ActionPermissionDenied', { + code: Schema.Literal('action_permission_denied'), + ...safeReason, +}); +export type ActionPermissionDenied = InstanceType; export { ActionPermissionDeniedValue as ActionPermissionDenied }; -const ActionPermissionCheckErrorValue = actionErrorSchema( - 'ActionPermissionCheckError', - { - code: Schema.Literal('action_permission_check_failed'), - ...safeReason, - } -); -export type ActionPermissionCheckError = InstanceType< - typeof ActionPermissionCheckErrorValue ->; +const ActionPermissionCheckErrorValue = actionErrorSchema('ActionPermissionCheckError', { + code: Schema.Literal('action_permission_check_failed'), + ...safeReason, +}); +export type ActionPermissionCheckError = InstanceType; export { ActionPermissionCheckErrorValue as ActionPermissionCheckError }; -const ActionAlreadyCommittedValue = actionErrorSchema( - 'ActionAlreadyCommitted', - { - code: Schema.Literal('action_already_committed'), - invocationId: ActionInvocationIdSchema, - ...safeReason, - } -); -export type ActionAlreadyCommitted = InstanceType< - typeof ActionAlreadyCommittedValue ->; +const ActionAlreadyCommittedValue = actionErrorSchema('ActionAlreadyCommitted', { + code: Schema.Literal('action_already_committed'), + invocationId: ActionInvocationIdSchema, + ...safeReason, +}); +export type ActionAlreadyCommitted = InstanceType; export { ActionAlreadyCommittedValue as ActionAlreadyCommitted }; -const ActionRequestHashConflictValue = actionErrorSchema( - 'ActionRequestHashConflict', - { - code: Schema.Literal('action_request_hash_conflict'), - ...safeReason, - } -); -export type ActionRequestHashConflict = InstanceType< - typeof ActionRequestHashConflictValue ->; +const ActionRequestHashConflictValue = actionErrorSchema('ActionRequestHashConflict', { + code: Schema.Literal('action_request_hash_conflict'), + ...safeReason, +}); +export type ActionRequestHashConflict = InstanceType; export { ActionRequestHashConflictValue as ActionRequestHashConflict }; -const ActionInvocationPersistenceErrorValue = actionErrorSchema( - 'ActionInvocationPersistenceError', - { - code: Schema.Literal('action_invocation_persistence_failed'), - ...safeReason, - } -); -export type ActionInvocationPersistenceError = InstanceType< - typeof ActionInvocationPersistenceErrorValue ->; +const ActionInvocationPersistenceErrorValue = actionErrorSchema('ActionInvocationPersistenceError', { + code: Schema.Literal('action_invocation_persistence_failed'), + ...safeReason, +}); +export type ActionInvocationPersistenceError = InstanceType; const ActionInvocationPersistenceErrorInternals = (() => { let createWithCause: ( - props: ConstructorParameters< - typeof ActionInvocationPersistenceErrorValue - >[0], - cause?: unknown + props: ConstructorParameters[0], + cause?: unknown, ) => ActionInvocationPersistenceError; - let readCause: ( - failure: ActionInvocationPersistenceError - ) => Cause.Cause | undefined; + let readCause: (failure: ActionInvocationPersistenceError) => Cause.Cause | undefined; class RetainedError extends ActionInvocationPersistenceErrorValue { #cause: Cause.Cause | undefined; @@ -151,62 +96,42 @@ const ActionInvocationPersistenceErrorInternals = (() => { readCause = (failure) => (#cause in failure ? failure.#cause : undefined); } } - const ErrorClass: typeof ActionInvocationPersistenceErrorValue = - RetainedError; + const ErrorClass: typeof ActionInvocationPersistenceErrorValue = RetainedError; return { createWithCause, ErrorClass, readCause }; })(); -const ActionInvocationPersistenceErrorClass = - ActionInvocationPersistenceErrorInternals.ErrorClass; +const ActionInvocationPersistenceErrorClass = ActionInvocationPersistenceErrorInternals.ErrorClass; export { ActionInvocationPersistenceErrorClass as ActionInvocationPersistenceError }; // Core-only accessors: deliberately excluded from the package root exports. export const createActionInvocationPersistenceErrorWithCause = ActionInvocationPersistenceErrorInternals.createWithCause; -export const getActionInvocationPersistenceErrorCause = - ActionInvocationPersistenceErrorInternals.readCause; +export const getActionInvocationPersistenceErrorCause = ActionInvocationPersistenceErrorInternals.readCause; -const ActionInvocationNotFoundValue = actionErrorSchema( - 'ActionInvocationNotFound', - { - code: Schema.Literal('action_invocation_not_found'), - ...safeReason, - } -); -export type ActionInvocationNotFound = InstanceType< - typeof ActionInvocationNotFoundValue ->; +const ActionInvocationNotFoundValue = actionErrorSchema('ActionInvocationNotFound', { + code: Schema.Literal('action_invocation_not_found'), + ...safeReason, +}); +export type ActionInvocationNotFound = InstanceType; export { ActionInvocationNotFoundValue as ActionInvocationNotFound }; -const ActionInvocationStateErrorValue = actionErrorSchema( - 'ActionInvocationStateError', - { - code: Schema.Literal('action_invocation_state_invalid'), - ...safeReason, - } -); -export type ActionInvocationStateError = InstanceType< - typeof ActionInvocationStateErrorValue ->; +const ActionInvocationStateErrorValue = actionErrorSchema('ActionInvocationStateError', { + code: Schema.Literal('action_invocation_state_invalid'), + ...safeReason, +}); +export type ActionInvocationStateError = InstanceType; export { ActionInvocationStateErrorValue as ActionInvocationStateError }; const ActionCollectorErrorValue = actionErrorSchema('ActionCollectorError', { code: Schema.Literal('action_collector_invalid'), ...safeReason, }); -export type ActionCollectorError = InstanceType< - typeof ActionCollectorErrorValue ->; +export type ActionCollectorError = InstanceType; export { ActionCollectorErrorValue as ActionCollectorError }; -const ActionHandlerExecutionErrorValue = actionErrorSchema( - 'ActionHandlerExecutionError', - { - code: Schema.Literal('action_handler_execution_failed'), - ...safeReason, - } -); -export type ActionHandlerExecutionError = InstanceType< - typeof ActionHandlerExecutionErrorValue ->; +const ActionHandlerExecutionErrorValue = actionErrorSchema('ActionHandlerExecutionError', { + code: Schema.Literal('action_handler_execution_failed'), + ...safeReason, +}); +export type ActionHandlerExecutionError = InstanceType; export { ActionHandlerExecutionErrorValue as ActionHandlerExecutionError }; const ActionPolicyDeniedValue = actionErrorSchema('ActionPolicyDenied', { @@ -217,29 +142,19 @@ const ActionPolicyDeniedValue = actionErrorSchema('ActionPolicyDenied', { export type ActionPolicyDenied = InstanceType; export { ActionPolicyDeniedValue as ActionPolicyDenied }; -const ActionPolicyEvaluationErrorValue = actionErrorSchema( - 'ActionPolicyEvaluationError', - { - code: Schema.Literal('action_policy_evaluation_failed'), - ...safeReason, - } -); -export type ActionPolicyEvaluationError = InstanceType< - typeof ActionPolicyEvaluationErrorValue ->; +const ActionPolicyEvaluationErrorValue = actionErrorSchema('ActionPolicyEvaluationError', { + code: Schema.Literal('action_policy_evaluation_failed'), + ...safeReason, +}); +export type ActionPolicyEvaluationError = InstanceType; export { ActionPolicyEvaluationErrorValue as ActionPolicyEvaluationError }; -const ActionCommitIndeterminateValue = actionErrorSchema( - 'ActionCommitIndeterminate', - { - code: Schema.Literal('action_commit_indeterminate'), - invocationId: ActionInvocationIdSchema, - ...safeReason, - } -); -export type ActionCommitIndeterminate = InstanceType< - typeof ActionCommitIndeterminateValue ->; +const ActionCommitIndeterminateValue = actionErrorSchema('ActionCommitIndeterminate', { + code: Schema.Literal('action_commit_indeterminate'), + invocationId: ActionInvocationIdSchema, + ...safeReason, +}); +export type ActionCommitIndeterminate = InstanceType; export { ActionCommitIndeterminateValue as ActionCommitIndeterminate }; export type ActionCoreError = diff --git a/app/packages/core-runtime/src/actions/events.ts b/app/packages/core-runtime/src/actions/events.ts index 0102daa7f..967b9339d 100644 --- a/app/packages/core-runtime/src/actions/events.ts +++ b/app/packages/core-runtime/src/actions/events.ts @@ -1,40 +1,15 @@ import { Schema } from 'effect'; -import { - decodedStringBrand, - nonEmptyString, - TargetModuleKeySchema, - TargetResourceIdSchema, -} from './string-schemas.ts'; - -const nonNegativeInteger = Schema.Finite.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(0) -); -const EvidencePolicyKeySchema = decodedStringBrand( - nonEmptyString, - 'EvidencePolicyKey' -); -const ProducerModuleKeySchema = decodedStringBrand( - nonEmptyString, - 'ProducerModuleKey' -); -const ServingModuleKeySchema = decodedStringBrand( - nonEmptyString, - 'ServingModuleKey' -); -const SubjectModuleKeySchema = decodedStringBrand( - nonEmptyString, - 'SubjectModuleKey' -); -const SubjectResourceIdSchema = decodedStringBrand( - nonEmptyString, - 'SubjectResourceId' -); - -export type DomainEventContractMap = Readonly< - Record> ->; +import { decodedStringBrand, nonEmptyString, TargetModuleKeySchema, TargetResourceIdSchema } from './string-schemas.ts'; + +const nonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)); +const EvidencePolicyKeySchema = decodedStringBrand(nonEmptyString, 'EvidencePolicyKey'); +const ProducerModuleKeySchema = decodedStringBrand(nonEmptyString, 'ProducerModuleKey'); +const ServingModuleKeySchema = decodedStringBrand(nonEmptyString, 'ServingModuleKey'); +const SubjectModuleKeySchema = decodedStringBrand(nonEmptyString, 'SubjectModuleKey'); +const SubjectResourceIdSchema = decodedStringBrand(nonEmptyString, 'SubjectResourceId'); + +export type DomainEventContractMap = Readonly>>; export type ActionAccessEvidencePolicy = | { @@ -54,12 +29,7 @@ export type ActionAccessEvidencePolicy = export const DataAccessEventSchema = Schema.Struct({ accessKind: Schema.Literals(['read', 'list', 'search', 'export', 'download']), - evidenceCaptureMode: Schema.Literals([ - 'metadata_only', - 'hash_only', - 'redacted_payload', - 'stored_artifact', - ]), + evidenceCaptureMode: Schema.Literals(['metadata_only', 'hash_only', 'redacted_payload', 'stored_artifact']), evidencePayloadJson: Schema.optionalKey(Schema.Json), evidencePolicyKey: EvidencePolicyKeySchema, occurredAt: Schema.optionalKey(Schema.Date), @@ -79,10 +49,7 @@ export type DataAccessEvent = Schema.Schema.Type; /** Handler-supplied read facts. Core applies the descriptor-owned evidence policy. */ export type DataAccessEventInput = Omit< DataAccessEvent, - | 'evidenceCaptureMode' - | 'evidencePolicyKey' - | 'redactionProfile' - | 'resultFingerprintSchema' + 'evidenceCaptureMode' | 'evidencePolicyKey' | 'redactionProfile' | 'resultFingerprintSchema' >; export const DomainEventSchema = Schema.Struct({ @@ -98,10 +65,7 @@ export const DomainEventSchema = Schema.Struct({ export type DomainEvent = Schema.Schema.Type; export type DeclaredDomainEvent = { - readonly [EventType in keyof Contracts & string]: Omit< - DomainEvent, - 'eventType' | 'payloadJson' - > & { + readonly [EventType in keyof Contracts & string]: Omit & { readonly eventType: EventType; readonly payloadJson: Contracts[EventType]['Type']; }; @@ -115,9 +79,7 @@ export const OutboxMessageSchema = Schema.Struct({ export type OutboxMessage = Schema.Schema.Type; -const domainEventReferenceBrand: unique symbol = Symbol( - '@app/core-runtime/actions/events/DomainEventReference' -); +const domainEventReferenceBrand: unique symbol = Symbol('@app/core-runtime/actions/events/DomainEventReference'); /** Opaque reference produced only by one execution's Domain Event collector. */ export interface DomainEventReference { @@ -134,9 +96,7 @@ export interface CollectedOutboxMessage { } export interface ActionEvidenceSnapshot { - readonly auditEvidence: Readonly< - Record> - >; + readonly auditEvidence: Readonly>>; readonly dataAccessEvents: readonly DataAccessEvent[]; readonly domainEvents: readonly DomainEvent[]; readonly outboxMessages: readonly CollectedOutboxMessage[]; diff --git a/app/packages/core-runtime/src/actions/policy.ts b/app/packages/core-runtime/src/actions/policy.ts index 5d0440da8..f0660a3e8 100644 --- a/app/packages/core-runtime/src/actions/policy.ts +++ b/app/packages/core-runtime/src/actions/policy.ts @@ -1,10 +1,7 @@ import { Schema } from 'effect'; import type { Effect } from 'effect'; -import type { - ActionTransportMetadata, - TrustedPrincipalContext, -} from './context.ts'; +import type { ActionTransportMetadata, TrustedPrincipalContext } from './context.ts'; const policyReference = '__actionPolicyReference' as const; @@ -25,29 +22,21 @@ export interface ActionPolicyEvaluatorInput { readonly payload: Payload; readonly principal: Readonly; readonly target: Readonly; - readonly transport: Readonly< - Pick - >; + readonly transport: Readonly>; } const policyDeniedFields = { reason: Schema.String, reasonCode: Schema.String, }; -const PolicyDeniedContract = Schema.TaggedStruct( - 'PolicyDenied', - policyDeniedFields -); +const PolicyDeniedContract = Schema.TaggedStruct('PolicyDenied', policyDeniedFields); type PolicyDeniedSelf = typeof PolicyDeniedContract.Type; -const PolicyDeniedValue = Schema.TaggedError()( - 'PolicyDenied', - policyDeniedFields -); +const PolicyDeniedValue = Schema.TaggedError()('PolicyDenied', policyDeniedFields); export type PolicyDenied = InstanceType; export { PolicyDeniedValue as PolicyDenied }; export type ActionPolicyEvaluator = ( - input: ActionPolicyEvaluatorInput + input: ActionPolicyEvaluatorInput, ) => Effect.Effect; interface ActionPolicyBase { @@ -60,10 +49,7 @@ export interface GlobalActionPolicy extends ActionPolicyBase { readonly scope: 'global'; } -export interface MicroverticalActionPolicy< - Payload, - Owner extends string, -> extends ActionPolicyBase { +export interface MicroverticalActionPolicy extends ActionPolicyBase { readonly owningModuleKey: Owner; readonly scope: 'microvertical'; } @@ -88,15 +74,11 @@ const makePolicyDefinitionError = TypeError.bind(null); const requireStableIdentifier = (value: string, field: string): void => { if (value.trim().length === 0) { - throw makePolicyDefinitionError( - `${field} must be a non-empty stable identifier` - ); + throw makePolicyDefinitionError(`${field} must be a non-empty stable identifier`); } }; -const registerPolicy = ( - policy: Policy -): Readonly => { +const registerPolicy = (policy: Policy): Readonly => { Object.defineProperty(policy, policyReference, { enumerable: false, value: true, @@ -105,18 +87,13 @@ const registerPolicy = ( return frozen; }; -export const denyPolicy = ( - reasonCode: string, - reason: string -): PolicyDenied => { +export const denyPolicy = (reasonCode: string, reason: string): PolicyDenied => { requireStableIdentifier(reasonCode, 'Policy reason code'); requireStableIdentifier(reason, 'Policy denial reason'); return Object.freeze(new PolicyDeniedValue({ reason, reasonCode })); }; -export const defineGlobalPolicy = ( - input: DefineGlobalPolicyInput -): GlobalActionPolicy => { +export const defineGlobalPolicy = (input: DefineGlobalPolicyInput): GlobalActionPolicy => { requireStableIdentifier(input.policyKey, 'Policy key'); return registerPolicy({ evaluate: input.evaluate, @@ -127,7 +104,7 @@ export const defineGlobalPolicy = ( }; export const defineMicroverticalPolicy = ( - input: DefineMicroverticalPolicyInput + input: DefineMicroverticalPolicyInput, ): MicroverticalActionPolicy => { requireStableIdentifier(input.policyKey, 'Policy key'); requireStableIdentifier(input.owningModuleKey, 'Policy owning module key'); @@ -144,30 +121,18 @@ export const defineMicroverticalPolicy = ( const ActionPolicyReferenceSchema = Schema.Union([ Schema.Struct({ evaluate: Schema.Any, - policyKey: Schema.String.pipe( - Schema.brand('PolicyKey'), - Schema.decodeTo(Schema.String) - ), + policyKey: Schema.String.pipe(Schema.brand('PolicyKey'), Schema.decodeTo(Schema.String)), [policyReference]: Schema.Literal(true), scope: Schema.Literal('global'), }), Schema.Struct({ evaluate: Schema.Any, - owningModuleKey: Schema.String.pipe( - Schema.brand('OwningModuleKey'), - Schema.decodeTo(Schema.String) - ), - policyKey: Schema.String.pipe( - Schema.brand('PolicyKey'), - Schema.decodeTo(Schema.String) - ), + owningModuleKey: Schema.String.pipe(Schema.brand('OwningModuleKey'), Schema.decodeTo(Schema.String)), + policyKey: Schema.String.pipe(Schema.brand('PolicyKey'), Schema.decodeTo(Schema.String)), [policyReference]: Schema.Literal(true), scope: Schema.Literal('microvertical'), }), ]); -export const isActionPolicy: ( - value: ActionPolicy -) => value is ActionPolicy = Schema.is( - ActionPolicyReferenceSchema -); +export const isActionPolicy: (value: ActionPolicy) => value is ActionPolicy = + Schema.is(ActionPolicyReferenceSchema); diff --git a/app/packages/core-runtime/src/actions/principal-context.ts b/app/packages/core-runtime/src/actions/principal-context.ts index aca86cb3e..da8dfa6d4 100644 --- a/app/packages/core-runtime/src/actions/principal-context.ts +++ b/app/packages/core-runtime/src/actions/principal-context.ts @@ -4,10 +4,7 @@ import { decodedStringBrand, nonEmptyString } from './string-schemas.ts'; const uuid = Schema.String.check(Schema.isUUID()); const AuthBindingIdSchema = decodedStringBrand(uuid, 'AuthBindingId'); -const ImpersonatedByPrincipalIdSchema = decodedStringBrand( - uuid, - 'ImpersonatedByPrincipalId' -); +const ImpersonatedByPrincipalIdSchema = decodedStringBrand(uuid, 'ImpersonatedByPrincipalId'); const LegalEntityIdSchema = decodedStringBrand(uuid, 'LegalEntityId'); const PrincipalIdSchema = decodedStringBrand(uuid, 'PrincipalId'); const TenantIdSchema = decodedStringBrand(uuid, 'TenantId'); @@ -15,29 +12,17 @@ const TenantIdSchema = decodedStringBrand(uuid, 'TenantId'); const TrustedPrincipalContextFieldsSchema = Schema.Struct({ authBindingId: Schema.optionalKey(AuthBindingIdSchema), authContextRef: Schema.optionalKey(nonEmptyString), - authMethod: Schema.Literals([ - 'session', - 'api_key', - 'system', - 'support_impersonation', - ]), - impersonatedByPrincipalId: Schema.optionalKey( - ImpersonatedByPrincipalIdSchema - ), + authMethod: Schema.Literals(['session', 'api_key', 'system', 'support_impersonation']), + impersonatedByPrincipalId: Schema.optionalKey(ImpersonatedByPrincipalIdSchema), legalEntityId: Schema.optionalKey(LegalEntityIdSchema), principalId: PrincipalIdSchema, tenantId: TenantIdSchema, }); -type TrustedPrincipalContextFields = - typeof TrustedPrincipalContextFieldsSchema.Type; -type PrincipalContextValidator = ( - context: TrustedPrincipalContextFields -) => readonly Schema.FilterIssue[]; +type TrustedPrincipalContextFields = typeof TrustedPrincipalContextFieldsSchema.Type; +type PrincipalContextValidator = (context: TrustedPrincipalContextFields) => readonly Schema.FilterIssue[]; -const issue = (message: string): readonly Schema.FilterIssue[] => [ - { issue: message, path: ['authMethod'] }, -]; +const issue = (message: string): readonly Schema.FilterIssue[] => [{ issue: message, path: ['authMethod'] }]; const validateApiKeyContext: PrincipalContextValidator = (context) => context.authBindingId === undefined || @@ -53,16 +38,12 @@ const validateSessionContext: PrincipalContextValidator = (context) => ? issue('session context requires a binding and safe session reference') : []; -const validateSupportImpersonationContext: PrincipalContextValidator = ( - context -) => +const validateSupportImpersonationContext: PrincipalContextValidator = (context) => context.authBindingId === undefined || context.authContextRef?.startsWith('better-auth-session:') !== true || context.impersonatedByPrincipalId === undefined || context.impersonatedByPrincipalId === context.principalId - ? issue( - 'support impersonation requires distinct effective and original principals' - ) + ? issue('support impersonation requires distinct effective and original principals') : []; const validateSystemContext: PrincipalContextValidator = (context) => @@ -78,18 +59,10 @@ const principalContextValidators = { session: validateSessionContext, support_impersonation: validateSupportImpersonationContext, system: validateSystemContext, -} satisfies Record< - TrustedPrincipalContextFields['authMethod'], - PrincipalContextValidator ->; +} satisfies Record; -export const TrustedPrincipalContextSchema = - TrustedPrincipalContextFieldsSchema.check( - Schema.makeFilter((context) => - principalContextValidators[context.authMethod](context) - ) - ); +export const TrustedPrincipalContextSchema = TrustedPrincipalContextFieldsSchema.check( + Schema.makeFilter((context) => principalContextValidators[context.authMethod](context)), +); -export type TrustedPrincipalContext = Schema.Schema.Type< - typeof TrustedPrincipalContextSchema ->; +export type TrustedPrincipalContext = Schema.Schema.Type; diff --git a/app/packages/core-runtime/src/actions/repository.ts b/app/packages/core-runtime/src/actions/repository.ts index f17ae4bc6..0c397ef93 100644 --- a/app/packages/core-runtime/src/actions/repository.ts +++ b/app/packages/core-runtime/src/actions/repository.ts @@ -2,15 +2,7 @@ import { createHash, randomUUID } from 'node:crypto'; import { and, eq, inArray, isNull } from 'drizzle-orm'; import type { Cause } from 'effect'; -import { - Context, - DateTime, - Effect, - Layer, - Predicate, - Result, - Schema, -} from 'effect'; +import { Context, DateTime, Effect, Layer, Predicate, Result, Schema } from 'effect'; import { isSqlError } from 'effect/unstable/sql/SqlError'; import type { ActionInvocationStatus } from '../db/schema.ts'; @@ -23,15 +15,9 @@ import { tenants, } from '../db/schema.ts'; import type { CoreDatabaseExecutor, CoreTransaction } from '../db/types.ts'; -import type { - ActionTransportMetadata, - TrustedPrincipalContext, -} from './context.ts'; +import type { ActionTransportMetadata, TrustedPrincipalContext } from './context.ts'; import type { ActionAuditProfile } from './definition.ts'; -import type { - ActionInvocationPersistenceError, - ActionTransactionError, -} from './errors.ts'; +import type { ActionInvocationPersistenceError, ActionTransactionError } from './errors.ts'; import { ActionInvocationNotFound, ActionInvocationStateError, @@ -39,24 +25,15 @@ import { getActionInvocationPersistenceErrorCause, } from './errors.ts'; import type { ActionEvidenceSnapshot } from './events.ts'; -import { - createActionTransactionErrorWithCause, - getActionTransactionErrorCause, -} from './transaction-error.ts'; +import { createActionTransactionErrorWithCause, getActionTransactionErrorCause } from './transaction-error.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); export interface ActionRequestHashInput { readonly actionKey: string; @@ -64,10 +41,7 @@ export interface ActionRequestHashInput { readonly owningModuleKey: string; readonly principal: TrustedPrincipalContext; readonly schemaVersion: string; - readonly target: Pick< - ActionTransportMetadata, - 'targetModuleKey' | 'targetResourceId' | 'targetResourceType' - >; + readonly target: Pick; } type CanonicalValue = @@ -81,19 +55,13 @@ type CanonicalValue = | readonly ['string', string] | readonly ['undefined']; -const CanonicalValueError = Schema.TaggedError()( - 'CanonicalValueError', - { - reason: Schema.String, - } -); +const CanonicalValueError = Schema.TaggedError()('CanonicalValueError', { + reason: Schema.String, +}); -const RepositoryInvariantError = Schema.TaggedError()( - 'RepositoryInvariantError', - { - reason: Schema.String, - } -); +const RepositoryInvariantError = Schema.TaggedError()('RepositoryInvariantError', { + reason: Schema.String, +}); const canonicalValueCodec = Schema.fromJsonString(Schema.Any); @@ -113,7 +81,7 @@ const compareCodeUnits = (left: string, right: string): number => { const normalizeObjectForHash = ( value: Value, seen: WeakSet, - normalize: (value: Item, seen: WeakSet) => CanonicalValue + normalize: (value: Item, seen: WeakSet) => CanonicalValue, ): CanonicalValue => { if (Array.isArray(value)) { if (seen.has(value)) { @@ -150,10 +118,7 @@ const normalizeNumberForHash = (value: number): CanonicalValue => [ Object.is(value, -0) ? '-0' : String(value), ]; -const normalizeForHash = ( - value: Value, - seen: WeakSet -): CanonicalValue => { +const normalizeForHash = (value: Value, seen: WeakSet): CanonicalValue => { if (value === undefined) { return ['undefined']; } @@ -184,35 +149,31 @@ const normalizeForHash = ( }); }; -const markInvocationRejected = Effect.fnUntraced( - function* markInvocationRejected( - transaction: CoreTransaction, - actionInvocationId: string - ) { - const completedAt = yield* DateTime.nowAsDate; - const rejected = yield* transaction - .update(actionInvocations) - .set({ completedAt, status: 'rejected' }) - .where( - and( - eq(actionInvocations.actionInvocationId, actionInvocationId), - eq(actionInvocations.status, 'received'), - isNull(actionInvocations.completedAt) - ) - ) - .returning({ actionInvocationId: actionInvocations.actionInvocationId }); - if (rejected.length !== 1) { - return yield* new RepositoryInvariantError({ - reason: 'The Action invocation could not be marked rejected', - }); - } - return yield* Effect.void; +const markInvocationRejected = Effect.fnUntraced(function* markInvocationRejected( + transaction: CoreTransaction, + actionInvocationId: string, +) { + const completedAt = yield* DateTime.nowAsDate; + const rejected = yield* transaction + .update(actionInvocations) + .set({ completedAt, status: 'rejected' }) + .where( + and( + eq(actionInvocations.actionInvocationId, actionInvocationId), + eq(actionInvocations.status, 'received'), + isNull(actionInvocations.completedAt), + ), + ) + .returning({ actionInvocationId: actionInvocations.actionInvocationId }); + if (rejected.length !== 1) { + return yield* new RepositoryInvariantError({ + reason: 'The Action invocation could not be marked rejected', + }); } -); + return yield* Effect.void; +}); -export const computeActionRequestHash = ( - input: ActionRequestHashInput -): string => { +export const computeActionRequestHash = (input: ActionRequestHashInput): string => { const canonicalEnvelope = normalizeForHash( { actionKey: input.actionKey, @@ -226,12 +187,10 @@ export const computeActionRequestHash = ( schemaVersion: input.schemaVersion, target: input.target, }, - new WeakSet() + new WeakSet(), ); - return createHash('sha256') - .update(encodeCanonicalValue(canonicalEnvelope)) - .digest('hex'); + return createHash('sha256').update(encodeCanonicalValue(canonicalEnvelope)).digest('hex'); }; export const computeCanonicalValueHash = (value?: Value): string => @@ -297,39 +256,31 @@ export interface RejectPermissionDeniedInput { export interface ActionRepositoryService { readonly createOrResolveInvocation: ( executor: CoreDatabaseExecutor, - input: PrepareActionInvocationInput + input: PrepareActionInvocationInput, ) => Effect.Effect; readonly finalizePolicyDenial: ( executor: CoreDatabaseExecutor, - input: FinalizeActionPolicyDenialInput + input: FinalizeActionPolicyDenialInput, ) => Effect.Effect; readonly flushSuccess: ( transaction: CoreTransaction, - input: FlushActionSuccessInput + input: FlushActionSuccessInput, ) => Effect.Effect; readonly lockInvocation: ( transaction: CoreTransaction, - invocationId: string + invocationId: string, ) => Effect.Effect; readonly rejectPermissionDenied: ( executor: CoreDatabaseExecutor, - input: RejectPermissionDeniedInput - ) => Effect.Effect< - void, - | ActionInvocationPersistenceError - | ActionInvocationStateError - | ActionTransactionError - >; + input: RejectPermissionDeniedInput, + ) => Effect.Effect; readonly resolveInvocation: ( executor: CoreDatabaseExecutor, - input: ResolveActionInvocationInput - ) => Effect.Effect< - ActionInvocationRecord, - ActionInvocationNotFound | ActionInvocationPersistenceError - >; + input: ResolveActionInvocationInput, + ) => Effect.Effect; readonly transitionInvocationToRunning: ( executor: CoreDatabaseExecutor, - invocationId: string + invocationId: string, ) => Effect.Effect; } @@ -340,16 +291,13 @@ const invocationSelection = { status: actionInvocations.status, } as const; -const persistenceFailure = ( - reason: string, - cause?: FailureCause -) => { +const persistenceFailure = (reason: string, cause?: FailureCause) => { const failure = createActionInvocationPersistenceErrorWithCause( { code: 'action_invocation_persistence_failed', reason, }, - cause + cause, ); return failure; }; @@ -360,9 +308,8 @@ const persistenceFailure = ( * @internal */ export const getActionInvocationPersistenceFailureCause = ( - failure: ActionInvocationPersistenceError -): Cause.Cause | undefined => - getActionInvocationPersistenceErrorCause(failure); + failure: ActionInvocationPersistenceError, +): Cause.Cause | undefined => getActionInvocationPersistenceErrorCause(failure); /** * Logs the privately retained persistence defect. @@ -371,30 +318,21 @@ export const getActionInvocationPersistenceFailureCause = ( */ export const logActionInvocationPersistenceFailureCause = ( failure: ActionInvocationPersistenceError, - annotations: Readonly> + annotations: Readonly>, ): Effect.Effect => { const cause = getActionInvocationPersistenceErrorCause(failure); return cause === undefined ? Effect.void - : Effect.annotateLogs( - Effect.logError( - 'Unexpected Action invocation persistence failure', - cause - ), - annotations - ); + : Effect.annotateLogs(Effect.logError('Unexpected Action invocation persistence failure', cause), annotations); }; -const transactionFailure = ( - reason: string, - cause?: FailureCause -) => { +const transactionFailure = (reason: string, cause?: FailureCause) => { const failure = createActionTransactionErrorWithCause( { code: 'action_transaction_failed', reason, }, - cause + cause, ); return failure; }; @@ -404,9 +342,8 @@ const transactionFailure = ( * * @internal */ -export const getActionTransactionFailureCause = ( - failure: ActionTransactionError -): Cause.Cause | undefined => getActionTransactionErrorCause(failure); +export const getActionTransactionFailureCause = (failure: ActionTransactionError): Cause.Cause | undefined => + getActionTransactionErrorCause(failure); /** * Logs the privately retained transaction defect. @@ -416,127 +353,106 @@ export const getActionTransactionFailureCause = ( export const logActionTransactionFailureCause = ( failure: ActionTransactionError, message: string, - annotations: Readonly> + annotations: Readonly>, ): Effect.Effect => { const cause = getActionTransactionErrorCause(failure); - return cause === undefined - ? Effect.void - : Effect.annotateLogs(Effect.logError(message, cause), annotations); + return cause === undefined ? Effect.void : Effect.annotateLogs(Effect.logError(message, cause), annotations); }; export const makeActionRepository = (): ActionRepositoryService => { - const createOrResolveInvocation: ActionRepositoryService['createOrResolveInvocation'] = - Effect.fn('makeActionRepository.createOrResolveInvocation')( - function* createOrResolveInvocationEffect( - executor: CoreDatabaseExecutor, - input: PrepareActionInvocationInput - ) { - const failureReason = - 'Unable to create or resolve the Action invocation'; - const inserted = yield* executor - .insert(actionInvocations) - .values({ - actionKey: input.actionKey, - authBindingId: input.principal.authBindingId, - authContextRef: input.principal.authContextRef, - authMethod: input.principal.authMethod, - correlationId: input.transport.correlationId, - idempotencyKey: input.idempotencyKey, - impersonatedByPrincipalId: - input.principal.impersonatedByPrincipalId, - legalEntityId: input.principal.legalEntityId, - principalId: input.principal.principalId, - requestHash: input.requestHash, - status: 'received', - targetModuleKey: input.transport.targetModuleKey, - targetResourceId: input.transport.targetResourceId, - targetResourceType: input.transport.targetResourceType, - tenantId: input.principal.tenantId, - traceId: input.transport.traceId, - }) - .onConflictDoNothing() - .returning(invocationSelection) - .pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); - - const [created] = inserted; - if (created !== undefined) { - return created; - } - - const { idempotencyKey } = input; - if (idempotencyKey === undefined) { - return yield* persistenceFailure( - failureReason, - new RepositoryInvariantError({ - reason: - 'A non-idempotent invocation insert unexpectedly conflicted', - }) - ); - } + const createOrResolveInvocation: ActionRepositoryService['createOrResolveInvocation'] = Effect.fn( + 'makeActionRepository.createOrResolveInvocation', + )(function* createOrResolveInvocationEffect(executor: CoreDatabaseExecutor, input: PrepareActionInvocationInput) { + const failureReason = 'Unable to create or resolve the Action invocation'; + const inserted = yield* executor + .insert(actionInvocations) + .values({ + actionKey: input.actionKey, + authBindingId: input.principal.authBindingId, + authContextRef: input.principal.authContextRef, + authMethod: input.principal.authMethod, + correlationId: input.transport.correlationId, + idempotencyKey: input.idempotencyKey, + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, + legalEntityId: input.principal.legalEntityId, + principalId: input.principal.principalId, + requestHash: input.requestHash, + status: 'received', + targetModuleKey: input.transport.targetModuleKey, + targetResourceId: input.transport.targetResourceId, + targetResourceType: input.transport.targetResourceType, + tenantId: input.principal.tenantId, + traceId: input.transport.traceId, + }) + .onConflictDoNothing() + .returning(invocationSelection) + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); - const existing = yield* executor - .select(invocationSelection) - .from(actionInvocations) - .where( - and( - eq(actionInvocations.tenantId, input.principal.tenantId), - eq(actionInvocations.actionKey, input.actionKey), - eq(actionInvocations.principalId, input.principal.principalId), - eq(actionInvocations.idempotencyKey, idempotencyKey) - ) - ) - .limit(1) - .pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); + const [created] = inserted; + if (created !== undefined) { + return created; + } - const [resolved] = existing; - if (resolved === undefined) { - return yield* persistenceFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'The conflicting Action invocation could not be resolved', - }) - ); - } - return resolved; - } - ); + const { idempotencyKey } = input; + if (idempotencyKey === undefined) { + return yield* persistenceFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'A non-idempotent invocation insert unexpectedly conflicted', + }), + ); + } - const lockInvocation: ActionRepositoryService['lockInvocation'] = Effect.fn( - 'makeActionRepository.lockInvocation' - )(function* lockInvocationEffect( - transaction: CoreTransaction, - invocationId: string - ) { - const failureReason = 'Unable to lock the Action invocation'; - const rows = yield* transaction + const existing = yield* executor .select(invocationSelection) .from(actionInvocations) - .where(eq(actionInvocations.actionInvocationId, invocationId)) - .for('update') + .where( + and( + eq(actionInvocations.tenantId, input.principal.tenantId), + eq(actionInvocations.actionKey, input.actionKey), + eq(actionInvocations.principalId, input.principal.principalId), + eq(actionInvocations.idempotencyKey, idempotencyKey), + ), + ) .limit(1) - .pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); - const [invocation] = rows; - if (invocation === undefined) { + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); + + const [resolved] = existing; + if (resolved === undefined) { return yield* persistenceFailure( failureReason, new RepositoryInvariantError({ - reason: 'The Action invocation no longer exists', - }) + reason: 'The conflicting Action invocation could not be resolved', + }), ); } - return invocation; + return resolved; }); - const resolveInvocation: ActionRepositoryService['resolveInvocation'] = ( - executor, - input - ) => + const lockInvocation: ActionRepositoryService['lockInvocation'] = Effect.fn('makeActionRepository.lockInvocation')( + function* lockInvocationEffect(transaction: CoreTransaction, invocationId: string) { + const failureReason = 'Unable to lock the Action invocation'; + const rows = yield* transaction + .select(invocationSelection) + .from(actionInvocations) + .where(eq(actionInvocations.actionInvocationId, invocationId)) + .for('update') + .limit(1) + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); + const [invocation] = rows; + if (invocation === undefined) { + return yield* persistenceFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Action invocation no longer exists', + }), + ); + } + return invocation; + }, + ); + + const resolveInvocation: ActionRepositoryService['resolveInvocation'] = (executor, input) => executor .select(invocationSelection) .from(actionInvocations) @@ -544,527 +460,425 @@ export const makeActionRepository = (): ActionRepositoryService => { and( eq(actionInvocations.actionInvocationId, input.invocationId), eq(actionInvocations.tenantId, input.principal.tenantId), - eq(actionInvocations.principalId, input.principal.principalId) - ) + eq(actionInvocations.principalId, input.principal.principalId), + ), ) .for('update') .limit(1) .pipe( - Effect.mapError((cause) => - persistenceFailure( - 'Unable to resolve the Action invocation commit state', - cause - ) - ), + Effect.mapError((cause) => persistenceFailure('Unable to resolve the Action invocation commit state', cause)), Effect.flatMap(([invocation]) => invocation === undefined ? Effect.fail( new ActionInvocationNotFound({ code: 'action_invocation_not_found', - reason: - 'The Action invocation does not exist in this principal scope', - }) + reason: 'The Action invocation does not exist in this principal scope', + }), ) - : Effect.succeed(invocation) - ) + : Effect.succeed(invocation), + ), + ); + + const transitionInvocationToRunning: ActionRepositoryService['transitionInvocationToRunning'] = Effect.fn( + 'makeActionRepository.transitionInvocationToRunning', + )(function* transitionInvocationToRunningEffect(executor: CoreDatabaseExecutor, invocationId: string) { + const failureReason = 'Unable to transition the Action invocation to running'; + const transitioned = yield* executor + .update(actionInvocations) + .set({ status: 'running' }) + .where( + and( + eq(actionInvocations.actionInvocationId, invocationId), + inArray(actionInvocations.status, ['received', 'running']), + isNull(actionInvocations.completedAt), + ), + ) + .returning(invocationSelection) + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); + const [invocation] = transitioned; + if (invocation !== undefined) { + return invocation; + } + const current = yield* executor + .select(invocationSelection) + .from(actionInvocations) + .where(eq(actionInvocations.actionInvocationId, invocationId)) + .limit(1) + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); + const [resolved] = current; + if (resolved === undefined) { + return yield* persistenceFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Action invocation no longer exists', + }), ); + } + return resolved; + }); - const transitionInvocationToRunning: ActionRepositoryService['transitionInvocationToRunning'] = - Effect.fn('makeActionRepository.transitionInvocationToRunning')( - function* transitionInvocationToRunningEffect( - executor: CoreDatabaseExecutor, - invocationId: string - ) { - const failureReason = - 'Unable to transition the Action invocation to running'; - const transitioned = yield* executor - .update(actionInvocations) - .set({ status: 'running' }) + const rejectPermissionDenied: ActionRepositoryService['rejectPermissionDenied'] = Effect.fn( + 'makeActionRepository.rejectPermissionDenied', + )(function* rejectPermissionDeniedEffect(executor: CoreDatabaseExecutor, input: RejectPermissionDeniedInput) { + const failureReason = 'Unable to persist Action permission denial evidence'; + const transactionBody = Effect.fn('rejectPermissionDenied.transactionBody')( + function* rejectPermissionDeniedTransaction(transaction: CoreTransaction) { + const rows = yield* transaction + .select(invocationSelection) + .from(actionInvocations) .where( and( - eq(actionInvocations.actionInvocationId, invocationId), - inArray(actionInvocations.status, ['received', 'running']), - isNull(actionInvocations.completedAt) - ) + eq(actionInvocations.actionInvocationId, input.actionInvocationId), + eq(actionInvocations.actionKey, input.actionKey), + eq(actionInvocations.principalId, input.principal.principalId), + eq(actionInvocations.tenantId, input.principal.tenantId), + ), ) - .returning(invocationSelection) - .pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); - const [invocation] = transitioned; - if (invocation !== undefined) { - return invocation; - } - const current = yield* executor - .select(invocationSelection) - .from(actionInvocations) - .where(eq(actionInvocations.actionInvocationId, invocationId)) + .for('update') .limit(1) - .pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); - const [resolved] = current; - if (resolved === undefined) { - return yield* persistenceFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'The Action invocation no longer exists', - }) - ); + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + const [invocation] = rows; + if (invocation === undefined) { + return yield* persistenceFailure('The denied Action invocation no longer exists'); } - return resolved; - } - ); - - const rejectPermissionDenied: ActionRepositoryService['rejectPermissionDenied'] = - Effect.fn('makeActionRepository.rejectPermissionDenied')( - function* rejectPermissionDeniedEffect( - executor: CoreDatabaseExecutor, - input: RejectPermissionDeniedInput - ) { - const failureReason = - 'Unable to persist Action permission denial evidence'; - const transactionBody = Effect.fn( - 'rejectPermissionDenied.transactionBody' - )(function* rejectPermissionDeniedTransaction( - transaction: CoreTransaction - ) { - const rows = yield* transaction - .select(invocationSelection) - .from(actionInvocations) - .where( - and( - eq( - actionInvocations.actionInvocationId, - input.actionInvocationId - ), - eq(actionInvocations.actionKey, input.actionKey), - eq(actionInvocations.principalId, input.principal.principalId), - eq(actionInvocations.tenantId, input.principal.tenantId) - ) - ) - .for('update') - .limit(1) - .pipe( - Effect.mapError((cause) => - transactionFailure(failureReason, cause) - ) - ); - const [invocation] = rows; - if (invocation === undefined) { - return yield* persistenceFailure( - 'The denied Action invocation no longer exists' - ); - } - - if ( - invocation.status === 'rejected' && - invocation.completedAt !== null - ) { - return yield* Effect.void; - } - if ( - invocation.status !== 'received' || - invocation.completedAt !== null - ) { - return yield* new ActionInvocationStateError({ - code: 'action_invocation_state_invalid', - reason: - 'The Action invocation cannot be rejected from its current state', - }); - } - - yield* transaction - .insert(auditEvents) - .values({ - actionInvocationId: input.actionInvocationId, - auditProfile: input.auditProfile, - authBindingId: input.principal.authBindingId, - authContextRef: input.principal.authContextRef, - authMethod: input.principal.authMethod, - eventType: 'action.rejected', - evidenceJson: { actionKey: input.actionKey }, - impersonatedByPrincipalId: - input.principal.impersonatedByPrincipalId, - legalEntityId: input.principal.legalEntityId, - outcome: 'denied', - outcomeCode: 'spicedb_permission_denied', - outcomeStage: 'authz', - principalId: input.principal.principalId, - targetModuleKey: input.transport.targetModuleKey, - targetResourceId: input.transport.targetResourceId, - targetResourceType: input.transport.targetResourceType, - tenantId: input.principal.tenantId, - }) - .pipe( - Effect.mapError((cause) => - transactionFailure(failureReason, cause) - ) - ); - yield* markInvocationRejected( - transaction, - input.actionInvocationId - ).pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); + if (invocation.status === 'rejected' && invocation.completedAt !== null) { return yield* Effect.void; - }); + } + if (invocation.status !== 'received' || invocation.completedAt !== null) { + return yield* new ActionInvocationStateError({ + code: 'action_invocation_state_invalid', + reason: 'The Action invocation cannot be rejected from its current state', + }); + } - yield* executor.transaction(transactionBody).pipe( - Effect.catchTag('SqlError', (failure) => - Effect.fail(transactionFailure(failureReason, failure)) - ), - Effect.catchDefect((defect) => - isSqlError(defect) - ? Effect.fail(transactionFailure(failureReason, defect)) - : Effect.die(defect) - ) + yield* transaction + .insert(auditEvents) + .values({ + actionInvocationId: input.actionInvocationId, + auditProfile: input.auditProfile, + authBindingId: input.principal.authBindingId, + authContextRef: input.principal.authContextRef, + authMethod: input.principal.authMethod, + eventType: 'action.rejected', + evidenceJson: { actionKey: input.actionKey }, + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, + legalEntityId: input.principal.legalEntityId, + outcome: 'denied', + outcomeCode: 'spicedb_permission_denied', + outcomeStage: 'authz', + principalId: input.principal.principalId, + targetModuleKey: input.transport.targetModuleKey, + targetResourceId: input.transport.targetResourceId, + targetResourceType: input.transport.targetResourceType, + tenantId: input.principal.tenantId, + }) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + + yield* markInvocationRejected(transaction, input.actionInvocationId).pipe( + Effect.mapError((cause) => transactionFailure(failureReason, cause)), ); return yield* Effect.void; - } + }, ); - const finalizePolicyDenial: ActionRepositoryService['finalizePolicyDenial'] = - Effect.fn('makeActionRepository.finalizePolicyDenial')( - function* finalizePolicyDenialEffect( - executor: CoreDatabaseExecutor, - input: FinalizeActionPolicyDenialInput - ) { - const failureReason = - 'Unable to persist the rejected Action invocation'; - const transactionBody = Effect.fn( - 'finalizePolicyDenial.transactionBody' - )(function* finalizePolicyDenialTransaction( - transaction: CoreTransaction - ) { - const rows = yield* transaction - .select(invocationSelection) - .from(actionInvocations) - .where( - eq(actionInvocations.actionInvocationId, input.actionInvocationId) - ) - .for('update') - .limit(1) - .pipe( - Effect.mapError((cause) => - persistenceFailure(failureReason, cause) - ) - ); - const [invocation] = rows; - if (invocation === undefined) { - return yield* persistenceFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'The Action invocation no longer exists', - }) - ); - } - if ( - invocation.status === 'rejected' && - invocation.completedAt !== null - ) { - return yield* Effect.void; - } - if ( - invocation.status !== 'received' || - invocation.completedAt !== null - ) { - return yield* persistenceFailure( - failureReason, - new RepositoryInvariantError({ - reason: - 'The Action invocation is no longer open for Policy rejection', - }) - ); - } - - const policyEvidence = withOptionalProperty( - { actionKey: input.actionKey }, - input.policy.owningModuleKey !== undefined, - 'owningModuleKey', - input.policy.owningModuleKey, - { - policyKey: input.policy.policyKey, - policyScope: input.policy.scope, - } - ); - yield* transaction - .insert(auditEvents) - .values( - ['action.policy_checked', 'action.rejected'].map((eventType) => ({ - actionInvocationId: input.actionInvocationId, - auditProfile: input.auditProfile, - authBindingId: input.principal.authBindingId, - authContextRef: input.principal.authContextRef, - authMethod: input.principal.authMethod, - eventType, - evidenceJson: policyEvidence, - impersonatedByPrincipalId: - input.principal.impersonatedByPrincipalId, - legalEntityId: input.principal.legalEntityId, - outcome: 'denied', - outcomeCode: input.reasonCode, - outcomeStage: 'policy', - principalId: input.principal.principalId, - targetModuleKey: input.transport.targetModuleKey, - targetResourceId: input.transport.targetResourceId, - targetResourceType: input.transport.targetResourceType, - tenantId: input.principal.tenantId, - })) - ) - .pipe( - Effect.mapError((cause) => - persistenceFailure(failureReason, cause) - ) - ); - - yield* markInvocationRejected( - transaction, - input.actionInvocationId - ).pipe( - Effect.mapError((cause) => persistenceFailure(failureReason, cause)) - ); - return yield* Effect.void; - }); + yield* executor.transaction(transactionBody).pipe( + Effect.catchTag('SqlError', (failure) => Effect.fail(transactionFailure(failureReason, failure))), + Effect.catchDefect((defect) => + isSqlError(defect) ? Effect.fail(transactionFailure(failureReason, defect)) : Effect.die(defect), + ), + ); + return yield* Effect.void; + }); - yield* executor.transaction(transactionBody).pipe( - Effect.catchTag('SqlError', (failure) => - Effect.fail(persistenceFailure(failureReason, failure)) - ), - Effect.catchDefect((defect) => - isSqlError(defect) - ? Effect.fail(persistenceFailure(failureReason, defect)) - : Effect.die(defect) - ) + const finalizePolicyDenial: ActionRepositoryService['finalizePolicyDenial'] = Effect.fn( + 'makeActionRepository.finalizePolicyDenial', + )(function* finalizePolicyDenialEffect(executor: CoreDatabaseExecutor, input: FinalizeActionPolicyDenialInput) { + const failureReason = 'Unable to persist the rejected Action invocation'; + const transactionBody = Effect.fn('finalizePolicyDenial.transactionBody')(function* finalizePolicyDenialTransaction( + transaction: CoreTransaction, + ) { + const rows = yield* transaction + .select(invocationSelection) + .from(actionInvocations) + .where(eq(actionInvocations.actionInvocationId, input.actionInvocationId)) + .for('update') + .limit(1) + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); + const [invocation] = rows; + if (invocation === undefined) { + return yield* persistenceFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Action invocation no longer exists', + }), ); + } + if (invocation.status === 'rejected' && invocation.completedAt !== null) { return yield* Effect.void; } - ); + if (invocation.status !== 'received' || invocation.completedAt !== null) { + return yield* persistenceFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Action invocation is no longer open for Policy rejection', + }), + ); + } - const flushSuccess: ActionRepositoryService['flushSuccess'] = Effect.fn( - 'makeActionRepository.flushSuccess' - )(function* flushSuccessEffect( - transaction: CoreTransaction, - input: FlushActionSuccessInput - ) { - const failureReason = 'Unable to persist successful Action evidence'; - if (input.allowedPolicies.length > 0) { + const policyEvidence = withOptionalProperty( + { actionKey: input.actionKey }, + input.policy.owningModuleKey !== undefined, + 'owningModuleKey', + input.policy.owningModuleKey, + { + policyKey: input.policy.policyKey, + policyScope: input.policy.scope, + }, + ); yield* transaction .insert(auditEvents) .values( - input.allowedPolicies.map((policy) => ({ + ['action.policy_checked', 'action.rejected'].map((eventType) => ({ actionInvocationId: input.actionInvocationId, auditProfile: input.auditProfile, authBindingId: input.principal.authBindingId, authContextRef: input.principal.authContextRef, authMethod: input.principal.authMethod, - eventType: 'action.policy_checked', - evidenceJson: withOptionalProperty( - { - actionKey: input.actionKey, - }, - policy.owningModuleKey !== undefined, - 'owningModuleKey', - policy.owningModuleKey, - { - policyKey: policy.policyKey, - policyScope: policy.scope, - } - ), - impersonatedByPrincipalId: - input.principal.impersonatedByPrincipalId, + eventType, + evidenceJson: policyEvidence, + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, legalEntityId: input.principal.legalEntityId, - outcome: 'allowed', - outcomeCode: 'policy_allowed', + outcome: 'denied', + outcomeCode: input.reasonCode, outcomeStage: 'policy', principalId: input.principal.principalId, targetModuleKey: input.transport.targetModuleKey, targetResourceId: input.transport.targetResourceId, targetResourceType: input.transport.targetResourceType, tenantId: input.principal.tenantId, - })) + })), ) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); - } + .pipe(Effect.mapError((cause) => persistenceFailure(failureReason, cause))); - yield* transaction - .insert(auditEvents) - .values({ - actionInvocationId: input.actionInvocationId, - auditProfile: input.auditProfile, - authBindingId: input.principal.authBindingId, - authContextRef: input.principal.authContextRef, - authMethod: input.principal.authMethod, - eventType: 'action.executed', - evidenceJson: { - ...input.evidence.auditEvidence, - actionKey: input.actionKey, - resultHash: input.resultHash, - }, - impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, - legalEntityId: input.principal.legalEntityId, - outcome: 'succeeded', - outcomeCode: 'action_executed', - outcomeStage: 'execution', - principalId: input.principal.principalId, - targetModuleKey: input.transport.targetModuleKey, - targetResourceId: input.transport.targetResourceId, - targetResourceType: input.transport.targetResourceType, - tenantId: input.principal.tenantId, - }) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) + yield* markInvocationRejected(transaction, input.actionInvocationId).pipe( + Effect.mapError((cause) => persistenceFailure(failureReason, cause)), ); + return yield* Effect.void; + }); + + yield* executor.transaction(transactionBody).pipe( + Effect.catchTag('SqlError', (failure) => Effect.fail(persistenceFailure(failureReason, failure))), + Effect.catchDefect((defect) => + isSqlError(defect) ? Effect.fail(persistenceFailure(failureReason, defect)) : Effect.die(defect), + ), + ); + return yield* Effect.void; + }); + + const flushSuccess: ActionRepositoryService['flushSuccess'] = Effect.fn('makeActionRepository.flushSuccess')( + function* flushSuccessEffect(transaction: CoreTransaction, input: FlushActionSuccessInput) { + const failureReason = 'Unable to persist successful Action evidence'; + if (input.allowedPolicies.length > 0) { + yield* transaction + .insert(auditEvents) + .values( + input.allowedPolicies.map((policy) => ({ + actionInvocationId: input.actionInvocationId, + auditProfile: input.auditProfile, + authBindingId: input.principal.authBindingId, + authContextRef: input.principal.authContextRef, + authMethod: input.principal.authMethod, + eventType: 'action.policy_checked', + evidenceJson: withOptionalProperty( + { + actionKey: input.actionKey, + }, + policy.owningModuleKey !== undefined, + 'owningModuleKey', + policy.owningModuleKey, + { + policyKey: policy.policyKey, + policyScope: policy.scope, + }, + ), + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, + legalEntityId: input.principal.legalEntityId, + outcome: 'allowed', + outcomeCode: 'policy_allowed', + outcomeStage: 'policy', + principalId: input.principal.principalId, + targetModuleKey: input.transport.targetModuleKey, + targetResourceId: input.transport.targetResourceId, + targetResourceType: input.transport.targetResourceType, + tenantId: input.principal.tenantId, + })), + ) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + } - if (input.evidence.dataAccessEvents.length > 0) { yield* transaction - .insert(dataAccessEvents) - .values( - input.evidence.dataAccessEvents.map((event) => ({ - accessKind: event.accessKind, - actionInvocationId: input.actionInvocationId, - authBindingId: input.principal.authBindingId, - authContextRef: input.principal.authContextRef, - authMethod: input.principal.authMethod, - evidenceCaptureMode: event.evidenceCaptureMode, - evidencePayloadJson: event.evidencePayloadJson, - evidencePolicyKey: event.evidencePolicyKey, - impersonatedByPrincipalId: - input.principal.impersonatedByPrincipalId, - legalEntityId: input.principal.legalEntityId, - occurredAt: event.occurredAt, - outcome: 'allowed', - outcomeCode: 'action_read_allowed', - outcomeStage: 'execution', - principalId: input.principal.principalId, - queryHash: event.queryHash, - redactionProfile: event.redactionProfile, - resultCount: event.resultCount, - resultFingerprintHash: event.resultFingerprintHash, - resultFingerprintSchema: event.resultFingerprintSchema, - servingModuleKey: event.servingModuleKey, - targetModuleKey: event.targetModuleKey, - targetResourceId: event.targetResourceId, - targetResourceType: event.targetResourceType, - tenantId: input.principal.tenantId, - })) - ) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); - } + .insert(auditEvents) + .values({ + actionInvocationId: input.actionInvocationId, + auditProfile: input.auditProfile, + authBindingId: input.principal.authBindingId, + authContextRef: input.principal.authContextRef, + authMethod: input.principal.authMethod, + eventType: 'action.executed', + evidenceJson: { + ...input.evidence.auditEvidence, + actionKey: input.actionKey, + resultHash: input.resultHash, + }, + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, + legalEntityId: input.principal.legalEntityId, + outcome: 'succeeded', + outcomeCode: 'action_executed', + outcomeStage: 'execution', + principalId: input.principal.principalId, + targetModuleKey: input.transport.targetModuleKey, + targetResourceId: input.transport.targetResourceId, + targetResourceType: input.transport.targetResourceType, + tenantId: input.principal.tenantId, + }) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + + if (input.evidence.dataAccessEvents.length > 0) { + yield* transaction + .insert(dataAccessEvents) + .values( + input.evidence.dataAccessEvents.map((event) => ({ + accessKind: event.accessKind, + actionInvocationId: input.actionInvocationId, + authBindingId: input.principal.authBindingId, + authContextRef: input.principal.authContextRef, + authMethod: input.principal.authMethod, + evidenceCaptureMode: event.evidenceCaptureMode, + evidencePayloadJson: event.evidencePayloadJson, + evidencePolicyKey: event.evidencePolicyKey, + impersonatedByPrincipalId: input.principal.impersonatedByPrincipalId, + legalEntityId: input.principal.legalEntityId, + occurredAt: event.occurredAt, + outcome: 'allowed', + outcomeCode: 'action_read_allowed', + outcomeStage: 'execution', + principalId: input.principal.principalId, + queryHash: event.queryHash, + redactionProfile: event.redactionProfile, + resultCount: event.resultCount, + resultFingerprintHash: event.resultFingerprintHash, + resultFingerprintSchema: event.resultFingerprintSchema, + servingModuleKey: event.servingModuleKey, + targetModuleKey: event.targetModuleKey, + targetResourceId: event.targetResourceId, + targetResourceType: event.targetResourceType, + tenantId: input.principal.tenantId, + })), + ) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + } - if (input.evidence.domainEvents.length > 0) { - // The tenant row is the existing, typed per-tenant serialization - // anchor. Holding this lock until commit ensures sequence allocation - // order cannot overtake commit order for one tenant's event stream. - const lockedTenant = yield* transaction - .select({ tenantId: tenants.tenantId }) - .from(tenants) - .where(eq(tenants.tenantId, input.principal.tenantId)) - .for('update') - .limit(1) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); - if (lockedTenant.length !== 1) { - return yield* transactionFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'The Domain Event tenant does not exist', - }) - ); + if (input.evidence.domainEvents.length > 0) { + // The tenant row is the existing, typed per-tenant serialization + // anchor. Holding this lock until commit ensures sequence allocation + // order cannot overtake commit order for one tenant's event stream. + const lockedTenant = yield* transaction + .select({ tenantId: tenants.tenantId }) + .from(tenants) + .where(eq(tenants.tenantId, input.principal.tenantId)) + .for('update') + .limit(1) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + if (lockedTenant.length !== 1) { + return yield* transactionFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Domain Event tenant does not exist', + }), + ); + } } - } - const persistedDomainEvents = input.evidence.domainEvents.map((event) => ({ - actionInvocationId: input.actionInvocationId, - domainEventId: randomUUID(), - eventType: event.eventType, - legalEntityId: input.principal.legalEntityId, - occurredAt: event.occurredAt, - payloadJson: event.payloadJson, - producerModuleKey: event.producerModuleKey, - subjectModuleKey: event.subjectModuleKey, - subjectResourceId: event.subjectResourceId, - subjectResourceType: event.subjectResourceType, - tenantId: input.principal.tenantId, - })); + const persistedDomainEvents = input.evidence.domainEvents.map((event) => ({ + actionInvocationId: input.actionInvocationId, + domainEventId: randomUUID(), + eventType: event.eventType, + legalEntityId: input.principal.legalEntityId, + occurredAt: event.occurredAt, + payloadJson: event.payloadJson, + producerModuleKey: event.producerModuleKey, + subjectModuleKey: event.subjectModuleKey, + subjectResourceId: event.subjectResourceId, + subjectResourceType: event.subjectResourceType, + tenantId: input.principal.tenantId, + })); - if (persistedDomainEvents.length > 0) { - yield* transaction - .insert(domainEvents) - .values(persistedDomainEvents) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); - } + if (persistedDomainEvents.length > 0) { + yield* transaction + .insert(domainEvents) + .values(persistedDomainEvents) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + } - if (input.evidence.outboxMessages.length > 0) { - const persistedOutboxMessages = yield* Effect.forEach( - input.evidence.outboxMessages, - (collected) => { - const persistedDomainEvent = - persistedDomainEvents[collected.domainEventIndex]; - if (persistedDomainEvent === undefined) { - return Effect.fail( - transactionFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'An Outbox Message has no persisted Domain Event', - }) - ) - ); - } - return Effect.succeed({ - domainEventId: persistedDomainEvent.domainEventId, - payloadJson: collected.message.payloadJson, - producerModuleKey: collected.message.producerModuleKey, - tenantId: input.principal.tenantId, - topic: collected.message.topic, - }); - }, - { concurrency: 1 } - ); - yield* transaction - .insert(outboxMessages) - .values(persistedOutboxMessages) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) + if (input.evidence.outboxMessages.length > 0) { + const persistedOutboxMessages = yield* Effect.forEach( + input.evidence.outboxMessages, + (collected) => { + const persistedDomainEvent = persistedDomainEvents[collected.domainEventIndex]; + if (persistedDomainEvent === undefined) { + return Effect.fail( + transactionFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'An Outbox Message has no persisted Domain Event', + }), + ), + ); + } + return Effect.succeed({ + domainEventId: persistedDomainEvent.domainEventId, + payloadJson: collected.message.payloadJson, + producerModuleKey: collected.message.producerModuleKey, + tenantId: input.principal.tenantId, + topic: collected.message.topic, + }); + }, + { concurrency: 1 }, ); - } + yield* transaction + .insert(outboxMessages) + .values(persistedOutboxMessages) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); + } - const completedAt = yield* DateTime.nowAsDate; - const succeeded = yield* transaction - .update(actionInvocations) - .set({ - completedAt, - status: 'succeeded', - }) - .where( - and( - eq(actionInvocations.actionInvocationId, input.actionInvocationId), - eq(actionInvocations.status, 'running') + const completedAt = yield* DateTime.nowAsDate; + const succeeded = yield* transaction + .update(actionInvocations) + .set({ + completedAt, + status: 'succeeded', + }) + .where( + and( + eq(actionInvocations.actionInvocationId, input.actionInvocationId), + eq(actionInvocations.status, 'running'), + ), ) - ) - .returning({ actionInvocationId: actionInvocations.actionInvocationId }) - .pipe( - Effect.mapError((cause) => transactionFailure(failureReason, cause)) - ); + .returning({ actionInvocationId: actionInvocations.actionInvocationId }) + .pipe(Effect.mapError((cause) => transactionFailure(failureReason, cause))); - if (succeeded.length !== 1) { - return yield* transactionFailure( - failureReason, - new RepositoryInvariantError({ - reason: 'The Action invocation could not be marked succeeded', - }) - ); - } - return yield* Effect.void; - }); + if (succeeded.length !== 1) { + return yield* transactionFailure( + failureReason, + new RepositoryInvariantError({ + reason: 'The Action invocation could not be marked succeeded', + }), + ); + } + return yield* Effect.void; + }, + ); return Object.freeze({ createOrResolveInvocation, @@ -1077,12 +891,8 @@ export const makeActionRepository = (): ActionRepositoryService => { }); }; -export class ActionRepository extends Context.Service< - ActionRepository, - ActionRepositoryService ->()('@app/core-runtime/actions/repository/ActionRepository') {} +export class ActionRepository extends Context.Service()( + '@app/core-runtime/actions/repository/ActionRepository', +) {} -export const ActionRepositoryLive = Layer.succeed( - ActionRepository, - makeActionRepository() -); +export const ActionRepositoryLive = Layer.succeed(ActionRepository, makeActionRepository()); diff --git a/app/packages/core-runtime/src/actions/runtime.ts b/app/packages/core-runtime/src/actions/runtime.ts index 233395dbf..911524019 100644 --- a/app/packages/core-runtime/src/actions/runtime.ts +++ b/app/packages/core-runtime/src/actions/runtime.ts @@ -1,20 +1,6 @@ -import { - Cause, - Context, - Effect, - Exit, - Layer, - Option, - Ref, - Result, - Schema, -} from 'effect'; +import { Cause, Context, Effect, Exit, Layer, Option, Ref, Result, Schema } from 'effect'; import type { SqlError } from 'effect/unstable/sql/SqlError'; -import { - ConnectionError, - UnknownError, - isSqlError, -} from 'effect/unstable/sql/SqlError'; +import { ConnectionError, UnknownError, isSqlError } from 'effect/unstable/sql/SqlError'; import { decodeTrustedPrincipalContext, @@ -30,19 +16,13 @@ import { ModuleEntrypointGateway } from '../modules/module-entrypoint-gateway.ts import type { TenantModuleEntrypoint } from '../modules/module-entrypoint.ts'; import type { ModuleStateGateService } from '../modules/module-state-gate.ts'; import { ModuleStateGate } from '../modules/module-state-gate.ts'; -import type { - OperationalScope, - OperationalScopeResolverService, -} from '../operations/context.ts'; +import type { OperationalScope, OperationalScopeResolverService } from '../operations/context.ts'; import { OperationalScopeResolver } from '../operations/context.ts'; import { ContextAccess } from '../permissions/context-access.ts'; import type { ActionPermissionService } from '../permissions/service.ts'; import { ActionPermission } from '../permissions/service.ts'; import { createActionCollector } from './collector.ts'; -import type { - ActionTransportMetadata, - TrustedPrincipalContext, -} from './context.ts'; +import type { ActionTransportMetadata, TrustedPrincipalContext } from './context.ts'; import { ActionTransportMetadataSchema } from './context.ts'; import type { ActionLegalEntityPermission, @@ -79,11 +59,7 @@ import type { ActionCoreError, ActionInvocationNotFound } from './errors.ts'; import type { DomainEventContractMap } from './events.ts'; import type { ActionPolicy, ActionPolicyEvaluatorInput } from './policy.ts'; import { PolicyDenied } from './policy.ts'; -import type { - ActionInvocationRecord, - ActionPolicyEvidence, - ActionRepositoryService, -} from './repository.ts'; +import type { ActionInvocationRecord, ActionPolicyEvidence, ActionRepositoryService } from './repository.ts'; import { ActionRepository, computeActionRequestHash, @@ -92,37 +68,25 @@ import { logActionTransactionFailureCause, } from './repository.ts'; -const requireIdempotencyKey = ( - idempotency: string, - transport: ActionTransportMetadata -) => +const requireIdempotencyKey = (idempotency: string, transport: ActionTransportMetadata) => idempotency === 'required' && transport.idempotencyKey === undefined ? Effect.fail( new ActionIdempotencyKeyRequired({ code: 'action_idempotency_key_required', reason: 'This Action requires an idempotency key', - }) + }), ) : Effect.void; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; - -const attachFailureCause = ( - failure: Failure, - cause: FailureCause -): Failure => { + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); + +const attachFailureCause = (failure: Failure, cause: FailureCause): Failure => { Object.defineProperty(failure, 'cause', { configurable: false, enumerable: false, @@ -177,9 +141,7 @@ export interface ResolveActionCommitInput { readonly principal: unknown; } -const ActionInvocationIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('ActionInvocationId') -); +const ActionInvocationIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('ActionInvocationId')); const ActionCommitOpenSchema = Schema.TaggedStruct('ActionCommitOpen', { invocationId: ActionInvocationIdSchema, @@ -189,7 +151,7 @@ export type ActionCommitOpen = typeof ActionCommitOpenSchema.Type; export interface ActionRuntimeService { readonly resolveActionCommit: ( - input: ResolveActionCommitInput + input: ResolveActionCommitInput, ) => Effect.Effect< ActionCommitOpen, | ActionAlreadyCommitted @@ -218,12 +180,8 @@ export interface ActionRuntimeService { Owner, Services, HandlerRequirements - > - ) => Effect.Effect< - ResultSchema['Type'], - ActionCoreError | DomainErrorSchema['Type'], - HandlerRequirements - >; + >, + ) => Effect.Effect; } export interface ActionRuntimeOptions { @@ -238,15 +196,13 @@ export interface ActionRuntimeOptions { const logInvocationPersistenceFailure = ( failure: ActionInvocationPersistenceError, - annotations: Readonly> -): Effect.Effect => - logActionInvocationPersistenceFailureCause(failure, annotations); + annotations: Readonly>, +): Effect.Effect => logActionInvocationPersistenceFailureCause(failure, annotations); const isCommitAcknowledgementFailure = (failure: SqlError): boolean => isDatabaseCommitAcknowledgementAmbiguous(failure) || (Option.isNone(findPostgresFailure(failure)) && - (Schema.is(ConnectionError)(failure.reason) || - Schema.is(UnknownError)(failure.reason))); + (Schema.is(ConnectionError)(failure.reason) || Schema.is(UnknownError)(failure.reason))); const transactionFailure = () => new ActionTransactionError({ @@ -264,8 +220,7 @@ const alreadyCommitted = (invocationId: string) => const requestHashConflict = () => new ActionRequestHashConflict({ code: 'action_request_hash_conflict', - reason: - 'This idempotency key was already used for a different Action request', + reason: 'This idempotency key was already used for a different Action request', }); const permissionUnavailable = () => @@ -276,13 +231,8 @@ const permissionUnavailable = () => const resolveActionTenantPermission = ( payload: Payload, - resolver: - | ((payload: Payload) => ActionTenantPermission | undefined) - | undefined -): Effect.Effect< - Option.Option, - ActionPermissionCheckError -> => + resolver: ((payload: Payload) => ActionTenantPermission | undefined) | undefined, +): Effect.Effect, ActionPermissionCheckError> => Effect.try({ catch: (cause) => attachFailureCause( @@ -290,7 +240,7 @@ const resolveActionTenantPermission = ( code: 'action_permission_check_failed', reason: 'The declared tenant permission could not be resolved safely', }), - cause + cause, ), try: () => Option.fromNullishOr(resolver?.(payload)), }); @@ -298,34 +248,23 @@ const resolveActionTenantPermission = ( const ActionResourcePermissionTargetSchema = Schema.Struct({ permission: Schema.Literals(['read', 'write']), resource: Schema.Struct({ - moduleId: Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(300) - ).pipe(Schema.brand('ActionTargetModuleId')), - resourceId: Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(300) - ).pipe(Schema.brand('ActionTargetResourceId')), - resourceType: Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(300) - ).pipe(Schema.brand('ActionTargetResourceType')), + moduleId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)).pipe( + Schema.brand('ActionTargetModuleId'), + ), + resourceId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)).pipe( + Schema.brand('ActionTargetResourceId'), + ), + resourceType: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)).pipe( + Schema.brand('ActionTargetResourceType'), + ), }), }); const resolveActionResourcePermissionTarget = ( payload: Payload, scope: OperationalScope, - resolver: - | (( - payload: Payload, - scope: OperationalScope - ) => ActionResourcePermissionTarget) - | undefined -): Effect.Effect< - Option.Option, - ActionPermissionCheckError -> => + resolver: ((payload: Payload, scope: OperationalScope) => ActionResourcePermissionTarget) | undefined, +): Effect.Effect, ActionPermissionCheckError> => Effect.suspend(() => { if (resolver === undefined) { return Effect.succeed(Option.none()); @@ -335,45 +274,39 @@ const resolveActionResourcePermissionTarget = ( attachFailureCause( new ActionPermissionCheckError({ code: 'action_permission_check_failed', - reason: - 'The declared Action permission target could not be resolved safely', + reason: 'The declared Action permission target could not be resolved safely', }), - cause + cause, ), try: () => resolver(payload, scope), }).pipe( - Effect.flatMap( - Schema.decodeUnknownEffect(ActionResourcePermissionTargetSchema) - ), + Effect.flatMap(Schema.decodeUnknownEffect(ActionResourcePermissionTargetSchema)), Effect.mapError((cause) => attachFailureCause( new ActionPermissionCheckError({ code: 'action_permission_check_failed', reason: 'The declared Action permission target is invalid', }), - cause - ) + cause, + ), ), Effect.map((target) => Option.some( Object.freeze({ permission: target.permission, resource: Object.freeze({ ...target.resource }), - }) - ) - ) + }), + ), + ), ); }); const verifyInvocation = ( invocation: ActionInvocationRecord, - requestHash: string + requestHash: string, ): Effect.Effect< void, - | ActionAlreadyCommitted - | ActionCommitIndeterminate - | ActionInvocationStateError - | ActionRequestHashConflict + ActionAlreadyCommitted | ActionCommitIndeterminate | ActionInvocationStateError | ActionRequestHashConflict > => { if (invocation.requestHash !== requestHash) { return Effect.fail(requestHashConflict()); @@ -386,22 +319,18 @@ const verifyInvocation = ( new ActionCommitIndeterminate({ code: 'action_commit_indeterminate', invocationId: invocation.actionInvocationId, - reason: - 'This invocation requires commit resolution before it can execute', - }) + reason: 'This invocation requires commit resolution before it can execute', + }), ); } - if ( - (invocation.status === 'received' || invocation.status === 'running') && - invocation.completedAt === null - ) { + if ((invocation.status === 'received' || invocation.status === 'running') && invocation.completedAt === null) { return Effect.void; } return Effect.fail( new ActionInvocationStateError({ code: 'action_invocation_state_invalid', reason: 'This Action invocation is terminal and cannot execute again', - }) + }), ); }; @@ -409,33 +338,23 @@ const StoppedCheckpointPayloadSchema = Schema.Struct({ checkpoint: Schema.Literal('stopped'), }); -const validatePrincipal = < - Input, - Registration extends object = object, - Payload = undefined, ->( +const validatePrincipal = ( input: Input, actionRegistration?: Registration, - payload?: Payload -): Effect.Effect< - TrustedPrincipalContext, - ActionTrustedContextValidationError -> => + payload?: Payload, +): Effect.Effect => decodeTrustedPrincipalContext(input).pipe( Effect.filterOrFail( (principal) => !isTrustedSupportRecoveryPrincipalContext(principal) || (actionRegistration !== undefined && - isTrustedSupportRecoveryPrincipalContext( - principal, - actionRegistration - ) && + isTrustedSupportRecoveryPrincipalContext(principal, actionRegistration) && Schema.is(StoppedCheckpointPayloadSchema)(payload)), () => new ActionTrustedContextValidationError({ code: 'action_trusted_context_invalid', reason: 'The support recovery context does not authorize this Action', - }) + }), ), Effect.mapError((cause) => attachFailureCause( @@ -443,14 +362,12 @@ const validatePrincipal = < code: 'action_trusted_context_invalid', reason: 'The trusted principal context is incomplete or invalid', }), - cause - ) - ) + cause, + ), + ), ); -const validateTransport = ( - input: Input -): Effect.Effect => +const validateTransport = (input: Input): Effect.Effect => Schema.decodeUnknownEffect(ActionTransportMetadataSchema)(input).pipe( Effect.mapError((cause) => attachFailureCause( @@ -458,9 +375,9 @@ const validateTransport = ( code: 'action_payload_invalid', reason: 'The Action transport metadata is structurally invalid', }), - cause - ) - ) + cause, + ), + ), ); const makeHandlerExecutionError = () => @@ -469,9 +386,7 @@ const makeHandlerExecutionError = () => reason: 'The Action handler failed unexpectedly', }); -const policyEvidence = ( - policy: ActionPolicy -): ActionPolicyEvidence => +const policyEvidence = (policy: ActionPolicy): ActionPolicyEvidence => policy.scope === 'global' ? { policyKey: policy.policyKey, scope: policy.scope } : { @@ -481,11 +396,8 @@ const policyEvidence = ( }; const validateInvocationId = ( - input: Input -): Effect.Effect< - typeof ActionInvocationIdSchema.Type, - ActionPayloadValidationError -> => + input: Input, +): Effect.Effect => Schema.decodeUnknownEffect(ActionInvocationIdSchema)(input).pipe( Effect.mapError((cause) => attachFailureCause( @@ -493,9 +405,9 @@ const validateInvocationId = ( code: 'action_payload_invalid', reason: 'The Action invocation identifier is invalid', }), - cause - ) - ) + cause, + ), + ), ); type ActionRuntimeConstruction = readonly [ @@ -506,15 +418,11 @@ type ActionRuntimeConstruction = readonly [ options: ActionRuntimeOptions, ]; -export const makeActionRuntime = ( - ...construction: ActionRuntimeConstruction -): ActionRuntimeService => { - const [database, repository, permission, operationalScopeResolver, options] = - construction; +export const makeActionRuntime = (...construction: ActionRuntimeConstruction): ActionRuntimeService => { + const [database, repository, permission, operationalScopeResolver, options] = construction; const { contextAccess, moduleEntrypointGateway, moduleStateGate } = options; const resolveHandler = options.resolveHandler ?? getActionHandler; - const resolveServiceFactory = - options.resolveServiceFactory ?? getActionServiceFactory; + const resolveServiceFactory = options.resolveServiceFactory ?? getActionServiceFactory; const installScope = options.installScope ?? installOperationalScope; const notifyStage = (stage: ActionRuntimeStage): void => { options.onStage?.(stage); @@ -522,7 +430,7 @@ export const makeActionRuntime = ( const checkTenantActionPermission = ( principal: TrustedPrincipalContext, - tenantPermission: Option.Option + tenantPermission: Option.Option, ): Effect.Effect<'allowed' | 'denied', ActionPermissionCheckError> => { if (Option.isNone(tenantPermission)) { return Effect.succeed('allowed'); @@ -538,17 +446,16 @@ export const makeActionRuntime = ( }) .pipe( Effect.flatMap(([decision]) => - decision?.key === principal.tenantId && - (decision.decision === 'allowed' || decision.decision === 'denied') + decision?.key === principal.tenantId && (decision.decision === 'allowed' || decision.decision === 'denied') ? Effect.succeed(decision.decision) - : Effect.fail(permissionUnavailable()) - ) + : Effect.fail(permissionUnavailable()), + ), ); }; const checkActionLegalEntityPermission = ( scope: OperationalScope, - legalEntityPermission: ActionLegalEntityPermission | undefined + legalEntityPermission: ActionLegalEntityPermission | undefined, ): Effect.Effect<'allowed' | 'denied', ActionPermissionCheckError> => { if (legalEntityPermission === undefined) { return Effect.succeed('allowed'); @@ -569,14 +476,14 @@ export const makeActionRuntime = ( decision.key === scope.legalEntityId && (decision.decision === 'allowed' || decision.decision === 'denied') ? Effect.succeed(decision.decision) - : Effect.fail(permissionUnavailable()) - ) + : Effect.fail(permissionUnavailable()), + ), ); }; const checkActionResourcePermission = ( scope: OperationalScope, - resourceTarget: Option.Option + resourceTarget: Option.Option, ): Effect.Effect<'allowed' | 'denied', ActionPermissionCheckError> => { if (Option.isNone(resourceTarget)) { return Effect.succeed('allowed'); @@ -599,14 +506,12 @@ export const makeActionRuntime = ( `${target.resource.moduleId}:${target.resource.resourceType}:${target.resource.resourceId}` && (decision.decision === 'allowed' || decision.decision === 'denied') ? Effect.succeed(decision.decision) - : Effect.fail(permissionUnavailable()) - ) + : Effect.fail(permissionUnavailable()), + ), ); }; - const runAction: ActionRuntimeService['runAction'] = Effect.fn( - 'ActionRuntime.runAction' - )(function* runActionEffect< + const runAction: ActionRuntimeService['runAction'] = Effect.fn('ActionRuntime.runAction')(function* runActionEffect< PayloadSchema extends Schema.ConstraintDecoder, ResultSchema extends Schema.ConstraintDecoder, DomainErrorSchema extends Schema.ConstraintDecoder<{ @@ -625,19 +530,12 @@ export const makeActionRuntime = ( Owner, Services, HandlerRequirements - > + >, ) { - const payload = yield* decodeActionPayload( - input.registration.descriptor.payloadSchema, - input.payload - ); + const payload = yield* decodeActionPayload(input.registration.descriptor.payloadSchema, input.payload); notifyStage('payload_decoded'); - const principal = yield* validatePrincipal( - input.principal, - input.registration, - payload - ); + const principal = yield* validatePrincipal(input.principal, input.registration, payload); const transport = yield* validateTransport(input.transport); const scope = yield* operationalScopeResolver.resolve( withOptionalProperty( @@ -649,49 +547,31 @@ export const makeActionRuntime = ( transport.traceId !== undefined, 'traceId', transport.traceId, - {} - ) + {}, + ), ); notifyStage('trusted_context_validated'); - const moduleStateSnapshot = yield* moduleEntrypointGateway.prepareSnapshot( - scope, - [input.registration.descriptor.entrypoint] - ); - yield* moduleEntrypointGateway.check( - moduleStateSnapshot, - input.registration.descriptor.entrypoint - ); + const moduleStateSnapshot = yield* moduleEntrypointGateway.prepareSnapshot(scope, [ + input.registration.descriptor.entrypoint, + ]); + yield* moduleEntrypointGateway.check(moduleStateSnapshot, input.registration.descriptor.entrypoint); notifyStage('module_state_gate'); - yield* requireIdempotencyKey( - input.registration.descriptor.idempotency, - transport - ); + yield* requireIdempotencyKey(input.registration.descriptor.idempotency, transport); - const tenantPermission = isTrustedSupportRecoveryPrincipalContext( - principal, - input.registration - ) + const tenantPermission = isTrustedSupportRecoveryPrincipalContext(principal, input.registration) ? Option.none() - : yield* resolveActionTenantPermission( - payload, - input.registration.descriptor.tenantPermission - ); + : yield* resolveActionTenantPermission(payload, input.registration.descriptor.tenantPermission); const { legalEntityPermission } = input.registration.descriptor; - const hasCanonicalScopeTarget = - Option.isSome(tenantPermission) || legalEntityPermission !== undefined; + const hasCanonicalScopeTarget = Option.isSome(tenantPermission) || legalEntityPermission !== undefined; - const resourcePermissionTarget = - yield* resolveActionResourcePermissionTarget( - payload, - scope, - getActionResourcePermissionTargetResolver(input.registration) - ); - let actionTarget: Pick< - ActionTransportMetadata, - 'targetModuleKey' | 'targetResourceId' | 'targetResourceType' - >; + const resourcePermissionTarget = yield* resolveActionResourcePermissionTarget( + payload, + scope, + getActionResourcePermissionTargetResolver(input.registration), + ); + let actionTarget: Pick; let governedTransport: ActionTransportMetadata; if (Option.isSome(resourcePermissionTarget)) { const target = resourcePermissionTarget.value; @@ -711,12 +591,12 @@ export const makeActionRuntime = ( transport.idempotencyKey !== undefined, 'idempotencyKey', transport.idempotencyKey, - {} + {}, ), transport.traceId !== undefined, 'traceId', transport.traceId, - {} + {}, ); } else if (hasCanonicalScopeTarget) { actionTarget = {}; @@ -726,12 +606,12 @@ export const makeActionRuntime = ( transport.idempotencyKey !== undefined, 'idempotencyKey', transport.idempotencyKey, - {} + {}, ), transport.traceId !== undefined, 'traceId', transport.traceId, - {} + {}, ); } else { actionTarget = withOptionalProperty( @@ -741,17 +621,17 @@ export const makeActionRuntime = ( transport.targetModuleKey !== undefined, 'targetModuleKey', transport.targetModuleKey, - {} + {}, ), transport.targetResourceId !== undefined, 'targetResourceId', transport.targetResourceId, - {} + {}, ), transport.targetResourceType !== undefined, 'targetResourceType', transport.targetResourceType, - {} + {}, ); governedTransport = transport; } @@ -763,15 +643,13 @@ export const makeActionRuntime = ( code: 'action_payload_invalid', reason: 'The decoded Action payload cannot be encoded safely', }), - cause + cause, ), try: () => Result.getOrThrow( Schema.encodeUnknownResult( - Schema.make>( - input.registration.descriptor.payloadSchema.ast - ) - )(payload) + Schema.make>(input.registration.descriptor.payloadSchema.ast), + )(payload), ), }); @@ -782,7 +660,7 @@ export const makeActionRuntime = ( code: 'action_payload_invalid', reason: 'The decoded Action payload cannot be normalized safely', }), - cause + cause, ), try: () => computeActionRequestHash({ @@ -808,8 +686,8 @@ export const makeActionRuntime = ( logInvocationPersistenceFailure(error, { actionKey: input.registration.descriptor.actionKey, correlationId: transport.correlationId, - }) - ) + }), + ), ); notifyStage('invocation_prepared'); yield* verifyInvocation(invocation, requestHash); @@ -817,32 +695,24 @@ export const makeActionRuntime = ( // The trusted context already represents authentication. Authorization // uses only the immutable Action key and trusted principal identity. notifyStage('authentication_boundary'); - const logPermissionInvocationFailure = Effect.fn( - 'ActionRuntime.logPermissionInvocationFailure' - )(function* logPermissionInvocationFailureEffect( - failure: ActionInvocationPersistenceError - ) { - yield* logInvocationPersistenceFailure(failure, { - actionKey: input.registration.descriptor.actionKey, - correlationId: transport.correlationId, - invocationId: invocation.actionInvocationId, - }); - }); - const logPermissionTransactionFailure = Effect.fn( - 'ActionRuntime.logPermissionTransactionFailure' - )(function* logPermissionTransactionFailureEffect( - failure: ActionTransactionError - ) { - yield* logActionTransactionFailureCause( - failure, - 'Unexpected permission denial persistence failure', - { + const logPermissionInvocationFailure = Effect.fn('ActionRuntime.logPermissionInvocationFailure')( + function* logPermissionInvocationFailureEffect(failure: ActionInvocationPersistenceError) { + yield* logInvocationPersistenceFailure(failure, { actionKey: input.registration.descriptor.actionKey, correlationId: transport.correlationId, invocationId: invocation.actionInvocationId, - } - ); - }); + }); + }, + ); + const logPermissionTransactionFailure = Effect.fn('ActionRuntime.logPermissionTransactionFailure')( + function* logPermissionTransactionFailureEffect(failure: ActionTransactionError) { + yield* logActionTransactionFailureCause(failure, 'Unexpected permission denial persistence failure', { + actionKey: input.registration.descriptor.actionKey, + correlationId: transport.correlationId, + invocationId: invocation.actionInvocationId, + }); + }, + ); const rejectPermission = () => repository .rejectPermissionDenied(database.executor, { @@ -853,22 +723,16 @@ export const makeActionRuntime = ( transport: governedTransport, }) .pipe( - Effect.tapErrorTag( - 'ActionInvocationPersistenceError', - logPermissionInvocationFailure - ), - Effect.tapErrorTag( - 'ActionTransactionError', - logPermissionTransactionFailure - ), + Effect.tapErrorTag('ActionInvocationPersistenceError', logPermissionInvocationFailure), + Effect.tapErrorTag('ActionTransactionError', logPermissionTransactionFailure), Effect.flatMap(() => Effect.fail( new ActionPermissionDenied({ code: 'action_permission_denied', reason: 'The principal is not permitted to execute this Action', - }) - ) - ) + }), + ), + ), ); const permissionDecision = yield* permission .checkActionPermission({ @@ -877,18 +741,11 @@ export const makeActionRuntime = ( principalId: principal.principalId, }) .pipe(Effect.tapError((error) => Effect.logError(error.reason))); - const tenantPermissionDecision = yield* checkTenantActionPermission( - principal, - tenantPermission - ); + const tenantPermissionDecision = yield* checkTenantActionPermission(principal, tenantPermission); - const legalEntityPermissionDecision = - yield* checkActionLegalEntityPermission(scope, legalEntityPermission); + const legalEntityPermissionDecision = yield* checkActionLegalEntityPermission(scope, legalEntityPermission); - const resourcePermissionDecision = yield* checkActionResourcePermission( - scope, - resourcePermissionTarget - ); + const resourcePermissionDecision = yield* checkActionResourcePermission(scope, resourcePermissionTarget); notifyStage('permission_checked'); if ( permissionDecision === 'denied' || @@ -901,412 +758,340 @@ export const makeActionRuntime = ( notifyStage('policy_boundary'); - const policyInput: ActionPolicyEvaluatorInput = - Object.freeze({ - action: Object.freeze({ - actionKey: input.registration.descriptor.actionKey, - owningModuleKey: input.registration.descriptor.owningModuleKey, - schemaVersion: input.registration.descriptor.schemaVersion, - }), - payload, - principal: Object.freeze({ ...principal }), - target: Object.freeze({ ...actionTarget }), - transport: Object.freeze( - withOptionalProperty( - { - correlationId: transport.correlationId, - }, - transport.traceId !== undefined, - 'traceId', - transport.traceId, - {} - ) + const policyInput: ActionPolicyEvaluatorInput = Object.freeze({ + action: Object.freeze({ + actionKey: input.registration.descriptor.actionKey, + owningModuleKey: input.registration.descriptor.owningModuleKey, + schemaVersion: input.registration.descriptor.schemaVersion, + }), + payload, + principal: Object.freeze({ ...principal }), + target: Object.freeze({ ...actionTarget }), + transport: Object.freeze( + withOptionalProperty( + { + correlationId: transport.correlationId, + }, + transport.traceId !== undefined, + 'traceId', + transport.traceId, + {}, ), - }); - const evaluateActionPolicy = Effect.fn('ActionRuntime.evaluatePolicy')( - function* evaluatePolicy(policy: ActionPolicy) { - const policyExit = yield* Effect.exit( - Effect.suspend(() => policy.evaluate(policyInput)) - ); - if (Exit.isSuccess(policyExit)) { - return policyEvidence(policy); - } - - const failureReasons = policyExit.cause.reasons.filter( - Cause.isFailReason - ); - const [failureReason] = failureReasons; - if ( - failureReasons.length === policyExit.cause.reasons.length && - failureReason !== undefined && - Schema.is(PolicyDenied)(failureReason.error) - ) { - const denial = failureReason.error; - yield* repository - .finalizePolicyDenial(database.executor, { - actionInvocationId: invocation.actionInvocationId, - actionKey: input.registration.descriptor.actionKey, - auditProfile: input.registration.descriptor.auditProfile, - policy: policyEvidence(policy), - principal, - reasonCode: denial.reasonCode, - transport: governedTransport, - }) - .pipe( - Effect.tapError((error) => - logInvocationPersistenceFailure(error, { - actionKey: input.registration.descriptor.actionKey, - correlationId: transport.correlationId, - invocationId: invocation.actionInvocationId, - policyKey: policy.policyKey, - }) - ) - ); - return yield* new ActionPolicyDenied({ - code: 'action_policy_denied', - policyReasonCode: denial.reasonCode, - reason: denial.reason, - }); - } + ), + }); + const evaluateActionPolicy = Effect.fn('ActionRuntime.evaluatePolicy')(function* evaluatePolicy( + policy: ActionPolicy, + ) { + const policyExit = yield* Effect.exit(Effect.suspend(() => policy.evaluate(policyInput))); + if (Exit.isSuccess(policyExit)) { + return policyEvidence(policy); + } - yield* Effect.logError( - 'Unexpected Action Policy evaluation failure', - policyExit.cause - ); - return yield* new ActionPolicyEvaluationError({ - code: 'action_policy_evaluation_failed', - reason: 'A required Action Policy could not be evaluated', + const failureReasons = policyExit.cause.reasons.filter(Cause.isFailReason); + const [failureReason] = failureReasons; + if ( + failureReasons.length === policyExit.cause.reasons.length && + failureReason !== undefined && + Schema.is(PolicyDenied)(failureReason.error) + ) { + const denial = failureReason.error; + yield* repository + .finalizePolicyDenial(database.executor, { + actionInvocationId: invocation.actionInvocationId, + actionKey: input.registration.descriptor.actionKey, + auditProfile: input.registration.descriptor.auditProfile, + policy: policyEvidence(policy), + principal, + reasonCode: denial.reasonCode, + transport: governedTransport, + }) + .pipe( + Effect.tapError((error) => + logInvocationPersistenceFailure(error, { + actionKey: input.registration.descriptor.actionKey, + correlationId: transport.correlationId, + invocationId: invocation.actionInvocationId, + policyKey: policy.policyKey, + }), + ), + ); + return yield* new ActionPolicyDenied({ + code: 'action_policy_denied', + policyReasonCode: denial.reasonCode, + reason: denial.reason, }); } - ); - const allowedPolicies = yield* Effect.forEach( - input.registration.descriptor.policies, - evaluateActionPolicy, - { concurrency: 1 } - ); + + yield* Effect.logError('Unexpected Action Policy evaluation failure', policyExit.cause); + return yield* new ActionPolicyEvaluationError({ + code: 'action_policy_evaluation_failed', + reason: 'A required Action Policy could not be evaluated', + }); + }); + const allowedPolicies = yield* Effect.forEach(input.registration.descriptor.policies, evaluateActionPolicy, { + concurrency: 1, + }); const runningInvocation = yield* repository - .transitionInvocationToRunning( - database.executor, - invocation.actionInvocationId - ) + .transitionInvocationToRunning(database.executor, invocation.actionInvocationId) .pipe( Effect.tapError((error) => logInvocationPersistenceFailure(error, { actionKey: input.registration.descriptor.actionKey, correlationId: transport.correlationId, invocationId: invocation.actionInvocationId, - }) - ) + }), + ), ); yield* verifyInvocation(runningInvocation, requestHash); notifyStage('invocation_running'); const transactionBodyCompleted = yield* Ref.make(false); - const transactionBodyExit = yield* Ref.make | null>(null); - const transactionProgram = Effect.fn('ActionRuntime.transaction')( - function* executeTransaction(drizzleTransaction: CoreTransaction) { - const lockedInvocation = yield* repository - .lockInvocation(drizzleTransaction, invocation.actionInvocationId) - .pipe( - Effect.tapError((error) => - logInvocationPersistenceFailure(error, { - actionKey: input.registration.descriptor.actionKey, - correlationId: transport.correlationId, - invocationId: invocation.actionInvocationId, - }) - ) - ); - notifyStage('invocation_locked'); - yield* verifyInvocation(lockedInvocation, requestHash); - - const scopedTransaction = yield* installScope( - drizzleTransaction, - scope - ); - notifyStage('database_scope_installed'); - - const actionEntrypoint = input.registration.descriptor.entrypoint; - if (actionEntrypoint.scope === 'tenant') { - const tenantEntrypoint = Object.freeze({ - ...actionEntrypoint, - scope: 'tenant' as const, - }) satisfies TenantModuleEntrypoint<'action', 'write', Owner>; - yield* moduleStateGate.recheckWrite( - drizzleTransaction, - scope.tenantId, - tenantEntrypoint - ); - } - notifyStage('module_state_rechecked'); - const serviceFactory = resolveServiceFactory(input.registration); - const services = yield* serviceFactory(scopedTransaction, scope); - const handler = resolveHandler(input.registration); - - const collector = createActionCollector( - input.registration.descriptor.domainEvents, - input.registration.descriptor.owningModuleKey, - input.registration.descriptor.accessEvidencePolicy, - input.registration.descriptor.auditEvidenceSchema - ); - const handlerContext = Object.freeze({ - actionInvocationId: lockedInvocation.actionInvocationId, - addDomainEvent: collector.addDomainEvent, - addOutboxMessage: collector.addOutboxMessage, - recordAuditEvidence: collector.recordAuditEvidence, - recordDataAccess: collector.recordDataAccess, - scope, - services, - }); + const transactionBodyExit = yield* Ref.make | null>(null); + const transactionProgram = Effect.fn('ActionRuntime.transaction')(function* executeTransaction( + drizzleTransaction: CoreTransaction, + ) { + const lockedInvocation = yield* repository.lockInvocation(drizzleTransaction, invocation.actionInvocationId).pipe( + Effect.tapError((error) => + logInvocationPersistenceFailure(error, { + actionKey: input.registration.descriptor.actionKey, + correlationId: transport.correlationId, + invocationId: invocation.actionInvocationId, + }), + ), + ); + notifyStage('invocation_locked'); + yield* verifyInvocation(lockedInvocation, requestHash); + + const scopedTransaction = yield* installScope(drizzleTransaction, scope); + notifyStage('database_scope_installed'); + + const actionEntrypoint = input.registration.descriptor.entrypoint; + if (actionEntrypoint.scope === 'tenant') { + const tenantEntrypoint = Object.freeze({ + ...actionEntrypoint, + scope: 'tenant' as const, + }) satisfies TenantModuleEntrypoint<'action', 'write', Owner>; + yield* moduleStateGate.recheckWrite(drizzleTransaction, scope.tenantId, tenantEntrypoint); + } + notifyStage('module_state_rechecked'); + const serviceFactory = resolveServiceFactory(input.registration); + const services = yield* serviceFactory(scopedTransaction, scope); + const handler = resolveHandler(input.registration); + + const collector = createActionCollector( + input.registration.descriptor.domainEvents, + input.registration.descriptor.owningModuleKey, + input.registration.descriptor.accessEvidencePolicy, + input.registration.descriptor.auditEvidenceSchema, + ); + const handlerContext = Object.freeze({ + actionInvocationId: lockedInvocation.actionInvocationId, + addDomainEvent: collector.addDomainEvent, + addOutboxMessage: collector.addOutboxMessage, + recordAuditEvidence: collector.recordAuditEvidence, + recordDataAccess: collector.recordDataAccess, + scope, + services, + }); - const handlerExit = yield* Effect.exit( - Effect.suspend(() => handler(payload, handlerContext)) - ); + const handlerExit = yield* Effect.exit(Effect.suspend(() => handler(payload, handlerContext))); - if (Exit.isFailure(handlerExit)) { - const failureReasons = handlerExit.cause.reasons.filter( - Cause.isFailReason - ); - const [failureReason] = failureReasons; - if ( - failureReasons.length === handlerExit.cause.reasons.length && - failureReason !== undefined - ) { - if (Schema.is(ActionCollectorError)(failureReason.error)) { - return yield* failureReason.error; - } - const decodedDomainError = yield* Effect.option( - Schema.decodeUnknownEffect( - input.registration.descriptor.domainErrorSchema - )(failureReason.error) - ); - if (Option.isSome(decodedDomainError)) { - return yield* Effect.fail(decodedDomainError.value); - } + if (Exit.isFailure(handlerExit)) { + const failureReasons = handlerExit.cause.reasons.filter(Cause.isFailReason); + const [failureReason] = failureReasons; + if (failureReasons.length === handlerExit.cause.reasons.length && failureReason !== undefined) { + if (Schema.is(ActionCollectorError)(failureReason.error)) { + return yield* failureReason.error; } - yield* Effect.logError( - 'Unexpected Action execution defect', - handlerExit.cause + const decodedDomainError = yield* Effect.option( + Schema.decodeUnknownEffect(input.registration.descriptor.domainErrorSchema)(failureReason.error), ); - return yield* makeHandlerExecutionError(); + if (Option.isSome(decodedDomainError)) { + return yield* Effect.fail(decodedDomainError.value); + } } - notifyStage('handler_executed'); - - const result = yield* decodeActionResult( - input.registration.descriptor.resultSchema, - handlerExit.value - ); - - const resultHash = yield* Effect.try({ - catch: (cause) => - attachFailureCause( - new ActionResultValidationError({ - code: 'action_result_invalid', - reason: 'The decoded Action result cannot be normalized safely', - }), - cause - ), - try: () => - computeCanonicalValueHash( - Result.getOrThrow( - Schema.encodeUnknownResult( - Schema.make>( - input.registration.descriptor.resultSchema.ast - ) - )(result) - ) - ), - }); - yield* repository - .flushSuccess(drizzleTransaction, { - actionInvocationId: invocation.actionInvocationId, - actionKey: input.registration.descriptor.actionKey, - allowedPolicies, - auditProfile: input.registration.descriptor.auditProfile, - evidence: collector.snapshot(), - principal, - resultHash, - transport: governedTransport, - }) - .pipe( - Effect.tapError((error) => - logActionTransactionFailureCause( - error, - 'Unexpected Action success evidence persistence failure', - { - actionKey: input.registration.descriptor.actionKey, - correlationId: transport.correlationId, - invocationId: invocation.actionInvocationId, - } - ) - ) - ); - notifyStage('success_evidence_flushed'); - yield* Ref.set(transactionBodyCompleted, true); - return result; + yield* Effect.logError('Unexpected Action execution defect', handlerExit.cause); + return yield* makeHandlerExecutionError(); } - ); + notifyStage('handler_executed'); + + const result = yield* decodeActionResult(input.registration.descriptor.resultSchema, handlerExit.value); + + const resultHash = yield* Effect.try({ + catch: (cause) => + attachFailureCause( + new ActionResultValidationError({ + code: 'action_result_invalid', + reason: 'The decoded Action result cannot be normalized safely', + }), + cause, + ), + try: () => + computeCanonicalValueHash( + Result.getOrThrow( + Schema.encodeUnknownResult( + Schema.make>(input.registration.descriptor.resultSchema.ast), + )(result), + ), + ), + }); + yield* repository + .flushSuccess(drizzleTransaction, { + actionInvocationId: invocation.actionInvocationId, + actionKey: input.registration.descriptor.actionKey, + allowedPolicies, + auditProfile: input.registration.descriptor.auditProfile, + evidence: collector.snapshot(), + principal, + resultHash, + transport: governedTransport, + }) + .pipe( + Effect.tapError((error) => + logActionTransactionFailureCause(error, 'Unexpected Action success evidence persistence failure', { + actionKey: input.registration.descriptor.actionKey, + correlationId: transport.correlationId, + invocationId: invocation.actionInvocationId, + }), + ), + ); + notifyStage('success_evidence_flushed'); + yield* Ref.set(transactionBodyCompleted, true); + return result; + }); // The driver/body stay interruptible; classify the settled Cause before interruption resumes. return yield* Effect.uninterruptibleMask( - Effect.fn('ActionRuntime.classifyTransactionOutcome')( - function* classifyTransactionOutcome( - restore: ( - effect: Effect.Effect - ) => Effect.Effect - ) { - const transactionExit = yield* Effect.exit( - restore( - database.executor.transaction((transaction) => - transactionProgram(transaction).pipe( - Effect.onExit((exit) => Ref.set(transactionBodyExit, exit)) - ) - ) - ) - ); - if (Exit.isSuccess(transactionExit)) { - return transactionExit.value; - } - - const bodyExit = yield* Ref.get(transactionBodyExit); - const bodyCompleted = - (yield* Ref.get(transactionBodyCompleted)) && - bodyExit !== null && - Exit.isSuccess(bodyExit); - if ( - bodyExit !== null && - Exit.isFailure(bodyExit) && - transactionExit.cause.reasons.some( - (reason) => Cause.isDieReason(reason) && isSqlError(reason.defect) - ) - ) { - yield* Effect.logError( - 'Action body failed before transaction rollback failed', - bodyExit.cause - ); - } - return yield* Effect.failCause( - Cause.fromReasons( - transactionExit.cause.reasons.map((reason) => { - if (Cause.isInterruptReason(reason)) { - return reason; - } - const failure = Cause.isFailReason(reason) - ? reason.error - : reason.defect; - if (!isSqlError(failure)) { - return reason; - } - return Cause.makeFailReason( - bodyCompleted && isCommitAcknowledgementFailure(failure) - ? new ActionCommitIndeterminate({ - code: 'action_commit_indeterminate', - invocationId: invocation.actionInvocationId, - reason: - 'The database did not confirm whether the Action commit completed', - }) - : transactionFailure() - ); - }) - ) - ); - } - ) - ); - }); - - const resolveActionCommit: ActionRuntimeService['resolveActionCommit'] = - Effect.fn('ActionRuntime.resolveActionCommit')( - function* resolveActionCommitEffect(input: ResolveActionCommitInput) { - const principal = yield* validatePrincipal(input.principal); - const invocationId = yield* validateInvocationId(input.invocationId); - const invocation = yield* repository - .resolveInvocation(database.executor, { - invocationId, - principal, - }) - .pipe( - Effect.tapError((error) => - Schema.is(ActionInvocationPersistenceError)(error) - ? logInvocationPersistenceFailure(error, { - invocationId, - principalId: principal.principalId, - tenantId: principal.tenantId, - }) - : Effect.void + Effect.fn('ActionRuntime.classifyTransactionOutcome')(function* classifyTransactionOutcome( + restore: ( + effect: Effect.Effect, + ) => Effect.Effect, + ) { + const transactionExit = yield* Effect.exit( + restore( + database.executor.transaction((transaction) => + transactionProgram(transaction).pipe(Effect.onExit((exit) => Ref.set(transactionBodyExit, exit))), ), - Effect.mapError((error) => - Schema.is(ActionInvocationPersistenceError)(error) - ? new ActionCommitIndeterminate({ - code: 'action_commit_indeterminate', - invocationId, - reason: - 'The database cannot confirm the Action commit state yet', - }) - : error - ) - ); - - if (invocation.status === 'succeeded') { - return yield* alreadyCommitted(invocation.actionInvocationId); + ), + ); + if (Exit.isSuccess(transactionExit)) { + return transactionExit.value; } + + const bodyExit = yield* Ref.get(transactionBodyExit); + const bodyCompleted = + (yield* Ref.get(transactionBodyCompleted)) && bodyExit !== null && Exit.isSuccess(bodyExit); if ( - (invocation.status === 'received' || - invocation.status === 'running' || - invocation.status === 'indeterminate') && - invocation.completedAt === null + bodyExit !== null && + Exit.isFailure(bodyExit) && + transactionExit.cause.reasons.some((reason) => Cause.isDieReason(reason) && isSqlError(reason.defect)) ) { - return Object.freeze({ - _tag: 'ActionCommitOpen', - invocationId, - }) satisfies ActionCommitOpen; + yield* Effect.logError('Action body failed before transaction rollback failed', bodyExit.cause); } - return yield* new ActionInvocationStateError({ - code: 'action_invocation_state_invalid', - reason: 'This Action invocation has a terminal non-committed state', - }); - } + return yield* Effect.failCause( + Cause.fromReasons( + transactionExit.cause.reasons.map((reason) => { + if (Cause.isInterruptReason(reason)) { + return reason; + } + const failure = Cause.isFailReason(reason) ? reason.error : reason.defect; + if (!isSqlError(failure)) { + return reason; + } + return Cause.makeFailReason( + bodyCompleted && isCommitAcknowledgementFailure(failure) + ? new ActionCommitIndeterminate({ + code: 'action_commit_indeterminate', + invocationId: invocation.actionInvocationId, + reason: 'The database did not confirm whether the Action commit completed', + }) + : transactionFailure(), + ); + }), + ), + ); + }), ); + }); + + const resolveActionCommit: ActionRuntimeService['resolveActionCommit'] = Effect.fn( + 'ActionRuntime.resolveActionCommit', + )(function* resolveActionCommitEffect(input: ResolveActionCommitInput) { + const principal = yield* validatePrincipal(input.principal); + const invocationId = yield* validateInvocationId(input.invocationId); + const invocation = yield* repository + .resolveInvocation(database.executor, { + invocationId, + principal, + }) + .pipe( + Effect.tapError((error) => + Schema.is(ActionInvocationPersistenceError)(error) + ? logInvocationPersistenceFailure(error, { + invocationId, + principalId: principal.principalId, + tenantId: principal.tenantId, + }) + : Effect.void, + ), + Effect.mapError((error) => + Schema.is(ActionInvocationPersistenceError)(error) + ? new ActionCommitIndeterminate({ + code: 'action_commit_indeterminate', + invocationId, + reason: 'The database cannot confirm the Action commit state yet', + }) + : error, + ), + ); + + if (invocation.status === 'succeeded') { + return yield* alreadyCommitted(invocation.actionInvocationId); + } + if ( + (invocation.status === 'received' || invocation.status === 'running' || invocation.status === 'indeterminate') && + invocation.completedAt === null + ) { + return Object.freeze({ + _tag: 'ActionCommitOpen', + invocationId, + }) satisfies ActionCommitOpen; + } + return yield* new ActionInvocationStateError({ + code: 'action_invocation_state_invalid', + reason: 'This Action invocation has a terminal non-committed state', + }); + }); return Object.freeze({ resolveActionCommit, runAction }); }; -export class ActionRuntime extends Context.Service< - ActionRuntime, - ActionRuntimeService ->()('@app/core-runtime/actions/runtime/ActionRuntime') {} +export class ActionRuntime extends Context.Service()( + '@app/core-runtime/actions/runtime/ActionRuntime', +) {} export const ActionRuntimeLive = Layer.effect( ActionRuntime, Effect.gen(function* makeActionRuntimeService() { - const [ - database, - repository, - permission, - moduleEntrypointGateway, - moduleStateGate, - scopeResolver, - contextAccess, - ] = yield* Effect.all( - [ - CoreDatabaseService, - ActionRepository, - ActionPermission, - ModuleEntrypointGateway, - ModuleStateGate, - OperationalScopeResolver, - ContextAccess, - ] as const, - { concurrency: 7 } - ); + const [database, repository, permission, moduleEntrypointGateway, moduleStateGate, scopeResolver, contextAccess] = + yield* Effect.all( + [ + CoreDatabaseService, + ActionRepository, + ActionPermission, + ModuleEntrypointGateway, + ModuleStateGate, + OperationalScopeResolver, + ContextAccess, + ] as const, + { concurrency: 7 }, + ); return makeActionRuntime(database, repository, permission, scopeResolver, { contextAccess, moduleEntrypointGateway, moduleStateGate, }); - }) + }), ); export const runAction = < @@ -1326,7 +1111,7 @@ export const runAction = < Owner, Services, HandlerRequirements - > + >, ): Effect.Effect< ResultSchema['Type'], ActionCoreError | DomainErrorSchema['Type'], @@ -1334,7 +1119,7 @@ export const runAction = < > => ActionRuntime.pipe(Effect.flatMap((runtime) => runtime.runAction(input))); export const resolveActionCommit = ( - input: ResolveActionCommitInput + input: ResolveActionCommitInput, ): Effect.Effect< ActionCommitOpen, | ActionAlreadyCommitted @@ -1344,7 +1129,4 @@ export const resolveActionCommit = ( | ActionPayloadValidationError | ActionTrustedContextValidationError, ActionRuntime -> => - ActionRuntime.pipe( - Effect.flatMap((runtime) => runtime.resolveActionCommit(input)) - ); +> => ActionRuntime.pipe(Effect.flatMap((runtime) => runtime.resolveActionCommit(input))); diff --git a/app/packages/core-runtime/src/actions/string-schemas.ts b/app/packages/core-runtime/src/actions/string-schemas.ts index 7a03c542c..5a60e8183 100644 --- a/app/packages/core-runtime/src/actions/string-schemas.ts +++ b/app/packages/core-runtime/src/actions/string-schemas.ts @@ -2,16 +2,8 @@ import { Schema } from 'effect'; export const nonEmptyString = Schema.String.check(Schema.isMinLength(1)); -export const decodedStringBrand = ( - schema: Schema.String, - brand: Brand -) => schema.pipe(Schema.brand(brand), Schema.decodeTo(Schema.String)); +export const decodedStringBrand = (schema: Schema.String, brand: Brand) => + schema.pipe(Schema.brand(brand), Schema.decodeTo(Schema.String)); -export const TargetModuleKeySchema = decodedStringBrand( - nonEmptyString, - 'TargetModuleKey' -); -export const TargetResourceIdSchema = decodedStringBrand( - nonEmptyString, - 'TargetResourceId' -); +export const TargetModuleKeySchema = decodedStringBrand(nonEmptyString, 'TargetModuleKey'); +export const TargetResourceIdSchema = decodedStringBrand(nonEmptyString, 'TargetResourceId'); diff --git a/app/packages/core-runtime/src/actions/transaction-error.ts b/app/packages/core-runtime/src/actions/transaction-error.ts index 6a816d5b7..f3e9152dc 100644 --- a/app/packages/core-runtime/src/actions/transaction-error.ts +++ b/app/packages/core-runtime/src/actions/transaction-error.ts @@ -2,24 +2,17 @@ import { Cause, Schema } from 'effect'; import { actionErrorSchema } from './error-schema.ts'; -const ActionTransactionErrorValue = actionErrorSchema( - 'ActionTransactionError', - { - code: Schema.Literal('action_transaction_failed'), - reason: Schema.String, - } -); -export type ActionTransactionError = InstanceType< - typeof ActionTransactionErrorValue ->; +const ActionTransactionErrorValue = actionErrorSchema('ActionTransactionError', { + code: Schema.Literal('action_transaction_failed'), + reason: Schema.String, +}); +export type ActionTransactionError = InstanceType; const ActionTransactionErrorInternals = (() => { let createWithCause: ( props: ConstructorParameters[0], - cause?: unknown + cause?: unknown, ) => ActionTransactionError; - let readCause: ( - failure: ActionTransactionError - ) => Cause.Cause | undefined; + let readCause: (failure: ActionTransactionError) => Cause.Cause | undefined; class RetainedError extends ActionTransactionErrorValue { #cause: Cause.Cause | undefined; @@ -41,7 +34,5 @@ const ActionTransactionErrorInternals = (() => { const ActionTransactionErrorClass = ActionTransactionErrorInternals.ErrorClass; export { ActionTransactionErrorClass as ActionTransactionError }; // Core-only accessors: deliberately excluded from the package root exports. -export const createActionTransactionErrorWithCause = - ActionTransactionErrorInternals.createWithCause; -export const getActionTransactionErrorCause = - ActionTransactionErrorInternals.readCause; +export const createActionTransactionErrorWithCause = ActionTransactionErrorInternals.createWithCause; +export const getActionTransactionErrorCause = ActionTransactionErrorInternals.readCause; diff --git a/app/packages/core-runtime/src/auth/gateway-assertion-redemption-unavailable-error.ts b/app/packages/core-runtime/src/auth/gateway-assertion-redemption-unavailable-error.ts index 5b2e128c6..b778d22a0 100644 --- a/app/packages/core-runtime/src/auth/gateway-assertion-redemption-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/gateway-assertion-redemption-unavailable-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class GatewayAssertionRedemptionUnavailableError extends Schema.TaggedError()( 'GatewayAssertionRedemptionUnavailableError', - { reason: Schema.String } + { reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/gateway-assertion-redemption.ts b/app/packages/core-runtime/src/auth/gateway-assertion-redemption.ts index fbd1a13a2..8ea3f5a39 100644 --- a/app/packages/core-runtime/src/auth/gateway-assertion-redemption.ts +++ b/app/packages/core-runtime/src/auth/gateway-assertion-redemption.ts @@ -14,19 +14,13 @@ export interface GatewayAssertionRedemptionInput { readonly jti: string; } -export type GatewayAssertionRedemptionError = - | GatewayAssertionReplayError - | GatewayAssertionRedemptionUnavailableError; +export type GatewayAssertionRedemptionError = GatewayAssertionReplayError | GatewayAssertionRedemptionUnavailableError; export interface GatewayAssertionRedemption { - readonly consume: ( - input: GatewayAssertionRedemptionInput - ) => Effect.Effect; + readonly consume: (input: GatewayAssertionRedemptionInput) => Effect.Effect; } export class GatewayAssertionRedemptionService extends Context.Service< GatewayAssertionRedemptionService, GatewayAssertionRedemption ->()( - '@app/core-runtime/auth/gateway-assertion-redemption/GatewayAssertionRedemptionService' -) {} +>()('@app/core-runtime/auth/gateway-assertion-redemption/GatewayAssertionRedemptionService') {} diff --git a/app/packages/core-runtime/src/auth/gateway-assertion-replay-error.ts b/app/packages/core-runtime/src/auth/gateway-assertion-replay-error.ts index ccd333556..1a9170d5f 100644 --- a/app/packages/core-runtime/src/auth/gateway-assertion-replay-error.ts +++ b/app/packages/core-runtime/src/auth/gateway-assertion-replay-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class GatewayAssertionReplayError extends Schema.TaggedError()( 'GatewayAssertionReplayError', - { reason: Schema.String } + { reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/identity-lifecycle-conflict-error.ts b/app/packages/core-runtime/src/auth/identity-lifecycle-conflict-error.ts index 7e5f25a5f..65404c9ac 100644 --- a/app/packages/core-runtime/src/auth/identity-lifecycle-conflict-error.ts +++ b/app/packages/core-runtime/src/auth/identity-lifecycle-conflict-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class IdentityLifecycleConflictError extends Schema.TaggedError()( 'IdentityLifecycleConflictError', - { code: Schema.Literal('identity_lifecycle_conflict'), reason: Schema.String } + { code: Schema.Literal('identity_lifecycle_conflict'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/identity-persistence-unavailable-error.ts b/app/packages/core-runtime/src/auth/identity-persistence-unavailable-error.ts index 5f6c07a87..75d7950d1 100644 --- a/app/packages/core-runtime/src/auth/identity-persistence-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/identity-persistence-unavailable-error.ts @@ -5,5 +5,5 @@ export class IdentityPersistenceUnavailableError extends Schema.TaggedError()( 'IdentityTargetInvalidError', - { code: Schema.Literal('identity_target_invalid'), reason: Schema.String } + { code: Schema.Literal('identity_target_invalid'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context-ambiguous-error.ts b/app/packages/core-runtime/src/auth/legal-entity-context-ambiguous-error.ts index 9c4b388ef..c0a59fe17 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context-ambiguous-error.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context-ambiguous-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class LegalEntityContextAmbiguousError extends Schema.TaggedError()( 'LegalEntityContextAmbiguousError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context-inactive-error.ts b/app/packages/core-runtime/src/auth/legal-entity-context-inactive-error.ts index bd35227d3..e05783d5b 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context-inactive-error.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context-inactive-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class LegalEntityContextInactiveError extends Schema.TaggedError()( 'LegalEntityContextInactiveError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context-invalid-error.ts b/app/packages/core-runtime/src/auth/legal-entity-context-invalid-error.ts index 623cfcb28..3baf160ff 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context-invalid-error.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context-invalid-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class LegalEntityContextInvalidError extends Schema.TaggedError()( 'LegalEntityContextInvalidError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context-missing-error.ts b/app/packages/core-runtime/src/auth/legal-entity-context-missing-error.ts index 58daaca98..a7a0c320d 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context-missing-error.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context-missing-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class LegalEntityContextMissingError extends Schema.TaggedError()( 'LegalEntityContextMissingError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context-unavailable-error.ts b/app/packages/core-runtime/src/auth/legal-entity-context-unavailable-error.ts index 765075868..0048196e4 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context-unavailable-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class LegalEntityContextUnavailableError extends Schema.TaggedError()( 'LegalEntityContextUnavailableError', - { reason: Schema.String } + { reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/legal-entity-context.ts b/app/packages/core-runtime/src/auth/legal-entity-context.ts index c9d625a0a..83e208bf7 100644 --- a/app/packages/core-runtime/src/auth/legal-entity-context.ts +++ b/app/packages/core-runtime/src/auth/legal-entity-context.ts @@ -16,8 +16,7 @@ export { LegalEntityContextInvalidError } from './legal-entity-context-invalid-e export { LegalEntityContextMissingError } from './legal-entity-context-missing-error.ts'; export { LegalEntityContextUnavailableError } from './legal-entity-context-unavailable-error.ts'; -const uuidPattern = - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; export type LegalEntityContextError = | LegalEntityContextMissingError @@ -48,10 +47,7 @@ const compareText = (left: string, right: string): number => { return 0; }; -const validRecord = ( - record: LegalEntityContextRecord, - tenantId: string -): boolean => +const validRecord = (record: LegalEntityContextRecord, tenantId: string): boolean => uuidPattern.test(record.legalEntityId) && uuidPattern.test(record.tenantId) && record.tenantId === tenantId && @@ -60,22 +56,16 @@ const validRecord = ( const validateRecords = ( records: readonly LegalEntityContextRecord[], - tenantId: string + tenantId: string, ): Effect.Effect< readonly LegalEntityContextRecord[], LegalEntityContextInvalidError | LegalEntityContextAmbiguousError > => { - if ( - !uuidPattern.test(tenantId) || - records.some((record) => !validRecord(record, tenantId)) - ) { + if (!uuidPattern.test(tenantId) || records.some((record) => !validRecord(record, tenantId))) { return Effect.fail(new LegalEntityContextInvalidError()); } - if ( - new Set(records.map((record) => record.legalEntityId)).size !== - records.length - ) { + if (new Set(records.map((record) => record.legalEntityId)).size !== records.length) { return Effect.fail(new LegalEntityContextAmbiguousError()); } @@ -84,106 +74,85 @@ const validateRecords = ( export const classifyActiveLegalEntities = ( records: readonly LegalEntityContextRecord[], - tenantId: string -): Effect.Effect< - readonly SafeLegalEntity[], - LegalEntityContextInvalidError | LegalEntityContextAmbiguousError -> => + tenantId: string, +): Effect.Effect => validateRecords(records, tenantId).pipe( Effect.map((validated) => validated - .flatMap(({ legalEntityId, legalName, status }) => - status === 'active' ? [{ legalEntityId, legalName }] : [] - ) + .flatMap(({ legalEntityId, legalName, status }) => (status === 'active' ? [{ legalEntityId, legalName }] : [])) .toSorted( (left, right) => - compareText(left.legalName, right.legalName) || - compareText(left.legalEntityId, right.legalEntityId) - ) - ) - ); - -export const classifySelectedLegalEntity = Effect.fn( - 'LegalEntityContext.classifySelectedLegalEntity' -)(function* classifySelectedLegalEntityEffect( - records: readonly LegalEntityContextRecord[], - tenantId: string, - legalEntityId: string -): Effect.fn.Return< - SafeLegalEntity, - Exclude -> { - const validated = yield* validateRecords(records, tenantId); - if (!uuidPattern.test(legalEntityId)) { - return yield* new LegalEntityContextInvalidError(); - } - - const matching = validated.filter( - (record) => record.legalEntityId === legalEntityId + compareText(left.legalName, right.legalName) || compareText(left.legalEntityId, right.legalEntityId), + ), + ), ); - if (matching.length === 0) { - return yield* new LegalEntityContextMissingError(); - } - if (matching.length !== 1) { - return yield* new LegalEntityContextAmbiguousError(); - } - const [selected] = matching; - if (selected === undefined) { - return yield* new LegalEntityContextMissingError(); - } - if (selected.status !== 'active') { - return yield* new LegalEntityContextInactiveError(); - } - - return { - legalEntityId: selected.legalEntityId, - legalName: selected.legalName, - }; -}); +export const classifySelectedLegalEntity = Effect.fn('LegalEntityContext.classifySelectedLegalEntity')( + function* classifySelectedLegalEntityEffect( + records: readonly LegalEntityContextRecord[], + tenantId: string, + legalEntityId: string, + ): Effect.fn.Return> { + const validated = yield* validateRecords(records, tenantId); + if (!uuidPattern.test(legalEntityId)) { + return yield* new LegalEntityContextInvalidError(); + } + + const matching = validated.filter((record) => record.legalEntityId === legalEntityId); + if (matching.length === 0) { + return yield* new LegalEntityContextMissingError(); + } + if (matching.length !== 1) { + return yield* new LegalEntityContextAmbiguousError(); + } + + const [selected] = matching; + if (selected === undefined) { + return yield* new LegalEntityContextMissingError(); + } + if (selected.status !== 'active') { + return yield* new LegalEntityContextInactiveError(); + } + + return { + legalEntityId: selected.legalEntityId, + legalName: selected.legalName, + }; + }, +); export interface LegalEntityContextService { readonly listActiveForTenant: ( - tenantId: string + tenantId: string, ) => Effect.Effect; readonly validateSelection: ( tenantId: string, - legalEntityId: string + legalEntityId: string, ) => Effect.Effect; } -export class LegalEntityContext extends Context.Service< - LegalEntityContext, - LegalEntityContextService ->()('@app/core-runtime/auth/legal-entity-context/LegalEntityContext') {} +export class LegalEntityContext extends Context.Service()( + '@app/core-runtime/auth/legal-entity-context/LegalEntityContext', +) {} type LegalEntityContextRecordLoadResult = Effect.Effect< readonly LegalEntityContextRecord[], LegalEntityContextUnavailableError >; -interface LegalEntityContextRecordReader< - Result extends LegalEntityContextRecordLoadResult, -> { +interface LegalEntityContextRecordReader { readonly load: (tenantId: string, legalEntityId?: string) => Result; } -const attachCause = ( - failure: Failure, - cause?: FailureCause -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); +const attachCause = (failure: Failure, cause?: FailureCause): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); -const unavailable = ( - cause?: FailureCause -): LegalEntityContextUnavailableError => +const unavailable = (cause?: FailureCause): LegalEntityContextUnavailableError => attachCause( new LegalEntityContextUnavailableError({ reason: 'Unable to resolve the legal-entity context', }), - cause + cause, ); const DATABASE_OPERATION_TIMEOUT = Duration.seconds(30); @@ -191,10 +160,7 @@ const DATABASE_OPERATION_TIMEOUT = Duration.seconds(30); const legalEntityContextRepositoryFromDatabase = (database: { readonly executor: Pick; }): LegalEntityContextRecordReader< - Effect.Effect< - readonly LegalEntityContextRecord[], - LegalEntityContextUnavailableError - > + Effect.Effect > => ({ load: (tenantId, legalEntityId) => database.executor @@ -208,57 +174,42 @@ const legalEntityContextRepositoryFromDatabase = (database: { .where( and( eq(legalEntities.tenantId, tenantId), - ...(legalEntityId === undefined - ? [] - : [eq(legalEntities.legalEntityId, legalEntityId)]) - ) + ...(legalEntityId === undefined ? [] : [eq(legalEntities.legalEntityId, legalEntityId)]), + ), ) .pipe( Effect.mapError(unavailable), Effect.timeoutOrElse({ duration: DATABASE_OPERATION_TIMEOUT, orElse: () => Effect.fail(unavailable()), - }) + }), ), }); -const legalEntityContextFromRepository = < - Result extends LegalEntityContextRecordLoadResult, ->( - repository: LegalEntityContextRecordReader +const legalEntityContextFromRepository = ( + repository: LegalEntityContextRecordReader, ): LegalEntityContextService => { const loadRecords = ( tenantId: string, - legalEntityId?: string - ): Effect.Effect< - readonly LegalEntityContextRecord[], - LegalEntityContextUnavailableError - > => repository.load(tenantId, legalEntityId); + legalEntityId?: string, + ): Effect.Effect => + repository.load(tenantId, legalEntityId); return { listActiveForTenant: (tenantId) => - loadRecords(tenantId).pipe( - Effect.flatMap((records) => - classifyActiveLegalEntities(records, tenantId) - ) - ), + loadRecords(tenantId).pipe(Effect.flatMap((records) => classifyActiveLegalEntities(records, tenantId))), validateSelection: (tenantId, legalEntityId) => loadRecords(tenantId, legalEntityId).pipe( - Effect.flatMap((records) => - classifySelectedLegalEntity(records, tenantId, legalEntityId) - ) + Effect.flatMap((records) => classifySelectedLegalEntity(records, tenantId, legalEntityId)), ), }; }; export const makeLegalEntityContext = (database: { readonly executor: Pick; -}): LegalEntityContextService => - legalEntityContextFromRepository( - legalEntityContextRepositoryFromDatabase(database) - ); +}): LegalEntityContextService => legalEntityContextFromRepository(legalEntityContextRepositoryFromDatabase(database)); export const LegalEntityContextLive = Layer.effect( LegalEntityContext, - CoreDatabase.pipe(Effect.map(makeLegalEntityContext)) + CoreDatabase.pipe(Effect.map(makeLegalEntityContext)), ); diff --git a/app/packages/core-runtime/src/auth/principal-administration-reads.ts b/app/packages/core-runtime/src/auth/principal-administration-reads.ts index 03232beb6..9b83830c0 100644 --- a/app/packages/core-runtime/src/auth/principal-administration-reads.ts +++ b/app/packages/core-runtime/src/auth/principal-administration-reads.ts @@ -13,10 +13,7 @@ const PrincipalIdSchema = uuid.pipe(Schema.brand('PrincipalId')); const BindingStatusSchema = Schema.Literals(['active', 'disabled', 'revoked']); const databaseReadTimeout = '30 seconds'; const paginationInput = { - limit: Schema.Finite.check( - Schema.isInt(), - Schema.isBetween({ maximum: 100, minimum: 1 }) - ), + limit: Schema.Finite.check(Schema.isInt(), Schema.isBetween({ maximum: 100, minimum: 1 })), offset: Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), }; const bindingMetadata = Schema.Struct({ @@ -47,10 +44,7 @@ const ManagedResult = Schema.Struct({ const SelfResultJson = Schema.toCodecJson(SelfResult); const ManagedResultJson = Schema.toCodecJson(ManagedResult); -const readUnavailable = ( - reason: string, - cause: unknown -): ReadHandlerUnavailable => { +const readUnavailable = (reason: string, cause: unknown): ReadHandlerUnavailable => { const error = new ReadHandlerUnavailable({ code: 'read_handler_unavailable', reason, @@ -63,23 +57,17 @@ interface IdentityReadServices { readonly listManaged: (input: { readonly limit: number; readonly offset: number; - }) => Effect.Effect< - Schema.Schema.Type, - ReadHandlerUnavailable - >; + }) => Effect.Effect, ReadHandlerUnavailable>; readonly listSelf: (input: { readonly limit: number; readonly offset: number; - }) => Effect.Effect< - Schema.Schema.Type, - ReadHandlerUnavailable - >; + }) => Effect.Effect, ReadHandlerUnavailable>; } const services = ( transaction: ScopedTransactionExecutor, tenantId: string, - principalId: string + principalId: string, ): IdentityReadServices => ({ listManaged: ({ limit, offset }) => transaction @@ -99,46 +87,34 @@ const services = ( and( eq(principalAuthBindings.tenantId, principals.tenantId), eq(principalAuthBindings.principalId, principals.principalId), - eq(principalAuthBindings.subjectType, 'api_key') - ) + eq(principalAuthBindings.subjectType, 'api_key'), + ), ) .where( - and( - eq(principals.tenantId, tenantId), - or(eq(principals.kind, 'service'), eq(principals.kind, 'integration')) - ) - ) - .orderBy( - asc(principals.displayName), - asc(principals.principalId), - asc(principalAuthBindings.createdAt) + and(eq(principals.tenantId, tenantId), or(eq(principals.kind, 'service'), eq(principals.kind, 'integration'))), ) + .orderBy(asc(principals.displayName), asc(principals.principalId), asc(principalAuthBindings.createdAt)) .limit(limit + 1) .offset(offset) .pipe( - Effect.mapError((cause) => - readUnavailable( - 'Managed identities are temporarily unavailable', - cause - ) - ), + Effect.mapError((cause) => readUnavailable('Managed identities are temporarily unavailable', cause)), Effect.timeoutOrElse({ duration: databaseReadTimeout, orElse: () => Effect.fail( readUnavailable( 'Managed identities are temporarily unavailable', - new Cause.TimeoutError('Database read timed out') - ) + new Cause.TimeoutError('Database read timed out'), + ), ), }), Effect.map((rows) => { const eligible = rows.filter( ( - row + row, ): row is typeof row & { readonly kind: 'integration' | 'service'; - } => row.kind === 'service' || row.kind === 'integration' + } => row.kind === 'service' || row.kind === 'integration', ); return { items: eligible.slice(0, limit).map((row) => ({ @@ -146,15 +122,11 @@ const services = ( bindingCreatedAt: row.bindingCreatedAt === null ? null - : DateTime.formatIso( - DateTime.fromDateUnsafe(row.bindingCreatedAt) - ), + : DateTime.formatIso(DateTime.fromDateUnsafe(row.bindingCreatedAt)), bindingRevokedAt: row.bindingRevokedAt === null ? null - : DateTime.formatIso( - DateTime.fromDateUnsafe(row.bindingRevokedAt) - ), + : DateTime.formatIso(DateTime.fromDateUnsafe(row.bindingRevokedAt)), kind: row.kind, })), nextOffset: rows.length > limit ? offset + limit : null, @@ -162,14 +134,9 @@ const services = ( }), Effect.flatMap((result) => Schema.decodeEffect(ManagedResultJson)(result).pipe( - Effect.mapError((cause) => - readUnavailable( - 'Managed identities are temporarily unavailable', - cause - ) - ) - ) - ) + Effect.mapError((cause) => readUnavailable('Managed identities are temporarily unavailable', cause)), + ), + ), ), listSelf: ({ limit, offset }) => transaction @@ -184,55 +151,37 @@ const services = ( and( eq(principalAuthBindings.tenantId, tenantId), eq(principalAuthBindings.principalId, principalId), - eq(principalAuthBindings.subjectType, 'api_key') - ) - ) - .orderBy( - asc(principalAuthBindings.createdAt), - asc(principalAuthBindings.principalAuthBindingId) + eq(principalAuthBindings.subjectType, 'api_key'), + ), ) + .orderBy(asc(principalAuthBindings.createdAt), asc(principalAuthBindings.principalAuthBindingId)) .limit(limit + 1) .offset(offset) .pipe( - Effect.mapError((cause) => - readUnavailable( - 'Identity bindings are temporarily unavailable', - cause - ) - ), + Effect.mapError((cause) => readUnavailable('Identity bindings are temporarily unavailable', cause)), Effect.timeoutOrElse({ duration: databaseReadTimeout, orElse: () => Effect.fail( readUnavailable( 'Identity bindings are temporarily unavailable', - new Cause.TimeoutError('Database read timed out') - ) + new Cause.TimeoutError('Database read timed out'), + ), ), }), Effect.map((rows) => ({ items: rows.slice(0, limit).map((row) => ({ ...row, - createdAt: DateTime.formatIso( - DateTime.fromDateUnsafe(row.createdAt) - ), - revokedAt: - row.revokedAt === null - ? null - : DateTime.formatIso(DateTime.fromDateUnsafe(row.revokedAt)), + createdAt: DateTime.formatIso(DateTime.fromDateUnsafe(row.createdAt)), + revokedAt: row.revokedAt === null ? null : DateTime.formatIso(DateTime.fromDateUnsafe(row.revokedAt)), })), nextOffset: rows.length > limit ? offset + limit : null, })), Effect.flatMap((result) => Schema.decodeEffect(SelfResultJson)(result).pipe( - Effect.mapError((cause) => - readUnavailable( - 'Identity bindings are temporarily unavailable', - cause - ) - ) - ) - ) + Effect.mapError((cause) => readUnavailable('Identity bindings are temporarily unavailable', cause)), + ), + ), ), }); @@ -274,11 +223,10 @@ export const selfApiKeyBindingsRead = defineRead< Effect.map((result) => ({ evidence: { resultCount: result.items.length }, result, - })) + })), ), - (transaction, scope) => - Effect.succeed(services(transaction, scope.tenantId, scope.principalId)), - () => ({ kind: 'tenant', permission: 'access' }) + (transaction, scope) => Effect.succeed(services(transaction, scope.tenantId, scope.principalId)), + () => ({ kind: 'tenant', permission: 'access' }), ); export const managedPrincipalsRead = defineRead< @@ -319,9 +267,8 @@ export const managedPrincipalsRead = defineRead< Effect.map((result) => ({ evidence: { resultCount: result.items.length }, result, - })) + })), ), - (transaction, scope) => - Effect.succeed(services(transaction, scope.tenantId, scope.principalId)), - () => ({ kind: 'tenant', permission: 'manage_identity' }) + (transaction, scope) => Effect.succeed(services(transaction, scope.tenantId, scope.principalId)), + () => ({ kind: 'tenant', permission: 'manage_identity' }), ); diff --git a/app/packages/core-runtime/src/auth/principal-binding-ambiguous-error.ts b/app/packages/core-runtime/src/auth/principal-binding-ambiguous-error.ts index b4c7e6cf0..7b9cbf699 100644 --- a/app/packages/core-runtime/src/auth/principal-binding-ambiguous-error.ts +++ b/app/packages/core-runtime/src/auth/principal-binding-ambiguous-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class PrincipalBindingAmbiguousError extends Schema.TaggedError()( 'PrincipalBindingAmbiguousError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/principal-binding-inactive-error.ts b/app/packages/core-runtime/src/auth/principal-binding-inactive-error.ts index 19b916368..0cd825568 100644 --- a/app/packages/core-runtime/src/auth/principal-binding-inactive-error.ts +++ b/app/packages/core-runtime/src/auth/principal-binding-inactive-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class PrincipalBindingInactiveError extends Schema.TaggedError()( 'PrincipalBindingInactiveError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/principal-binding-missing-error.ts b/app/packages/core-runtime/src/auth/principal-binding-missing-error.ts index e98fc1ae0..06cbcfe9f 100644 --- a/app/packages/core-runtime/src/auth/principal-binding-missing-error.ts +++ b/app/packages/core-runtime/src/auth/principal-binding-missing-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class PrincipalBindingMissingError extends Schema.TaggedError()( 'PrincipalBindingMissingError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/principal-inactive-error.ts b/app/packages/core-runtime/src/auth/principal-inactive-error.ts index 006e90ca6..435e43135 100644 --- a/app/packages/core-runtime/src/auth/principal-inactive-error.ts +++ b/app/packages/core-runtime/src/auth/principal-inactive-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class PrincipalInactiveError extends Schema.TaggedError()( 'PrincipalInactiveError', - {} + {}, ) {} diff --git a/app/packages/core-runtime/src/auth/principal-management-errors.ts b/app/packages/core-runtime/src/auth/principal-management-errors.ts index 5c65e405d..547def7c4 100644 --- a/app/packages/core-runtime/src/auth/principal-management-errors.ts +++ b/app/packages/core-runtime/src/auth/principal-management-errors.ts @@ -13,6 +13,4 @@ export const PrincipalManagementErrorSchema = Schema.Union([ IdentityTargetInvalidError, IdentityPersistenceUnavailableError, ]); -export type PrincipalManagementError = Schema.Schema.Type< - typeof PrincipalManagementErrorSchema ->; +export type PrincipalManagementError = Schema.Schema.Type; diff --git a/app/packages/core-runtime/src/auth/principal-management.ts b/app/packages/core-runtime/src/auth/principal-management.ts index 29d87132e..ba56da622 100644 --- a/app/packages/core-runtime/src/auth/principal-management.ts +++ b/app/packages/core-runtime/src/auth/principal-management.ts @@ -1,11 +1,7 @@ import { and, eq, isNull } from 'drizzle-orm'; import { Context, DateTime, Effect, Option } from 'effect'; -import type { - BindingStatus, - PrincipalKind, - PrincipalStatus, -} from '../db/schema.ts'; +import type { BindingStatus, PrincipalKind, PrincipalStatus } from '../db/schema.ts'; import { principalAuthBindings, principals } from '../db/schema.ts'; import type { ScopedTransactionExecutor } from '../db/scoped-transaction.ts'; import type { PrincipalManagementError } from './principal-management-errors.ts'; @@ -33,8 +29,7 @@ const conflict = (reason: string) => code: 'identity_lifecycle_conflict', reason, }); -const invalid = (reason: string) => - new IdentityTargetInvalidError({ code: 'identity_target_invalid', reason }); +const invalid = (reason: string) => new IdentityTargetInvalidError({ code: 'identity_target_invalid', reason }); type PrincipalRecord = Readonly<{ readonly kind: PrincipalKind; @@ -49,63 +44,37 @@ type SupportBindingRecord = Readonly<{ readonly authBindingId: string }>; export interface PrincipalManagementPersistence { readonly createPrincipal: ( - input: CreateNonHumanPrincipalInput - ) => Effect.Effect< - Option.Option<{ readonly principalId: string }>, - IdentityPersistenceUnavailableError - >; + input: CreateNonHumanPrincipalInput, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; readonly insertApiKeyBinding: ( - input: BindApiKeyInput - ) => Effect.Effect< - Option.Option<{ readonly authBindingId: string }>, - IdentityPersistenceUnavailableError - >; + input: BindApiKeyInput, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; readonly loadApiKeyBinding: ( - input: SetApiKeyBindingStatusInput - ) => Effect.Effect< - Option.Option, - IdentityPersistenceUnavailableError - >; + input: SetApiKeyBindingStatusInput, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; readonly loadPrincipal: ( tenantId: string, - principalId: string - ) => Effect.Effect< - Option.Option, - IdentityPersistenceUnavailableError - >; + principalId: string, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; readonly loadSupportBindings: (input: { readonly activeOnly: boolean; readonly authBindingId?: string; readonly principalId: string; readonly tenantId: string; - }) => Effect.Effect< - readonly SupportBindingRecord[], - IdentityPersistenceUnavailableError - >; + }) => Effect.Effect; readonly updateApiKeyBindingStatus: ( - input: SetApiKeyBindingStatusInput - ) => Effect.Effect< - Option.Option<{ readonly status: BindingStatus }>, - IdentityPersistenceUnavailableError - >; + input: SetApiKeyBindingStatusInput, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; readonly updatePrincipalStatus: ( - input: ChangePrincipalStatusInput - ) => Effect.Effect< - Option.Option<{ readonly status: PrincipalStatus }>, - IdentityPersistenceUnavailableError - >; + input: ChangePrincipalStatusInput, + ) => Effect.Effect, IdentityPersistenceUnavailableError>; } export interface PrincipalManagementRepositoryService { readonly bindApiKey: ( - input: BindApiKeyInput - ) => Effect.Effect< - { readonly authBindingId: string; readonly status: 'active' }, - PrincipalManagementError - >; - readonly changePrincipalStatus: ( - input: ChangePrincipalStatusInput - ) => Effect.Effect< + input: BindApiKeyInput, + ) => Effect.Effect<{ readonly authBindingId: string; readonly status: 'active' }, PrincipalManagementError>; + readonly changePrincipalStatus: (input: ChangePrincipalStatusInput) => Effect.Effect< { readonly previousStatus: PrincipalStatus; readonly status: PrincipalStatus; @@ -113,31 +82,26 @@ export interface PrincipalManagementRepositoryService { PrincipalManagementError >; readonly createNonHumanPrincipal: ( - input: CreateNonHumanPrincipalInput - ) => Effect.Effect< - { readonly principalId: string; readonly status: 'active' }, - PrincipalManagementError - >; + input: CreateNonHumanPrincipalInput, + ) => Effect.Effect<{ readonly principalId: string; readonly status: 'active' }, PrincipalManagementError>; readonly setApiKeyBindingStatus: ( - input: SetApiKeyBindingStatusInput + input: SetApiKeyBindingStatusInput, ) => Effect.Effect< { readonly previousStatus: BindingStatus; readonly status: BindingStatus }, PrincipalManagementError >; readonly validateSupportImpersonation: ( - input: ValidateSupportImpersonationInput + input: ValidateSupportImpersonationInput, ) => Effect.Effect; } export class PrincipalManagementRepository extends Context.Service< PrincipalManagementRepository, PrincipalManagementRepositoryService ->()( - '@app/core-runtime/auth/principal-management/PrincipalManagementRepository' -) {} +>()('@app/core-runtime/auth/principal-management/PrincipalManagementRepository') {} const principalManagementPersistenceFromTransaction = ( - transaction: Pick + transaction: Pick, ): PrincipalManagementPersistence => ({ createPrincipal: (input) => transaction @@ -151,7 +115,7 @@ const principalManagementPersistenceFromTransaction = ( .returning({ principalId: principals.principalId }) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([created]) => Option.fromNullishOr(created)) + Effect.map(([created]) => Option.fromNullishOr(created)), ), insertApiKeyBinding: (input) => transaction @@ -170,7 +134,7 @@ const principalManagementPersistenceFromTransaction = ( }) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([created]) => Option.fromNullishOr(created)) + Effect.map(([created]) => Option.fromNullishOr(created)), ), loadApiKeyBinding: (input) => transaction @@ -184,36 +148,31 @@ const principalManagementPersistenceFromTransaction = ( principals, and( eq(principals.tenantId, principalAuthBindings.tenantId), - eq(principals.principalId, principalAuthBindings.principalId) - ) + eq(principals.principalId, principalAuthBindings.principalId), + ), ) .where( and( eq(principalAuthBindings.tenantId, input.tenantId), eq(principalAuthBindings.principalAuthBindingId, input.authBindingId), eq(principalAuthBindings.principalId, input.principalId), - eq(principalAuthBindings.subjectType, 'api_key') - ) + eq(principalAuthBindings.subjectType, 'api_key'), + ), ) .limit(1) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([record]) => Option.fromNullishOr(record)) + Effect.map(([record]) => Option.fromNullishOr(record)), ), loadPrincipal: (tenantId, principalId) => transaction .select({ kind: principals.kind, status: principals.status }) .from(principals) - .where( - and( - eq(principals.tenantId, tenantId), - eq(principals.principalId, principalId) - ) - ) + .where(and(eq(principals.tenantId, tenantId), eq(principals.principalId, principalId))) .limit(1) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([record]) => Option.fromNullishOr(record)) + Effect.map(([record]) => Option.fromNullishOr(record)), ), loadSupportBindings: (input) => transaction @@ -223,8 +182,8 @@ const principalManagementPersistenceFromTransaction = ( principals, and( eq(principals.tenantId, principalAuthBindings.tenantId), - eq(principals.principalId, principalAuthBindings.principalId) - ) + eq(principals.principalId, principalAuthBindings.principalId), + ), ) .where( and( @@ -241,13 +200,8 @@ const principalManagementPersistenceFromTransaction = ( : []), ...(input.authBindingId === undefined ? [] - : [ - eq( - principalAuthBindings.principalAuthBindingId, - input.authBindingId - ), - ]) - ) + : [eq(principalAuthBindings.principalAuthBindingId, input.authBindingId)]), + ), ) .limit(2) .pipe(Effect.mapError(persistenceFailure)), @@ -263,36 +217,33 @@ const principalManagementPersistenceFromTransaction = ( .where( and( eq(principalAuthBindings.principalAuthBindingId, input.authBindingId), - eq(principalAuthBindings.status, input.expectedStatus) - ) + eq(principalAuthBindings.status, input.expectedStatus), + ), ) .returning({ status: principalAuthBindings.status }) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([updated]) => Option.fromNullishOr(updated)) + Effect.map(([updated]) => Option.fromNullishOr(updated)), ); }, updatePrincipalStatus: (input) => transaction .update(principals) .set({ - disabledAt: - input.newStatus === 'disabled' - ? DateTime.toDateUtc(DateTime.nowUnsafe()) - : null, + disabledAt: input.newStatus === 'disabled' ? DateTime.toDateUtc(DateTime.nowUnsafe()) : null, status: input.newStatus, }) .where( and( eq(principals.tenantId, input.tenantId), eq(principals.principalId, input.principalId), - eq(principals.status, input.expectedStatus) - ) + eq(principals.status, input.expectedStatus), + ), ) .returning({ status: principals.status }) .pipe( Effect.mapError(persistenceFailure), - Effect.map(([updated]) => Option.fromNullishOr(updated)) + Effect.map(([updated]) => Option.fromNullishOr(updated)), ), }); @@ -302,21 +253,19 @@ export interface CreateNonHumanPrincipalInput { readonly tenantId: string; } -const createNonHumanPrincipalFor = ( - persistence: PrincipalManagementPersistence -) => - Effect.fn('PrincipalManagement.createNonHumanPrincipal')( - function* createPrincipal(input: CreateNonHumanPrincipalInput) { - const created = yield* persistence.createPrincipal(input); - if (Option.isNone(created)) { - return yield* persistenceFailure(); - } - return { - principalId: created.value.principalId, - status: 'active' as const, - }; +const createNonHumanPrincipalFor = (persistence: PrincipalManagementPersistence) => + Effect.fn('PrincipalManagement.createNonHumanPrincipal')(function* createPrincipal( + input: CreateNonHumanPrincipalInput, + ) { + const created = yield* persistence.createPrincipal(input); + if (Option.isNone(created)) { + return yield* persistenceFailure(); } - ); + return { + principalId: created.value.principalId, + status: 'active' as const, + }; + }); export interface ChangePrincipalStatusInput { readonly expectedStatus: PrincipalStatus; @@ -326,37 +275,21 @@ export interface ChangePrincipalStatusInput { readonly tenantId: string; } -const hasStatusChangeReason = (reason: string | undefined): boolean => - reason !== undefined && reason.trim().length > 0; +const hasStatusChangeReason = (reason: string | undefined): boolean => reason !== undefined && reason.trim().length > 0; -const principalTransitionAllowed = ( - current: PrincipalStatus, - next: PrincipalStatus -): boolean => +const principalTransitionAllowed = (current: PrincipalStatus, next: PrincipalStatus): boolean => (current === 'active' && ['disabled', 'archived'].includes(next)) || (current === 'disabled' && ['active', 'archived'].includes(next)); -const bindingTransitionAllowed = ( - current: BindingStatus, - next: BindingStatus -): boolean => +const bindingTransitionAllowed = (current: BindingStatus, next: BindingStatus): boolean => (current === 'active' && ['disabled', 'revoked'].includes(next)) || (current === 'disabled' && ['active', 'revoked'].includes(next)); -const changePrincipalStatusFor = ( - persistence: PrincipalManagementPersistence -) => - Effect.fn('PrincipalManagement.changePrincipalStatus')(function* changeStatus( - input: ChangePrincipalStatusInput - ) { - const target = yield* persistence.loadPrincipal( - input.tenantId, - input.principalId - ); +const changePrincipalStatusFor = (persistence: PrincipalManagementPersistence) => + Effect.fn('PrincipalManagement.changePrincipalStatus')(function* changeStatus(input: ChangePrincipalStatusInput) { + const target = yield* persistence.loadPrincipal(input.tenantId, input.principalId); if (Option.isNone(target) || target.value.kind === 'human') { - return yield* invalid( - 'The target is not a tenant-local non-human principal' - ); + return yield* invalid('The target is not a tenant-local non-human principal'); } if (target.value.status !== input.expectedStatus) { return yield* conflict('The principal status changed concurrently'); @@ -385,21 +318,10 @@ export interface BindApiKeyInput { } const bindApiKeyFor = (persistence: PrincipalManagementPersistence) => - Effect.fn('PrincipalManagement.bindApiKey')(function* bindKey( - input: BindApiKeyInput - ) { - const target = yield* persistence.loadPrincipal( - input.tenantId, - input.principalId - ); - const allowedKinds: readonly PrincipalKind[] = input.managed - ? ['service', 'integration'] - : ['human']; - if ( - Option.isNone(target) || - target.value.status !== 'active' || - !allowedKinds.includes(target.value.kind) - ) { + Effect.fn('PrincipalManagement.bindApiKey')(function* bindKey(input: BindApiKeyInput) { + const target = yield* persistence.loadPrincipal(input.tenantId, input.principalId); + const allowedKinds: readonly PrincipalKind[] = input.managed ? ['service', 'integration'] : ['human']; + if (Option.isNone(target) || target.value.status !== 'active' || !allowedKinds.includes(target.value.kind)) { return yield* invalid('The API key target is not eligible'); } const created = yield* persistence.insertApiKeyBinding(input); @@ -430,102 +352,71 @@ export interface ValidateSupportImpersonationInput { readonly tenantId: string; } -const validateSupportImpersonationFor = ( - persistence: PrincipalManagementPersistence -) => - Effect.fn('PrincipalManagement.validateSupportImpersonation')( - function* validateSupportParticipants( - input: ValidateSupportImpersonationInput - ) { - const loadHumanBindings = ( - principalId: string, - authBindingId?: string - ) => { - const query = { - activeOnly: input.checkpoint !== 'stopped', - principalId, - tenantId: input.tenantId, - }; - return persistence.loadSupportBindings( - authBindingId === undefined ? query : { ...query, authBindingId } - ); +const validateSupportImpersonationFor = (persistence: PrincipalManagementPersistence) => + Effect.fn('PrincipalManagement.validateSupportImpersonation')(function* validateSupportParticipants( + input: ValidateSupportImpersonationInput, + ) { + const loadHumanBindings = (principalId: string, authBindingId?: string) => { + const query = { + activeOnly: input.checkpoint !== 'stopped', + principalId, + tenantId: input.tenantId, }; - const [original, target] = yield* Effect.all( - [ - loadHumanBindings( - input.originalPrincipalId, - input.originalAuthBindingId - ), - loadHumanBindings(input.targetPrincipalId), - ], - { concurrency: 1 } + return persistence.loadSupportBindings(authBindingId === undefined ? query : { ...query, authBindingId }); + }; + const [original, target] = yield* Effect.all( + [ + loadHumanBindings(input.originalPrincipalId, input.originalAuthBindingId), + loadHumanBindings(input.targetPrincipalId), + ], + { concurrency: 1 }, + ); + if (original.length !== 1 || target.length === 0) { + return yield* invalid( + input.checkpoint === 'stopped' + ? 'The impersonation participants are not tenant-local users' + : 'The impersonation participants are not active tenant-local users', ); - if (original.length !== 1 || target.length === 0) { - return yield* invalid( - input.checkpoint === 'stopped' - ? 'The impersonation participants are not tenant-local users' - : 'The impersonation participants are not active tenant-local users' - ); - } } - ); + }); -const isEligibleBindingTarget = ( - managed: boolean, - status: PrincipalStatus, - kind: PrincipalKind -): boolean => { - const allowedKinds: readonly PrincipalKind[] = managed - ? ['service', 'integration'] - : ['human']; +const isEligibleBindingTarget = (managed: boolean, status: PrincipalStatus, kind: PrincipalKind): boolean => { + const allowedKinds: readonly PrincipalKind[] = managed ? ['service', 'integration'] : ['human']; return status === 'active' && allowedKinds.includes(kind); }; -const setApiKeyBindingStatusFor = ( - persistence: PrincipalManagementPersistence -) => - Effect.fn('PrincipalManagement.setApiKeyBindingStatus')( - function* setBindingStatus(input: SetApiKeyBindingStatusInput) { - const binding = yield* persistence.loadApiKeyBinding(input); - if (Option.isNone(binding)) { - return yield* invalid('The API key binding is unavailable'); - } - if ( - !isEligibleBindingTarget( - input.managed, - binding.value.principalStatus, - binding.value.principalKind - ) - ) { - return yield* invalid('The API key binding target is not eligible'); - } - if (binding.value.bindingStatus !== input.expectedStatus) { - return yield* conflict('The binding status changed concurrently'); - } - if ( - !bindingTransitionAllowed(binding.value.bindingStatus, input.newStatus) - ) { - return yield* conflict('The binding transition is not allowed'); - } - if ( - input.newStatus === 'revoked' && - !hasStatusChangeReason(input.reason) - ) { - return yield* invalid('A reason is required for revocation'); - } - const updated = yield* persistence.updateApiKeyBindingStatus(input); - if (Option.isNone(updated)) { - return yield* conflict('The binding status changed concurrently'); - } - return { - previousStatus: input.expectedStatus, - status: updated.value.status, - }; +const setApiKeyBindingStatusFor = (persistence: PrincipalManagementPersistence) => + Effect.fn('PrincipalManagement.setApiKeyBindingStatus')(function* setBindingStatus( + input: SetApiKeyBindingStatusInput, + ) { + const binding = yield* persistence.loadApiKeyBinding(input); + if (Option.isNone(binding)) { + return yield* invalid('The API key binding is unavailable'); + } + if (!isEligibleBindingTarget(input.managed, binding.value.principalStatus, binding.value.principalKind)) { + return yield* invalid('The API key binding target is not eligible'); + } + if (binding.value.bindingStatus !== input.expectedStatus) { + return yield* conflict('The binding status changed concurrently'); } - ); + if (!bindingTransitionAllowed(binding.value.bindingStatus, input.newStatus)) { + return yield* conflict('The binding transition is not allowed'); + } + if (input.newStatus === 'revoked' && !hasStatusChangeReason(input.reason)) { + return yield* invalid('A reason is required for revocation'); + } + const updated = yield* persistence.updateApiKeyBindingStatus(input); + if (Option.isNone(updated)) { + return yield* conflict('The binding status changed concurrently'); + } + return { + previousStatus: input.expectedStatus, + status: updated.value.status, + }; + }); export const principalManagementRepositoryFromPersistence = ( - persistence: PrincipalManagementPersistence + persistence: PrincipalManagementPersistence, ): PrincipalManagementRepositoryService => Object.freeze({ bindApiKey: bindApiKeyFor(persistence), @@ -536,37 +427,21 @@ export const principalManagementRepositoryFromPersistence = ( }); export const principalManagementRepositoryFromTransaction = ( - transaction: Pick + transaction: Pick, ): PrincipalManagementRepositoryService => - principalManagementRepositoryFromPersistence( - principalManagementPersistenceFromTransaction(transaction) - ); + principalManagementRepositoryFromPersistence(principalManagementPersistenceFromTransaction(transaction)); export const createNonHumanPrincipal = (input: CreateNonHumanPrincipalInput) => - PrincipalManagementRepository.pipe( - Effect.flatMap((repository) => repository.createNonHumanPrincipal(input)) - ); + PrincipalManagementRepository.pipe(Effect.flatMap((repository) => repository.createNonHumanPrincipal(input))); export const changePrincipalStatus = (input: ChangePrincipalStatusInput) => - PrincipalManagementRepository.pipe( - Effect.flatMap((repository) => repository.changePrincipalStatus(input)) - ); + PrincipalManagementRepository.pipe(Effect.flatMap((repository) => repository.changePrincipalStatus(input))); export const bindApiKey = (input: BindApiKeyInput) => - PrincipalManagementRepository.pipe( - Effect.flatMap((repository) => repository.bindApiKey(input)) - ); + PrincipalManagementRepository.pipe(Effect.flatMap((repository) => repository.bindApiKey(input))); -export const validateSupportImpersonation = ( - input: ValidateSupportImpersonationInput -) => - PrincipalManagementRepository.pipe( - Effect.flatMap((repository) => - repository.validateSupportImpersonation(input) - ) - ); +export const validateSupportImpersonation = (input: ValidateSupportImpersonationInput) => + PrincipalManagementRepository.pipe(Effect.flatMap((repository) => repository.validateSupportImpersonation(input))); export const setApiKeyBindingStatus = (input: SetApiKeyBindingStatusInput) => - PrincipalManagementRepository.pipe( - Effect.flatMap((repository) => repository.setApiKeyBindingStatus(input)) - ); + PrincipalManagementRepository.pipe(Effect.flatMap((repository) => repository.setApiKeyBindingStatus(input))); diff --git a/app/packages/core-runtime/src/auth/principal-resolver-unavailable-error.ts b/app/packages/core-runtime/src/auth/principal-resolver-unavailable-error.ts index 1b3f734ee..3eab3e180 100644 --- a/app/packages/core-runtime/src/auth/principal-resolver-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/principal-resolver-unavailable-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class PrincipalResolverUnavailableError extends Schema.TaggedError()( 'PrincipalResolverUnavailableError', - { reason: Schema.String } + { reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/auth/principal-resolver.ts b/app/packages/core-runtime/src/auth/principal-resolver.ts index 80b60c965..b081a21b1 100644 --- a/app/packages/core-runtime/src/auth/principal-resolver.ts +++ b/app/packages/core-runtime/src/auth/principal-resolver.ts @@ -4,13 +4,7 @@ import { Context, Effect, Layer, Schema } from 'effect'; import { CoreDatabase } from '../db/client.ts'; import type { PrincipalKind } from '../db/schema.ts'; -import { - actionInvocations, - auditEvents, - principalAuthBindings, - principals, - tenants, -} from '../db/schema.ts'; +import { actionInvocations, auditEvents, principalAuthBindings, principals, tenants } from '../db/schema.ts'; import type { CoreDatabaseExecutor } from '../db/types.ts'; import type { PrincipalResolutionError } from './principal-resolver-errors.ts'; import { @@ -40,10 +34,9 @@ export interface ApiKeyBindingAdministration { readonly status: 'active' | 'disabled' | 'revoked'; } -const ProviderSubjectIdSchema = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -).pipe(Schema.brand('ProviderSubjectId')); +const ProviderSubjectIdSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)).pipe( + Schema.brand('ProviderSubjectId'), +); export const ProviderSubjectSchema = Schema.Struct({ provider: Schema.Literal('better_auth'), @@ -57,9 +50,7 @@ export interface ProviderSubject { readonly subjectType: 'api_key' | 'user'; } -const EvidencePrincipalIdSchema = Schema.String.pipe( - Schema.brand('PrincipalId') -); +const EvidencePrincipalIdSchema = Schema.String.pipe(Schema.brand('PrincipalId')); const SupportImpersonationStartedEvidenceSchema = Schema.Struct({ checkpoint: Schema.Literal('started'), originalPrincipalId: EvidencePrincipalIdSchema, @@ -82,51 +73,28 @@ export interface PrincipalResolutionRecord { readonly tenantStatus: string; } -type PrincipalResolutionRecordLoadResult = Effect.Effect< - readonly PrincipalResolutionRecord[], - EffectDrizzleQueryError ->; +type PrincipalResolutionRecordLoadResult = Effect.Effect; -interface PrincipalResolutionRecordReader< - Result extends PrincipalResolutionRecordLoadResult, -> { +interface PrincipalResolutionRecordReader { readonly load: (subject: ProviderSubject, tenantId?: string) => Result; } -type PrincipalResolutionRecordRepository = - PrincipalResolutionRecordReader; - -const attachCause = ( - failure: Failure, - cause: unknown -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); - -const unavailable = ( - reason: string, - cause?: unknown -): PrincipalResolverUnavailableError => +type PrincipalResolutionRecordRepository = PrincipalResolutionRecordReader; + +const attachCause = (failure: Failure, cause: unknown): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); + +const unavailable = (reason: string, cause?: unknown): PrincipalResolverUnavailableError => attachCause(new PrincipalResolverUnavailableError({ reason }), cause); -const loadPrincipalResolutionRecords = < - Result extends PrincipalResolutionRecordLoadResult, ->( +const loadPrincipalResolutionRecords = ( repository: PrincipalResolutionRecordReader, subject: ProviderSubject, - tenantId?: string -): Effect.Effect< - readonly PrincipalResolutionRecord[], - PrincipalResolverUnavailableError -> => + tenantId?: string, +): Effect.Effect => repository .load(subject, tenantId) - .pipe( - Effect.mapError((cause) => - unavailable('Unable to resolve the authenticated principal', cause) - ) - ); + .pipe(Effect.mapError((cause) => unavailable('Unable to resolve the authenticated principal', cause))); const compareText = (left: string, right: string): number => { if (left < right) { @@ -139,33 +107,25 @@ const compareText = (left: string, right: string): number => { }; const eligibleRecords = ( - records: readonly PrincipalResolutionRecord[] -): Effect.Effect< - readonly PrincipalResolutionRecord[], - PrincipalResolutionError -> => { + records: readonly PrincipalResolutionRecord[], +): Effect.Effect => { if (records.length === 0) { return Effect.fail(new PrincipalBindingMissingError()); } const activeBindings = records.filter( - (record) => - record.bindingStatus === 'active' && record.bindingRevokedAt === null + (record) => record.bindingStatus === 'active' && record.bindingRevokedAt === null, ); if (activeBindings.length === 0) { return Effect.fail(new PrincipalBindingInactiveError()); } - const activePrincipals = activeBindings.filter( - (record) => record.principalStatus === 'active' - ); + const activePrincipals = activeBindings.filter((record) => record.principalStatus === 'active'); if (activePrincipals.length === 0) { return Effect.fail(new PrincipalInactiveError()); } - const activeTenants = activePrincipals.filter( - (record) => record.tenantStatus === 'active' - ); + const activeTenants = activePrincipals.filter((record) => record.tenantStatus === 'active'); if (activeTenants.length === 0) { return Effect.fail(new TenantInactiveError()); } @@ -178,9 +138,7 @@ const eligibleRecords = ( return Effect.succeed(activeTenants); }; -const toResolvedIdentity = ( - record: PrincipalResolutionRecord -): ResolvedPrincipalIdentity => ({ +const toResolvedIdentity = (record: PrincipalResolutionRecord): ResolvedPrincipalIdentity => ({ authBindingId: record.authBindingId, displayName: record.displayName, principalId: record.principalId, @@ -189,24 +147,17 @@ const toResolvedIdentity = ( }); const eligibleHumanRecords = ( - records: readonly PrincipalResolutionRecord[] -): Effect.Effect< - readonly PrincipalResolutionRecord[], - PrincipalResolutionError -> => + records: readonly PrincipalResolutionRecord[], +): Effect.Effect => eligibleRecords(records).pipe( Effect.flatMap((eligible) => { - const humans = eligible.filter( - (record) => record.principalKind === 'human' - ); - return humans.length === 0 - ? Effect.fail(new PrincipalInactiveError()) - : Effect.succeed(humans); - }) + const humans = eligible.filter((record) => record.principalKind === 'human'); + return humans.length === 0 ? Effect.fail(new PrincipalInactiveError()) : Effect.succeed(humans); + }), ); export const classifyAvailableTenants = ( - records: readonly PrincipalResolutionRecord[] + records: readonly PrincipalResolutionRecord[], ): Effect.Effect => eligibleHumanRecords(records).pipe( Effect.map((eligible) => @@ -215,19 +166,13 @@ export const classifyAvailableTenants = ( name: record.tenantName, tenantId: record.tenantId, })) - .toSorted( - (left, right) => - compareText(left.name, right.name) || - compareText(left.tenantId, right.tenantId) - ) - ) + .toSorted((left, right) => compareText(left.name, right.name) || compareText(left.tenantId, right.tenantId)), + ), ); -const listAvailableTenantsFromRepository = < - Result extends PrincipalResolutionRecordLoadResult, ->( +const listAvailableTenantsFromRepository = ( repository: PrincipalResolutionRecordReader, - betterAuthUserId: string + betterAuthUserId: string, ): Effect.Effect => loadPrincipalResolutionRecords(repository, { provider: 'better_auth', @@ -236,57 +181,51 @@ const listAvailableTenantsFromRepository = < }).pipe(Effect.flatMap(classifyAvailableTenants)); export const classifyDefaultPrincipal = ( - records: readonly PrincipalResolutionRecord[] + records: readonly PrincipalResolutionRecord[], ): Effect.Effect => eligibleHumanRecords(records).pipe( Effect.map((eligible) => eligible.toSorted( (left, right) => left.bindingCreatedAt.getTime() - right.bindingCreatedAt.getTime() || - compareText(left.tenantId, right.tenantId) - ) + compareText(left.tenantId, right.tenantId), + ), ), Effect.flatMap(([first]) => - first === undefined - ? Effect.fail(new PrincipalBindingMissingError()) - : Effect.succeed(toResolvedIdentity(first)) - ) + first === undefined ? Effect.fail(new PrincipalBindingMissingError()) : Effect.succeed(toResolvedIdentity(first)), + ), ); export const classifySelectedPrincipal = ( records: readonly PrincipalResolutionRecord[], - selectedTenantId: string + selectedTenantId: string, ): Effect.Effect => eligibleHumanRecords(records).pipe( Effect.flatMap((eligible) => { - const selected = eligible.find( - (record) => record.tenantId === selectedTenantId - ); + const selected = eligible.find((record) => record.tenantId === selectedTenantId); return selected === undefined ? Effect.fail(new PrincipalBindingMissingError()) : Effect.succeed(toResolvedIdentity(selected)); - }) + }), ); -export const classifyApiKeyPrincipal = Effect.fn( - 'PrincipalResolver.classifyApiKeyPrincipal' -)(function* classifyApiKeyPrincipalEffect( - records: readonly PrincipalResolutionRecord[] -) { - const eligible = yield* eligibleRecords(records); - const [only] = eligible; - if (eligible.length !== 1 || only === undefined) { - return yield* new PrincipalBindingAmbiguousError(); - } - if (!['human', 'service', 'integration'].includes(only.principalKind)) { - return yield* new PrincipalInactiveError(); - } - return toResolvedIdentity(only); -}); +export const classifyApiKeyPrincipal = Effect.fn('PrincipalResolver.classifyApiKeyPrincipal')( + function* classifyApiKeyPrincipalEffect(records: readonly PrincipalResolutionRecord[]) { + const eligible = yield* eligibleRecords(records); + const [only] = eligible; + if (eligible.length !== 1 || only === undefined) { + return yield* new PrincipalBindingAmbiguousError(); + } + if (!['human', 'service', 'integration'].includes(only.principalKind)) { + return yield* new PrincipalInactiveError(); + } + return toResolvedIdentity(only); + }, +); export interface PrincipalResolverService { readonly listAvailableTenants: ( - betterAuthUserId: string + betterAuthUserId: string, ) => Effect.Effect; readonly loadApiKeyBindingForAdministration: (input: { readonly authBindingId: string; @@ -299,7 +238,7 @@ export interface PrincipalResolverService { readonly tenantId: string; }) => Effect.Effect; readonly resolveBetterAuthApiKey: ( - betterAuthApiKeyId: string + betterAuthApiKeyId: string, ) => Effect.Effect; readonly resolveBetterAuthUserForPrincipal: (input: { readonly principalId: string; @@ -307,14 +246,14 @@ export interface PrincipalResolverService { }) => Effect.Effect; readonly resolveBetterAuthUserForTenant: ( betterAuthUserId: string, - tenantId: string + tenantId: string, ) => Effect.Effect; readonly resolveDefaultBetterAuthUser: ( - betterAuthUserId: string + betterAuthUserId: string, ) => Effect.Effect; readonly resolveProviderSubject: ( subject: ProviderSubject, - tenantId?: string + tenantId?: string, ) => Effect.Effect; readonly verifySupportImpersonationStarted: (input: { readonly actionId: string; @@ -326,10 +265,9 @@ export interface PrincipalResolverService { }) => Effect.Effect; } -export class PrincipalResolver extends Context.Service< - PrincipalResolver, - PrincipalResolverService ->()('@app/core-runtime/auth/principal-resolver/PrincipalResolver') {} +export class PrincipalResolver extends Context.Service()( + '@app/core-runtime/auth/principal-resolver/PrincipalResolver', +) {} export const makePrincipalResolver = (database: { readonly executor: CoreDatabaseExecutor; @@ -355,25 +293,17 @@ export const makePrincipalResolver = (database: { principals, and( eq(principals.principalId, principalAuthBindings.principalId), - eq(principals.tenantId, principalAuthBindings.tenantId) - ) - ) - .innerJoin( - tenants, - eq(tenants.tenantId, principalAuthBindings.tenantId) + eq(principals.tenantId, principalAuthBindings.tenantId), + ), ) + .innerJoin(tenants, eq(tenants.tenantId, principalAuthBindings.tenantId)) .where( and( eq(principalAuthBindings.provider, subject.provider), eq(principalAuthBindings.subjectType, subject.subjectType), - eq( - principalAuthBindings.providerSubjectId, - subject.providerSubjectId - ), - ...(tenantId === undefined - ? [] - : [eq(principalAuthBindings.tenantId, tenantId)]) - ) + eq(principalAuthBindings.providerSubjectId, subject.providerSubjectId), + ...(tenantId === undefined ? [] : [eq(principalAuthBindings.tenantId, tenantId)]), + ), ), }; const loadRecords = (subject: ProviderSubject, tenantId?: string) => @@ -395,46 +325,32 @@ export const makePrincipalResolver = (database: { eq(principalAuthBindings.principalAuthBindingId, input.authBindingId), eq(principalAuthBindings.tenantId, input.tenantId), eq(principalAuthBindings.principalId, input.principalId), - eq(principalAuthBindings.subjectType, 'api_key') - ) + eq(principalAuthBindings.subjectType, 'api_key'), + ), ) .limit(1) .pipe( - Effect.mapError((cause) => - unavailable('Unable to resolve the API key binding', cause) - ), - Effect.map(([record]) => record) + Effect.mapError((cause) => unavailable('Unable to resolve the API key binding', cause)), + Effect.map(([record]) => record), ); return { - listAvailableTenants: (betterAuthUserId) => - listAvailableTenantsFromRepository(recordRepository, betterAuthUserId), + listAvailableTenants: (betterAuthUserId) => listAvailableTenantsFromRepository(recordRepository, betterAuthUserId), loadApiKeyBindingForAdministration: (input) => loadApiKeyBindingSubject(input).pipe( - Effect.flatMap( - ( - record - ): Effect.Effect< - ApiKeyBindingAdministration, - PrincipalBindingMissingError - > => - record === undefined - ? Effect.fail(new PrincipalBindingMissingError()) - : Effect.succeed({ - providerSubjectId: record.providerSubjectId, - status: record.status, - }) - ) + Effect.flatMap((record): Effect.Effect => + record === undefined + ? Effect.fail(new PrincipalBindingMissingError()) + : Effect.succeed({ + providerSubjectId: record.providerSubjectId, + status: record.status, + }), + ), ), resolveApiKeyBindingSubject: (input) => loadApiKeyBindingSubject(input).pipe( Effect.flatMap( - ( - record - ): Effect.Effect< - string, - PrincipalBindingInactiveError | PrincipalBindingMissingError - > => { + (record): Effect.Effect => { if (record === undefined) { return Effect.fail(new PrincipalBindingMissingError()); } @@ -442,8 +358,8 @@ export const makePrincipalResolver = (database: { return Effect.fail(new PrincipalBindingInactiveError()); } return Effect.succeed(record.providerSubjectId); - } - ) + }, + ), ), resolveBetterAuthApiKey: (betterAuthApiKeyId) => loadRecords({ @@ -463,8 +379,8 @@ export const makePrincipalResolver = (database: { principals, and( eq(principals.tenantId, principalAuthBindings.tenantId), - eq(principals.principalId, principalAuthBindings.principalId) - ) + eq(principals.principalId, principalAuthBindings.principalId), + ), ) .where( and( @@ -473,27 +389,14 @@ export const makePrincipalResolver = (database: { eq(principalAuthBindings.provider, 'better_auth'), eq(principalAuthBindings.subjectType, 'user'), eq(principals.kind, 'human'), - eq(principals.status, 'active') - ) + eq(principals.status, 'active'), + ), ) .pipe( - Effect.mapError((cause) => - unavailable( - 'Unable to resolve the principal provider subject', - cause - ) - ), + Effect.mapError((cause) => unavailable('Unable to resolve the principal provider subject', cause)), Effect.flatMap( - ( - records - ): Effect.Effect< - string, - PrincipalBindingAmbiguousError | PrincipalBindingMissingError - > => { - const active = records.filter( - (record) => - record.status === 'active' && record.revokedAt === null - ); + (records): Effect.Effect => { + const active = records.filter((record) => record.status === 'active' && record.revokedAt === null); if (active.length === 0) { return Effect.fail(new PrincipalBindingMissingError()); } @@ -502,8 +405,8 @@ export const makePrincipalResolver = (database: { return Effect.fail(new PrincipalBindingAmbiguousError()); } return Effect.succeed(only.providerSubjectId); - } - ) + }, + ), ), resolveBetterAuthUserForTenant: (betterAuthUserId, tenantId) => loadRecords( @@ -512,12 +415,8 @@ export const makePrincipalResolver = (database: { providerSubjectId: betterAuthUserId, subjectType: 'user', }, - tenantId - ).pipe( - Effect.flatMap((records) => - classifySelectedPrincipal(records, tenantId) - ) - ), + tenantId, + ).pipe(Effect.flatMap((records) => classifySelectedPrincipal(records, tenantId))), resolveDefaultBetterAuthUser: (betterAuthUserId) => loadRecords({ provider: 'better_auth', @@ -534,7 +433,7 @@ export const makePrincipalResolver = (database: { return classifyDefaultPrincipal(records); } return classifySelectedPrincipal(records, tenantId); - }) + }), ), verifySupportImpersonationStarted: (input) => database.executor @@ -544,38 +443,25 @@ export const makePrincipalResolver = (database: { auditEvents, and( eq(auditEvents.tenantId, actionInvocations.tenantId), - eq( - auditEvents.actionInvocationId, - actionInvocations.actionInvocationId - ) - ) + eq(auditEvents.actionInvocationId, actionInvocations.actionInvocationId), + ), ) .where( and( eq(actionInvocations.tenantId, input.tenantId), eq(actionInvocations.principalId, input.originalPrincipalId), - eq( - actionInvocations.actionKey, - 'core.identity.record-support-impersonation' - ), + eq(actionInvocations.actionKey, 'core.identity.record-support-impersonation'), eq(actionInvocations.idempotencyKey, `${input.actionId}:started`), eq(actionInvocations.status, 'succeeded'), eq(auditEvents.eventType, 'action.executed'), - eq(auditEvents.outcome, 'succeeded') - ) + eq(auditEvents.outcome, 'succeeded'), + ), ) .pipe( - Effect.mapError((cause) => - unavailable( - 'Unable to verify the support impersonation lifecycle', - cause - ) - ), + Effect.mapError((cause) => unavailable('Unable to verify the support impersonation lifecycle', cause)), Effect.map((records) => records.some(({ evidence }) => { - if ( - !Schema.is(SupportImpersonationStartedEvidenceSchema)(evidence) - ) { + if (!Schema.is(SupportImpersonationStartedEvidenceSchema)(evidence)) { return false; } return ( @@ -585,13 +471,13 @@ export const makePrincipalResolver = (database: { evidence.targetPrincipalId === input.targetPrincipalId && evidence.sessionRef === `better-auth-session:${input.sessionId}` ); - }) - ) + }), + ), ), }; }; export const PrincipalResolverLive = Layer.effect( PrincipalResolver, - CoreDatabase.pipe(Effect.map(makePrincipalResolver)) + CoreDatabase.pipe(Effect.map(makePrincipalResolver)), ); diff --git a/app/packages/core-runtime/src/auth/support-recovery-principal-context-denied-error.ts b/app/packages/core-runtime/src/auth/support-recovery-principal-context-denied-error.ts index d279b7c6f..1dcda8b11 100644 --- a/app/packages/core-runtime/src/auth/support-recovery-principal-context-denied-error.ts +++ b/app/packages/core-runtime/src/auth/support-recovery-principal-context-denied-error.ts @@ -5,5 +5,5 @@ export class SupportRecoveryPrincipalContextDeniedError extends Schema.TaggedErr { code: Schema.Literal('support_recovery_context_denied'), reason: Schema.String, - } + }, ) {} diff --git a/app/packages/core-runtime/src/auth/support-recovery-principal-context-unavailable-error.ts b/app/packages/core-runtime/src/auth/support-recovery-principal-context-unavailable-error.ts index 43d92a528..060095660 100644 --- a/app/packages/core-runtime/src/auth/support-recovery-principal-context-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/support-recovery-principal-context-unavailable-error.ts @@ -5,5 +5,5 @@ export class SupportRecoveryPrincipalContextUnavailableError extends Schema.Tagg { code: Schema.Literal('support_recovery_context_unavailable'), reason: Schema.String, - } + }, ) {} diff --git a/app/packages/core-runtime/src/auth/support-recovery-principal-context.ts b/app/packages/core-runtime/src/auth/support-recovery-principal-context.ts index d35c665c0..3df34cc1e 100644 --- a/app/packages/core-runtime/src/auth/support-recovery-principal-context.ts +++ b/app/packages/core-runtime/src/auth/support-recovery-principal-context.ts @@ -25,10 +25,7 @@ export interface SupportRecoveryPrincipalContextResolverService { readonly originalPrincipalId: string; readonly originalSessionId: string; readonly tenantId: string; - }) => Effect.Effect< - TrustedPrincipalContext, - SupportRecoveryPrincipalContextError - >; + }) => Effect.Effect; } interface SupportRecoveryPrincipalContextRecord { @@ -53,37 +50,28 @@ type SupportRecoveryPrincipalContextRepositoryLoadResult = Effect.Effect< interface SupportRecoveryPrincipalContextRecordReader< Result extends SupportRecoveryPrincipalContextRepositoryLoadResult, > { - readonly load: ( - input: SupportRecoveryPrincipalContextRepositoryInput - ) => Result; + readonly load: (input: SupportRecoveryPrincipalContextRepositoryInput) => Result; } interface SupportRecoveryPrincipalContextEffectRecordReader { readonly load: ( - input: SupportRecoveryPrincipalContextRepositoryInput + input: SupportRecoveryPrincipalContextRepositoryInput, ) => Effect.Effect< Option.Option, SupportRecoveryPrincipalContextUnavailableError >; } -const attachCause = ( - failure: Failure, - cause: unknown -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); - -const unavailable = ( - cause?: unknown -): SupportRecoveryPrincipalContextUnavailableError => +const attachCause = (failure: Failure, cause: unknown): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); + +const unavailable = (cause?: unknown): SupportRecoveryPrincipalContextUnavailableError => attachCause( new SupportRecoveryPrincipalContextUnavailableError({ code: 'support_recovery_context_unavailable', reason: 'The support recovery identity could not be revalidated', }), - cause + cause, ); const DATABASE_OPERATION_TIMEOUT = Duration.seconds(30); @@ -105,21 +93,18 @@ const supportRecoveryPrincipalContextRepositoryFromDatabase = (database: { principals, and( eq(principals.tenantId, principalAuthBindings.tenantId), - eq(principals.principalId, principalAuthBindings.principalId) - ) + eq(principals.principalId, principalAuthBindings.principalId), + ), ) .innerJoin(tenants, eq(tenants.tenantId, principalAuthBindings.tenantId)) .where( and( - eq( - principalAuthBindings.principalAuthBindingId, - input.originalAuthBindingId - ), + eq(principalAuthBindings.principalAuthBindingId, input.originalAuthBindingId), eq(principalAuthBindings.tenantId, input.tenantId), eq(principalAuthBindings.principalId, input.originalPrincipalId), eq(principalAuthBindings.provider, 'better_auth'), - eq(principalAuthBindings.subjectType, 'user') - ) + eq(principalAuthBindings.subjectType, 'user'), + ), ) .limit(1) .pipe( @@ -128,14 +113,12 @@ const supportRecoveryPrincipalContextRepositoryFromDatabase = (database: { Effect.timeoutOrElse({ duration: DATABASE_OPERATION_TIMEOUT, orElse: () => Effect.fail(unavailable()), - }) + }), ), }); const isInvalidRecoveryInput = ( - input: Parameters< - SupportRecoveryPrincipalContextResolverService['resolveStoppedImpersonation'] - >[0] + input: Parameters[0], ): boolean => !Schema.is(uuid)(input.originalAuthBindingId) || !Schema.is(uuid)(input.originalPrincipalId) || @@ -145,61 +128,58 @@ const isInvalidRecoveryInput = ( /\s/u.test(input.originalSessionId); const supportRecoveryPrincipalContextResolverFromEffectRecordReader = ( - repository: SupportRecoveryPrincipalContextEffectRecordReader + repository: SupportRecoveryPrincipalContextEffectRecordReader, ): SupportRecoveryPrincipalContextResolverService => ({ - resolveStoppedImpersonation: Effect.fn( - 'SupportRecoveryPrincipalContext.resolveStoppedImpersonation' - )(function* resolveStoppedImpersonation(input): Effect.fn.Return< - TrustedPrincipalContext, - SupportRecoveryPrincipalContextError - > { - if (isInvalidRecoveryInput(input)) { - return yield* new SupportRecoveryPrincipalContextDeniedError({ - code: 'support_recovery_context_denied', - reason: 'The support recovery identity is invalid', - }); - } - const maybeRecord = yield* repository.load({ - originalAuthBindingId: input.originalAuthBindingId, - originalPrincipalId: input.originalPrincipalId, - tenantId: input.tenantId, - }); - if (Option.isNone(maybeRecord)) { - return yield* new SupportRecoveryPrincipalContextDeniedError({ - code: 'support_recovery_context_denied', - reason: - 'The support recovery identity is not a historical tenant-local user binding', - }); - } - const record = maybeRecord.value; - if ( - record.bindingPrincipalId !== input.originalPrincipalId || - record.bindingTenantId !== input.tenantId || - record.principalKind !== 'human' || - record.principalTenantId !== input.tenantId || - record.tenantId !== input.tenantId - ) { - return yield* new SupportRecoveryPrincipalContextDeniedError({ - code: 'support_recovery_context_denied', - reason: - 'The support recovery identity is not a historical tenant-local user binding', - }); - } - return trustSupportRecoveryPrincipalContext( - Object.freeze({ - authBindingId: input.originalAuthBindingId, - authContextRef: `better-auth-session:${input.originalSessionId}`, - authMethod: 'session' as const, - principalId: input.originalPrincipalId, + resolveStoppedImpersonation: Effect.fn('SupportRecoveryPrincipalContext.resolveStoppedImpersonation')( + function* resolveStoppedImpersonation( + input, + ): Effect.fn.Return { + if (isInvalidRecoveryInput(input)) { + return yield* new SupportRecoveryPrincipalContextDeniedError({ + code: 'support_recovery_context_denied', + reason: 'The support recovery identity is invalid', + }); + } + const maybeRecord = yield* repository.load({ + originalAuthBindingId: input.originalAuthBindingId, + originalPrincipalId: input.originalPrincipalId, tenantId: input.tenantId, - }), - recordSupportImpersonationAction - ); - }), + }); + if (Option.isNone(maybeRecord)) { + return yield* new SupportRecoveryPrincipalContextDeniedError({ + code: 'support_recovery_context_denied', + reason: 'The support recovery identity is not a historical tenant-local user binding', + }); + } + const record = maybeRecord.value; + if ( + record.bindingPrincipalId !== input.originalPrincipalId || + record.bindingTenantId !== input.tenantId || + record.principalKind !== 'human' || + record.principalTenantId !== input.tenantId || + record.tenantId !== input.tenantId + ) { + return yield* new SupportRecoveryPrincipalContextDeniedError({ + code: 'support_recovery_context_denied', + reason: 'The support recovery identity is not a historical tenant-local user binding', + }); + } + return trustSupportRecoveryPrincipalContext( + Object.freeze({ + authBindingId: input.originalAuthBindingId, + authContextRef: `better-auth-session:${input.originalSessionId}`, + authMethod: 'session' as const, + principalId: input.originalPrincipalId, + tenantId: input.tenantId, + }), + recordSupportImpersonationAction, + ); + }, + ), }); export const supportRecoveryPrincipalContextResolverFromRepository = ( - repository: SupportRecoveryPrincipalContextRecordReader + repository: SupportRecoveryPrincipalContextRecordReader, ): SupportRecoveryPrincipalContextResolverService => supportRecoveryPrincipalContextResolverFromEffectRecordReader(repository); @@ -207,17 +187,15 @@ export const makeSupportRecoveryPrincipalContextResolver = (database: { readonly executor: Pick; }): SupportRecoveryPrincipalContextResolverService => supportRecoveryPrincipalContextResolverFromEffectRecordReader( - supportRecoveryPrincipalContextRepositoryFromDatabase(database) + supportRecoveryPrincipalContextRepositoryFromDatabase(database), ); export class SupportRecoveryPrincipalContextResolver extends Context.Service< SupportRecoveryPrincipalContextResolver, SupportRecoveryPrincipalContextResolverService ->()( - '@app/core-runtime/auth/support-recovery-principal-context/SupportRecoveryPrincipalContextResolver' -) {} +>()('@app/core-runtime/auth/support-recovery-principal-context/SupportRecoveryPrincipalContextResolver') {} export const SupportRecoveryPrincipalContextResolverLive = Layer.effect( SupportRecoveryPrincipalContextResolver, - CoreDatabase.pipe(Effect.map(makeSupportRecoveryPrincipalContextResolver)) + CoreDatabase.pipe(Effect.map(makeSupportRecoveryPrincipalContextResolver)), ); diff --git a/app/packages/core-runtime/src/auth/system-principal-context-denied-error.ts b/app/packages/core-runtime/src/auth/system-principal-context-denied-error.ts index ffd48e7c1..9ed83e841 100644 --- a/app/packages/core-runtime/src/auth/system-principal-context-denied-error.ts +++ b/app/packages/core-runtime/src/auth/system-principal-context-denied-error.ts @@ -5,5 +5,5 @@ export class SystemPrincipalContextDeniedError extends Schema.TaggedError()( - 'PrincipalContextProvenanceInvariant', - { - reason: Schema.String, - } -); +const PrincipalContextProvenanceInvariant = Schema.TaggedError()('PrincipalContextProvenanceInvariant', { + reason: Schema.String, +}); export class TrustedPrincipalContextDecodeError extends Schema.TaggedError()( 'TrustedPrincipalContextDecodeError', - {} + {}, ) {} -type PrincipalContextProvenanceToken = - | typeof supportRecoveryProvenance - | typeof systemProvenance; +type PrincipalContextProvenanceToken = typeof supportRecoveryProvenance | typeof systemProvenance; type PrincipalContextProvenanceAccess = ( candidate: TrustedPrincipalContext, - token: PrincipalContextProvenanceToken + token: PrincipalContextProvenanceToken, ) => boolean | object; -const PrincipalContextProvenanceAccessSchema = - Schema.declare( - (value): value is PrincipalContextProvenanceAccess => - Predicate.isFunction(value) - ); +const PrincipalContextProvenanceAccessSchema = Schema.declare( + (value): value is PrincipalContextProvenanceAccess => Predicate.isFunction(value), +); const PrincipalContextProvenanceCarrierSchema = Schema.Struct({ - [provenanceAccessProperty]: Schema.optionalKey( - PrincipalContextProvenanceAccessSchema - ), + [provenanceAccessProperty]: Schema.optionalKey(PrincipalContextProvenanceAccessSchema), }); const attachPrincipalContextProvenance = < @@ -50,19 +41,14 @@ const attachPrincipalContextProvenance = < >( context: Context, provenance: PrincipalContextProvenanceToken, - actionRegistration?: Registration + actionRegistration?: Registration, ): Context => { const carrier = { ...context }; - const accessProvenance: PrincipalContextProvenanceAccess = ( - candidate, - token - ) => { + const accessProvenance: PrincipalContextProvenanceAccess = (candidate, token) => { if (candidate !== carrier || token !== provenance) { return false; } - return provenance === systemProvenance - ? true - : (actionRegistration ?? false); + return provenance === systemProvenance ? true : (actionRegistration ?? false); }; Object.defineProperty(carrier, provenanceAccessProperty, { value: accessProvenance, @@ -81,9 +67,7 @@ const hasSystemProvenance = (context: Context): boolean => { return accessProvenance?.(context, systemProvenance) === true; }; -const readSupportRecoveryAction = ( - context: Context -): object | null => { +const readSupportRecoveryAction = (context: Context): object | null => { if ( !Schema.is(TrustedPrincipalContextSchema)(context) || !Schema.is(PrincipalContextProvenanceCarrierSchema)(context) @@ -95,107 +79,76 @@ const readSupportRecoveryAction = ( return null; } const registration = accessProvenance(context, supportRecoveryProvenance); - return Predicate.isObjectKeyword(registration) && registration !== null - ? registration - : null; + return Predicate.isObjectKeyword(registration) && registration !== null ? registration : null; }; const failProvenanceInvariant = (reason: string): never => { throw new PrincipalContextProvenanceInvariant({ reason }); }; -export const trustResolvedSystemPrincipalContext = < - Context extends TrustedPrincipalContext, ->( - context: Context +export const trustResolvedSystemPrincipalContext = ( + context: Context, ): Context => { if (!Schema.is(SystemPrincipalContextSchema)(context)) { - return failProvenanceInvariant( - 'Only resolved system contexts can carry system provenance' - ); + return failProvenanceInvariant('Only resolved system contexts can carry system provenance'); } return attachPrincipalContextProvenance(context, systemProvenance); }; -export const isTrustedSystemPrincipalContext = ( - context: Context -): boolean => - hasSystemProvenance(context) && - Schema.is(SystemPrincipalContextSchema)(context); +export const isTrustedSystemPrincipalContext = (context: Context): boolean => + hasSystemProvenance(context) && Schema.is(SystemPrincipalContextSchema)(context); export const trustSupportRecoveryPrincipalContext = < Context extends TrustedPrincipalContext, Registration extends object, >( context: Context, - actionRegistration: Registration + actionRegistration: Registration, ): Context => { if (!Schema.is(SessionPrincipalContextSchema)(context)) { - return failProvenanceInvariant( - 'Only resolved session contexts can carry support recovery provenance' - ); + return failProvenanceInvariant('Only resolved session contexts can carry support recovery provenance'); } - return attachPrincipalContextProvenance( - context, - supportRecoveryProvenance, - actionRegistration - ); + return attachPrincipalContextProvenance(context, supportRecoveryProvenance, actionRegistration); }; -export const isTrustedSupportRecoveryPrincipalContext = < - Context, - Registration extends object = object, ->( +export const isTrustedSupportRecoveryPrincipalContext = ( context: Context, - actionRegistration?: Registration + actionRegistration?: Registration, ): boolean => { const trustedActionRegistration = readSupportRecoveryAction(context); return ( trustedActionRegistration !== null && - (actionRegistration === undefined || - trustedActionRegistration === actionRegistration) && + (actionRegistration === undefined || trustedActionRegistration === actionRegistration) && Schema.is(SessionPrincipalContextSchema)(context) ); }; -export const preserveSystemPrincipalContextTrust = < - Source, - Context extends TrustedPrincipalContext, ->( +export const preserveSystemPrincipalContextTrust = ( source: Source, - context: Context + context: Context, ): Context => { if (isTrustedSystemPrincipalContext(source)) { return trustResolvedSystemPrincipalContext(context); } const recoveryActionRegistration = readSupportRecoveryAction(source); if (recoveryActionRegistration !== null) { - return trustSupportRecoveryPrincipalContext( - context, - recoveryActionRegistration - ); + return trustSupportRecoveryPrincipalContext(context, recoveryActionRegistration); } return context; }; export const decodeTrustedPrincipalContext = ( - input: Input -): Effect.Effect< - TrustedPrincipalContext, - TrustedPrincipalContextDecodeError -> => { - if ( - Schema.is(SystemPrincipalContextSchema)(input) && - !isTrustedSystemPrincipalContext(input) - ) { + input: Input, +): Effect.Effect => { + if (Schema.is(SystemPrincipalContextSchema)(input) && !isTrustedSystemPrincipalContext(input)) { return Effect.fail(new TrustedPrincipalContextDecodeError()); } return Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)(input).pipe( Effect.mapError((cause) => Object.defineProperty(new TrustedPrincipalContextDecodeError(), 'cause', { value: cause, - }) + }), ), - Effect.map((context) => preserveSystemPrincipalContextTrust(input, context)) + Effect.map((context) => preserveSystemPrincipalContextTrust(input, context)), ); }; diff --git a/app/packages/core-runtime/src/auth/system-principal-context-unavailable-error.ts b/app/packages/core-runtime/src/auth/system-principal-context-unavailable-error.ts index fbd343576..f2b24c52d 100644 --- a/app/packages/core-runtime/src/auth/system-principal-context-unavailable-error.ts +++ b/app/packages/core-runtime/src/auth/system-principal-context-unavailable-error.ts @@ -5,5 +5,5 @@ export class SystemPrincipalContextUnavailableError extends Schema.TaggedError; -interface SystemPrincipalContextRecordReader< - Result extends SystemPrincipalContextRepositoryLoadResult, -> { - readonly load: (input: { - readonly principalId: string; - readonly tenantId: string; - }) => Result; +interface SystemPrincipalContextRecordReader { + readonly load: (input: { readonly principalId: string; readonly tenantId: string }) => Result; } -const attachCause = ( - failure: Failure, - cause: unknown -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); +const attachCause = (failure: Failure, cause: unknown): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); const unavailable = (cause?: unknown): SystemPrincipalContextUnavailableError => attachCause( @@ -86,20 +76,16 @@ const unavailable = (cause?: unknown): SystemPrincipalContextUnavailableError => code: 'system_principal_context_unavailable', reason: 'The system principal could not be revalidated', }), - cause + cause, ); const DATABASE_OPERATION_TIMEOUT = Duration.seconds(30); -const loadSystemPrincipalContextRecord = < - Result extends SystemPrincipalContextRepositoryLoadResult, ->( +const loadSystemPrincipalContextRecord = ( repository: SystemPrincipalContextRecordReader, - input: { readonly principalId: string; readonly tenantId: string } -): Effect.Effect< - Option.Option, - SystemPrincipalContextUnavailableError -> => repository.load(input); + input: { readonly principalId: string; readonly tenantId: string }, +): Effect.Effect, SystemPrincipalContextUnavailableError> => + repository.load(input); const systemPrincipalContextRepositoryFromDatabase = (database: { readonly executor: Pick; @@ -113,12 +99,7 @@ const systemPrincipalContextRepositoryFromDatabase = (database: { }) .from(principals) .innerJoin(tenants, eq(tenants.tenantId, principals.tenantId)) - .where( - and( - eq(principals.tenantId, input.tenantId), - eq(principals.principalId, input.principalId) - ) - ) + .where(and(eq(principals.tenantId, input.tenantId), eq(principals.principalId, input.principalId))) .limit(1) .pipe( Effect.mapError(unavailable), @@ -126,28 +107,20 @@ const systemPrincipalContextRepositoryFromDatabase = (database: { Effect.timeoutOrElse({ duration: DATABASE_OPERATION_TIMEOUT, orElse: () => Effect.fail(unavailable()), - }) + }), ), }); const isEligibleSystemPrincipal = ( record: SystemPrincipalContextRecord, - registration: SystemWorkloadRegistration + registration: SystemWorkloadRegistration, ): boolean => { - const kindAllowed = - record.kind === 'system' || - (registration.allowServicePrincipal && record.kind === 'service'); - return ( - record.principalStatus === 'active' && - record.tenantStatus === 'active' && - kindAllowed - ); + const kindAllowed = record.kind === 'system' || (registration.allowServicePrincipal && record.kind === 'service'); + return record.principalStatus === 'active' && record.tenantStatus === 'active' && kindAllowed; }; -export const systemPrincipalContextResolverFromRepository = < - Result extends SystemPrincipalContextRepositoryLoadResult, ->( - repository: SystemPrincipalContextRecordReader +export const systemPrincipalContextResolverFromRepository = ( + repository: SystemPrincipalContextRecordReader, ) => ({ resolve: Effect.fn('systemPrincipalContextResolverFromRepository.resolve')( function* resolveSystemPrincipalContext(input: { @@ -174,16 +147,14 @@ export const systemPrincipalContextResolverFromRepository = < if (Option.isNone(maybeRecord)) { return yield* new SystemPrincipalContextDeniedError({ code: 'system_principal_context_denied', - reason: - 'The configured system principal is not active and eligible in this tenant', + reason: 'The configured system principal is not active and eligible in this tenant', }); } const record = maybeRecord.value; if (!isEligibleSystemPrincipal(record, input.registration)) { return yield* new SystemPrincipalContextDeniedError({ code: 'system_principal_context_denied', - reason: - 'The configured system principal is not active and eligible in this tenant', + reason: 'The configured system principal is not active and eligible in this tenant', }); } return trustResolvedSystemPrincipalContext( @@ -192,15 +163,12 @@ export const systemPrincipalContextResolverFromRepository = < authMethod: 'system' as const, principalId: input.principalId, tenantId: input.tenantId, - }) + }), ); - } + }, ), }); export const makeSystemPrincipalContextResolver = (database: { readonly executor: Pick; -}) => - systemPrincipalContextResolverFromRepository( - systemPrincipalContextRepositoryFromDatabase(database) - ); +}) => systemPrincipalContextResolverFromRepository(systemPrincipalContextRepositoryFromDatabase(database)); diff --git a/app/packages/core-runtime/src/auth/system-workload-registration-invalid-error.ts b/app/packages/core-runtime/src/auth/system-workload-registration-invalid-error.ts index 30ff7e0ff..6170f331e 100644 --- a/app/packages/core-runtime/src/auth/system-workload-registration-invalid-error.ts +++ b/app/packages/core-runtime/src/auth/system-workload-registration-invalid-error.ts @@ -5,5 +5,5 @@ export class SystemWorkloadRegistrationInvalidError extends Schema.TaggedError()( - 'TenantInactiveError', - {} -) {} +export class TenantInactiveError extends Schema.TaggedError()('TenantInactiveError', {}) {} diff --git a/app/packages/core-runtime/src/authorization/entrypoint-classification.ts b/app/packages/core-runtime/src/authorization/entrypoint-classification.ts index 43950410c..6d87e8309 100644 --- a/app/packages/core-runtime/src/authorization/entrypoint-classification.ts +++ b/app/packages/core-runtime/src/authorization/entrypoint-classification.ts @@ -1,21 +1,14 @@ import { Result, Schema } from 'effect'; -export const ACTION_PROVISIONING_INTENTS = [ - 'tenant_membership_default', - 'explicit', -] as const; +export const ACTION_PROVISIONING_INTENTS = ['tenant_membership_default', 'explicit'] as const; -export const ActionProvisioningIntentSchema = Schema.Literals( - ACTION_PROVISIONING_INTENTS -); -export type ActionProvisioningIntent = Schema.Schema.Type< - typeof ActionProvisioningIntentSchema ->; +export const ActionProvisioningIntentSchema = Schema.Literals(ACTION_PROVISIONING_INTENTS); +export type ActionProvisioningIntent = Schema.Schema.Type; const stablePermissionSchema = Schema.String.check( Schema.isMinLength(3), Schema.isMaxLength(200), - Schema.isPattern(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u) + Schema.isPattern(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u), ); export const IntentionalPublicAuthorizationSchema = Schema.Struct({ @@ -54,20 +47,14 @@ export const EntrypointAuthorizationSchema = Schema.Union([ CapabilityIssuanceAuthorizationSchema, ]); -export type EntrypointAuthorization = Schema.Schema.Type< - typeof EntrypointAuthorizationSchema ->; -export type ActionExecutionAuthorization = Schema.Schema.Type< - typeof ActionExecutionAuthorizationSchema ->; +export type EntrypointAuthorization = Schema.Schema.Type; +export type ActionExecutionAuthorization = Schema.Schema.Type; -export const decodeEntrypointAuthorization = ( - input: Input -): EntrypointAuthorization => +export const decodeEntrypointAuthorization = (input: Input): EntrypointAuthorization => Object.freeze( Result.getOrThrow( Schema.decodeUnknownResult(EntrypointAuthorizationSchema, { onExcessProperty: 'error', - })(input) - ) + })(input), + ), ); diff --git a/app/packages/core-runtime/src/authorization/rollout-decision.ts b/app/packages/core-runtime/src/authorization/rollout-decision.ts index eeab58aef..9c12d3ffc 100644 --- a/app/packages/core-runtime/src/authorization/rollout-decision.ts +++ b/app/packages/core-runtime/src/authorization/rollout-decision.ts @@ -2,12 +2,8 @@ import { DateTime, Schema } from 'effect'; export const AUTHORIZATION_WOULD_DENY_SCHEMA_VERSION = 1 as const; -const AuthorizationRolloutModeSchema = Schema.Literals([ - 'enforced', - 'report_only', -]); -export type AuthorizationRolloutMode = - typeof AuthorizationRolloutModeSchema.Type; +const AuthorizationRolloutModeSchema = Schema.Literals(['enforced', 'report_only']); +export type AuthorizationRolloutMode = typeof AuthorizationRolloutModeSchema.Type; const AuthorizationDenialReasonSchema = Schema.Literals([ 'cross_tenant', 'expired_credential', @@ -18,8 +14,7 @@ const AuthorizationDenialReasonSchema = Schema.Literals([ 'replayed_credential', 'wrong_audience', ]); -export type AuthorizationDenialReason = - typeof AuthorizationDenialReasonSchema.Type; +export type AuthorizationDenialReason = typeof AuthorizationDenialReasonSchema.Type; export interface AuthorizationRolloutRuntimeContract { readonly activatedAtEpochMs: number; @@ -67,17 +62,14 @@ const nonBypassableReasons = new Set([ 'wrong_audience', ]); -const isReportOnlyActive = ( - contract: AuthorizationRolloutRuntimeContract, - nowEpochMs: number -): boolean => +const isReportOnlyActive = (contract: AuthorizationRolloutRuntimeContract, nowEpochMs: number): boolean => contract.mode === 'report_only' && nowEpochMs >= contract.activatedAtEpochMs && nowEpochMs < contract.expiresAtEpochMs; export const decideAuthorizationRollout = ( input: AuthorizationRolloutDecisionInput, - options: AuthorizationRolloutDecisionOptions + options: AuthorizationRolloutDecisionOptions, ): 'allowed' | 'denied' => { if (input.current === 'denied') { return 'denied'; diff --git a/app/packages/core-runtime/src/database/driver-failure.ts b/app/packages/core-runtime/src/database/driver-failure.ts index 38164da82..bbd2d1188 100644 --- a/app/packages/core-runtime/src/database/driver-failure.ts +++ b/app/packages/core-runtime/src/database/driver-failure.ts @@ -2,13 +2,8 @@ import { Option, Schema } from 'effect'; import { findPostgresFailure } from './postgres-failure.ts'; -export const DatabaseDriverFailureKindSchema = Schema.Literals([ - 'socket', - 'sqlstate', -]); -export type DatabaseDriverFailureKind = Schema.Schema.Type< - typeof DatabaseDriverFailureKindSchema ->; +export const DatabaseDriverFailureKindSchema = Schema.Literals(['socket', 'sqlstate']); +export type DatabaseDriverFailureKind = Schema.Schema.Type; const driverFailureFields = { code: Schema.String, @@ -17,135 +12,81 @@ const driverFailureFields = { const DatabaseCommitAcknowledgementAmbiguousContract = Schema.TaggedStruct( 'DatabaseCommitAcknowledgementAmbiguous', - driverFailureFields + driverFailureFields, ); type DatabaseCommitAcknowledgementAmbiguousSelf = Schema.Schema.Type< typeof DatabaseCommitAcknowledgementAmbiguousContract >; -export const DatabaseCommitAcknowledgementAmbiguous = - Schema.TaggedError()( - 'DatabaseCommitAcknowledgementAmbiguous', - driverFailureFields - ); +export const DatabaseCommitAcknowledgementAmbiguous = Schema.TaggedError()( + 'DatabaseCommitAcknowledgementAmbiguous', + driverFailureFields, +); -const DatabaseTransactionFailureContract = Schema.TaggedStruct( +const DatabaseTransactionFailureContract = Schema.TaggedStruct('DatabaseTransactionFailure', driverFailureFields); +type DatabaseTransactionFailureSelf = Schema.Schema.Type; +export const DatabaseTransactionFailure = Schema.TaggedError()( 'DatabaseTransactionFailure', - driverFailureFields + driverFailureFields, ); -type DatabaseTransactionFailureSelf = Schema.Schema.Type< - typeof DatabaseTransactionFailureContract ->; -export const DatabaseTransactionFailure = - Schema.TaggedError()( - 'DatabaseTransactionFailure', - driverFailureFields - ); const DatabaseDriverUnavailableFailureContract = Schema.TaggedStruct( 'DatabaseDriverUnavailableFailure', - driverFailureFields + driverFailureFields, +); +type DatabaseDriverUnavailableFailureSelf = Schema.Schema.Type; +export const DatabaseDriverUnavailableFailure = Schema.TaggedError()( + 'DatabaseDriverUnavailableFailure', + driverFailureFields, ); -type DatabaseDriverUnavailableFailureSelf = Schema.Schema.Type< - typeof DatabaseDriverUnavailableFailureContract ->; -export const DatabaseDriverUnavailableFailure = - Schema.TaggedError()( - 'DatabaseDriverUnavailableFailure', - driverFailureFields - ); export const DatabaseDriverFailureSchema = Schema.Union([ DatabaseCommitAcknowledgementAmbiguous, DatabaseTransactionFailure, DatabaseDriverUnavailableFailure, ]); -export type DatabaseDriverFailure = Schema.Schema.Type< - typeof DatabaseDriverFailureSchema ->; +export type DatabaseDriverFailure = Schema.Schema.Type; export const DatabaseDriverFailureInputSchema = Schema.Unknown; -export type DatabaseDriverFailureInput = Schema.Schema.Type< - typeof DatabaseDriverFailureInputSchema ->; +export type DatabaseDriverFailureInput = Schema.Schema.Type; const connectionSqlStateClass = ['0', '8'].join(''); const transactionSqlStateClass = ['4', '0'].join(''); const administrativeShutdownSqlState = ['57', 'P01'].join(''); -const unavailableSqlStateClasses = new Set([ - '08', - '40', - '53', - '55', - '57', - '58', -]); -const unavailableSocketCodes = new Set( - 'ECONNREFUSED ECONNRESET EPIPE ETIMEDOUT'.split(' ') -); +const unavailableSqlStateClasses = new Set(['08', '40', '53', '55', '57', '58']); +const unavailableSocketCodes = new Set('ECONNREFUSED ECONNRESET EPIPE ETIMEDOUT'.split(' ')); const commitAcknowledgementSocketCodes = new Set( - 'ECONNABORTED ECONNRESET EHOSTDOWN EHOSTUNREACH ENETDOWN ENETRESET ENETUNREACH EPIPE ETIMEDOUT'.split( - ' ' - ) + 'ECONNABORTED ECONNRESET EHOSTDOWN EHOSTUNREACH ENETDOWN ENETRESET ENETUNREACH EPIPE ETIMEDOUT'.split(' '), ); -const decodeDriverCodeFailure = ( - code: string -): Option.Option => { +const decodeDriverCodeFailure = (code: string): Option.Option => { const sqlStateClass = code.slice(0, 2); - if ( - sqlStateClass === connectionSqlStateClass || - code === administrativeShutdownSqlState - ) { - return Option.some( - new DatabaseCommitAcknowledgementAmbiguous({ code, kind: 'sqlstate' }) - ); + if (sqlStateClass === connectionSqlStateClass || code === administrativeShutdownSqlState) { + return Option.some(new DatabaseCommitAcknowledgementAmbiguous({ code, kind: 'sqlstate' })); } if (commitAcknowledgementSocketCodes.has(code)) { - return Option.some( - new DatabaseCommitAcknowledgementAmbiguous({ code, kind: 'socket' }) - ); + return Option.some(new DatabaseCommitAcknowledgementAmbiguous({ code, kind: 'socket' })); } if (sqlStateClass === transactionSqlStateClass) { - return Option.some( - new DatabaseTransactionFailure({ code, kind: 'sqlstate' }) - ); + return Option.some(new DatabaseTransactionFailure({ code, kind: 'sqlstate' })); } if (unavailableSqlStateClasses.has(sqlStateClass)) { - return Option.some( - new DatabaseDriverUnavailableFailure({ code, kind: 'sqlstate' }) - ); + return Option.some(new DatabaseDriverUnavailableFailure({ code, kind: 'sqlstate' })); } return unavailableSocketCodes.has(code) - ? Option.some( - new DatabaseDriverUnavailableFailure({ code, kind: 'socket' }) - ) + ? Option.some(new DatabaseDriverUnavailableFailure({ code, kind: 'socket' })) : Option.none(); }; const isUnavailableDriverCode = (code: string): boolean => - unavailableSqlStateClasses.has(code.slice(0, 2)) || - unavailableSocketCodes.has(code); + unavailableSqlStateClasses.has(code.slice(0, 2)) || unavailableSocketCodes.has(code); -export const decodeDatabaseDriverFailure = ( - input: DatabaseDriverFailureInput -): Option.Option => +export const decodeDatabaseDriverFailure = (input: DatabaseDriverFailureInput): Option.Option => Option.flatMap( - findPostgresFailure(input, ({ code }) => - Option.isSome(decodeDriverCodeFailure(code)) - ), - ({ code }) => decodeDriverCodeFailure(code) + findPostgresFailure(input, ({ code }) => Option.isSome(decodeDriverCodeFailure(code))), + ({ code }) => decodeDriverCodeFailure(code), ); -export const isDatabaseUnavailableFailure = ( - input: DatabaseDriverFailureInput -): boolean => - Option.exists(decodeDatabaseDriverFailure(input), ({ code }) => - isUnavailableDriverCode(code) - ); +export const isDatabaseUnavailableFailure = (input: DatabaseDriverFailureInput): boolean => + Option.exists(decodeDatabaseDriverFailure(input), ({ code }) => isUnavailableDriverCode(code)); -export const isDatabaseCommitAcknowledgementAmbiguous = ( - input: DatabaseDriverFailureInput -): boolean => - Option.exists( - decodeDatabaseDriverFailure(input), - Schema.is(DatabaseCommitAcknowledgementAmbiguous) - ); +export const isDatabaseCommitAcknowledgementAmbiguous = (input: DatabaseDriverFailureInput): boolean => + Option.exists(decodeDatabaseDriverFailure(input), Schema.is(DatabaseCommitAcknowledgementAmbiguous)); diff --git a/app/packages/core-runtime/src/database/postgres-failure.ts b/app/packages/core-runtime/src/database/postgres-failure.ts index 3cd951b48..e9fcee57f 100644 --- a/app/packages/core-runtime/src/database/postgres-failure.ts +++ b/app/packages/core-runtime/src/database/postgres-failure.ts @@ -10,22 +10,13 @@ export type PostgresFailureMetadata = Readonly<{ readonly constraint?: string; }>; type PostgresFailureInput = Schema.Schema.Type; -type PostgresFailurePredicate = ( - metadata: Readonly -) => boolean; +type PostgresFailurePredicate = (metadata: Readonly) => boolean; -const decodePostgresFailureCode = Schema.decodeUnknownOption( - PostgresFailureCodeSchema -); -const decodePostgresFailureConstraint = Schema.decodeUnknownOption( - PostgresFailureConstraintSchema -); +const decodePostgresFailureCode = Schema.decodeUnknownOption(PostgresFailureCodeSchema); +const decodePostgresFailureConstraint = Schema.decodeUnknownOption(PostgresFailureConstraintSchema); const decodeCauseWrapper = Schema.decodeUnknownOption(CauseWrapperSchema); -const enqueueFailureReasons = ( - cause: Cause.Cause, - pending: unknown[] -): void => { +const enqueueFailureReasons = (cause: Cause.Cause, pending: unknown[]): void => { for (const reason of cause.reasons.toReversed()) { if (Cause.isFailReason(reason)) { pending.push(reason.error); @@ -35,9 +26,7 @@ const enqueueFailureReasons = ( } }; -const decodeFailureMetadata = ( - current: PostgresFailureInput -): Option.Option> => +const decodeFailureMetadata = (current: PostgresFailureInput): Option.Option> => Option.map(decodePostgresFailureCode(current), ({ code }) => { const constraint = decodePostgresFailureConstraint(current); const metadata: PostgresFailureMetadata = Option.isSome(constraint) @@ -52,18 +41,14 @@ const decodeFailureMetadata = ( */ export const findPostgresFailure = ( input: PostgresFailureInput, - predicate: PostgresFailurePredicate = () => true + predicate: PostgresFailurePredicate = () => true, ): Option.Option> => { const pending: unknown[] = [input]; const visited = new Set(); while (pending.length > 0) { const current = pending.pop(); - if ( - !Predicate.isObjectKeyword(current) || - current === null || - visited.has(current) - ) { + if (!Predicate.isObjectKeyword(current) || current === null || visited.has(current)) { continue; } visited.add(current); diff --git a/app/packages/core-runtime/src/db/catalog.ts b/app/packages/core-runtime/src/db/catalog.ts index 66a296237..3e26f7cbd 100644 --- a/app/packages/core-runtime/src/db/catalog.ts +++ b/app/packages/core-runtime/src/db/catalog.ts @@ -17,32 +17,19 @@ export interface CatalogDifference { readonly unexpected: readonly string[]; } -export const expectedCoreTableCatalog = CORE_TABLE_INVENTORY.map( - (tableName) => `${CORE_SCHEMA_NAME}.${tableName}` -); +export const expectedCoreTableCatalog = CORE_TABLE_INVENTORY.map((tableName) => `${CORE_SCHEMA_NAME}.${tableName}`); -export const compareApplicationCatalog = ( - entries: readonly CatalogEntry[] -): CatalogDifference => { +export const compareApplicationCatalog = (entries: readonly CatalogEntry[]): CatalogDifference => { const actualTables = new Set( entries - .filter( - (entry): entry is Extract => - entry.kind === 'table' - ) - .map((entry) => `${entry.schemaName}.${entry.tableName}`) + .filter((entry): entry is Extract => entry.kind === 'table') + .map((entry) => `${entry.schemaName}.${entry.tableName}`), ); const expectedTables = new Set(expectedCoreTableCatalog); - const missing = [...expectedTables] - .filter((name) => !actualTables.has(name)) - .toSorted(); - const unexpectedTables = [...actualTables] - .filter((name) => !expectedTables.has(name)) - .toSorted(); + const missing = [...expectedTables].filter((name) => !actualTables.has(name)).toSorted(); + const unexpectedTables = [...actualTables].filter((name) => !expectedTables.has(name)).toSorted(); const unexpectedSchemas = entries - .flatMap((entry) => - entry.kind === 'schema' ? [`${entry.schemaName}.*`] : [] - ) + .flatMap((entry) => (entry.kind === 'schema' ? [`${entry.schemaName}.*`] : [])) .toSorted(); return { diff --git a/app/packages/core-runtime/src/db/client.ts b/app/packages/core-runtime/src/db/client.ts index bfc8bc46e..df9cbc38b 100644 --- a/app/packages/core-runtime/src/db/client.ts +++ b/app/packages/core-runtime/src/db/client.ts @@ -28,10 +28,7 @@ export interface PoolResource { readonly end: () => Promise; } -const connectionFailure = ( - reason: string, - cause: unknown -): DatabaseConnectionError => +const connectionFailure = (reason: string, cause: unknown): DatabaseConnectionError => Object.defineProperty(new DatabaseConnectionError({ reason }), 'cause', { configurable: false, enumerable: false, @@ -40,69 +37,51 @@ const connectionFailure = ( }); export const acquirePoolResource = ( - acquire: () => Resource + acquire: () => Resource, ): Effect.Effect => Effect.acquireRelease( Effect.try({ - catch: (cause) => - connectionFailure( - 'Unable to initialize the PostgreSQL connection pool', - cause - ), + catch: (cause) => connectionFailure('Unable to initialize the PostgreSQL connection pool', cause), try: acquire, }), // pg overloads end(callback); invoke it with no arguments so the AbortSignal is never a callback. // eslint-disable-next-line typescript/promise-function-async -- Effect owns this foreign Promise boundary. - (pool) => Effect.promise(() => pool.end()) + (pool) => Effect.promise(() => pool.end()), ); export type PoolFactory = (configuration: PoolConfig) => Pool; -const defaultPoolFactory: PoolFactory = (configuration) => - new Pool(configuration); +const defaultPoolFactory: PoolFactory = (configuration) => new Pool(configuration); -export const makeCoreDatabase = Effect.fn('Client.makeCoreDatabase')( - function* makeDatabase( - configuration: DatabaseConfigValue & { - readonly poolDeadlines?: Partial; - }, - poolFactory: PoolFactory = defaultPoolFactory - ): Effect.fn.Return< - (typeof CoreDatabase)['Service'], - DatabaseConnectionError, - Scope.Scope - > { - const poolConfiguration = yield* configureDatabasePool( - Redacted.make(configuration.connectionString), - configuration.poolDeadlines - ); - const pool = yield* acquirePoolResource(() => - poolFactory(poolConfiguration) - ); - const reactivity = yield* Reactivity.make; - const client = yield* PgClient.fromPool({ - acquire: Effect.succeed(pool), - }).pipe( - Effect.provideService(Reactivity.Reactivity, reactivity), - Effect.mapError((cause) => - connectionFailure( - 'Unable to initialize the native PostgreSQL client', - cause - ) - ) - ); - return { - executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client) - ), - }; - } -); +export const makeCoreDatabase = Effect.fn('Client.makeCoreDatabase')(function* makeDatabase( + configuration: DatabaseConfigValue & { + readonly poolDeadlines?: Partial; + }, + poolFactory: PoolFactory = defaultPoolFactory, +): Effect.fn.Return<(typeof CoreDatabase)['Service'], DatabaseConnectionError, Scope.Scope> { + const poolConfiguration = yield* configureDatabasePool( + Redacted.make(configuration.connectionString), + configuration.poolDeadlines, + ); + const pool = yield* acquirePoolResource(() => poolFactory(poolConfiguration)); + const reactivity = yield* Reactivity.make; + const client = yield* PgClient.fromPool({ + acquire: Effect.succeed(pool), + }).pipe( + Effect.provideService(Reactivity.Reactivity, reactivity), + Effect.mapError((cause) => connectionFailure('Unable to initialize the native PostgreSQL client', cause)), + ); + return { + executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( + Effect.provideService(PgClient.PgClient, client), + ), + }; +}); export const CoreDatabaseLive = Layer.effect( CoreDatabase, Effect.gen(function* makeCoreDatabaseService() { const configuration = yield* DatabaseConfig; return yield* makeCoreDatabase(configuration); - }) + }), ); diff --git a/app/packages/core-runtime/src/db/config-error.ts b/app/packages/core-runtime/src/db/config-error.ts index 62e2a6436..11286dd11 100644 --- a/app/packages/core-runtime/src/db/config-error.ts +++ b/app/packages/core-runtime/src/db/config-error.ts @@ -1,8 +1,5 @@ import { Schema } from 'effect'; -export class DatabaseConfigError extends Schema.TaggedError()( - 'DatabaseConfigError', - { - reason: Schema.String, - } -) {} +export class DatabaseConfigError extends Schema.TaggedError()('DatabaseConfigError', { + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/db/config.ts b/app/packages/core-runtime/src/db/config.ts index a94a81040..24b8098ca 100644 --- a/app/packages/core-runtime/src/db/config.ts +++ b/app/packages/core-runtime/src/db/config.ts @@ -1,12 +1,4 @@ -import { - Config, - ConfigProvider, - Context, - Effect, - Layer, - Redacted, - Schema, -} from 'effect'; +import { Config, ConfigProvider, Context, Effect, Layer, Redacted, Schema } from 'effect'; import { loadDotEnvProvider } from '../environment/dotenv-provider.ts'; import { APP_ENV_PATH } from '../environment/workspace-environment.ts'; @@ -16,11 +8,8 @@ export { DatabaseConfigError } from './config-error.ts'; export const ROOT_ENV_PATH = APP_ENV_PATH; -const requiredDatabaseUrlSchema = Schema.Trim.pipe( - Schema.check(Schema.isMinLength(1)) -); -const INVALID_DATABASE_URL_REASON = - 'DATABASE_URL must be a valid PostgreSQL connection URL'; +const requiredDatabaseUrlSchema = Schema.Trim.pipe(Schema.check(Schema.isMinLength(1))); +const INVALID_DATABASE_URL_REASON = 'DATABASE_URL must be a valid PostgreSQL connection URL'; interface DatabaseConfigFields { readonly connectionString: Redacted.Redacted; @@ -48,10 +37,9 @@ export interface DatabaseConnectionPair { readonly runtime: DatabaseConfigValue; } -export class DatabaseConfig extends Context.Service< - DatabaseConfig, - DatabaseConfigValue ->()('@app/core-runtime/db/config/DatabaseConfig') {} +export class DatabaseConfig extends Context.Service()( + '@app/core-runtime/db/config/DatabaseConfig', +) {} export interface DatabaseEnvironment { readonly DATABASE_ADMIN_URL?: string; @@ -63,14 +51,9 @@ export interface LoadDatabaseConfigOptions { readonly envPath?: string; } -const configFailure = ( - reason: string, - cause?: unknown -): DatabaseConfigError => { +const configFailure = (reason: string, cause?: unknown): DatabaseConfigError => { const failure = new DatabaseConfigError({ reason }); - return cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); + return cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); }; interface ReadDatabaseUrlOptions { @@ -92,59 +75,44 @@ const hasValidDatabaseFields = ({ port <= 65_535 && user.length > 0; -const readDatabaseUrl = Effect.fn('Config.readDatabaseUrl')( - function* readDatabaseUrlEffect(options: ReadDatabaseUrlOptions) { - const connectionString = yield* Config.schema( - Schema.Redacted(requiredDatabaseUrlSchema), - options.configKey - ) - .parse(options.provider) - .pipe( - Effect.mapError((error) => configFailure(options.requiredReason, error)) - ); - const parsed = yield* Schema.decodeEffect(Schema.URLFromString)( - Redacted.value(connectionString) - ).pipe( - Effect.mapError((error) => - configFailure(INVALID_DATABASE_URL_REASON, error) - ) - ); - - if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') { - return yield* configFailure(INVALID_DATABASE_URL_REASON); - } +const readDatabaseUrl = Effect.fn('Config.readDatabaseUrl')(function* readDatabaseUrlEffect( + options: ReadDatabaseUrlOptions, +) { + const connectionString = yield* Config.schema(Schema.Redacted(requiredDatabaseUrlSchema), options.configKey) + .parse(options.provider) + .pipe(Effect.mapError((error) => configFailure(options.requiredReason, error))); + const parsed = yield* Schema.decodeEffect(Schema.URLFromString)(Redacted.value(connectionString)).pipe( + Effect.mapError((error) => configFailure(INVALID_DATABASE_URL_REASON, error)), + ); - const decoded = yield* Effect.try({ - catch: (error) => configFailure(INVALID_DATABASE_URL_REASON, error), - try: () => ({ - authorityUser: decodeURIComponent(parsed.username), - database: decodeURIComponent(parsed.pathname.replace(/^\/+/u, '')), - }), - }); - const host = parsed.hostname; - const port = - parsed.port.length > 0 ? Math.trunc(Number(parsed.port)) : 5432; - const queryUser = parsed.searchParams.getAll('user').at(-1); - const user = - queryUser === undefined || queryUser.length === 0 - ? decoded.authorityUser - : queryUser; + if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') { + return yield* configFailure(INVALID_DATABASE_URL_REASON); + } - if ( - !hasValidDatabaseFields({ database: decoded.database, host, port, user }) - ) { - return yield* configFailure(INVALID_DATABASE_URL_REASON); - } + const decoded = yield* Effect.try({ + catch: (error) => configFailure(INVALID_DATABASE_URL_REASON, error), + try: () => ({ + authorityUser: decodeURIComponent(parsed.username), + database: decodeURIComponent(parsed.pathname.replace(/^\/+/u, '')), + }), + }); + const host = parsed.hostname; + const port = parsed.port.length > 0 ? Math.trunc(Number(parsed.port)) : 5432; + const queryUser = parsed.searchParams.getAll('user').at(-1); + const user = queryUser === undefined || queryUser.length === 0 ? decoded.authorityUser : queryUser; - return makeDatabaseConfigValue({ - connectionString, - database: decoded.database, - host, - port, - user, - }); + if (!hasValidDatabaseFields({ database: decoded.database, host, port, user })) { + return yield* configFailure(INVALID_DATABASE_URL_REASON); } -); + + return makeDatabaseConfigValue({ + connectionString, + database: decoded.database, + host, + port, + user, + }); +}); const parseDatabaseConfigWith = (provider: ConfigProvider.ConfigProvider) => readDatabaseUrl({ @@ -153,55 +121,45 @@ const parseDatabaseConfigWith = (provider: ConfigProvider.ConfigProvider) => requiredReason: 'DATABASE_URL is required', }); -const parseDatabaseConnectionPairWith = Effect.fn( - 'Config.readDatabaseConnectionPair' -)(function* readDatabaseConnectionPairEffect( - provider: ConfigProvider.ConfigProvider -) { - const [runtime, admin] = yield* Effect.all( - [ - parseDatabaseConfigWith(provider), - readDatabaseUrl({ - configKey: 'DATABASE_ADMIN_URL', - provider, - requiredReason: 'DATABASE_ADMIN_URL is required', - }), - ], - { concurrency: 1 } - ); - - if ( - admin.connectionString === runtime.connectionString || - admin.user === runtime.user || - runtime.user === 'postgres' - ) { - return yield* configFailure( - 'Administrative and runtime PostgreSQL identities must be distinct' +const parseDatabaseConnectionPairWith = Effect.fn('Config.readDatabaseConnectionPair')( + function* readDatabaseConnectionPairEffect(provider: ConfigProvider.ConfigProvider) { + const [runtime, admin] = yield* Effect.all( + [ + parseDatabaseConfigWith(provider), + readDatabaseUrl({ + configKey: 'DATABASE_ADMIN_URL', + provider, + requiredReason: 'DATABASE_ADMIN_URL is required', + }), + ], + { concurrency: 1 }, ); - } - return Object.freeze({ admin, runtime }); -}); + if ( + admin.connectionString === runtime.connectionString || + admin.user === runtime.user || + runtime.user === 'postgres' + ) { + return yield* configFailure('Administrative and runtime PostgreSQL identities must be distinct'); + } + + return Object.freeze({ admin, runtime }); + }, +); export const parseDatabaseConfig = ( - environment: DatabaseEnvironment + environment: DatabaseEnvironment, ): Effect.Effect => - parseDatabaseConfigWith( - ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true }) - ); + parseDatabaseConfigWith(ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true })); export const parseDatabaseConnectionPair = ( - environment: DatabaseEnvironment + environment: DatabaseEnvironment, ): Effect.Effect => - parseDatabaseConnectionPairWith( - ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true }) - ); + parseDatabaseConnectionPairWith(ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true })); const loadWithProvider = ( - parse: ( - provider: ConfigProvider.ConfigProvider - ) => Effect.Effect, - options: LoadDatabaseConfigOptions + parse: (provider: ConfigProvider.ConfigProvider) => Effect.Effect, + options: LoadDatabaseConfigOptions, ): Effect.Effect => { const environmentProvider = options.environment === undefined @@ -213,23 +171,17 @@ const loadWithProvider = ( return loadDotEnvProvider(envPath, configFailure).pipe( Effect.withSpan('Config.loadDotEnvProvider'), - Effect.flatMap((fileProvider) => - parse(ConfigProvider.orElse(environmentProvider, fileProvider)) - ) + Effect.flatMap((fileProvider) => parse(ConfigProvider.orElse(environmentProvider, fileProvider))), ); }; export const loadDatabaseConfig = ( - options: LoadDatabaseConfigOptions = {} -): Effect.Effect => - loadWithProvider(parseDatabaseConfigWith, options); + options: LoadDatabaseConfigOptions = {}, +): Effect.Effect => loadWithProvider(parseDatabaseConfigWith, options); export const loadDatabaseConnectionPair = ( - options: LoadDatabaseConfigOptions = {} + options: LoadDatabaseConfigOptions = {}, ): Effect.Effect => loadWithProvider(parseDatabaseConnectionPairWith, options); -export const DatabaseConfigLive = Layer.effect( - DatabaseConfig, - loadDatabaseConfig() -); +export const DatabaseConfigLive = Layer.effect(DatabaseConfig, loadDatabaseConfig()); diff --git a/app/packages/core-runtime/src/db/connection-error.ts b/app/packages/core-runtime/src/db/connection-error.ts index 3b25dafe7..881ee98be 100644 --- a/app/packages/core-runtime/src/db/connection-error.ts +++ b/app/packages/core-runtime/src/db/connection-error.ts @@ -1,8 +1,5 @@ import { Schema } from 'effect'; -export class DatabaseConnectionError extends Schema.TaggedError()( - 'DatabaseConnectionError', - { - reason: Schema.String, - } -) {} +export class DatabaseConnectionError extends Schema.TaggedError()('DatabaseConnectionError', { + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/db/pool-configuration.ts b/app/packages/core-runtime/src/db/pool-configuration.ts index 7b414ec2e..b2e749bdd 100644 --- a/app/packages/core-runtime/src/db/pool-configuration.ts +++ b/app/packages/core-runtime/src/db/pool-configuration.ts @@ -14,64 +14,61 @@ export interface DatabasePoolDeadlines { } // Runtime work should fail promptly under saturation without cutting off ordinary queries. -export const DEFAULT_DATABASE_POOL_DEADLINES: Readonly = - Object.freeze({ - connectionTimeoutMillis: 5000, - statement_timeout: 30_000, - }); +export const DEFAULT_DATABASE_POOL_DEADLINES: Readonly = Object.freeze({ + connectionTimeoutMillis: 5000, + statement_timeout: 30_000, +}); /** * Override deadlines only through poolDeadlines, never connection-string parameters. * URL startup options are unsupported because PostgreSQL can use them to override deadlines. * Other URL settings, including SSL, are passed to pg unchanged. */ -export const configureDatabasePool = Effect.fn( - 'PoolConfiguration.configureDatabasePool' -)(function* configureDatabasePool( - connectionString: Redacted.Redacted, - poolDeadlines?: Partial -): Effect.fn.Return { - const options = { ...DEFAULT_DATABASE_POOL_DEADLINES, ...poolDeadlines }; - for (const value of Object.values(options)) { - if (value === undefined) { - continue; +export const configureDatabasePool = Effect.fn('PoolConfiguration.configureDatabasePool')( + function* configureDatabasePool( + connectionString: Redacted.Redacted, + poolDeadlines?: Partial, + ): Effect.fn.Return { + const options = { ...DEFAULT_DATABASE_POOL_DEADLINES, ...poolDeadlines }; + for (const value of Object.values(options)) { + if (value === undefined) { + continue; + } + // Zero disables pg deadlines; oversized Node timers overflow to 1ms. + if (!Number.isInteger(value) || value <= 0 || value > 2_147_483_647) { + return yield* new DatabaseConnectionError({ + reason: 'Database pool deadlines must be positive 32-bit millisecond integers', + }); + } } - // Zero disables pg deadlines; oversized Node timers overflow to 1ms. - if (!Number.isInteger(value) || value <= 0 || value > 2_147_483_647) { + + const unredactedConnectionString = Redacted.value(connectionString); + const url = URL.parse(unredactedConnectionString); + if (url === null || !['postgres:', 'postgresql:'].includes(url.protocol)) { return yield* new DatabaseConnectionError({ - reason: - 'Database pool deadlines must be positive 32-bit millisecond integers', + reason: 'Database connection string must be a PostgreSQL URL', }); } - } - const unredactedConnectionString = Redacted.value(connectionString); - const url = URL.parse(unredactedConnectionString); - if (url === null || !['postgres:', 'postgresql:'].includes(url.protocol)) { - return yield* new DatabaseConnectionError({ - reason: 'Database connection string must be a PostgreSQL URL', - }); - } - - // pg merges parsed URI parameters over explicit options. Reject rather than strip them, - // preserving all unrelated URL settings verbatim and keeping one deadline policy. - if ( - [ - 'connectionTimeoutMillis', - 'connect_timeout', - 'lock_timeout', - 'statement_timeout', - 'query_timeout', - 'options', - ].some((key) => url.searchParams.has(key)) - ) { - return yield* new DatabaseConnectionError({ - reason: - 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', - }); - } + // pg merges parsed URI parameters over explicit options. Reject rather than strip them, + // preserving all unrelated URL settings verbatim and keeping one deadline policy. + if ( + [ + 'connectionTimeoutMillis', + 'connect_timeout', + 'lock_timeout', + 'statement_timeout', + 'query_timeout', + 'options', + ].some((key) => url.searchParams.has(key)) + ) { + return yield* new DatabaseConnectionError({ + reason: 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', + }); + } - // These server deadlines belong in the startup packet. Do not use query_timeout or a Promise - // race: those reject without cancelling server work. - return { connectionString: unredactedConnectionString, ...options }; -}); + // These server deadlines belong in the startup packet. Do not use query_timeout or a Promise + // race: those reject without cancelling server work. + return { connectionString: unredactedConnectionString, ...options }; + }, +); diff --git a/app/packages/core-runtime/src/db/schema.ts b/app/packages/core-runtime/src/db/schema.ts index 8f8164dd3..95e09fa85 100644 --- a/app/packages/core-runtime/src/db/schema.ts +++ b/app/packages/core-runtime/src/db/schema.ts @@ -52,25 +52,13 @@ export const ACTION_INVOCATION_STATUSES = [ 'replayed', ] as const; -export type ActionInvocationStatus = - (typeof ACTION_INVOCATION_STATUSES)[number]; +export type ActionInvocationStatus = (typeof ACTION_INVOCATION_STATUSES)[number]; -export const ACTION_AUTH_METHODS = [ - 'session', - 'api_key', - 'system', - 'support_impersonation', -] as const; +export const ACTION_AUTH_METHODS = ['session', 'api_key', 'system', 'support_impersonation'] as const; export type ActionAuthMethod = (typeof ACTION_AUTH_METHODS)[number]; -export const PRINCIPAL_KINDS = [ - 'human', - 'service', - 'integration', - 'agent', - 'system', -] as const; +export const PRINCIPAL_KINDS = ['human', 'service', 'integration', 'agent', 'system'] as const; export type PrincipalKind = (typeof PRINCIPAL_KINDS)[number]; export const PRINCIPAL_STATUSES = ['active', 'disabled', 'archived'] as const; @@ -83,19 +71,12 @@ export const BINDING_STATUSES = ['active', 'disabled', 'revoked'] as const; export type BindingStatus = (typeof BINDING_STATUSES)[number]; export const coreSchema = pgSchema(CORE_SCHEMA_NAME); -export const domainEventTenantSequence = coreSchema.sequence( - 'domain_event_tenant_sequence_no_seq' -); +export const domainEventTenantSequence = coreSchema.sequence('domain_event_tenant_sequence_no_seq'); -const createdAt = () => - timestamp('created_at', { withTimezone: true }).defaultNow().notNull(); -const updatedAt = () => - timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(); -const occurredAt = () => - timestamp('occurred_at', { withTimezone: true }).defaultNow().notNull(); -const enableCoreGovernedRls = (table: { - readonly enableRLS: () => Table; -}): Table => table.enableRLS(); +const createdAt = () => timestamp('created_at', { withTimezone: true }).defaultNow().notNull(); +const updatedAt = () => timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(); +const occurredAt = () => timestamp('occurred_at', { withTimezone: true }).defaultNow().notNull(); +const enableCoreGovernedRls =
(table: { readonly enableRLS: () => Table }): Table => table.enableRLS(); export const tenants = coreSchema.table( 'tenants', @@ -110,11 +91,8 @@ export const tenants = coreSchema.table( }, (table) => [ uniqueIndex('core_tenants_slug_uk').on(table.slug), - check( - 'core_tenants_status_ck', - sql`${table.status} in ('active', 'suspended', 'archived')` - ), - ] + check('core_tenants_status_ck', sql`${table.status} in ('active', 'suspended', 'archived')`), + ], ); const tenantId = () => @@ -136,21 +114,15 @@ export const legalEntities = coreSchema.table( updatedAt: updatedAt(), }, (table) => [ - uniqueIndex('core_legal_entities_tenant_id_uk').on( - table.tenantId, - table.legalEntityId - ), + uniqueIndex('core_legal_entities_tenant_id_uk').on(table.tenantId, table.legalEntityId), uniqueIndex('core_legal_entities_registration_uk').on( table.tenantId, table.registrationCountry, - table.registrationNumber + table.registrationNumber, ), index('core_legal_entities_tenant_idx').on(table.tenantId), - check( - 'core_legal_entities_status_ck', - sql`${table.status} in ('active', 'suspended', 'archived')` - ), - ] + check('core_legal_entities_status_ck', sql`${table.status} in ('active', 'suspended', 'archived')`), + ], ); export const principals = coreSchema.table( @@ -165,20 +137,11 @@ export const principals = coreSchema.table( disabledAt: timestamp('disabled_at', { withTimezone: true }), }, (table) => [ - uniqueIndex('core_principals_tenant_id_uk').on( - table.tenantId, - table.principalId - ), + uniqueIndex('core_principals_tenant_id_uk').on(table.tenantId, table.principalId), index('core_principals_tenant_kind_idx').on(table.tenantId, table.kind), - check( - 'core_principals_kind_ck', - sql`${table.kind} in ('human', 'service', 'integration', 'agent', 'system')` - ), - check( - 'core_principals_status_ck', - sql`${table.status} in ('active', 'disabled', 'archived')` - ), - ] + check('core_principals_kind_ck', sql`${table.kind} in ('human', 'service', 'integration', 'agent', 'system')`), + check('core_principals_status_ck', sql`${table.status} in ('active', 'disabled', 'archived')`), + ], ); const principalId = (columnName = 'principal_id') => uuid(columnName).notNull(); @@ -188,9 +151,7 @@ const optionalPrincipalId = (columnName = 'principal_id') => uuid(columnName); export const principalAuthBindings = coreSchema.table( 'principal_auth_bindings', { - principalAuthBindingId: uuid('principal_auth_binding_id') - .defaultRandom() - .primaryKey(), + principalAuthBindingId: uuid('principal_auth_binding_id').defaultRandom().primaryKey(), tenantId: tenantId(), principalId: principalId(), provider: text('provider').notNull(), @@ -202,15 +163,12 @@ export const principalAuthBindings = coreSchema.table( revokedAt: timestamp('revoked_at', { withTimezone: true }), }, (table) => [ - uniqueIndex('core_auth_bindings_tenant_id_uk').on( - table.tenantId, - table.principalAuthBindingId - ), + uniqueIndex('core_auth_bindings_tenant_id_uk').on(table.tenantId, table.principalAuthBindingId), uniqueIndex('core_auth_bindings_subject_uk').on( table.tenantId, table.provider, table.subjectType, - table.providerSubjectId + table.providerSubjectId, ), uniqueIndex('core_auth_bindings_api_key_subject_global_uk') .on(table.provider, table.subjectType, table.providerSubjectId) @@ -221,23 +179,14 @@ export const principalAuthBindings = coreSchema.table( foreignColumns: [principals.tenantId, principals.principalId], name: 'core_auth_bindings_tenant_principal_fk', }).onDelete('restrict'), - check( - 'core_auth_bindings_provider_ck', - sql`${table.provider} in ('better_auth')` - ), - check( - 'core_auth_bindings_subject_type_ck', - sql`${table.subjectType} in ('user', 'api_key')` - ), - check( - 'core_auth_bindings_status_ck', - sql`${table.status} in ('active', 'revoked', 'disabled')` - ), + check('core_auth_bindings_provider_ck', sql`${table.provider} in ('better_auth')`), + check('core_auth_bindings_subject_type_ck', sql`${table.subjectType} in ('user', 'api_key')`), + check('core_auth_bindings_status_ck', sql`${table.status} in ('active', 'revoked', 'disabled')`), check( 'core_auth_bindings_lifecycle_ck', - sql`(${table.status} = 'revoked' and ${table.revokedAt} is not null) or (${table.status} in ('active', 'disabled') and ${table.revokedAt} is null)` + sql`(${table.status} = 'revoked' and ${table.revokedAt} is not null) or (${table.status} in ('active', 'disabled') and ${table.revokedAt} is null)`, ), - ] + ], ); const legalEntityId = () => uuid('legal_entity_id'); @@ -247,17 +196,13 @@ const authBindingId = () => uuid('auth_binding_id'); const authContextRefColumns = () => ({ authBindingId: authBindingId(), authContextRef: text('auth_context_ref'), - impersonatedByPrincipalId: optionalPrincipalId( - 'impersonated_by_principal_id' - ), + impersonatedByPrincipalId: optionalPrincipalId('impersonated_by_principal_id'), }); export const tenantModuleStates = coreSchema.table( 'tenant_module_states', { - tenantModuleStateId: uuid('tenant_module_state_id') - .defaultRandom() - .primaryKey(), + tenantModuleStateId: uuid('tenant_module_state_id').defaultRandom().primaryKey(), tenantId: tenantId(), moduleKey: text('module_key').notNull(), state: text('state').notNull(), @@ -266,15 +211,12 @@ export const tenantModuleStates = coreSchema.table( updatedAt: updatedAt(), }, (table) => [ - uniqueIndex('core_module_states_tenant_module_uk').on( - table.tenantId, - table.moduleKey - ), + uniqueIndex('core_module_states_tenant_module_uk').on(table.tenantId, table.moduleKey), check( 'core_module_states_state_ck', - sql`${table.state} in ('inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived')` + sql`${table.state} in ('inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived')`, ), - ] + ], ); const authContextForeignKeys = ( @@ -284,7 +226,7 @@ const authContextForeignKeys = ( readonly impersonatedByPrincipalId: AnyPgColumn; readonly principalId: AnyPgColumn; readonly tenantId: AnyPgColumn; - } + }, ) => [ foreignKey({ columns: [table.tenantId, table.principalId], @@ -293,10 +235,7 @@ const authContextForeignKeys = ( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.authBindingId], - foreignColumns: [ - principalAuthBindings.tenantId, - principalAuthBindings.principalAuthBindingId, - ], + foreignColumns: [principalAuthBindings.tenantId, principalAuthBindings.principalAuthBindingId], name: `${prefix}_tenant_auth_binding_fk`, }).onDelete('restrict'), foreignKey({ @@ -309,9 +248,7 @@ const authContextForeignKeys = ( export const actionInvocations = coreSchema.table( 'action_invocations', { - actionInvocationId: uuid('action_invocation_id') - .defaultRandom() - .primaryKey(), + actionInvocationId: uuid('action_invocation_id').defaultRandom().primaryKey(), tenantId: tenantId(), legalEntityId: legalEntityId(), principalId: optionalPrincipalId(), @@ -327,33 +264,20 @@ export const actionInvocations = coreSchema.table( targetResourceId: text('target_resource_id'), status: text('status').$type().notNull(), requestHash: text('request_hash').notNull(), - startedAt: timestamp('started_at', { withTimezone: true }) - .defaultNow() - .notNull(), + startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(), completedAt: timestamp('completed_at', { withTimezone: true }), }, (table) => [ - uniqueIndex('core_action_invocations_tenant_id_uk').on( - table.tenantId, - table.actionInvocationId - ), + uniqueIndex('core_action_invocations_tenant_id_uk').on(table.tenantId, table.actionInvocationId), uniqueIndex('core_action_invocations_idempotency_uk') - .on( - table.tenantId, - table.actionKey, - table.principalId, - table.idempotencyKey - ) + .on(table.tenantId, table.actionKey, table.principalId, table.idempotencyKey) .where(sql`${table.idempotencyKey} is not null`), - index('core_action_invocations_tenant_started_idx').on( - table.tenantId, - table.startedAt - ), + index('core_action_invocations_tenant_started_idx').on(table.tenantId, table.startedAt), index('core_action_invocations_target_idx').on( table.tenantId, table.targetModuleKey, table.targetResourceType, - table.targetResourceId + table.targetResourceId, ), foreignKey({ columns: [table.tenantId, table.legalEntityId], @@ -363,13 +287,13 @@ export const actionInvocations = coreSchema.table( ...authContextForeignKeys('core_action_invocations', table), check( 'core_action_invocations_auth_method_ck', - sql`${table.authMethod} is null or ${table.authMethod} in ('session', 'api_key', 'system', 'support_impersonation')` + sql`${table.authMethod} is null or ${table.authMethod} in ('session', 'api_key', 'system', 'support_impersonation')`, ), check( 'core_action_invocations_status_ck', - sql`${table.status} in ('received', 'rejected', 'running', 'succeeded', 'failed', 'indeterminate', 'replayed')` + sql`${table.status} in ('received', 'rejected', 'running', 'succeeded', 'failed', 'indeterminate', 'replayed')`, ), - ] + ], ); const actionInvocationId = () => uuid('action_invocation_id'); @@ -377,9 +301,7 @@ const actionInvocationId = () => uuid('action_invocation_id'); export const tenantModuleStateChanges = coreSchema.table( 'tenant_module_state_changes', { - moduleStateChangeId: uuid('module_state_change_id') - .defaultRandom() - .primaryKey(), + moduleStateChangeId: uuid('module_state_change_id').defaultRandom().primaryKey(), tenantId: tenantId(), moduleKey: text('module_key').notNull(), previousState: text('previous_state'), @@ -391,11 +313,7 @@ export const tenantModuleStateChanges = coreSchema.table( occurredAt: occurredAt(), }, (table) => [ - index('core_module_state_changes_tenant_module_idx').on( - table.tenantId, - table.moduleKey, - table.occurredAt - ), + index('core_module_state_changes_tenant_module_idx').on(table.tenantId, table.moduleKey, table.occurredAt), foreignKey({ columns: [table.tenantId, table.changedByPrincipalId], foreignColumns: [principals.tenantId, principals.principalId], @@ -403,21 +321,15 @@ export const tenantModuleStateChanges = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_module_state_changes_tenant_invocation_fk', }).onDelete('restrict'), - check( - 'core_module_state_changes_source_ck', - sql`${table.changeSource} in ('user', 'support', 'system')` - ), + check('core_module_state_changes_source_ck', sql`${table.changeSource} in ('user', 'support', 'system')`), check( 'core_module_state_changes_new_state_ck', - sql`${table.newState} in ('inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived')` + sql`${table.newState} in ('inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived')`, ), - ] + ], ); export const auditEvents = coreSchema.table( @@ -444,14 +356,8 @@ export const auditEvents = coreSchema.table( occurredAt: occurredAt(), }, (table) => [ - uniqueIndex('core_audit_events_tenant_id_uk').on( - table.tenantId, - table.auditEventId - ), - index('core_audit_events_tenant_occurred_idx').on( - table.tenantId, - table.occurredAt - ), + uniqueIndex('core_audit_events_tenant_id_uk').on(table.tenantId, table.auditEventId), + index('core_audit_events_tenant_occurred_idx').on(table.tenantId, table.occurredAt), index('core_audit_events_action_idx').on(table.actionInvocationId), foreignKey({ columns: [table.tenantId, table.legalEntityId], @@ -460,26 +366,17 @@ export const auditEvents = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_audit_events_tenant_invocation_fk', }).onDelete('restrict'), ...authContextForeignKeys('core_audit_events', table), - check( - 'core_audit_events_outcome_ck', - sql`${table.outcome} in ('allowed', 'denied', 'succeeded', 'failed')` - ), + check('core_audit_events_outcome_ck', sql`${table.outcome} in ('allowed', 'denied', 'succeeded', 'failed')`), check( 'core_audit_events_stage_ck', - sql`${table.outcomeStage} in ('system', 'authn', 'authz', 'policy', 'validation', 'execution')` - ), - check( - 'core_audit_events_profile_ck', - sql`${table.auditProfile} in ('standard', 'sensitive', 'minimal')` + sql`${table.outcomeStage} in ('system', 'authn', 'authz', 'policy', 'validation', 'execution')`, ), - ] + check('core_audit_events_profile_ck', sql`${table.auditProfile} in ('standard', 'sensitive', 'minimal')`), + ], ); const auditEventId = () => uuid('audit_event_id'); @@ -487,9 +384,7 @@ const auditEventId = () => uuid('audit_event_id'); export const dataAccessEvents = coreSchema.table( 'data_access_events', { - dataAccessEventId: uuid('data_access_event_id') - .defaultRandom() - .primaryKey(), + dataAccessEventId: uuid('data_access_event_id').defaultRandom().primaryKey(), tenantId: tenantId(), legalEntityId: legalEntityId(), actionInvocationId: actionInvocationId(), @@ -515,14 +410,8 @@ export const dataAccessEvents = coreSchema.table( occurredAt: occurredAt(), }, (table) => [ - uniqueIndex('core_data_access_events_tenant_id_uk').on( - table.tenantId, - table.dataAccessEventId - ), - index('core_data_access_events_tenant_occurred_idx').on( - table.tenantId, - table.occurredAt - ), + uniqueIndex('core_data_access_events_tenant_id_uk').on(table.tenantId, table.dataAccessEventId), + index('core_data_access_events_tenant_occurred_idx').on(table.tenantId, table.occurredAt), foreignKey({ columns: [table.tenantId, table.legalEntityId], foreignColumns: [legalEntities.tenantId, legalEntities.legalEntityId], @@ -530,34 +419,28 @@ export const dataAccessEvents = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_data_access_events_tenant_invocation_fk', }).onDelete('restrict'), ...authContextForeignKeys('core_data_access_events', table), - check( - 'core_data_access_events_outcome_ck', - sql`${table.outcome} in ('allowed', 'denied', 'failed')` - ), + check('core_data_access_events_outcome_ck', sql`${table.outcome} in ('allowed', 'denied', 'failed')`), check( 'core_data_access_events_stage_ck', - sql`${table.outcomeStage} in ('authn', 'context', 'module_state', 'authz', 'policy', 'execution', 'evidence')` + sql`${table.outcomeStage} in ('authn', 'context', 'module_state', 'authz', 'policy', 'execution', 'evidence')`, ), check( 'core_data_access_events_access_kind_ck', - sql`${table.accessKind} in ('read', 'list', 'search', 'export', 'download')` + sql`${table.accessKind} in ('read', 'list', 'search', 'export', 'download')`, ), check( 'core_data_access_events_capture_mode_ck', - sql`${table.evidenceCaptureMode} in ('metadata_only', 'hash_only', 'redacted_payload', 'stored_artifact')` + sql`${table.evidenceCaptureMode} in ('metadata_only', 'hash_only', 'redacted_payload', 'stored_artifact')`, ), check( 'core_data_access_events_redaction_ck', - sql`(${table.evidenceCaptureMode} = 'redacted_payload' and ${table.redactionProfile} is not null and ${table.evidencePayloadJson} is not null) or (${table.evidenceCaptureMode} <> 'redacted_payload' and ${table.redactionProfile} is null)` + sql`(${table.evidenceCaptureMode} = 'redacted_payload' and ${table.redactionProfile} is not null and ${table.evidencePayloadJson} is not null) or (${table.evidenceCaptureMode} <> 'redacted_payload' and ${table.redactionProfile} is null)`, ), - ] + ], ); const dataAccessEventId = () => uuid('data_access_event_id'); @@ -581,26 +464,18 @@ export const domainEvents = coreSchema.table( // every tenant stream, permits gaps, and is safe across concurrent // transactions without application-side max + 1 allocation. tenantSequenceNo: bigint('tenant_sequence_no', { mode: 'bigint' }) - .default( - sql`nextval('core.domain_event_tenant_sequence_no_seq'::regclass)` - ) + .default(sql`nextval('core.domain_event_tenant_sequence_no_seq'::regclass)`) .notNull(), occurredAt: occurredAt(), }, (table) => [ - uniqueIndex('core_domain_events_tenant_id_uk').on( - table.tenantId, - table.domainEventId - ), - uniqueIndex('core_domain_events_tenant_sequence_uk').on( - table.tenantId, - table.tenantSequenceNo - ), + uniqueIndex('core_domain_events_tenant_id_uk').on(table.tenantId, table.domainEventId), + uniqueIndex('core_domain_events_tenant_sequence_uk').on(table.tenantId, table.tenantSequenceNo), index('core_domain_events_subject_idx').on( table.tenantId, table.subjectModuleKey, table.subjectResourceType, - table.subjectResourceId + table.subjectResourceId, ), foreignKey({ columns: [table.tenantId, table.legalEntityId], @@ -609,13 +484,10 @@ export const domainEvents = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_domain_events_tenant_invocation_fk', }).onDelete('restrict'), - ] + ], ); const domainEventId = () => uuid('domain_event_id'); @@ -643,7 +515,7 @@ export const outboxMessages = coreSchema.table( foreignColumns: [domainEvents.tenantId, domainEvents.domainEventId], name: 'core_outbox_messages_tenant_domain_event_fk', }).onDelete('restrict'), - ] + ], ); export const outboxDeliveries = coreSchema.table( @@ -659,9 +531,7 @@ export const outboxDeliveries = coreSchema.table( consumerModuleKey: text('consumer_module_key').notNull(), status: text('status').default('pending').notNull(), attemptsCount: integer('attempts_count').default(0).notNull(), - availableAt: timestamp('available_at', { withTimezone: true }) - .defaultNow() - .notNull(), + availableAt: timestamp('available_at', { withTimezone: true }).defaultNow().notNull(), claimedBy: text('claimed_by'), claimedAt: timestamp('claimed_at', { withTimezone: true }), claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }), @@ -669,27 +539,15 @@ export const outboxDeliveries = coreSchema.table( updatedAt: updatedAt(), }, (table) => [ - uniqueIndex('core_outbox_deliveries_message_worker_uk').on( - table.outboxMessageId, - table.workerKey - ), + uniqueIndex('core_outbox_deliveries_message_worker_uk').on(table.outboxMessageId, table.workerKey), index('core_outbox_deliveries_pending_idx') .on(table.availableAt) .where(sql`${table.status} = 'pending'`), index('core_outbox_deliveries_message_idx').on(table.outboxMessageId), - index('core_outbox_deliveries_worker_status_idx').on( - table.workerKey, - table.status - ), - check( - 'core_outbox_deliveries_status_ck', - sql`${table.status} in ('pending', 'processing', 'done', 'dead')` - ), - check( - 'core_outbox_deliveries_attempts_count_ck', - sql`${table.attemptsCount} >= 0` - ), - ] + index('core_outbox_deliveries_worker_status_idx').on(table.workerKey, table.status), + check('core_outbox_deliveries_status_ck', sql`${table.status} in ('pending', 'processing', 'done', 'dead')`), + check('core_outbox_deliveries_attempts_count_ck', sql`${table.attemptsCount} >= 0`), + ], ); export const outboxAttempts = coreSchema.table( @@ -701,18 +559,11 @@ export const outboxAttempts = coreSchema.table( .references(() => outboxDeliveries.outboxDeliveryId, { onDelete: 'cascade', }), - startedAt: timestamp('started_at', { withTimezone: true }) - .defaultNow() - .notNull(), + startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(), finishedAt: timestamp('finished_at', { withTimezone: true }), errorMessage: text('error_message'), }, - (table) => [ - index('core_outbox_attempts_delivery_started_idx').on( - table.outboxDeliveryId, - table.startedAt - ), - ] + (table) => [index('core_outbox_attempts_delivery_started_idx').on(table.outboxDeliveryId, table.startedAt)], ); export const mediaAssets = coreSchema.table( @@ -738,14 +589,11 @@ export const mediaAssets = coreSchema.table( updatedAt: updatedAt(), }, (table) => [ - uniqueIndex('core_media_assets_tenant_id_uk').on( - table.tenantId, - table.mediaAssetId - ), + uniqueIndex('core_media_assets_tenant_id_uk').on(table.tenantId, table.mediaAssetId), uniqueIndex('core_media_assets_storage_uk').on( table.storageProvider, table.storageKey, - table.storageObjectVersionRef + table.storageObjectVersionRef, ), index('core_media_assets_tenant_idx').on(table.tenantId), foreignKey({ @@ -760,13 +608,13 @@ export const mediaAssets = coreSchema.table( }).onDelete('restrict'), check( 'core_media_assets_ingestion_source_ck', - sql`${table.ingestionSource} in ('user', 'integration', 'import', 'system')` + sql`${table.ingestionSource} in ('user', 'integration', 'import', 'system')`, ), check( 'core_media_assets_processing_status_ck', - sql`${table.processingStatus} in ('uploaded', 'scanning', 'ready', 'failed')` + sql`${table.processingStatus} in ('uploaded', 'scanning', 'ready', 'failed')`, ), - ] + ], ); const mediaAssetId = () => uuid('media_asset_id').notNull(); @@ -792,7 +640,7 @@ export const mediaLinks = coreSchema.table( table.tenantId, table.targetModuleKey, table.targetResourceType, - table.targetResourceId + table.targetResourceId, ), foreignKey({ columns: [table.tenantId, table.mediaAssetId], @@ -806,25 +654,17 @@ export const mediaLinks = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_media_links_tenant_invocation_fk', }).onDelete('restrict'), - check( - 'core_media_links_source_ck', - sql`${table.linkSource} in ('user', 'integration', 'import', 'system')` - ), - ] + check('core_media_links_source_ck', sql`${table.linkSource} in ('user', 'integration', 'import', 'system')`), + ], ); export const evidenceReferences = coreSchema.table( 'evidence_references', { - evidenceReferenceId: uuid('evidence_reference_id') - .defaultRandom() - .primaryKey(), + evidenceReferenceId: uuid('evidence_reference_id').defaultRandom().primaryKey(), tenantId: tenantId(), legalEntityId: legalEntityId(), mediaAssetId: mediaAssetId(), @@ -867,7 +707,7 @@ export const evidenceReferences = coreSchema.table( table.tenantId, table.subjectModuleKey, table.subjectResourceType, - table.subjectResourceId + table.subjectResourceId, ), foreignKey({ columns: [table.tenantId, table.legalEntityId], @@ -881,10 +721,7 @@ export const evidenceReferences = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.actionInvocationId], - foreignColumns: [ - actionInvocations.tenantId, - actionInvocations.actionInvocationId, - ], + foreignColumns: [actionInvocations.tenantId, actionInvocations.actionInvocationId], name: 'core_evidence_tenant_invocation_fk', }).onDelete('restrict'), foreignKey({ @@ -894,10 +731,7 @@ export const evidenceReferences = coreSchema.table( }).onDelete('restrict'), foreignKey({ columns: [table.tenantId, table.dataAccessEventId], - foreignColumns: [ - dataAccessEvents.tenantId, - dataAccessEvents.dataAccessEventId, - ], + foreignColumns: [dataAccessEvents.tenantId, dataAccessEvents.dataAccessEventId], name: 'core_evidence_tenant_data_access_fk', }).onDelete('restrict'), foreignKey({ @@ -907,34 +741,32 @@ export const evidenceReferences = coreSchema.table( }).onDelete('restrict'), check( 'core_evidence_references_source_kind_ck', - sql`${table.sourceKind} in ('action', 'audit', 'data_access', 'domain_event')` + sql`${table.sourceKind} in ('action', 'audit', 'data_access', 'domain_event')`, ), check( 'core_evidence_references_source_one_ck', - sql`num_nonnulls(${table.actionInvocationId}, ${table.auditEventId}, ${table.dataAccessEventId}, ${table.domainEventId}) = 1` + sql`num_nonnulls(${table.actionInvocationId}, ${table.auditEventId}, ${table.dataAccessEventId}, ${table.domainEventId}) = 1`, ), check( 'core_evidence_references_subject_all_ck', - sql`num_nonnulls(${table.subjectModuleKey}, ${table.subjectResourceType}, ${table.subjectResourceId}) in (0, 3)` + sql`num_nonnulls(${table.subjectModuleKey}, ${table.subjectResourceType}, ${table.subjectResourceId}) in (0, 3)`, ), check( 'core_evidence_references_disposition_ck', - sql`${table.dispositionStatus} in ('active', 'expired', 'deleted', 'legal_hold')` + sql`${table.dispositionStatus} in ('active', 'expired', 'deleted', 'legal_hold')`, ), check( 'core_evidence_references_classification_ck', - sql`${table.dataClassification} in ('internal', 'confidential', 'restricted')` + sql`${table.dataClassification} in ('internal', 'confidential', 'restricted')`, ), - ] + ], ); export const searchIndexEntries = enableCoreGovernedRls( coreSchema.table( 'search_index_entries', { - searchIndexEntryId: uuid('search_index_entry_id') - .defaultRandom() - .primaryKey(), + searchIndexEntryId: uuid('search_index_entry_id').defaultRandom().primaryKey(), tenantId: tenantId(), legalEntityId: legalEntityId(), sourceModuleKey: text('source_module_key').notNull(), @@ -957,14 +789,14 @@ export const searchIndexEntries = enableCoreGovernedRls( table.tenantId, table.sourceModuleKey, table.sourceResourceType, - table.sourceResourceId + table.sourceResourceId, ), index('core_search_index_entries_query_idx').on( table.tenantId, table.sourceModuleKey, table.sourceResourceType, table.legalEntityId, - table.deleted + table.deleted, ), foreignKey({ columns: [table.tenantId, table.legalEntityId], @@ -973,12 +805,9 @@ export const searchIndexEntries = enableCoreGovernedRls( }).onDelete('restrict'), check( 'core_search_index_entries_document_ck', - sql`${table.deleted} or (length(btrim(${table.title})) > 0 and length(${table.title}) <= 300 and length(${table.bodyText}) <= 40000)` - ), - check( - 'core_search_index_entries_version_ck', - sql`${table.projectionVersion} > 0` + sql`${table.deleted} or (length(btrim(${table.title})) > 0 and length(${table.title}) <= 300 and length(${table.bodyText}) <= 40000)`, ), + check('core_search_index_entries_version_ck', sql`${table.projectionVersion} > 0`), pgPolicy('core_search_index_entries_tenant_select', { for: 'select', to: 'ontos_runtime', @@ -1000,8 +829,8 @@ export const searchIndexEntries = enableCoreGovernedRls( to: 'ontos_runtime', using: sql`${table.tenantId} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`, }), - ] - ) + ], + ), ); /** Core-only snapshot ordering; event sequence allocation is not commit ordering. */ @@ -1020,10 +849,7 @@ export const searchProjectionGenerations = enableCoreGovernedRls( name: 'core_search_projection_generations_pk', columns: [table.tenantId, table.sourceModuleKey], }), - check( - 'core_search_projection_generations_positive_ck', - sql`${table.generation} > 0` - ), + check('core_search_projection_generations_positive_ck', sql`${table.generation} > 0`), pgPolicy('core_search_projection_generations_tenant_select', { for: 'select', to: 'ontos_runtime', @@ -1040,8 +866,8 @@ export const searchProjectionGenerations = enableCoreGovernedRls( using: sql`${table.tenantId} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`, withCheck: sql`${table.tenantId} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`, }), - ] - ) + ], + ), ); /** Atomic projection-unit floor protects even resources never seen before a rebuild. */ @@ -1059,20 +885,10 @@ export const searchProjectionRebuilds = enableCoreGovernedRls( (table) => [ primaryKey({ name: 'core_search_projection_rebuilds_pk', - columns: [ - table.tenantId, - table.sourceModuleKey, - table.sourceResourceType, - ], + columns: [table.tenantId, table.sourceModuleKey, table.sourceResourceType], }), - check( - 'core_search_projection_rebuilds_version_ck', - sql`${table.rebuildVersion} > 0` - ), - check( - 'core_search_projection_rebuilds_fingerprint_ck', - sql`${table.fingerprint} ~ '^[a-f0-9]{64}$'` - ), + check('core_search_projection_rebuilds_version_ck', sql`${table.rebuildVersion} > 0`), + check('core_search_projection_rebuilds_fingerprint_ck', sql`${table.fingerprint} ~ '^[a-f0-9]{64}$'`), pgPolicy('core_search_projection_rebuilds_tenant_select', { for: 'select', to: 'ontos_runtime', @@ -1089,8 +905,8 @@ export const searchProjectionRebuilds = enableCoreGovernedRls( using: sql`${table.tenantId} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`, withCheck: sql`${table.tenantId} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`, }), - ] - ) + ], + ), ); export const workerCheckpoints = coreSchema.table( @@ -1109,7 +925,7 @@ export const workerCheckpoints = coreSchema.table( name: 'core_worker_checkpoints_pk', columns: [table.tenantId, table.consumerName, table.streamKey], }), - ] + ], ); export const coreDatabaseSchema = { diff --git a/app/packages/core-runtime/src/db/scoped-transaction.ts b/app/packages/core-runtime/src/db/scoped-transaction.ts index 220c20ee9..89b211b1a 100644 --- a/app/packages/core-runtime/src/db/scoped-transaction.ts +++ b/app/packages/core-runtime/src/db/scoped-transaction.ts @@ -8,9 +8,7 @@ import type { OperationalScope } from '../operations/context.ts'; import { OperationContextUnavailable } from '../operations/errors.ts'; import type { CoreTransaction } from './types.ts'; -const scopedTransaction: unique symbol = Symbol( - '@app/core-runtime/db/scoped-transaction' -); +const scopedTransaction: unique symbol = Symbol('@app/core-runtime/db/scoped-transaction'); /** Private owner-factory capability. It is never supplied to an Action or read handler. */ export interface ScopedTransactionExecutor { @@ -29,15 +27,10 @@ interface SettingRow extends Record { export interface OperationalScopeTransactionService { readonly delete: CoreTransaction['delete']; readonly insert: CoreTransaction['insert']; - readonly install: ( - scope: OperationalScope - ) => Effect.Effect; + readonly install: (scope: OperationalScope) => Effect.Effect; readonly select: CoreTransaction['select']; readonly update: CoreTransaction['update']; - readonly verify: Effect.Effect< - Option.Option, - OperationContextUnavailable - >; + readonly verify: Effect.Effect, OperationContextUnavailable>; } export class OperationalScopeTransaction extends Context.Service< @@ -60,7 +53,7 @@ const operationContextUnavailable = (cause?: unknown) => { }; const operationalScopeTransactionFromCoreTransaction = ( - transaction: CoreTransaction + transaction: CoreTransaction, ): OperationalScopeTransactionService => ({ delete: transaction.delete.bind(transaction), insert: transaction.insert.bind(transaction), @@ -68,7 +61,7 @@ const operationalScopeTransactionFromCoreTransaction = ( transaction .execute( sql`select set_config('ontos.tenant_id', ${scope.tenantId}, true), set_config('ontos.legal_entity_id', ${scope.legalEntityId ?? ''}, true)`, - 'objects' + 'objects', ) .pipe(Effect.mapError(operationContextUnavailable), Effect.asVoid), select: transaction.select.bind(transaction), @@ -80,50 +73,44 @@ const operationalScopeTransactionFromCoreTransaction = ( current_setting('ontos.tenant_id', true) as tenant_id, current_setting('ontos.legal_entity_id', true) as legal_entity_id `, - 'objects' + 'objects', ) .pipe( Effect.mapError(operationContextUnavailable), - Effect.map((verified) => Option.fromUndefinedOr(verified[0])) + Effect.map((verified) => Option.fromUndefinedOr(verified[0])), ), }); -export const installOperationalScopeFromTransactionService = Effect.fn( - 'installOperationalScopeFromTransactionService' -)(function* installOperationalScopeFromTransactionServiceEffect( - scope: OperationalScope -) { - const transaction = yield* OperationalScopeTransaction; - yield* transaction.install(scope); - const setting = yield* transaction.verify; - if ( - Option.isNone(setting) || - setting.value.tenant_id !== scope.tenantId || - setting.value.legal_entity_id !== (scope.legalEntityId ?? '') - ) { - return yield* operationContextUnavailable(); - } - return Object.freeze({ - delete: transaction.delete.bind(transaction), - insert: transaction.insert.bind(transaction), - [scopedTransaction]: true as const, - select: transaction.select.bind(transaction), - update: transaction.update.bind(transaction), - }); -}); +export const installOperationalScopeFromTransactionService = Effect.fn('installOperationalScopeFromTransactionService')( + function* installOperationalScopeFromTransactionServiceEffect(scope: OperationalScope) { + const transaction = yield* OperationalScopeTransaction; + yield* transaction.install(scope); + const setting = yield* transaction.verify; + if ( + Option.isNone(setting) || + setting.value.tenant_id !== scope.tenantId || + setting.value.legal_entity_id !== (scope.legalEntityId ?? '') + ) { + return yield* operationContextUnavailable(); + } + return Object.freeze({ + delete: transaction.delete.bind(transaction), + insert: transaction.insert.bind(transaction), + [scopedTransaction]: true as const, + select: transaction.select.bind(transaction), + update: transaction.update.bind(transaction), + }); + }, +); export const installOperationalScope = ( transaction: CoreTransaction, - scope: OperationalScope + scope: OperationalScope, ): Effect.Effect => installOperationalScopeFromTransactionService(scope).pipe( Effect.updateContext((context: Context.Context) => - Context.add( - context, - OperationalScopeTransaction, - operationalScopeTransactionFromCoreTransaction(transaction) - ) - ) + Context.add(context, OperationalScopeTransaction, operationalScopeTransactionFromCoreTransaction(transaction)), + ), ); const operationalRlsPolicies = (prefix: string, predicate: SQL) => @@ -151,10 +138,7 @@ const operationalRlsPolicies = (prefix: string, predicate: SQL) => }), ] as const; -export const tenantRlsPolicies = ( - prefix: string, - tenantColumn: AnyPgColumn -) => { +export const tenantRlsPolicies = (prefix: string, tenantColumn: AnyPgColumn) => { const predicate = sql`${tenantColumn} = nullif(current_setting('ontos.tenant_id', true), '')::uuid`; return operationalRlsPolicies(prefix, predicate); }; @@ -162,7 +146,7 @@ export const tenantRlsPolicies = ( export const tenantLegalEntityRlsPolicies = ( prefix: string, tenantColumn: AnyPgColumn, - legalEntityColumn: AnyPgColumn + legalEntityColumn: AnyPgColumn, ) => { const predicate = sql`${tenantColumn} = nullif(current_setting('ontos.tenant_id', true), '')::uuid and ${legalEntityColumn} = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; return operationalRlsPolicies(prefix, predicate); diff --git a/app/packages/core-runtime/src/db/types.ts b/app/packages/core-runtime/src/db/types.ts index 478840ac2..99401d28b 100644 --- a/app/packages/core-runtime/src/db/types.ts +++ b/app/packages/core-runtime/src/db/types.ts @@ -4,9 +4,7 @@ import type { coreRelations } from './schema.ts'; export type CoreDatabaseExecutor = EffectPgDatabase; -type CoreTransactionCallback = Parameters< - CoreDatabaseExecutor['transaction'] ->[0]; +type CoreTransactionCallback = Parameters[0]; export type CoreTransaction = Parameters[0]; diff --git a/app/packages/core-runtime/src/environment/dotenv-provider.ts b/app/packages/core-runtime/src/environment/dotenv-provider.ts index 3eca99d21..d0c0b06f5 100644 --- a/app/packages/core-runtime/src/environment/dotenv-provider.ts +++ b/app/packages/core-runtime/src/environment/dotenv-provider.ts @@ -2,46 +2,40 @@ import { ConfigProvider, Effect, Match, Predicate } from 'effect'; const nodeFileSystem = process.getBuiltinModule('node:fs'); -export const loadDotEnvProvider = Effect.fn('Config.loadDotEnvProvider')( - function* loadProvider( - envPath: string, - configFailure: (reason: string, cause: unknown) => Failure - ) { - const result = yield* Effect.sync(() => { - try { - return { - contents: nodeFileSystem.readFileSync(envPath, 'utf-8'), - status: 'loaded', - } as const; - } catch (error) { - if ( - Predicate.hasProperty(error, 'code') && - (error.code === 'ENOENT' || - error.code === 'NOT_FOUND_DOTENV_ENVIRONMENT') - ) { - return { status: 'missing' } as const; - } - return { - error: configFailure( - `Unable to load the root environment from ${envPath}`, - error - ), - status: 'failed', - } as const; +export const loadDotEnvProvider = Effect.fn('Config.loadDotEnvProvider')(function* loadProvider( + envPath: string, + configFailure: (reason: string, cause: unknown) => Failure, +) { + const result = yield* Effect.sync(() => { + try { + return { + contents: nodeFileSystem.readFileSync(envPath, 'utf-8'), + status: 'loaded', + } as const; + } catch (error) { + if ( + Predicate.hasProperty(error, 'code') && + (error.code === 'ENOENT' || error.code === 'NOT_FOUND_DOTENV_ENVIRONMENT') + ) { + return { status: 'missing' } as const; } - }); + return { + error: configFailure(`Unable to load the root environment from ${envPath}`, error), + status: 'failed', + } as const; + } + }); - return yield* Match.value(result).pipe( - Match.discriminatorsExhaustive('status')({ - failed: ({ error }) => Effect.fail(error), - loaded: ({ contents }) => - Effect.succeed( - ConfigProvider.fromDotEnvContents(contents, { - preserveEmptyStrings: true, - }) - ), - missing: () => Effect.succeed(ConfigProvider.fromUnknown({})), - }) - ); - } -); + return yield* Match.value(result).pipe( + Match.discriminatorsExhaustive('status')({ + failed: ({ error }) => Effect.fail(error), + loaded: ({ contents }) => + Effect.succeed( + ConfigProvider.fromDotEnvContents(contents, { + preserveEmptyStrings: true, + }), + ), + missing: () => Effect.succeed(ConfigProvider.fromUnknown({})), + }), + ); +}); diff --git a/app/packages/core-runtime/src/environment/drizzle-config.ts b/app/packages/core-runtime/src/environment/drizzle-config.ts index 13e826c98..1514fd213 100644 --- a/app/packages/core-runtime/src/environment/drizzle-config.ts +++ b/app/packages/core-runtime/src/environment/drizzle-config.ts @@ -7,23 +7,15 @@ const nodeFileSystem = process.getBuiltinModule('node:fs'); const nodeProcess = process.getBuiltinModule('node:process'); const nodeUtilities = process.getBuiltinModule('node:util'); const fileConfig = nodeFileSystem.existsSync(APP_ENV_PATH) - ? Result.getOrThrow( - Result.try(() => - nodeUtilities.parseEnv( - nodeFileSystem.readFileSync(APP_ENV_PATH, 'utf-8') - ) - ) - ) + ? Result.getOrThrow(Result.try(() => nodeUtilities.parseEnv(nodeFileSystem.readFileSync(APP_ENV_PATH, 'utf-8')))) : {}; const configValues = { ...fileConfig, ...nodeProcess.env }; const databaseUrl = Redacted.value( Result.getOrThrow( - Schema.decodeUnknownResult( - Schema.RedactedFromValue( - Schema.Trim.pipe(Schema.check(Schema.isMinLength(1))) - ) - )(configValues['DATABASE_ADMIN_URL']) - ) + Schema.decodeUnknownResult(Schema.RedactedFromValue(Schema.Trim.pipe(Schema.check(Schema.isMinLength(1)))))( + configValues['DATABASE_ADMIN_URL'], + ), + ), ); export const defineWorkspaceDrizzleConfig = (options: { diff --git a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs index 200a97cef..6b7affe79 100644 --- a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs +++ b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.cjs @@ -1,22 +1,14 @@ /* oxlint-disable typescript/consistent-return, typescript/no-unsafe-argument -- Existing compatibility boundary; expires: 2026-12-31. */ const { existsSync } = process.getBuiltinModule('node:fs'); const path = process.getBuiltinModule('node:path'); -const ambientEnvironmentDescriptor = Object.getOwnPropertyDescriptor( - process, - 'env' -); +const ambientEnvironmentDescriptor = Object.getOwnPropertyDescriptor(process, 'env'); const environmentValue = (name) => { const variableDescriptor = ambientEnvironmentDescriptor === undefined ? undefined - : Object.getOwnPropertyDescriptor( - ambientEnvironmentDescriptor.value, - name - ); - return variableDescriptor === undefined - ? undefined - : String(variableDescriptor.value); + : Object.getOwnPropertyDescriptor(ambientEnvironmentDescriptor.value, name); + return variableDescriptor === undefined ? undefined : String(variableDescriptor.value); }; const isAppWorkspace = (candidate) => @@ -46,13 +38,9 @@ const resolveAppWorkspaceRootSync = (startDirectory) => { * @returns {{ APP_ENV_PATH: string, APP_WORKSPACE_ROOT: string }} The resolved workspace paths. */ const resolveWorkspaceEnvironmentSync = (candidates) => { - const usableCandidates = candidates.filter( - (candidate) => candidate !== undefined && candidate.length > 0 - ); + const usableCandidates = candidates.filter((candidate) => candidate !== undefined && candidate.length > 0); const APP_WORKSPACE_ROOT = - usableCandidates - .map(resolveAppWorkspaceRootSync) - .find((candidate) => candidate !== undefined) ?? candidates[1]; + usableCandidates.map(resolveAppWorkspaceRootSync).find((candidate) => candidate !== undefined) ?? candidates[1]; return { APP_ENV_PATH: path.join(APP_WORKSPACE_ROOT, '.env'), APP_WORKSPACE_ROOT, diff --git a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts index 5c5b6e6a9..6e2d7451e 100644 --- a/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts +++ b/app/packages/core-runtime/src/environment/workspace-environment-bootstrap.d.cts @@ -1,9 +1,7 @@ declare const bootstrapEnvironment: { readonly APP_ENV_PATH: string; readonly APP_WORKSPACE_ROOT: string; - readonly resolveAppWorkspaceRootSync: ( - startDirectory: string - ) => string | undefined; + readonly resolveAppWorkspaceRootSync: (startDirectory: string) => string | undefined; }; export = bootstrapEnvironment; diff --git a/app/packages/core-runtime/src/environment/workspace-environment.ts b/app/packages/core-runtime/src/environment/workspace-environment.ts index 5bdc19589..bb94487c4 100644 --- a/app/packages/core-runtime/src/environment/workspace-environment.ts +++ b/app/packages/core-runtime/src/environment/workspace-environment.ts @@ -2,28 +2,21 @@ import { Effect, FileSystem, Option, Path } from 'effect'; import bootstrapEnvironment from './workspace-environment-bootstrap.cjs'; -const isAppWorkspace = Effect.fn('WorkspaceEnvironment.isAppWorkspace')( - function* isAppWorkspaceEffect(candidate: string) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - return ( - (yield* fileSystem.exists(path.join(candidate, 'pnpm-workspace.yaml'))) && - (yield* fileSystem.exists( - path.join(candidate, 'packages/core-runtime/package.json') - )) - ); - } -); +const isAppWorkspace = Effect.fn('WorkspaceEnvironment.isAppWorkspace')(function* isAppWorkspaceEffect( + candidate: string, +) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return ( + (yield* fileSystem.exists(path.join(candidate, 'pnpm-workspace.yaml'))) && + (yield* fileSystem.exists(path.join(candidate, 'packages/core-runtime/package.json'))) + ); +}); -const workspaceCandidates = ( - path: Path.Path, - candidate: string -): readonly string[] => { +const workspaceCandidates = (path: Path.Path, candidate: string): readonly string[] => { const nestedApp = path.join(candidate, 'app'); const parent = path.dirname(candidate); - return parent === candidate - ? [candidate, nestedApp] - : [candidate, nestedApp, ...workspaceCandidates(path, parent)]; + return parent === candidate ? [candidate, nestedApp] : [candidate, nestedApp, ...workspaceCandidates(path, parent)]; }; /** @@ -32,22 +25,18 @@ const workspaceCandidates = ( * Modern.js bundles server modules into a cache directory, so module-relative * paths do not identify the source workspace at runtime. */ -export const resolveAppWorkspaceRootEffect = Effect.fn( - 'WorkspaceEnvironment.resolveAppWorkspaceRootEffect' -)(function* resolveAppWorkspaceRootEffect(startDirectory: string) { - const path = yield* Path.Path; - const candidates = workspaceCandidates(path, path.resolve(startDirectory)); - return Option.getOrUndefined( - yield* Effect.findFirst(candidates, isAppWorkspace) - ); -}); +export const resolveAppWorkspaceRootEffect = Effect.fn('WorkspaceEnvironment.resolveAppWorkspaceRootEffect')( + function* resolveAppWorkspaceRootEffect(startDirectory: string) { + const path = yield* Path.Path; + const candidates = workspaceCandidates(path, path.resolve(startDirectory)); + return Option.getOrUndefined(yield* Effect.findFirst(candidates, isAppWorkspace)); + }, +); -export const resolveAppWorkspaceRoot: ( - startDirectory: string -) => string | undefined = bootstrapEnvironment.resolveAppWorkspaceRootSync; +export const resolveAppWorkspaceRoot: (startDirectory: string) => string | undefined = + bootstrapEnvironment.resolveAppWorkspaceRootSync; /** The application workspace owns the single local environment file. */ -export const APP_WORKSPACE_ROOT: string = - bootstrapEnvironment.APP_WORKSPACE_ROOT; +export const APP_WORKSPACE_ROOT: string = bootstrapEnvironment.APP_WORKSPACE_ROOT; export const APP_ENV_PATH: string = bootstrapEnvironment.APP_ENV_PATH; diff --git a/app/packages/core-runtime/src/http/governed-read.ts b/app/packages/core-runtime/src/http/governed-read.ts index 637936e90..d3b773a9c 100644 --- a/app/packages/core-runtime/src/http/governed-read.ts +++ b/app/packages/core-runtime/src/http/governed-read.ts @@ -45,18 +45,10 @@ export interface GovernedReadHttpProblemSet< readonly unavailable: ProblemFactory; } -export type GovernedReadPrincipalAuthentication< - Requirements, - Authentication, - Unavailable, -> = ( +export type GovernedReadPrincipalAuthentication = ( authorization: Redacted.Redacted, - problems: PrincipalAuthenticationProblems -) => Effect.Effect< - TrustedPrincipalContext, - Authentication | Unavailable, - Requirements ->; + problems: PrincipalAuthenticationProblems, +) => Effect.Effect; interface PrincipalAuthenticationProblems { readonly authentication: ProblemFactory; @@ -84,16 +76,8 @@ export function classifyReadCoreError< PolicyConflict, PolicyIneligible, Unavailable - > -): - | Authentication - | Forbidden - | Internal - | Invalid - | NotFound - | PolicyConflict - | PolicyIneligible - | Unavailable; + >, +): Authentication | Forbidden | Internal | Invalid | NotFound | PolicyConflict | PolicyIneligible | Unavailable; export function classifyReadCoreError( error: ReadCoreError, problems: GovernedReadHttpProblemSet< @@ -105,14 +89,10 @@ export function classifyReadCoreError( HttpProblem<409>, HttpProblem<422>, HttpProblem<503> - > + >, ): HttpProblem { - const readPolicyDenied = ( - denial: Extract - ) => - denial.httpStatus === 409 - ? problems.policyConflict() - : problems.policyIneligible(); + const readPolicyDenied = (denial: Extract) => + denial.httpStatus === 409 ? problems.policyConflict() : problems.policyIneligible(); return Match.value(error).pipe( Match.tags({ ModuleStateCheckUnavailableError: problems.unavailable, @@ -133,21 +113,16 @@ export function classifyReadCoreError( ReadPolicyEvaluationError: problems.unavailable, ReadResultValidationError: problems.internal, }), - Match.exhaustive + Match.exhaustive, ); } -const bearerChallenge = HttpEffect.appendPreResponseHandler( - (_request, response) => - Effect.succeed( - HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer') - ) +const bearerChallenge = HttpEffect.appendPreResponseHandler((_request, response) => + Effect.succeed(HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer')), ); const failProblem = >(problem: Problem) => - (problem.status === 401 ? bearerChallenge : Effect.void).pipe( - Effect.andThen(Effect.fail(problem)) - ); + (problem.status === 401 ? bearerChallenge : Effect.void).pipe(Effect.andThen(Effect.fail(problem))); interface GovernedReadRequest { readonly payload: Payload; @@ -186,14 +161,7 @@ export const makeGovernedReadHttpHandler = < PolicyIneligible, Unavailable >; - readonly registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - HandlerError, - ReadRequirements - >; + readonly registration: ReadRegistration; }) => Effect.fn('GovernedReadHttp.handle')(function* handleGovernedRead({ payload, @@ -205,13 +173,10 @@ export const makeGovernedReadHttpHandler = < if (correlationId === undefined || correlationId.trim().length === 0) { return yield* Effect.fail(options.problems.invalid()); } - const principal = yield* options.authenticatePrincipal( - Redacted.make(request.headers['authorization']), - { - authentication: options.problems.authentication, - unavailable: options.problems.unavailable, - } - ); + const principal = yield* options.authenticatePrincipal(Redacted.make(request.headers['authorization']), { + authentication: options.problems.authentication, + unavailable: options.problems.unavailable, + }); const runtime = yield* ReadRuntime; return yield* runtime .runRead({ @@ -220,24 +185,14 @@ export const makeGovernedReadHttpHandler = < registration: options.registration, transport: { correlationId }, }) - .pipe( - Effect.catch((error) => - failProblem(classifyReadCoreError(error, options.problems)) - ) - ); + .pipe(Effect.catch((error) => failProblem(classifyReadCoreError(error, options.problems)))); }); return yield* execute.pipe( Effect.catchCauseIf(Cause.hasDies, () => - Effect.annotateLogs( - Effect.logError('Unexpected governed-read HTTP defect'), - { - correlationId: - correlationId === undefined || correlationId.trim().length === 0 - ? 'missing' - : correlationId, - } - ).pipe(Effect.andThen(Effect.fail(options.problems.internal()))) - ) + Effect.annotateLogs(Effect.logError('Unexpected governed-read HTTP defect'), { + correlationId: correlationId === undefined || correlationId.trim().length === 0 ? 'missing' : correlationId, + }).pipe(Effect.andThen(Effect.fail(options.problems.internal()))), + ), ); }); diff --git a/app/packages/core-runtime/src/http/http-instrumentation-seam.ts b/app/packages/core-runtime/src/http/http-instrumentation-seam.ts index 1fc70d7fa..d2e6e337e 100644 --- a/app/packages/core-runtime/src/http/http-instrumentation-seam.ts +++ b/app/packages/core-runtime/src/http/http-instrumentation-seam.ts @@ -20,13 +20,12 @@ export interface ActionHttpRequestHeaders { export interface ActionHttpPrincipalAuthentication { readonly authenticate: ( - authorization: Redacted.Redacted + authorization: Redacted.Redacted, ) => Effect.Effect; } export interface GovernedActionHttpRunnerInput< - PayloadSchema extends Schema.ConstraintDecoder & - Schema.ConstraintEncoder, + PayloadSchema extends Schema.ConstraintDecoder & Schema.ConstraintEncoder, ResultSchema extends Schema.ConstraintDecoder, DomainErrorSchema extends Schema.ConstraintDecoder<{ readonly _tag: string }>, DomainEvents extends DomainEventContractMap, @@ -42,14 +41,9 @@ export interface GovernedActionHttpRunnerInput< readonly endpointHeaders: ActionHttpEndpointHeaders; readonly internalProblem: () => InternalProblem; readonly invalidCorrelationProblem: () => InvalidProblem; - readonly mapError: ( - error: ActionCoreError | DomainErrorSchema['Type'] - ) => MappedProblem; + readonly mapError: (error: ActionCoreError | DomainErrorSchema['Type']) => MappedProblem; readonly payload: NoInfer; - readonly principal: ActionHttpPrincipalAuthentication< - PrincipalProblem, - PrincipalRequirements - >; + readonly principal: ActionHttpPrincipalAuthentication; readonly registration: ActionRegistration< PayloadSchema, ResultSchema, @@ -63,8 +57,7 @@ export interface GovernedActionHttpRunnerInput< } export type GovernedActionHttpEndpointInput< - PayloadSchema extends Schema.ConstraintDecoder & - Schema.ConstraintEncoder, + PayloadSchema extends Schema.ConstraintDecoder & Schema.ConstraintEncoder, ResultSchema extends Schema.ConstraintDecoder, DomainErrorSchema extends Schema.ConstraintDecoder<{ readonly _tag: string }>, DomainEvents extends DomainEventContractMap, @@ -98,7 +91,7 @@ const recoverUnexpectedDefect = ( effect: Effect.Effect, actionKey: string, safeCorrelationId: string, - internalProblem: () => InternalFailure + internalProblem: () => InternalFailure, ): Effect.Effect => Effect.exit(effect).pipe( Effect.flatMap((exit): Effect.Effect => { @@ -108,14 +101,11 @@ const recoverUnexpectedDefect = ( if (!exit.cause.reasons.some(Cause.isDieReason)) { return Effect.failCause(exit.cause); } - return Effect.logError( - 'Unexpected governed Action HTTP defect', - exit.cause - ).pipe( + return Effect.logError('Unexpected governed Action HTTP defect', exit.cause).pipe( Effect.annotateLogs({ actionKey, correlationId: safeCorrelationId }), - Effect.andThen(Effect.fail(internalProblem())) + Effect.andThen(Effect.fail(internalProblem())), ); - }) + }), ); /** @@ -123,8 +113,7 @@ const recoverUnexpectedDefect = ( * The caller owns authentication and the exhaustive public Action/domain failure mapping. */ export const runGovernedActionHttp = < - PayloadSchema extends Schema.ConstraintDecoder & - Schema.ConstraintEncoder, + PayloadSchema extends Schema.ConstraintDecoder & Schema.ConstraintEncoder, ResultSchema extends Schema.ConstraintDecoder, DomainErrorSchema extends Schema.ConstraintDecoder<{ readonly _tag: string }>, DomainEvents extends DomainEventContractMap, @@ -150,27 +139,24 @@ export const runGovernedActionHttp = < InternalProblem, PrincipalProblem, PrincipalRequirements - > + >, ): Effect.Effect< ResultSchema['Type'], InternalProblem | InvalidProblem | MappedProblem | PrincipalProblem, ActionRuntime | HandlerRequirements | PrincipalRequirements > => { const correlationId = input.requestHeaders['x-correlation-id']; - const correlationIsInvalid = - correlationId === undefined || correlationId.trim().length === 0; + const correlationIsInvalid = correlationId === undefined || correlationId.trim().length === 0; const safeCorrelationId = correlationIsInvalid ? 'invalid' : correlationId; const program = Effect.gen(function* runGovernedActionProgram() { if (correlationIsInvalid) { return yield* Effect.fail(input.invalidCorrelationProblem()); } - const principal = yield* input.principal.authenticate( - input.requestHeaders.authorization + const principal = yield* input.principal.authenticate(input.requestHeaders.authorization); + const encodedPayload = yield* Schema.encodeEffect(input.registration.descriptor.payloadSchema)(input.payload).pipe( + Effect.orDie, ); - const encodedPayload = yield* Schema.encodeEffect( - input.registration.descriptor.payloadSchema - )(input.payload).pipe(Effect.orDie); const runtime = yield* ActionRuntime; let transport: ActionTransportMetadata; if (input.endpointHeaders.idempotencyKey === undefined) { @@ -205,21 +191,17 @@ export const runGovernedActionHttp = < program, input.registration.descriptor.actionKey, safeCorrelationId, - input.internalProblem + input.internalProblem, ); }; /** Binds one deployment's generated principal adapter without owning endpoint semantics. */ export const bindGovernedActionHttp = ( - principal: ActionHttpPrincipalAuthentication< - PrincipalProblem, - PrincipalRequirements - > + principal: ActionHttpPrincipalAuthentication, ) => < - PayloadSchema extends Schema.ConstraintDecoder & - Schema.ConstraintEncoder, + PayloadSchema extends Schema.ConstraintDecoder & Schema.ConstraintEncoder, ResultSchema extends Schema.ConstraintDecoder, DomainErrorSchema extends Schema.ConstraintDecoder<{ readonly _tag: string; @@ -243,6 +225,6 @@ export const bindGovernedActionHttp = MappedProblem, InvalidProblem, InternalProblem - > + >, ) => runGovernedActionHttp({ ...input, principal }); diff --git a/app/packages/core-runtime/src/http/principal-authentication.ts b/app/packages/core-runtime/src/http/principal-authentication.ts index 7a431b816..67983fabd 100644 --- a/app/packages/core-runtime/src/http/principal-authentication.ts +++ b/app/packages/core-runtime/src/http/principal-authentication.ts @@ -7,43 +7,26 @@ import type { TrustedPrincipalContext } from '../actions/principal-context.ts'; const verificationErrorFields = { reason: Schema.String }; export const OperationPrincipalVerificationErrorSchema = Schema.Union([ - Schema.TaggedStruct( - 'ActionPrincipalConfigurationError', - verificationErrorFields - ), + Schema.TaggedStruct('ActionPrincipalConfigurationError', verificationErrorFields), Schema.TaggedStruct('ActionPrincipalExpiredError', verificationErrorFields), Schema.TaggedStruct('ActionPrincipalInvalidError', verificationErrorFields), Schema.TaggedStruct('ActionPrincipalMissingError', verificationErrorFields), Schema.TaggedStruct('ActionPrincipalScopeError', verificationErrorFields), - Schema.TaggedStruct( - 'ActionPrincipalUnavailableError', - verificationErrorFields - ), + Schema.TaggedStruct('ActionPrincipalUnavailableError', verificationErrorFields), ]); -export type OperationPrincipalVerificationError = - typeof OperationPrincipalVerificationErrorSchema.Type; +export type OperationPrincipalVerificationError = typeof OperationPrincipalVerificationErrorSchema.Type; -export interface PrincipalAuthenticationProblems< - AuthenticationProblem, - UnavailableProblem, -> { +export interface PrincipalAuthenticationProblems { readonly authentication: () => AuthenticationProblem; readonly unavailable: () => UnavailableProblem; } export type AudienceBoundOperationPrincipalVerifier = ( - authorization: Redacted.Redacted -) => Effect.Effect< - TrustedPrincipalContext, - OperationPrincipalVerificationError, - Requirements ->; + authorization: Redacted.Redacted, +) => Effect.Effect; -const bearerChallenge = HttpEffect.appendPreResponseHandler( - (_request, response) => - Effect.succeed( - HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer') - ) +const bearerChallenge = HttpEffect.appendPreResponseHandler((_request, response) => + Effect.succeed(HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer')), ); /** @@ -51,24 +34,16 @@ const bearerChallenge = HttpEffect.appendPreResponseHandler( * Endpoint call sites retain ownership of their declared public 401 and 503 values. */ export const makeMicroverticalHttpPrincipalAuthentication = - ( - verify: AudienceBoundOperationPrincipalVerifier - ) => + (verify: AudienceBoundOperationPrincipalVerifier) => ( authorization: Redacted.Redacted, - problems: PrincipalAuthenticationProblems< - AuthenticationProblem, - UnavailableProblem - > + problems: PrincipalAuthenticationProblems, ): Effect.Effect< TrustedPrincipalContext, AuthenticationProblem | UnavailableProblem, HttpServerRequest.HttpServerRequest | Requirements > => { - const authentication = () => - bearerChallenge.pipe( - Effect.andThen(Effect.fail(problems.authentication())) - ); + const authentication = () => bearerChallenge.pipe(Effect.andThen(Effect.fail(problems.authentication()))); const unavailable = () => Effect.fail(problems.unavailable()); return verify(authorization).pipe( Effect.catchTags({ @@ -78,6 +53,6 @@ export const makeMicroverticalHttpPrincipalAuthentication = ActionPrincipalMissingError: authentication, ActionPrincipalScopeError: authentication, ActionPrincipalUnavailableError: unavailable, - }) + }), ); }; diff --git a/app/packages/core-runtime/src/index.ts b/app/packages/core-runtime/src/index.ts index 65f57ec57..8d9f8a635 100644 --- a/app/packages/core-runtime/src/index.ts +++ b/app/packages/core-runtime/src/index.ts @@ -82,10 +82,7 @@ export type { SupportRecoveryPrincipalContextError, SupportRecoveryPrincipalContextResolverService, } from './auth/support-recovery-principal-context.ts'; -export type { - SystemPrincipalContextError, - SystemWorkloadRegistration, -} from './auth/system-principal-context.ts'; +export type { SystemPrincipalContextError, SystemWorkloadRegistration } from './auth/system-principal-context.ts'; export { IdentityLifecycleConflictError, IdentityPersistenceUnavailableError, @@ -93,10 +90,7 @@ export { PrincipalManagementErrorSchema, } from './auth/principal-management-errors.ts'; export type { PrincipalManagementError } from './auth/principal-management-errors.ts'; -export { - managedPrincipalsRead, - selfApiKeyBindingsRead, -} from './auth/principal-administration-reads.ts'; +export { managedPrincipalsRead, selfApiKeyBindingsRead } from './auth/principal-administration-reads.ts'; export { LegalEntityContext, LegalEntityContextAmbiguousError, @@ -116,10 +110,7 @@ export type { SafeLegalEntity, } from './auth/legal-entity-context.ts'; export { DatabaseConnectionError } from './db/client.ts'; -export { - DEFAULT_DATABASE_POOL_DEADLINES, - configureDatabasePool, -} from './db/pool-configuration.ts'; +export { DEFAULT_DATABASE_POOL_DEADLINES, configureDatabasePool } from './db/pool-configuration.ts'; export type { DatabasePoolDeadlines } from './db/pool-configuration.ts'; export { CorePersistenceLive } from './runtime-infrastructure.ts'; export { @@ -142,16 +133,8 @@ export { PRINCIPAL_KINDS, PRINCIPAL_STATUSES, } from './db/schema.ts'; -export type { - BindingStatus, - BindingSubjectType, - PrincipalKind, - PrincipalStatus, -} from './db/schema.ts'; -export { - tenantLegalEntityRlsPolicies, - tenantRlsPolicies, -} from './db/scoped-transaction.ts'; +export type { BindingStatus, BindingSubjectType, PrincipalKind, PrincipalStatus } from './db/schema.ts'; +export { tenantLegalEntityRlsPolicies, tenantRlsPolicies } from './db/scoped-transaction.ts'; export { DatabaseCommitAcknowledgementAmbiguous, DatabaseDriverFailureKindSchema, @@ -190,11 +173,7 @@ export type { ResourceAccessTarget, TenantPermissionKey, } from './permissions/context-access.ts'; -export { - defineAction, - defineActionResourcePermission, - isActionRegistration, -} from './actions/definition.ts'; +export { defineAction, defineActionResourcePermission, isActionRegistration } from './actions/definition.ts'; export type { ActionAuditProfile, ActionDescriptor, @@ -210,12 +189,7 @@ export type { ActionTenantPermission, AnyActionRegistration, } from './actions/definition.ts'; -export { - PolicyDenied, - defineGlobalPolicy, - defineMicroverticalPolicy, - denyPolicy, -} from './actions/policy.ts'; +export { PolicyDenied, defineGlobalPolicy, defineMicroverticalPolicy, denyPolicy } from './actions/policy.ts'; export type { ActionPolicy, ActionPolicyEvaluator, @@ -227,22 +201,14 @@ export type { GlobalActionPolicy, MicroverticalActionPolicy, } from './actions/policy.ts'; -export { - ActionRuntime, - ActionRuntimeLive, - resolveActionCommit, - runAction, -} from './actions/runtime.ts'; +export { ActionRuntime, ActionRuntimeLive, resolveActionCommit, runAction } from './actions/runtime.ts'; export type { ActionCommitOpen, ActionRuntimeService, ResolveActionCommitInput, RunActionInput, } from './actions/runtime.ts'; -export { - ActionTransportMetadataSchema, - TrustedPrincipalContextSchema, -} from './actions/context.ts'; +export { ActionTransportMetadataSchema, TrustedPrincipalContextSchema } from './actions/context.ts'; export type { ActionCollectorMethods, ActionHandlerContext, @@ -296,21 +262,9 @@ export type { ReadServiceFactory, ResolvedReadPermissionTarget, } from './reads/definition.ts'; -export type { - ReadEvidenceMetadata, - ReadHandlerContext, - ReadHandlerResult, -} from './reads/context.ts'; -export { - READ_RUNTIME_STAGES, - ReadRuntime, - ReadRuntimeLive, -} from './reads/runtime.ts'; -export type { - ReadRuntimeOptions, - ReadRuntimeService, - ReadRuntimeStage, -} from './reads/runtime.ts'; +export type { ReadEvidenceMetadata, ReadHandlerContext, ReadHandlerResult } from './reads/context.ts'; +export { READ_RUNTIME_STAGES, ReadRuntime, ReadRuntimeLive } from './reads/runtime.ts'; +export type { ReadRuntimeOptions, ReadRuntimeService, ReadRuntimeStage } from './reads/runtime.ts'; export { ReadEvidencePersistenceError, ReadEvidenceValidationError, @@ -378,20 +332,13 @@ export type { CoreSearchPartyLifecycleTopic, CoreSearchPartyProjectorWorkerKey, } from './search/ingestion.ts'; -export { - CoreSearchWorkerSnapshot, - CoreSearchWorkerSnapshotLive, -} from './search/worker-snapshot.ts'; +export { CoreSearchWorkerSnapshot, CoreSearchWorkerSnapshotLive } from './search/worker-snapshot.ts'; export type { CoreSearchSnapshotReadExecutor, CoreSearchWorkerSnapshotService, CoreSearchWorkerSnapshotView, } from './search/worker-snapshot.ts'; -export { - DataAccessEventSchema, - DomainEventSchema, - OutboxMessageSchema, -} from './actions/events.ts'; +export { DataAccessEventSchema, DomainEventSchema, OutboxMessageSchema } from './actions/events.ts'; export type { ActionAccessEvidencePolicy, DataAccessEvent, @@ -484,10 +431,7 @@ export type { SystemModuleEntrypoint, TenantModuleEntrypoint, } from './modules/module-entrypoint.ts'; -export { - ModuleStateCheckUnavailableError, - ModuleStateDeniedError, -} from './modules/module-state-gate-errors.ts'; +export { ModuleStateCheckUnavailableError, ModuleStateDeniedError } from './modules/module-state-gate-errors.ts'; export type { ModuleStateGateError } from './modules/module-state-gate-errors.ts'; export { ModuleStateGate, @@ -495,14 +439,8 @@ export { decideModuleStateAccess, tenantStatesAllowingAccess, } from './modules/module-state-gate.ts'; -export type { - ModuleStateGateService, - ModuleStateSnapshot, -} from './modules/module-state-gate.ts'; -export { - ModuleEntrypointGateway, - ModuleEntrypointGatewayLive, -} from './modules/module-entrypoint-gateway.ts'; +export type { ModuleStateGateService, ModuleStateSnapshot } from './modules/module-state-gate.ts'; +export { ModuleEntrypointGateway, ModuleEntrypointGatewayLive } from './modules/module-entrypoint-gateway.ts'; export type { ModuleEntrypointGatewayService, RunGatedModuleEntrypointInput, @@ -551,10 +489,7 @@ export { ShellTimelineContributionSchema, validateShellContributions, } from './modules/shell-contribution.ts'; -export type { - OntosShellContributions, - ShellContributionReferenceSets, -} from './modules/shell-contribution.ts'; +export type { OntosShellContributions, ShellContributionReferenceSets } from './modules/shell-contribution.ts'; export type { OntosActionContract, OntosApiContract, @@ -623,10 +558,7 @@ export { setManagedApiKeyBindingStatusAction } from './modules/actions/set-manag export { setSelfApiKeyBindingStatusAction } from './modules/actions/set-self-api-key-binding-status.action.ts'; // -export { - defineOutboxWorker, - extractOutboxWorkerSubscriptions, -} from './outbox/definition.ts'; +export { defineOutboxWorker, extractOutboxWorkerSubscriptions } from './outbox/definition.ts'; export type { AnyOutboxWorkerRegistration, OutboxWorkerDescriptor, @@ -645,14 +577,8 @@ export { OutboxPersistenceError, OutboxWorkerDescriptorError, } from './outbox/errors.ts'; -export type { - OutboxWorkerHealth, - OutboxWorkerHealthServer, -} from './outbox/health.ts'; -export { - parseOutboxPollingConfig, - runOutboxPollingLoop, -} from './outbox/poller.ts'; +export type { OutboxWorkerHealth, OutboxWorkerHealthServer } from './outbox/health.ts'; +export { parseOutboxPollingConfig, runOutboxPollingLoop } from './outbox/poller.ts'; export type { OutboxCycleRunner, OutboxPollingConfig, @@ -660,16 +586,8 @@ export type { RunOutboxPollingLoopInput, } from './outbox/poller.ts'; export { OutboxRepositoryLive } from './outbox/repository.ts'; -export type { - RunOutboxWorkerProcessInput, - StartOutboxWorkerProcessInput, -} from './outbox/process.ts'; -export { - OutboxRuntime, - OutboxRuntimeLive, - matchOutboxMessages, - runOutboxCycle, -} from './outbox/runtime.ts'; +export type { RunOutboxWorkerProcessInput, StartOutboxWorkerProcessInput } from './outbox/process.ts'; +export { OutboxRuntime, OutboxRuntimeLive, matchOutboxMessages, runOutboxCycle } from './outbox/runtime.ts'; export type { MatchOutboxMessagesInput, OutboxCycleError, diff --git a/app/packages/core-runtime/src/install/action-authorization-provisioning.ts b/app/packages/core-runtime/src/install/action-authorization-provisioning.ts index 9f46b3e9b..0d814fc86 100644 --- a/app/packages/core-runtime/src/install/action-authorization-provisioning.ts +++ b/app/packages/core-runtime/src/install/action-authorization-provisioning.ts @@ -5,8 +5,7 @@ import { fullyConsistent } from '../permissions/client.ts'; import { ONTOS_SPICEDB_SCHEMA } from '../permissions/schema.ts'; import { toSpiceDbActionObjectId } from '../permissions/service.ts'; -export const ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID = - '00000000-0000-4000-8000-000000000019'; +export const ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID = '00000000-0000-4000-8000-000000000019'; export interface ActionAuthorizationContext { readonly principalId: string; @@ -41,14 +40,12 @@ export interface ActionAuthorizationProvisioningResult { export interface ActionAuthorizationProvisioningClient { readonly checkPermission: ( - request: v1.CheckPermissionRequest + request: v1.CheckPermissionRequest, ) => Effect.Effect, Error>; readonly writeRelationships: ( - request: v1.WriteRelationshipsRequest + request: v1.WriteRelationshipsRequest, ) => Effect.Effect; - readonly writeSchema: ( - request: v1.WriteSchemaRequest - ) => Effect.Effect; + readonly writeSchema: (request: v1.WriteSchemaRequest) => Effect.Effect; } export class ActionAuthorizationProvisioningError extends Schema.TaggedError()( @@ -63,100 +60,67 @@ export class ActionAuthorizationProvisioningError extends Schema.TaggedError - new ActionAuthorizationProvisioningError({ code, reason }); + reason: string, +): ActionAuthorizationProvisioningError => new ActionAuthorizationProvisioningError({ code, reason }); -const hasInvalidActions = ( - actions: readonly ActionAuthorizationProvisioningAction[] -): boolean => { +const hasInvalidActions = (actions: readonly ActionAuthorizationProvisioningAction[]): boolean => { const actionKeys = actions.map(({ actionKey }) => actionKey); return ( actions.length === 0 || - actionKeys.some( - (actionKey) => actionKey.length === 0 || actionKey.length > 256 - ) || + actionKeys.some((actionKey) => actionKey.length === 0 || actionKey.length > 256) || new Set(actionKeys).size !== actionKeys.length || - actions.some( - ({ provisioning }) => - provisioning !== 'tenant_membership_default' && - provisioning !== 'explicit' - ) + actions.some(({ provisioning }) => provisioning !== 'tenant_membership_default' && provisioning !== 'explicit') ); }; -const hasInvalidContexts = ( - contexts: readonly ActionAuthorizationContext[] -): boolean => +const hasInvalidContexts = (contexts: readonly ActionAuthorizationContext[]): boolean => contexts.length === 0 || - contexts.some( - ({ principalId, tenantId }) => - principalId.length === 0 || tenantId.length === 0 - ) || + contexts.some(({ principalId, tenantId }) => principalId.length === 0 || tenantId.length === 0) || new Set(contexts.map(({ tenantId }) => tenantId)).size !== contexts.length; const isInvalidExplicitAssertionSet = ( { actionKey, assertions }: ActionAuthorizationExplicitAssertionSet, - explicitActionKeys: ReadonlySet + explicitActionKeys: ReadonlySet, ): boolean => !explicitActionKeys.has(actionKey) || assertions.length < 2 || - new Set(assertions.map(({ principalId }) => principalId)).size !== - assertions.length || + new Set(assertions.map(({ principalId }) => principalId)).size !== assertions.length || assertions.some( - ({ expected, principalId }) => - principalId.length === 0 || - (expected !== 'allowed' && expected !== 'denied') + ({ expected, principalId }) => principalId.length === 0 || (expected !== 'allowed' && expected !== 'denied'), ) || !assertions.some(({ expected }) => expected === 'allowed') || !assertions.some(({ expected }) => expected === 'denied'); const hasInvalidExplicitAssertions = ( explicitActionAssertions: readonly ActionAuthorizationExplicitAssertionSet[], - explicitActionKeys: ReadonlySet + explicitActionKeys: ReadonlySet, ): boolean => explicitActionAssertions.length !== explicitActionKeys.size || - explicitActionAssertions.some((assertionSet) => - isInvalidExplicitAssertionSet(assertionSet, explicitActionKeys) - ) || - new Set(explicitActionAssertions.map(({ actionKey }) => actionKey)).size !== - explicitActionAssertions.length; + explicitActionAssertions.some((assertionSet) => isInvalidExplicitAssertionSet(assertionSet, explicitActionKeys)) || + new Set(explicitActionAssertions.map(({ actionKey }) => actionKey)).size !== explicitActionAssertions.length; -const assertProvisioningInput = ( - input: ActionAuthorizationProvisioningInput -) => { - const actions = input.actions.toSorted((left, right) => - left.actionKey.localeCompare(right.actionKey) - ); - const contexts = input.contexts.toSorted((left, right) => - left.tenantId.localeCompare(right.tenantId) - ); +const assertProvisioningInput = (input: ActionAuthorizationProvisioningInput) => { + const actions = input.actions.toSorted((left, right) => left.actionKey.localeCompare(right.actionKey)); + const contexts = input.contexts.toSorted((left, right) => left.tenantId.localeCompare(right.tenantId)); if (hasInvalidActions(actions)) { - throw failure( - 'action_authorization_input_invalid', - 'Current Action discovery must produce a non-empty unique set' - ); + throw failure('action_authorization_input_invalid', 'Current Action discovery must produce a non-empty unique set'); } if (hasInvalidContexts(contexts)) { throw failure( 'action_authorization_input_invalid', - 'Authorization provisioning requires unique fixed Tenant contexts' + 'Authorization provisioning requires unique fixed Tenant contexts', ); } - const deniedPrincipalId = - input.deniedPrincipalId ?? ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID; - if ( - deniedPrincipalId.length === 0 || - contexts.some(({ principalId }) => principalId === deniedPrincipalId) - ) { + const deniedPrincipalId = input.deniedPrincipalId ?? ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID; + if (deniedPrincipalId.length === 0 || contexts.some(({ principalId }) => principalId === deniedPrincipalId)) { throw failure( 'action_authorization_input_invalid', - 'The denied verification Principal must be outside the fixed context set' + 'The denied verification Principal must be outside the fixed context set', ); } const explicitActionKeys = new Set(); @@ -165,15 +129,13 @@ const assertProvisioningInput = ( explicitActionKeys.add(actionKey); } } - const explicitActionAssertions = ( - input.explicitActionAssertions ?? [] - ).toSorted((left, right) => left.actionKey.localeCompare(right.actionKey)); - if ( - hasInvalidExplicitAssertions(explicitActionAssertions, explicitActionKeys) - ) { + const explicitActionAssertions = (input.explicitActionAssertions ?? []).toSorted((left, right) => + left.actionKey.localeCompare(right.actionKey), + ); + if (hasInvalidExplicitAssertions(explicitActionAssertions, explicitActionKeys)) { throw failure( 'action_authorization_input_invalid', - 'Each explicit Action requires unique recorded allowed and denied verification assertions' + 'Each explicit Action requires unique recorded allowed and denied verification assertions', ); } return { actions, contexts, deniedPrincipalId, explicitActionAssertions }; @@ -213,7 +175,7 @@ const actionExecuteRequest = (actionKey: string, principalId: string) => export const buildActionAuthorizationRelationships = ( actionKeys: readonly string[], - contexts: readonly ActionAuthorizationContext[] + contexts: readonly ActionAuthorizationContext[], ): readonly v1.Relationship[] => contexts .flatMap(({ tenantId }) => @@ -231,8 +193,8 @@ export const buildActionAuthorizationRelationships = ( }), optionalRelation: 'member', }), - }) - ) + }), + ), ) .toSorted((left, right) => { const leftKey = `${left.resource?.objectId ?? ''}:${left.subject?.object?.objectId ?? ''}`; @@ -240,43 +202,36 @@ export const buildActionAuthorizationRelationships = ( return leftKey.localeCompare(rightKey); }); -const serviceFailure = ( - cause?: unknown -): ActionAuthorizationProvisioningError => { +const serviceFailure = (cause?: unknown): ActionAuthorizationProvisioningError => { const error = failure( 'action_authorization_service_unavailable', - 'The authorization service could not provision current Action rules safely' + 'The authorization service could not provision current Action rules safely', ); - return cause === undefined - ? error - : Object.defineProperty(error, 'cause', { value: cause }); + return cause === undefined ? error : Object.defineProperty(error, 'cause', { value: cause }); }; const callClient = (operation: Effect.Effect) => operation.pipe( Effect.mapError((cause) => - Schema.is(ActionAuthorizationProvisioningError)(cause) - ? cause - : serviceFailure(cause) - ) + Schema.is(ActionAuthorizationProvisioningError)(cause) ? cause : serviceFailure(cause), + ), ); const checkHasPermission = ( client: ActionAuthorizationProvisioningClient, request: v1.CheckPermissionRequest, - error: ActionAuthorizationProvisioningError + error: ActionAuthorizationProvisioningError, ) => callClient(client.checkPermission(request)).pipe( Effect.flatMap( Option.match({ onNone: () => Effect.fail(error), onSome: (response) => - response.permissionship === - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION + response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION ? Effect.void : Effect.fail(error), - }) - ) + }), + ), ); const checkNoPermission = ( @@ -284,141 +239,116 @@ const checkNoPermission = ( request: v1.CheckPermissionRequest, error: ActionAuthorizationProvisioningError = failure( 'action_authorization_verification_failed', - 'The representative non-member authorization check did not deny' - ) + 'The representative non-member authorization check did not deny', + ), ) => callClient(client.checkPermission(request)).pipe( Effect.flatMap( Option.match({ onNone: () => Effect.fail(error), onSome: (response) => - response.permissionship === - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION + response.permissionship === v1.CheckPermissionResponse_Permissionship.NO_PERMISSION ? Effect.void : Effect.fail(error), - }) - ) + }), + ), ); -export const provisionActionAuthorization = Effect.fn( - 'ActionAuthorizationProvisioning.provisionActionAuthorization' -)(function* provisionActionAuthorizationEffect( - client: ActionAuthorizationProvisioningClient, - input: ActionAuthorizationProvisioningInput -): Effect.fn.Return< - ActionAuthorizationProvisioningResult, - ActionAuthorizationProvisioningError -> { - const { actions, contexts, deniedPrincipalId, explicitActionAssertions } = - yield* Effect.try({ - catch: (error) => - Schema.is(ActionAuthorizationProvisioningError)(error) - ? error - : serviceFailure(error), +export const provisionActionAuthorization = Effect.fn('ActionAuthorizationProvisioning.provisionActionAuthorization')( + function* provisionActionAuthorizationEffect( + client: ActionAuthorizationProvisioningClient, + input: ActionAuthorizationProvisioningInput, + ): Effect.fn.Return { + const { actions, contexts, deniedPrincipalId, explicitActionAssertions } = yield* Effect.try({ + catch: (error) => (Schema.is(ActionAuthorizationProvisioningError)(error) ? error : serviceFailure(error)), try: () => assertProvisioningInput(input), }); - yield* callClient( - client.writeSchema( - v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }) - ) - ); - - yield* Effect.forEach( - contexts, - (context) => - checkHasPermission( - client, - tenantAccessRequest(context), - failure( - 'action_authorization_membership_missing', - 'A fixed provisioning Principal is not an active member of its Tenant' - ) - ), - { concurrency: 1, discard: true } - ); + yield* callClient(client.writeSchema(v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }))); - const defaultActionKeys = actions.flatMap(({ actionKey, provisioning }) => - provisioning === 'tenant_membership_default' ? [actionKey] : [] - ); - const relationships = buildActionAuthorizationRelationships( - defaultActionKeys, - contexts - ); - yield* callClient( - client.writeRelationships( - v1.WriteRelationshipsRequest.create({ - updates: relationships.map((relationship) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship, - }) + yield* Effect.forEach( + contexts, + (context) => + checkHasPermission( + client, + tenantAccessRequest(context), + failure( + 'action_authorization_membership_missing', + 'A fixed provisioning Principal is not an active member of its Tenant', + ), ), - }) - ) - ); + { concurrency: 1, discard: true }, + ); - yield* Effect.forEach( - defaultActionKeys, - (actionKey) => - Effect.forEach( - contexts, - (context) => - checkHasPermission( - client, - actionExecuteRequest(actionKey, context.principalId), - failure( - 'action_authorization_verification_failed', - 'An expected fixed Tenant Action grant did not verify' - ) + const defaultActionKeys = actions.flatMap(({ actionKey, provisioning }) => + provisioning === 'tenant_membership_default' ? [actionKey] : [], + ); + const relationships = buildActionAuthorizationRelationships(defaultActionKeys, contexts); + yield* callClient( + client.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: relationships.map((relationship) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship, + }), ), - { concurrency: 1, discard: true } - ).pipe( - Effect.andThen( - checkNoPermission( - client, - actionExecuteRequest(actionKey, deniedPrincipalId) - ) - ) + }), ), - { concurrency: 1, discard: true } - ); - yield* Effect.forEach( - explicitActionAssertions, - ({ actionKey, assertions }) => - Effect.forEach( - assertions, - (assertion) => { - const request = actionExecuteRequest( - actionKey, - assertion.principalId - ); - return assertion.expected === 'allowed' - ? checkHasPermission( - client, - request, - failure( - 'action_authorization_verification_failed', - 'An explicit Action allowed assertion did not verify' - ) - ) - : checkNoPermission( - client, - request, - failure( - 'action_authorization_verification_failed', - 'An explicit Action denied assertion did not verify' + ); + + yield* Effect.forEach( + defaultActionKeys, + (actionKey) => + Effect.forEach( + contexts, + (context) => + checkHasPermission( + client, + actionExecuteRequest(actionKey, context.principalId), + failure( + 'action_authorization_verification_failed', + 'An expected fixed Tenant Action grant did not verify', + ), + ), + { concurrency: 1, discard: true }, + ).pipe(Effect.andThen(checkNoPermission(client, actionExecuteRequest(actionKey, deniedPrincipalId)))), + { concurrency: 1, discard: true }, + ); + yield* Effect.forEach( + explicitActionAssertions, + ({ actionKey, assertions }) => + Effect.forEach( + assertions, + (assertion) => { + const request = actionExecuteRequest(actionKey, assertion.principalId); + return assertion.expected === 'allowed' + ? checkHasPermission( + client, + request, + failure( + 'action_authorization_verification_failed', + 'An explicit Action allowed assertion did not verify', + ), ) - ); - }, - { concurrency: 1, discard: true } - ), - { concurrency: 1, discard: true } - ); + : checkNoPermission( + client, + request, + failure( + 'action_authorization_verification_failed', + 'An explicit Action denied assertion did not verify', + ), + ); + }, + { concurrency: 1, discard: true }, + ), + { concurrency: 1, discard: true }, + ); - return { - actionCount: actions.length, - grantCount: relationships.length, - tenantCount: contexts.length, - }; -}); + return { + actionCount: actions.length, + grantCount: relationships.length, + tenantCount: contexts.length, + }; + }, +); diff --git a/app/packages/core-runtime/src/install/context-bootstrap-shared.ts b/app/packages/core-runtime/src/install/context-bootstrap-shared.ts index 16746a4d9..8d40a9606 100644 --- a/app/packages/core-runtime/src/install/context-bootstrap-shared.ts +++ b/app/packages/core-runtime/src/install/context-bootstrap-shared.ts @@ -1,11 +1,7 @@ import { v1 } from '@authzed/authzed-node'; import { and, eq, or } from 'drizzle-orm'; -import { - legalEntities, - principalAuthBindings, - principals, -} from '../db/schema.ts'; +import { legalEntities, principalAuthBindings, principals } from '../db/schema.ts'; import type { CoreTransaction } from '../db/types.ts'; interface BootstrapIdentity { @@ -19,10 +15,7 @@ interface BootstrapIdentity { readonly tenantId: string; } -export const selectBootstrapLegalEntities = ( - transaction: CoreTransaction, - context: BootstrapIdentity -) => +export const selectBootstrapLegalEntities = (transaction: CoreTransaction, context: BootstrapIdentity) => transaction .select({ legalEntityId: legalEntities.legalEntityId, @@ -39,16 +32,13 @@ export const selectBootstrapLegalEntities = ( and( eq(legalEntities.tenantId, context.tenantId), eq(legalEntities.registrationCountry, context.registrationCountry), - eq(legalEntities.registrationNumber, context.registrationNumber) - ) - ) + eq(legalEntities.registrationNumber, context.registrationNumber), + ), + ), ) .limit(2); -export const selectBootstrapPrincipals = ( - transaction: CoreTransaction, - context: BootstrapIdentity -) => +export const selectBootstrapPrincipals = (transaction: CoreTransaction, context: BootstrapIdentity) => transaction .select({ displayName: principals.displayName, @@ -64,7 +54,7 @@ export const selectBootstrapPrincipals = ( export const selectBootstrapAuthBindings = ( transaction: CoreTransaction, context: BootstrapIdentity, - authUserId: string + authUserId: string, ) => transaction .select({ @@ -84,9 +74,9 @@ export const selectBootstrapAuthBindings = ( eq(principalAuthBindings.tenantId, context.tenantId), eq(principalAuthBindings.provider, 'better_auth'), eq(principalAuthBindings.subjectType, 'user'), - eq(principalAuthBindings.providerSubjectId, authUserId) - ) - ) + eq(principalAuthBindings.providerSubjectId, authUserId), + ), + ), ) .limit(2); @@ -107,9 +97,7 @@ interface BootstrapRelationship { readonly subjectType: string; } -export const bootstrapRelationshipRequest = ( - relationships: readonly BootstrapRelationship[] -) => +export const bootstrapRelationshipRequest = (relationships: readonly BootstrapRelationship[]) => v1.WriteRelationshipsRequest.create({ updates: relationships.map((item) => v1.RelationshipUpdate.create({ @@ -127,6 +115,6 @@ export const bootstrapRelationshipRequest = ( }), }), }), - }) + }), ), }); diff --git a/app/packages/core-runtime/src/install/spicedb-database-config.ts b/app/packages/core-runtime/src/install/spicedb-database-config.ts index deeddad05..471ef677b 100644 --- a/app/packages/core-runtime/src/install/spicedb-database-config.ts +++ b/app/packages/core-runtime/src/install/spicedb-database-config.ts @@ -14,10 +14,8 @@ const SpiceDbDatabaseBootstrapEnvironmentSchema = Schema.Struct({ const PostgreSqlUrlSchema = Schema.URLFromString.check( Schema.makeFilter((url) => - url.protocol === 'postgres:' || url.protocol === 'postgresql:' - ? undefined - : 'URL must use PostgreSQL' - ) + url.protocol === 'postgres:' || url.protocol === 'postgresql:' ? undefined : 'URL must use PostgreSQL', + ), ); const PercentEncodedUriComponentSchema = Schema.String.check( @@ -29,7 +27,7 @@ const PercentEncodedUriComponentSchema = Schema.String.check( issue = 'URL credentials must use valid percent encoding'; } return issue; - }) + }), ); const SpiceDbDatabasePairSchema = Schema.Struct({ @@ -50,7 +48,7 @@ const SpiceDbDatabasePairSchema = Schema.Struct({ return admin.href === spicedb.href || admin.username === spicedb.username ? 'Administrative and SpiceDB PostgreSQL identities must be distinct' : undefined; - }) + }), ); const makeSpiceDbDatabaseBootstrapConfig = (fields: { @@ -66,39 +64,25 @@ const makeSpiceDbDatabaseBootstrapConfig = (fields: { user: 'spicedb' as const, }); -export type SpiceDbDatabaseBootstrapConfig = ReturnType< - typeof makeSpiceDbDatabaseBootstrapConfig ->; +export type SpiceDbDatabaseBootstrapConfig = ReturnType; export const parseSpiceDbDatabaseBootstrapConfig = ( - environment: SpiceDbDatabaseBootstrapEnvironment + environment: SpiceDbDatabaseBootstrapEnvironment, ): SpiceDbDatabaseBootstrapConfig => { - const source = Result.getOrThrow( - Schema.decodeUnknownResult(SpiceDbDatabaseBootstrapEnvironmentSchema)( - environment - ) - ); - const admin = Result.getOrThrow( - Schema.decodeResult(PostgreSqlUrlSchema)(source.DATABASE_ADMIN_URL) - ); - const spicedb = Result.getOrThrow( - Schema.decodeResult(PostgreSqlUrlSchema)(source.SPICEDB_DATABASE_URL) - ); + const source = Result.getOrThrow(Schema.decodeUnknownResult(SpiceDbDatabaseBootstrapEnvironmentSchema)(environment)); + const admin = Result.getOrThrow(Schema.decodeResult(PostgreSqlUrlSchema)(source.DATABASE_ADMIN_URL)); + const spicedb = Result.getOrThrow(Schema.decodeResult(PostgreSqlUrlSchema)(source.SPICEDB_DATABASE_URL)); const pair = Result.getOrThrow( Schema.decodeResult(SpiceDbDatabasePairSchema)({ admin, spicedb, spicedbUser: decodeURIComponent( - Result.getOrThrow( - Schema.decodeResult(PercentEncodedUriComponentSchema)( - spicedb.username - ) - ) + Result.getOrThrow(Schema.decodeResult(PercentEncodedUriComponentSchema)(spicedb.username)), ), - }) + }), ); const encodedPassword = Result.getOrThrow( - Schema.decodeResult(PercentEncodedUriComponentSchema)(pair.spicedb.password) + Schema.decodeResult(PercentEncodedUriComponentSchema)(pair.spicedb.password), ); return makeSpiceDbDatabaseBootstrapConfig({ diff --git a/app/packages/core-runtime/src/install/stage-context-bootstrap.ts b/app/packages/core-runtime/src/install/stage-context-bootstrap.ts index d5467a9a9..2b5923dfa 100644 --- a/app/packages/core-runtime/src/install/stage-context-bootstrap.ts +++ b/app/packages/core-runtime/src/install/stage-context-bootstrap.ts @@ -7,20 +7,11 @@ import { isSqlError } from 'effect/unstable/sql/SqlError'; // eslint-disable-next-line anti-slop-effect/no-service-constructor-imports -- Native scoped database composition at the installer boundary. import { makeCoreDatabase } from '../db/client.ts'; import { parseDatabaseConfig } from '../db/config.ts'; -import { - legalEntities, - principalAuthBindings, - principals, - tenantModuleStates, - tenants, -} from '../db/schema.ts'; +import { legalEntities, principalAuthBindings, principals, tenantModuleStates, tenants } from '../db/schema.ts'; import type { CoreDatabaseExecutor, CoreTransaction } from '../db/types.ts'; import { spiceDbClientSecurity } from '../permissions/client.ts'; import { parseSpiceDbConfig } from '../permissions/config.ts'; -import { - toLegalEntityAccessObjectId, - toModuleAccessObjectId, -} from '../permissions/context-access.ts'; +import { toLegalEntityAccessObjectId, toModuleAccessObjectId } from '../permissions/context-access.ts'; import { bootstrapPrincipalRecord, bootstrapRelationshipRequest, @@ -90,10 +81,7 @@ export interface StageContextBootstrapResult { } export type StageContextBootstrapProviderUserIds = readonly [string, string]; -export type StageContextBootstrapResults = readonly [ - StageContextBootstrapResult, - StageContextBootstrapResult, -]; +export type StageContextBootstrapResults = readonly [StageContextBootstrapResult, StageContextBootstrapResult]; export class StageContextBootstrapError extends Schema.TaggedError()( 'StageContextBootstrapError', @@ -101,96 +89,63 @@ export class StageContextBootstrapError extends Schema.TaggedError new StageContextBootstrapError( cause === undefined ? { code: 'stage_context_bootstrap_failed', reason } - : { cause, code: 'stage_context_bootstrap_failed', reason } + : { cause, code: 'stage_context_bootstrap_failed', reason }, ); -const TrimmedNonEmptyString = Schema.Trim.pipe( - Schema.check(Schema.isNonEmpty()) -); -const StageEnvironmentSchema = Schema.Trim.pipe( - Schema.decodeTo(Schema.Literal('stage')) -); +const TrimmedNonEmptyString = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty())); +const StageEnvironmentSchema = Schema.Trim.pipe(Schema.decodeTo(Schema.Literal('stage'))); const CauseMessageSchema = Schema.Struct({ message: Schema.String }); -const bootstrapFailureFromCause = ( - cause: unknown -): StageContextBootstrapError => { +const bootstrapFailureFromCause = (cause: unknown): StageContextBootstrapError => { if (Schema.is(StageContextBootstrapError)(cause)) { return cause; } return Schema.decodeUnknownOption(CauseMessageSchema)(cause).pipe( Option.match({ - onNone: () => - failure('The fixed stage Core context could not be reconciled', cause), + onNone: () => failure('The fixed stage Core context could not be reconciled', cause), onSome: ({ message }) => failure(message, cause), - }) + }), ); }; const tryBootstrapPromise = ( - evaluate: () => PromiseLike + evaluate: () => PromiseLike, ): Effect.Effect => Effect.tryPromise({ catch: bootstrapFailureFromCause, try: evaluate }).pipe( Effect.timeoutOrElse({ duration: '30 seconds', - orElse: () => - Effect.fail(failure('Stage authorization reconciliation timed out')), - }) + orElse: () => Effect.fail(failure('Stage authorization reconciliation timed out')), + }), ); -const loadConfiguration = (): Effect.Effect< - StageContextBootstrapConfiguration, - StageContextBootstrapError -> => +const loadConfiguration = (): Effect.Effect => Effect.gen(function* loadStageContextBootstrapConfiguration() { const source = yield* Effect.all( { - databaseAdminUrl: Config.schema( - Schema.Redacted(TrimmedNonEmptyString), - 'DATABASE_ADMIN_URL' - ).pipe( - Effect.mapError((cause) => - failure('DATABASE_ADMIN_URL is required', cause) - ) + databaseAdminUrl: Config.schema(Schema.Redacted(TrimmedNonEmptyString), 'DATABASE_ADMIN_URL').pipe( + Effect.mapError((cause) => failure('DATABASE_ADMIN_URL is required', cause)), ), - deploymentEnvironment: Config.schema( - StageEnvironmentSchema, - 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT' - ).pipe( - Effect.mapError((cause) => - failure( - 'The Core installation bootstrap can run only in stage', - cause - ) - ) + deploymentEnvironment: Config.schema(StageEnvironmentSchema, 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT').pipe( + Effect.mapError((cause) => failure('The Core installation bootstrap can run only in stage', cause)), ), - spiceDbEndpoint: Config.schema( - TrimmedNonEmptyString, - 'SPICEDB_ENDPOINT' - ).pipe( - Effect.mapError((cause) => - failure('SPICEDB_ENDPOINT is required', cause) - ) + spiceDbEndpoint: Config.schema(TrimmedNonEmptyString, 'SPICEDB_ENDPOINT').pipe( + Effect.mapError((cause) => failure('SPICEDB_ENDPOINT is required', cause)), ), spiceDbInsecure: Config.schema(Schema.Trim, 'SPICEDB_INSECURE').pipe( - Effect.mapError((cause) => - failure('SPICEDB_INSECURE must be explicitly true or false', cause) - ) + Effect.mapError((cause) => failure('SPICEDB_INSECURE must be explicitly true or false', cause)), ), spiceDbPreSharedKey: Config.redacted('SPICEDB_PRESHARED_KEY').pipe( - Effect.mapError((cause) => - failure('SPICEDB_PRESHARED_KEY is required', cause) - ) + Effect.mapError((cause) => failure('SPICEDB_PRESHARED_KEY is required', cause)), ), }, - { concurrency: 5 } + { concurrency: 5 }, ); yield* parseDatabaseConfig({ DATABASE_URL: Redacted.value(source.databaseAdminUrl), @@ -202,9 +157,7 @@ const loadConfiguration = (): Effect.Effect< ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: source.deploymentEnvironment, }).pipe(Effect.mapError((error) => failure(error.reason, error))); if (spiceDb.endpoint !== 'spicedb:50051' || !spiceDb.insecureLocal) { - return yield* failure( - 'The Core installation bootstrap requires stage-private SpiceDB' - ); + return yield* failure('The Core installation bootstrap requires stage-private SpiceDB'); } return { databaseAdminUrl: source.databaseAdminUrl, @@ -217,355 +170,282 @@ const loadConfiguration = (): Effect.Effect< const classifyExactRecord = ( label: string, existing: ExactRecord | undefined, - expected: Expected + expected: Expected, ): Effect.Effect<'create' | 'existing', StageContextBootstrapError> => { if (existing === undefined) { return Effect.succeed('create'); } - const conflictingFields = Object.entries(expected).flatMap(([key, value]) => - existing[key] === value ? [] : [key] - ); + const conflictingFields = Object.entries(expected).flatMap(([key, value]) => (existing[key] === value ? [] : [key])); if (conflictingFields.length > 0) { return Effect.fail( - failure( - `Existing ${label} conflicts with the stage bootstrap definition (${conflictingFields.join(', ')})` - ) + failure(`Existing ${label} conflicts with the stage bootstrap definition (${conflictingFields.join(', ')})`), ); } return Effect.succeed('existing'); }; -const reconcilePostgresTransaction = Effect.fn( - 'StageContextBootstrap.reconcilePostgresTransaction' -)(function* reconcileStagePostgresTransaction( - transaction: CoreTransaction, - context: StageContext, - authUserId: string -): Effect.fn.Return { - const tenantCandidates = yield* transaction - .select({ - defaultLocale: tenants.defaultLocale, - name: tenants.name, - slug: tenants.slug, - status: tenants.status, - tenantId: tenants.tenantId, - }) - .from(tenants) - .where( - or( - eq(tenants.tenantId, context.tenantId), - eq(tenants.slug, context.tenantSlug) - ) - ) - .limit(2) - .pipe(Effect.mapError(bootstrapFailureFromCause)); - if (tenantCandidates.length > 1) { - return yield* failure('The stage tenant identity conflicts'); - } - const expectedTenant = { - defaultLocale: context.defaultLocale, - name: context.tenantName, - slug: context.tenantSlug, - status: 'active', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactRecord( - 'tenant', - tenantCandidates[0], - expectedTenant - )) === 'create' - ) { - yield* transaction - .insert(tenants) - .values(expectedTenant) +const reconcilePostgresTransaction = Effect.fn('StageContextBootstrap.reconcilePostgresTransaction')( + function* reconcileStagePostgresTransaction( + transaction: CoreTransaction, + context: StageContext, + authUserId: string, + ): Effect.fn.Return { + const tenantCandidates = yield* transaction + .select({ + defaultLocale: tenants.defaultLocale, + name: tenants.name, + slug: tenants.slug, + status: tenants.status, + tenantId: tenants.tenantId, + }) + .from(tenants) + .where(or(eq(tenants.tenantId, context.tenantId), eq(tenants.slug, context.tenantSlug))) + .limit(2) .pipe(Effect.mapError(bootstrapFailureFromCause)); - } + if (tenantCandidates.length > 1) { + return yield* failure('The stage tenant identity conflicts'); + } + const expectedTenant = { + defaultLocale: context.defaultLocale, + name: context.tenantName, + slug: context.tenantSlug, + status: 'active', + tenantId: context.tenantId, + } as const; + if ((yield* classifyExactRecord('tenant', tenantCandidates[0], expectedTenant)) === 'create') { + yield* transaction.insert(tenants).values(expectedTenant).pipe(Effect.mapError(bootstrapFailureFromCause)); + } - const legalEntityCandidates = yield* selectBootstrapLegalEntities( - transaction, - context - ).pipe(Effect.mapError(bootstrapFailureFromCause)); - if (legalEntityCandidates.length > 1) { - return yield* failure('The stage legal-entity identity conflicts'); - } - const expectedLegalEntity = { - legalEntityId: context.legalEntityId, - legalName: context.legalName, - registrationCountry: context.registrationCountry, - registrationNumber: context.registrationNumber, - status: 'active', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactRecord( - 'legal entity', - legalEntityCandidates[0], - expectedLegalEntity - )) === 'create' - ) { - yield* transaction - .insert(legalEntities) - .values(expectedLegalEntity) - .pipe(Effect.mapError(bootstrapFailureFromCause)); - } + const legalEntityCandidates = yield* selectBootstrapLegalEntities(transaction, context).pipe( + Effect.mapError(bootstrapFailureFromCause), + ); + if (legalEntityCandidates.length > 1) { + return yield* failure('The stage legal-entity identity conflicts'); + } + const expectedLegalEntity = { + legalEntityId: context.legalEntityId, + legalName: context.legalName, + registrationCountry: context.registrationCountry, + registrationNumber: context.registrationNumber, + status: 'active', + tenantId: context.tenantId, + } as const; + if ((yield* classifyExactRecord('legal entity', legalEntityCandidates[0], expectedLegalEntity)) === 'create') { + yield* transaction + .insert(legalEntities) + .values(expectedLegalEntity) + .pipe(Effect.mapError(bootstrapFailureFromCause)); + } - const expectedPrincipal = bootstrapPrincipalRecord(context); - const principalCandidates = yield* selectBootstrapPrincipals( - transaction, - context - ).pipe(Effect.mapError(bootstrapFailureFromCause)); - if ( - (yield* classifyExactRecord( - 'principal', - principalCandidates[0], - expectedPrincipal - )) === 'create' - ) { - yield* transaction - .insert(principals) - .values(expectedPrincipal) - .pipe(Effect.mapError(bootstrapFailureFromCause)); - } + const expectedPrincipal = bootstrapPrincipalRecord(context); + const principalCandidates = yield* selectBootstrapPrincipals(transaction, context).pipe( + Effect.mapError(bootstrapFailureFromCause), + ); + if ((yield* classifyExactRecord('principal', principalCandidates[0], expectedPrincipal)) === 'create') { + yield* transaction.insert(principals).values(expectedPrincipal).pipe(Effect.mapError(bootstrapFailureFromCause)); + } - const bindingCandidates = yield* selectBootstrapAuthBindings( - transaction, - context, - authUserId - ).pipe(Effect.mapError(bootstrapFailureFromCause)); - if (bindingCandidates.length > 1) { - return yield* failure('The stage authentication binding conflicts'); - } - const expectedBinding = { - principalAuthBindingId: context.authBindingId, - principalId: context.principalId, - provider: 'better_auth', - providerSubjectId: authUserId, - status: 'active', - subjectType: 'user', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactRecord( - 'authentication binding', - bindingCandidates[0], - expectedBinding - )) === 'create' - ) { - yield* transaction - .insert(principalAuthBindings) - .values(expectedBinding) - .pipe(Effect.mapError(bootstrapFailureFromCause)); - } + const bindingCandidates = yield* selectBootstrapAuthBindings(transaction, context, authUserId).pipe( + Effect.mapError(bootstrapFailureFromCause), + ); + if (bindingCandidates.length > 1) { + return yield* failure('The stage authentication binding conflicts'); + } + const expectedBinding = { + principalAuthBindingId: context.authBindingId, + principalId: context.principalId, + provider: 'better_auth', + providerSubjectId: authUserId, + status: 'active', + subjectType: 'user', + tenantId: context.tenantId, + } as const; + if ((yield* classifyExactRecord('authentication binding', bindingCandidates[0], expectedBinding)) === 'create') { + yield* transaction + .insert(principalAuthBindings) + .values(expectedBinding) + .pipe(Effect.mapError(bootstrapFailureFromCause)); + } - const moduleStateCandidates = yield* transaction - .select({ - moduleKey: tenantModuleStates.moduleKey, - state: tenantModuleStates.state, - tenantId: tenantModuleStates.tenantId, - tenantModuleStateId: tenantModuleStates.tenantModuleStateId, - }) - .from(tenantModuleStates) - .where( - or( - eq(tenantModuleStates.tenantModuleStateId, context.moduleStateId), - and( - eq(tenantModuleStates.tenantId, context.tenantId), - eq(tenantModuleStates.moduleKey, context.moduleId) - ) + const moduleStateCandidates = yield* transaction + .select({ + moduleKey: tenantModuleStates.moduleKey, + state: tenantModuleStates.state, + tenantId: tenantModuleStates.tenantId, + tenantModuleStateId: tenantModuleStates.tenantModuleStateId, + }) + .from(tenantModuleStates) + .where( + or( + eq(tenantModuleStates.tenantModuleStateId, context.moduleStateId), + and(eq(tenantModuleStates.tenantId, context.tenantId), eq(tenantModuleStates.moduleKey, context.moduleId)), + ), ) - ) - .limit(2) - .pipe(Effect.mapError(bootstrapFailureFromCause)); - if (moduleStateCandidates.length > 1) { - return yield* failure('The stage module-state identity conflicts'); - } - const expectedModuleState = { - moduleKey: context.moduleId, - state: 'active', - tenantId: context.tenantId, - tenantModuleStateId: context.moduleStateId, - } as const; - if ( - (yield* classifyExactRecord( - 'module state', - moduleStateCandidates[0], - expectedModuleState - )) === 'create' - ) { - yield* transaction - .insert(tenantModuleStates) - .values(expectedModuleState) + .limit(2) .pipe(Effect.mapError(bootstrapFailureFromCause)); - } - return yield* Effect.void; -}); + if (moduleStateCandidates.length > 1) { + return yield* failure('The stage module-state identity conflicts'); + } + const expectedModuleState = { + moduleKey: context.moduleId, + state: 'active', + tenantId: context.tenantId, + tenantModuleStateId: context.moduleStateId, + } as const; + if ((yield* classifyExactRecord('module state', moduleStateCandidates[0], expectedModuleState)) === 'create') { + yield* transaction + .insert(tenantModuleStates) + .values(expectedModuleState) + .pipe(Effect.mapError(bootstrapFailureFromCause)); + } + return yield* Effect.void; + }, +); -const reconcilePostgresContext = Effect.fn( - 'StageContextBootstrap.reconcilePostgresContext' -)(function* reconcileStagePostgresContext( - database: CoreDatabaseExecutor, - context: StageContext, - authUserId: string -): Effect.fn.Return { - const transactionBody = (transaction: CoreTransaction) => - reconcilePostgresTransaction(transaction, context, authUserId); - yield* database.transaction(transactionBody).pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.catchTag('SqlError', (sqlFailure) => - Effect.fail(bootstrapFailureFromCause(sqlFailure)) - ) - ); -}); +const reconcilePostgresContext = Effect.fn('StageContextBootstrap.reconcilePostgresContext')( + function* reconcileStagePostgresContext( + database: CoreDatabaseExecutor, + context: StageContext, + authUserId: string, + ): Effect.fn.Return { + const transactionBody = (transaction: CoreTransaction) => + reconcilePostgresTransaction(transaction, context, authUserId); + yield* database.transaction(transactionBody).pipe( + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.catchTag('SqlError', (sqlFailure) => Effect.fail(bootstrapFailureFromCause(sqlFailure))), + ); + }, +); -const buildRelationships = Effect.fn( - 'StageContextBootstrap.buildRelationships' -)(function* buildStageContextRelationships( - context: StageContext -): Effect.fn.Return< - readonly StageContextBootstrapRelationship[], - StageContextBootstrapError -> { - const legalEntityObjectId = toLegalEntityAccessObjectId( - context.tenantId, - context.legalEntityId - ); - const moduleObjectId = toModuleAccessObjectId( - context.tenantId, - context.legalEntityId, - context.moduleId - ); - if (legalEntityObjectId === undefined || moduleObjectId === undefined) { - return yield* failure('The stage authorization object IDs are invalid'); - } - return [ - { - relation: 'member', - resourceId: context.tenantId, - resourceType: 'tenant', - subjectId: context.principalId, - subjectType: 'principal', - }, - { - relation: 'tenant', - resourceId: legalEntityObjectId, - resourceType: 'legal_entity', - subjectId: context.tenantId, - subjectType: 'tenant', - }, - { - relation: 'member', - resourceId: legalEntityObjectId, - resourceType: 'legal_entity', - subjectId: context.principalId, - subjectType: 'principal', - }, - { - relation: 'legal_entity', - resourceId: moduleObjectId, - resourceType: 'module_access', - subjectId: legalEntityObjectId, - subjectType: 'legal_entity', - }, - { - relation: 'accessor', - resourceId: moduleObjectId, - resourceType: 'module_access', - subjectId: context.principalId, - subjectType: 'principal', - }, - ]; -}); +const buildRelationships = Effect.fn('StageContextBootstrap.buildRelationships')( + function* buildStageContextRelationships( + context: StageContext, + ): Effect.fn.Return { + const legalEntityObjectId = toLegalEntityAccessObjectId(context.tenantId, context.legalEntityId); + const moduleObjectId = toModuleAccessObjectId(context.tenantId, context.legalEntityId, context.moduleId); + if (legalEntityObjectId === undefined || moduleObjectId === undefined) { + return yield* failure('The stage authorization object IDs are invalid'); + } + return [ + { + relation: 'member', + resourceId: context.tenantId, + resourceType: 'tenant', + subjectId: context.principalId, + subjectType: 'principal', + }, + { + relation: 'tenant', + resourceId: legalEntityObjectId, + resourceType: 'legal_entity', + subjectId: context.tenantId, + subjectType: 'tenant', + }, + { + relation: 'member', + resourceId: legalEntityObjectId, + resourceType: 'legal_entity', + subjectId: context.principalId, + subjectType: 'principal', + }, + { + relation: 'legal_entity', + resourceId: moduleObjectId, + resourceType: 'module_access', + subjectId: legalEntityObjectId, + subjectType: 'legal_entity', + }, + { + relation: 'accessor', + resourceId: moduleObjectId, + resourceType: 'module_access', + subjectId: context.principalId, + subjectType: 'principal', + }, + ]; + }, +); -const touchRelationships = Effect.fn( - 'StageContextBootstrap.touchRelationships' -)(function* touchStageContextRelationships( - configuration: StageContextBootstrapConfiguration, - context: StageContext -): Effect.fn.Return { - const relationships = yield* buildRelationships(context); - const request = bootstrapRelationshipRequest(relationships); - yield* Effect.acquireUseRelease( - Effect.try({ - catch: bootstrapFailureFromCause, - try: () => - v1.NewClient( - Redacted.value(configuration.spiceDbPreSharedKey), - configuration.spiceDbEndpoint, - configuration.spiceDbSecurity - ), - }), - (client) => - tryBootstrapPromise( - client.promises.writeRelationships.bind(client.promises, request) - ).pipe(Effect.asVoid), - (client) => +const touchRelationships = Effect.fn('StageContextBootstrap.touchRelationships')( + function* touchStageContextRelationships( + configuration: StageContextBootstrapConfiguration, + context: StageContext, + ): Effect.fn.Return { + const relationships = yield* buildRelationships(context); + const request = bootstrapRelationshipRequest(relationships); + yield* Effect.acquireUseRelease( Effect.try({ catch: bootstrapFailureFromCause, - try: () => client.close(), - }) - ); -}); + try: () => + v1.NewClient( + Redacted.value(configuration.spiceDbPreSharedKey), + configuration.spiceDbEndpoint, + configuration.spiceDbSecurity, + ), + }), + (client) => + tryBootstrapPromise(client.promises.writeRelationships.bind(client.promises, request)).pipe(Effect.asVoid), + (client) => + Effect.try({ + catch: bootstrapFailureFromCause, + try: () => client.close(), + }), + ); + }, +); /** * Reconciles the complete fixed set of stage contexts before their principals/tenants can exist. * The caller supplies only the Shell-owned Better Auth user IDs in the documented fixed order. */ -export const reconcileStageContextBootstraps = Effect.fn( - 'StageContextBootstrap.reconcileStageContextBootstraps' -)(function* reconcileFixedStageContexts( - providerUserIds: StageContextBootstrapProviderUserIds -): Effect.fn.Return { - const [techsioProviderUserId, siamparkProviderUserId] = providerUserIds; - if ( - techsioProviderUserId.trim().length === 0 || - siamparkProviderUserId.trim().length === 0 - ) { - return yield* failure('Both Better Auth provider user IDs are required'); - } - if (techsioProviderUserId === siamparkProviderUserId) { - return yield* failure( - 'The stage contexts require distinct Better Auth provider user IDs' +export const reconcileStageContextBootstraps = Effect.fn('StageContextBootstrap.reconcileStageContextBootstraps')( + function* reconcileFixedStageContexts( + providerUserIds: StageContextBootstrapProviderUserIds, + ): Effect.fn.Return { + const [techsioProviderUserId, siamparkProviderUserId] = providerUserIds; + if (techsioProviderUserId.trim().length === 0 || siamparkProviderUserId.trim().length === 0) { + return yield* failure('Both Better Auth provider user IDs are required'); + } + if (techsioProviderUserId === siamparkProviderUserId) { + return yield* failure('The stage contexts require distinct Better Auth provider user IDs'); + } + const contexts = [ + { context: STAGE_CONTEXTS.techsio, providerUserId: techsioProviderUserId }, + { + context: STAGE_CONTEXTS.siampark, + providerUserId: siamparkProviderUserId, + }, + ] as const; + const configuration = yield* loadConfiguration(); + yield* Effect.scoped( + Effect.gen(function* reconcileStageDatabase() { + const databaseConfiguration = yield* parseDatabaseConfig({ + DATABASE_URL: Redacted.value(configuration.databaseAdminUrl), + }).pipe(Effect.mapError(bootstrapFailureFromCause)); + const { executor } = yield* makeCoreDatabase(databaseConfiguration).pipe( + Effect.mapError(bootstrapFailureFromCause), + ); + yield* Effect.forEach( + contexts, + ({ context, providerUserId }) => + reconcilePostgresContext(executor, context, providerUserId).pipe( + Effect.andThen(touchRelationships(configuration, context)), + ), + { concurrency: 1, discard: true }, + ); + }), ); - } - const contexts = [ - { context: STAGE_CONTEXTS.techsio, providerUserId: techsioProviderUserId }, - { - context: STAGE_CONTEXTS.siampark, - providerUserId: siamparkProviderUserId, - }, - ] as const; - const configuration = yield* loadConfiguration(); - yield* Effect.scoped( - Effect.gen(function* reconcileStageDatabase() { - const databaseConfiguration = yield* parseDatabaseConfig({ - DATABASE_URL: Redacted.value(configuration.databaseAdminUrl), - }).pipe(Effect.mapError(bootstrapFailureFromCause)); - const { executor } = yield* makeCoreDatabase(databaseConfiguration).pipe( - Effect.mapError(bootstrapFailureFromCause) - ); - yield* Effect.forEach( - contexts, - ({ context, providerUserId }) => - reconcilePostgresContext(executor, context, providerUserId).pipe( - Effect.andThen(touchRelationships(configuration, context)) - ), - { concurrency: 1, discard: true } - ); - }) - ); - return [ - { - legalEntityId: STAGE_CONTEXTS.techsio.legalEntityId, - principalId: STAGE_CONTEXTS.techsio.principalId, - tenantId: STAGE_CONTEXTS.techsio.tenantId, - }, - { - legalEntityId: STAGE_CONTEXTS.siampark.legalEntityId, - principalId: STAGE_CONTEXTS.siampark.principalId, - tenantId: STAGE_CONTEXTS.siampark.tenantId, - }, - ]; -}); + return [ + { + legalEntityId: STAGE_CONTEXTS.techsio.legalEntityId, + principalId: STAGE_CONTEXTS.techsio.principalId, + tenantId: STAGE_CONTEXTS.techsio.tenantId, + }, + { + legalEntityId: STAGE_CONTEXTS.siampark.legalEntityId, + principalId: STAGE_CONTEXTS.siampark.principalId, + tenantId: STAGE_CONTEXTS.siampark.tenantId, + }, + ]; + }, +); diff --git a/app/packages/core-runtime/src/modules/actions/bind-managed-api-key.action.ts b/app/packages/core-runtime/src/modules/actions/bind-managed-api-key.action.ts index a3860d1b0..34d3c8a4d 100644 --- a/app/packages/core-runtime/src/modules/actions/bind-managed-api-key.action.ts +++ b/app/packages/core-runtime/src/modules/actions/bind-managed-api-key.action.ts @@ -10,56 +10,47 @@ import { principalManagementRepositoryFromTransaction } from '../../auth/princip import type { PrincipalManagementRepositoryService } from '../../auth/principal-management.ts'; import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; -const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('PrincipalId') -); -const ProviderSubjectIdSchema = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -).pipe(Schema.brand('ProviderSubjectId')); -const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('AuthBindingId') +const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('PrincipalId')); +const ProviderSubjectIdSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)).pipe( + Schema.brand('ProviderSubjectId'), ); +const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('AuthBindingId')); const BindManagedApiKeyPayloadSchema = Schema.Struct({ principalId: PrincipalIdSchema, providerSubjectId: ProviderSubjectIdSchema, }); -export type BindManagedApiKeyPayload = Schema.Schema.Type< - typeof BindManagedApiKeyPayloadSchema ->; +export type BindManagedApiKeyPayload = Schema.Schema.Type; const BindManagedApiKeyResultSchema = Schema.Struct({ authBindingId: AuthBindingIdSchema, status: Schema.Literal('active'), }); -const handle = Effect.fn('BindManagedApiKeyAction.handle')( - function* bindManagedApiKeyActionHandle( - payload: BindManagedApiKeyPayload, - context: ActionHandlerContext< - Readonly>, - { readonly bind: PrincipalManagementRepositoryService['bindApiKey'] } - > - ) { - const result = yield* context.services.bind({ - managed: true, - principalId: payload.principalId, - providerSubjectId: payload.providerSubjectId, - tenantId: context.scope.tenantId, - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `principal-api-key-eligibility:${payload.principalId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: payload.principalId, - targetResourceType: 'principal', - }); - return { - ...result, - authBindingId: AuthBindingIdSchema.make(result.authBindingId), - }; - } -); +const handle = Effect.fn('BindManagedApiKeyAction.handle')(function* bindManagedApiKeyActionHandle( + payload: BindManagedApiKeyPayload, + context: ActionHandlerContext< + Readonly>, + { readonly bind: PrincipalManagementRepositoryService['bindApiKey'] } + >, +) { + const result = yield* context.services.bind({ + managed: true, + principalId: payload.principalId, + providerSubjectId: payload.providerSubjectId, + tenantId: context.scope.tenantId, + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `principal-api-key-eligibility:${payload.principalId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: payload.principalId, + targetResourceType: 'principal', + }); + return { + ...result, + authBindingId: AuthBindingIdSchema.make(result.authBindingId), + }; +}); export const bindManagedApiKeyAction = defineAction( { accessEvidencePolicy: { @@ -91,8 +82,7 @@ export const bindManagedApiKeyAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ bind: repository.bindApiKey }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/bind-self-api-key.action.ts b/app/packages/core-runtime/src/modules/actions/bind-self-api-key.action.ts index bcded4d15..80ae3b135 100644 --- a/app/packages/core-runtime/src/modules/actions/bind-self-api-key.action.ts +++ b/app/packages/core-runtime/src/modules/actions/bind-self-api-key.action.ts @@ -10,19 +10,14 @@ import { principalManagementRepositoryFromTransaction } from '../../auth/princip import type { PrincipalManagementRepositoryService } from '../../auth/principal-management.ts'; import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; -const ProviderSubjectIdSchema = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -).pipe(Schema.brand('ProviderSubjectId')); -const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('AuthBindingId') +const ProviderSubjectIdSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)).pipe( + Schema.brand('ProviderSubjectId'), ); +const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('AuthBindingId')); const BindSelfApiKeyPayloadSchema = Schema.Struct({ providerSubjectId: ProviderSubjectIdSchema, }); -export type BindSelfApiKeyPayload = Schema.Schema.Type< - typeof BindSelfApiKeyPayloadSchema ->; +export type BindSelfApiKeyPayload = Schema.Schema.Type; const BindSelfApiKeyResultSchema = Schema.Struct({ authBindingId: AuthBindingIdSchema, status: Schema.Literal('active'), @@ -30,35 +25,30 @@ const BindSelfApiKeyResultSchema = Schema.Struct({ type BindApiKey = PrincipalManagementRepositoryService['bindApiKey']; type Input = Parameters[0]; type Result = ReturnType; -const handle = Effect.fn('BindSelfApiKeyAction.handle')( - function* bindSelfApiKeyActionHandle( - payload: BindSelfApiKeyPayload, - context: ActionHandlerContext< - Readonly>, - { readonly bind: (input: Input) => Result } - > - ) { - const result = yield* context.services.bind({ - managed: false, - principalId: context.scope.principalId, - providerSubjectId: payload.providerSubjectId, - tenantId: context.scope.tenantId, - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `principal-api-key-eligibility:${context.scope.principalId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: context.scope.principalId, - targetResourceType: 'principal', - }); - return { - ...result, - authBindingId: AuthBindingIdSchema.make(result.authBindingId), - }; - } -); +const handle = Effect.fn('BindSelfApiKeyAction.handle')(function* bindSelfApiKeyActionHandle( + payload: BindSelfApiKeyPayload, + context: ActionHandlerContext>, { readonly bind: (input: Input) => Result }>, +) { + const result = yield* context.services.bind({ + managed: false, + principalId: context.scope.principalId, + providerSubjectId: payload.providerSubjectId, + tenantId: context.scope.tenantId, + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `principal-api-key-eligibility:${context.scope.principalId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: context.scope.principalId, + targetResourceType: 'principal', + }); + return { + ...result, + authBindingId: AuthBindingIdSchema.make(result.authBindingId), + }; +}); export const bindSelfApiKeyAction = defineAction( { accessEvidencePolicy: { @@ -89,8 +79,7 @@ export const bindSelfApiKeyAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ bind: repository.bindApiKey }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/catalog.ts b/app/packages/core-runtime/src/modules/actions/catalog.ts index a7418640d..5cde7f187 100644 --- a/app/packages/core-runtime/src/modules/actions/catalog.ts +++ b/app/packages/core-runtime/src/modules/actions/catalog.ts @@ -12,17 +12,15 @@ import { setSelfApiKeyBindingStatusAction } from './set-self-api-key-binding-sta export type CoreActionDescriptor = AnyActionRegistration['descriptor']; -export const coreActionCatalog: readonly CoreActionDescriptor[] = Object.freeze( - [ - // - bindManagedApiKeyAction.descriptor, - bindSelfApiKeyAction.descriptor, - changePrincipalStatusAction.descriptor, - changeTenantModuleStateAction.descriptor, - createNonHumanPrincipalAction.descriptor, - recordSupportImpersonationAction.descriptor, - setManagedApiKeyBindingStatusAction.descriptor, - setSelfApiKeyBindingStatusAction.descriptor, - // - ] -); +export const coreActionCatalog: readonly CoreActionDescriptor[] = Object.freeze([ + // + bindManagedApiKeyAction.descriptor, + bindSelfApiKeyAction.descriptor, + changePrincipalStatusAction.descriptor, + changeTenantModuleStateAction.descriptor, + createNonHumanPrincipalAction.descriptor, + recordSupportImpersonationAction.descriptor, + setManagedApiKeyBindingStatusAction.descriptor, + setSelfApiKeyBindingStatusAction.descriptor, + // +]); diff --git a/app/packages/core-runtime/src/modules/actions/change-principal-status.action.ts b/app/packages/core-runtime/src/modules/actions/change-principal-status.action.ts index de1e96752..a1ab28d69 100644 --- a/app/packages/core-runtime/src/modules/actions/change-principal-status.action.ts +++ b/app/packages/core-runtime/src/modules/actions/change-principal-status.action.ts @@ -10,14 +10,9 @@ import { principalManagementRepositoryFromTransaction } from '../../auth/princip import type { PrincipalManagementRepositoryService } from '../../auth/principal-management.ts'; import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; -const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('PrincipalId') -); +const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('PrincipalId')); const status = Schema.Literals(['active', 'disabled', 'archived']); -const reason = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -); +const reason = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)); const ChangePrincipalStatusPayloadSchema = Schema.Union([ Schema.Struct({ expectedStatus: status, @@ -32,39 +27,35 @@ const ChangePrincipalStatusPayloadSchema = Schema.Union([ reason, }), ]); -export type ChangePrincipalStatusPayload = Schema.Schema.Type< - typeof ChangePrincipalStatusPayloadSchema ->; +export type ChangePrincipalStatusPayload = Schema.Schema.Type; const ChangePrincipalStatusResultSchema = Schema.Struct({ previousStatus: status, status, }); -const handle = Effect.fn('ChangePrincipalStatusAction.handle')( - function* changePrincipalStatusActionHandle( - payload: ChangePrincipalStatusPayload, - context: ActionHandlerContext< - Readonly>, - { - readonly change: PrincipalManagementRepositoryService['changePrincipalStatus']; - } - > - ) { - const result = yield* context.services.change({ - ...payload, - tenantId: context.scope.tenantId, - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `principal-status-prior:${payload.principalId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: payload.principalId, - targetResourceType: 'principal', - }); - return result; - } -); +const handle = Effect.fn('ChangePrincipalStatusAction.handle')(function* changePrincipalStatusActionHandle( + payload: ChangePrincipalStatusPayload, + context: ActionHandlerContext< + Readonly>, + { + readonly change: PrincipalManagementRepositoryService['changePrincipalStatus']; + } + >, +) { + const result = yield* context.services.change({ + ...payload, + tenantId: context.scope.tenantId, + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `principal-status-prior:${payload.principalId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: payload.principalId, + targetResourceType: 'principal', + }); + return result; +}); export const changePrincipalStatusAction = defineAction( { @@ -97,8 +88,7 @@ export const changePrincipalStatusAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ change: repository.changePrincipalStatus }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/change-tenant-module-state.action.ts b/app/packages/core-runtime/src/modules/actions/change-tenant-module-state.action.ts index 92556035b..69f36af8a 100644 --- a/app/packages/core-runtime/src/modules/actions/change-tenant-module-state.action.ts +++ b/app/packages/core-runtime/src/modules/actions/change-tenant-module-state.action.ts @@ -29,25 +29,16 @@ import type { PersistTenantModuleStateChangeResult, } from '../tenant-module-state-service.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const moduleKeySchema = OntosModuleIdSchema.check(Schema.isMaxLength(128)); -const reasonSchema = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -); +const reasonSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)); const ChangeTenantModuleStatePayloadSchema = Schema.Struct({ expectedState: Schema.optionalKey(TenantModuleStateSchema), @@ -55,9 +46,7 @@ const ChangeTenantModuleStatePayloadSchema = Schema.Struct({ newState: TenantModuleStateSchema, reason: Schema.optionalKey(reasonSchema), }); -export type ChangeTenantModuleStatePayload = Schema.Schema.Type< - typeof ChangeTenantModuleStatePayloadSchema ->; +export type ChangeTenantModuleStatePayload = Schema.Schema.Type; const ChangeTenantModuleStateResultSchema = Schema.Struct({ moduleKey: moduleKeySchema, @@ -76,72 +65,60 @@ const ChangeTenantModuleStateError = Schema.Union([ TenantModuleStateValidationUnavailableError, ]); -type ChangeTenantModuleStateDomainEvents = Readonly< - Record> ->; +type ChangeTenantModuleStateDomainEvents = Readonly>>; interface ChangeTenantModuleStateServices { readonly persist: ( - input: PersistTenantModuleStateChangeInput - ) => Effect.Effect< - PersistTenantModuleStateChangeResult, - TenantModuleStateTransitionError - >; + input: PersistTenantModuleStateChangeInput, + ) => Effect.Effect; } -const handleChangeTenantModuleState = Effect.fn( - 'ChangeTenantModuleStateAction.handleChangeTenantModuleState' -)(function* changeTenantModuleStateHandler( - payload: ChangeTenantModuleStatePayload, - context: ActionHandlerContext< - ChangeTenantModuleStateDomainEvents, - ChangeTenantModuleStateServices - > -) { - const installedCatalog = yield* InstalledModuleCatalogService; - const catalog = yield* installedCatalog.load; - yield* validateTenantModuleStateTransition( - catalog, - payload.moduleKey, - payload.newState - ); - const result = yield* context.services.persist( - withOptionalProperty( +const handleChangeTenantModuleState = Effect.fn('ChangeTenantModuleStateAction.handleChangeTenantModuleState')( + function* changeTenantModuleStateHandler( + payload: ChangeTenantModuleStatePayload, + context: ActionHandlerContext, + ) { + const installedCatalog = yield* InstalledModuleCatalogService; + const catalog = yield* installedCatalog.load; + yield* validateTenantModuleStateTransition(catalog, payload.moduleKey, payload.newState); + const result = yield* context.services.persist( withOptionalProperty( + withOptionalProperty( + { + actionInvocationId: context.actionInvocationId, + authMethod: context.scope.authMethod, + }, + payload.expectedState !== undefined, + 'expectedState', + payload.expectedState, + { + moduleKey: payload.moduleKey, + newState: payload.newState, + principalId: context.scope.principalId, + }, + ), + payload.reason !== undefined, + 'reason', + payload.reason, { - actionInvocationId: context.actionInvocationId, - authMethod: context.scope.authMethod, + tenantId: context.scope.tenantId, }, - payload.expectedState !== undefined, - 'expectedState', - payload.expectedState, - { - moduleKey: payload.moduleKey, - newState: payload.newState, - principalId: context.scope.principalId, - } ), - payload.reason !== undefined, - 'reason', - payload.reason, - { - tenantId: context.scope.tenantId, - } - ) - ); + ); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `tenant-module-state-prior:${payload.moduleKey}`, - resultCount: result.previousState === null ? 0 : 1, - servingModuleKey: 'core.modules', - targetModuleKey: payload.moduleKey, - targetResourceId: payload.moduleKey, - targetResourceType: 'tenant-module-state', - }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `tenant-module-state-prior:${payload.moduleKey}`, + resultCount: result.previousState === null ? 0 : 1, + servingModuleKey: 'core.modules', + targetModuleKey: payload.moduleKey, + targetResourceId: payload.moduleKey, + targetResourceType: 'tenant-module-state', + }); - return result; -}); + return result; + }, +); export const changeTenantModuleStateAction = defineAction( { @@ -175,5 +152,5 @@ export const changeTenantModuleStateAction = defineAction( (transaction) => Effect.succeed({ persist: (input) => persistTenantModuleStateChange(transaction, input), - }) + }), ); diff --git a/app/packages/core-runtime/src/modules/actions/create-non-human-principal.action.ts b/app/packages/core-runtime/src/modules/actions/create-non-human-principal.action.ts index f4aebf321..383adc591 100644 --- a/app/packages/core-runtime/src/modules/actions/create-non-human-principal.action.ts +++ b/app/packages/core-runtime/src/modules/actions/create-non-human-principal.action.ts @@ -12,17 +12,12 @@ import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; const uuid = Schema.String.check(Schema.isUUID()); const PrincipalIdSchema = uuid.pipe(Schema.brand('PrincipalId')); -const displayName = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(200) -); +const displayName = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200)); const CreateNonHumanPrincipalPayloadSchema = Schema.Struct({ displayName, kind: Schema.Literals(['service', 'integration', 'system']), }); -export type CreateNonHumanPrincipalPayload = Schema.Schema.Type< - typeof CreateNonHumanPrincipalPayloadSchema ->; +export type CreateNonHumanPrincipalPayload = Schema.Schema.Type; const CreateNonHumanPrincipalResultSchema = Schema.Struct({ principalId: PrincipalIdSchema, status: Schema.Literal('active'), @@ -35,16 +30,14 @@ const handle = ( { readonly create: PrincipalManagementRepositoryService['createNonHumanPrincipal']; } - > + >, ) => - context.services - .create({ ...payload, tenantId: context.scope.tenantId }) - .pipe( - Effect.map((result) => ({ - ...result, - principalId: PrincipalIdSchema.make(result.principalId), - })) - ); + context.services.create({ ...payload, tenantId: context.scope.tenantId }).pipe( + Effect.map((result) => ({ + ...result, + principalId: PrincipalIdSchema.make(result.principalId), + })), + ); export const createNonHumanPrincipalAction = defineAction( { @@ -77,8 +70,7 @@ export const createNonHumanPrincipalAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ create: repository.createNonHumanPrincipal }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/record-support-impersonation.action.ts b/app/packages/core-runtime/src/modules/actions/record-support-impersonation.action.ts index 1a85ed99c..8ff7ed304 100644 --- a/app/packages/core-runtime/src/modules/actions/record-support-impersonation.action.ts +++ b/app/packages/core-runtime/src/modules/actions/record-support-impersonation.action.ts @@ -5,24 +5,16 @@ import { Effect, Schema } from 'effect'; import type { ActionHandlerContext } from '../../actions/context.ts'; import { defineAction } from '../../actions/definition.ts'; -import { - IdentityTargetInvalidError, - PrincipalManagementErrorSchema, -} from '../../auth/principal-management-errors.ts'; +import { IdentityTargetInvalidError, PrincipalManagementErrorSchema } from '../../auth/principal-management-errors.ts'; import { principalManagementRepositoryFromTransaction } from '../../auth/principal-management.ts'; import type { PrincipalManagementRepositoryService } from '../../auth/principal-management.ts'; import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; -const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('PrincipalId') -); -const reason = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -); +const PrincipalIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('PrincipalId')); +const reason = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)); const safeSessionRef = Schema.String.check( Schema.isPattern(/^better-auth-session:[^\s:][^\s]{0,278}$/u), - Schema.isMaxLength(300) + Schema.isMaxLength(300), ); const checkpointFields = { originalPrincipalId: PrincipalIdSchema, @@ -40,80 +32,75 @@ const RecordSupportImpersonationPayloadSchema = Schema.Union([ sessionRef: safeSessionRef, }), ]); -export type RecordSupportImpersonationPayload = Schema.Schema.Type< - typeof RecordSupportImpersonationPayloadSchema ->; +export type RecordSupportImpersonationPayload = Schema.Schema.Type; const RecordSupportImpersonationResultSchema = Schema.Struct({ checkpoint: Schema.Literals(['requested', 'started', 'stopped']), recorded: Schema.Literal(true), }); -type ValidateSupportImpersonation = - PrincipalManagementRepositoryService['validateSupportImpersonation']; -const handle = Effect.fn('RecordSupportImpersonationAction.handle')( - function* recordSupportImpersonationActionHandle( - payload: RecordSupportImpersonationPayload, - context: ActionHandlerContext< - Readonly>, - { - readonly validate: ( - input: Parameters[0] - ) => ReturnType; - } - > - ) { - if ( - payload.originalPrincipalId !== context.scope.principalId || - payload.targetPrincipalId === payload.originalPrincipalId || - context.scope.authMethod !== 'session' || - context.scope.authBindingId === undefined - ) { - return yield* new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The impersonation checkpoint is invalid', - }); +type ValidateSupportImpersonation = PrincipalManagementRepositoryService['validateSupportImpersonation']; +const handle = Effect.fn('RecordSupportImpersonationAction.handle')(function* recordSupportImpersonationActionHandle( + payload: RecordSupportImpersonationPayload, + context: ActionHandlerContext< + Readonly>, + { + readonly validate: ( + input: Parameters[0], + ) => ReturnType; } - yield* context.services.validate({ - checkpoint: payload.checkpoint, - originalAuthBindingId: context.scope.authBindingId, - originalPrincipalId: payload.originalPrincipalId, - targetPrincipalId: payload.targetPrincipalId, - tenantId: context.scope.tenantId, - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `support-original-eligibility:${payload.originalPrincipalId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: payload.originalPrincipalId, - targetResourceType: 'principal', - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `support-target-eligibility:${payload.targetPrincipalId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: payload.targetPrincipalId, - targetResourceType: 'principal', + >, +) { + if ( + payload.originalPrincipalId !== context.scope.principalId || + payload.targetPrincipalId === payload.originalPrincipalId || + context.scope.authMethod !== 'session' || + context.scope.authBindingId === undefined + ) { + return yield* new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The impersonation checkpoint is invalid', }); - yield* payload.checkpoint === 'requested' - ? context.recordAuditEvidence({ - checkpoint: payload.checkpoint, - originalPrincipalId: payload.originalPrincipalId, - reason: payload.reason, - targetPrincipalId: payload.targetPrincipalId, - }) - : context.recordAuditEvidence({ - checkpoint: payload.checkpoint, - originalPrincipalId: payload.originalPrincipalId, - reason: payload.reason, - sessionRef: payload.sessionRef, - targetPrincipalId: payload.targetPrincipalId, - }); - return { checkpoint: payload.checkpoint, recorded: true as const }; } -); + yield* context.services.validate({ + checkpoint: payload.checkpoint, + originalAuthBindingId: context.scope.authBindingId, + originalPrincipalId: payload.originalPrincipalId, + targetPrincipalId: payload.targetPrincipalId, + tenantId: context.scope.tenantId, + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `support-original-eligibility:${payload.originalPrincipalId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: payload.originalPrincipalId, + targetResourceType: 'principal', + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `support-target-eligibility:${payload.targetPrincipalId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: payload.targetPrincipalId, + targetResourceType: 'principal', + }); + yield* payload.checkpoint === 'requested' + ? context.recordAuditEvidence({ + checkpoint: payload.checkpoint, + originalPrincipalId: payload.originalPrincipalId, + reason: payload.reason, + targetPrincipalId: payload.targetPrincipalId, + }) + : context.recordAuditEvidence({ + checkpoint: payload.checkpoint, + originalPrincipalId: payload.originalPrincipalId, + reason: payload.reason, + sessionRef: payload.sessionRef, + targetPrincipalId: payload.targetPrincipalId, + }); + return { checkpoint: payload.checkpoint, recorded: true as const }; +}); export const recordSupportImpersonationAction = defineAction( { accessEvidencePolicy: { @@ -142,15 +129,13 @@ export const recordSupportImpersonationAction = defineAction( policies: [], resultSchema: RecordSupportImpersonationResultSchema, schemaVersion: '1', - tenantPermission: (payload) => - payload.checkpoint === 'stopped' ? undefined : 'impersonate', + tenantPermission: (payload) => (payload.checkpoint === 'stopped' ? undefined : 'impersonate'), }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ validate: repository.validateSupportImpersonation, }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/set-managed-api-key-binding-status.action.ts b/app/packages/core-runtime/src/modules/actions/set-managed-api-key-binding-status.action.ts index 60d94671f..b3d965b5f 100644 --- a/app/packages/core-runtime/src/modules/actions/set-managed-api-key-binding-status.action.ts +++ b/app/packages/core-runtime/src/modules/actions/set-managed-api-key-binding-status.action.ts @@ -14,10 +14,7 @@ const uuid = Schema.String.check(Schema.isUUID()); const AuthBindingIdSchema = uuid.pipe(Schema.brand('AuthBindingId')); const PrincipalIdSchema = uuid.pipe(Schema.brand('PrincipalId')); const status = Schema.Literals(['active', 'disabled', 'revoked']); -const reason = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -); +const reason = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)); const SetManagedApiKeyBindingStatusPayloadSchema = Schema.Union([ Schema.Struct({ authBindingId: AuthBindingIdSchema, @@ -49,7 +46,7 @@ const handle = Effect.fn('SetManagedApiKeyBindingStatusAction.handle')( { readonly setStatus: PrincipalManagementRepositoryService['setApiKeyBindingStatus']; } - > + >, ) { const result = yield* context.services.setStatus({ ...payload, @@ -66,7 +63,7 @@ const handle = Effect.fn('SetManagedApiKeyBindingStatusAction.handle')( targetResourceType: 'principal-auth-binding', }); return result; - } + }, ); export const setManagedApiKeyBindingStatusAction = defineAction( { @@ -99,8 +96,7 @@ export const setManagedApiKeyBindingStatusAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ setStatus: repository.setApiKeyBindingStatus }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/actions/set-self-api-key-binding-status.action.ts b/app/packages/core-runtime/src/modules/actions/set-self-api-key-binding-status.action.ts index 1601ad4fc..3fc134e37 100644 --- a/app/packages/core-runtime/src/modules/actions/set-self-api-key-binding-status.action.ts +++ b/app/packages/core-runtime/src/modules/actions/set-self-api-key-binding-status.action.ts @@ -10,14 +10,9 @@ import { principalManagementRepositoryFromTransaction } from '../../auth/princip import type { PrincipalManagementRepositoryService } from '../../auth/principal-management.ts'; import { defineSystemModuleEntrypoint } from '../module-entrypoint.ts'; -const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('AuthBindingId') -); +const AuthBindingIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('AuthBindingId')); const status = Schema.Literals(['active', 'disabled', 'revoked']); -const reason = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(500) -); +const reason = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)); const SetSelfApiKeyBindingStatusPayloadSchema = Schema.Union([ Schema.Struct({ authBindingId: AuthBindingIdSchema, @@ -32,41 +27,37 @@ const SetSelfApiKeyBindingStatusPayloadSchema = Schema.Union([ reason, }), ]); -export type SetSelfApiKeyBindingStatusPayload = Schema.Schema.Type< - typeof SetSelfApiKeyBindingStatusPayloadSchema ->; +export type SetSelfApiKeyBindingStatusPayload = Schema.Schema.Type; const SetSelfApiKeyBindingStatusResultSchema = Schema.Struct({ previousStatus: status, status, }); -const handle = Effect.fn('SetSelfApiKeyBindingStatusAction.handle')( - function* setSelfApiKeyBindingStatusActionHandle( - payload: SetSelfApiKeyBindingStatusPayload, - context: ActionHandlerContext< - Readonly>, - { - readonly setStatus: PrincipalManagementRepositoryService['setApiKeyBindingStatus']; - } - > - ) { - const result = yield* context.services.setStatus({ - ...payload, - managed: false, - principalId: context.scope.principalId, - tenantId: context.scope.tenantId, - }); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `api-key-binding-prior:${payload.authBindingId}`, - resultCount: 1, - servingModuleKey: 'core.identity', - targetModuleKey: 'core.identity', - targetResourceId: payload.authBindingId, - targetResourceType: 'principal-auth-binding', - }); - return result; - } -); +const handle = Effect.fn('SetSelfApiKeyBindingStatusAction.handle')(function* setSelfApiKeyBindingStatusActionHandle( + payload: SetSelfApiKeyBindingStatusPayload, + context: ActionHandlerContext< + Readonly>, + { + readonly setStatus: PrincipalManagementRepositoryService['setApiKeyBindingStatus']; + } + >, +) { + const result = yield* context.services.setStatus({ + ...payload, + managed: false, + principalId: context.scope.principalId, + tenantId: context.scope.tenantId, + }); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `api-key-binding-prior:${payload.authBindingId}`, + resultCount: 1, + servingModuleKey: 'core.identity', + targetModuleKey: 'core.identity', + targetResourceId: payload.authBindingId, + targetResourceType: 'principal-auth-binding', + }); + return result; +}); export const setSelfApiKeyBindingStatusAction = defineAction( { accessEvidencePolicy: { @@ -97,8 +88,7 @@ export const setSelfApiKeyBindingStatusAction = defineAction( }, handle, (transaction) => { - const repository = - principalManagementRepositoryFromTransaction(transaction); + const repository = principalManagementRepositoryFromTransaction(transaction); return Effect.succeed({ setStatus: repository.setApiKeyBindingStatus }); - } + }, ); diff --git a/app/packages/core-runtime/src/modules/application-composition.ts b/app/packages/core-runtime/src/modules/application-composition.ts index a36888277..9afdc0028 100644 --- a/app/packages/core-runtime/src/modules/application-composition.ts +++ b/app/packages/core-runtime/src/modules/application-composition.ts @@ -1,20 +1,13 @@ import { Effect, Order, Predicate, Result, Schema } from 'effect'; -import { - OntosComponentContractSchema, - OntosDeploymentIdentitySchema, - OntosModuleIdSchema, -} from './manifest.ts'; +import { OntosComponentContractSchema, OntosDeploymentIdentitySchema, OntosModuleIdSchema } from './manifest.ts'; export const ONTOS_APPLICATION_COMPOSITION_SCHEMA_VERSION = '1' as const; const sha256 = Schema.String.check(Schema.isPattern(/^[\da-f]{64}$/u)); -const version = Schema.String.check( - Schema.isPattern(/^[0-9]+(?:\.[0-9]+){0,2}$/u) -); +const version = Schema.String.check(Schema.isPattern(/^[0-9]+(?:\.[0-9]+){0,2}$/u)); const isLoopbackHostname = (hostname: string): boolean => - ['localhost', '127.0.0.1', '[::1]'].includes(hostname) || - hostname.endsWith('.localhost'); + ['localhost', '127.0.0.1', '[::1]'].includes(hostname) || hostname.endsWith('.localhost'); const artifactUrl = Schema.String.check( Schema.makeFilter((value) => { @@ -23,15 +16,14 @@ const artifactUrl = Schema.String.check( return 'artifact URL must be absolute'; } const loopback = isLoopbackHostname(url.hostname); - return (url.protocol === 'https:' || - (url.protocol === 'http:' && loopback)) && + return (url.protocol === 'https:' || (url.protocol === 'http:' && loopback)) && url.username === '' && url.password === '' && url.search === '' && url.hash === '' ? undefined : 'artifact URL must use HTTPS (or loopback HTTP) without credentials, query, or fragment'; - }) + }), ); export const ApplicationCompositionArtifactReferenceSchema = Schema.Struct({ @@ -66,9 +58,7 @@ export const ApplicationCompositionModuleSchema = Schema.Struct({ sha256, version, }), - requiredCoreCapabilities: Schema.Array( - ApplicationCompositionVersionedIdentitySchema - ), + requiredCoreCapabilities: Schema.Array(ApplicationCompositionVersionedIdentitySchema), requiredShellAbi: ApplicationCompositionVersionedIdentitySchema, sharedSingletons: Schema.Array(ApplicationCompositionSingletonSchema), }); @@ -79,26 +69,20 @@ export const ApplicationCompositionSchema = Schema.Struct({ schemaVersion: Schema.Literal(ONTOS_APPLICATION_COMPOSITION_SCHEMA_VERSION), shell: Schema.Struct({ contributionAbi: ApplicationCompositionVersionedIdentitySchema, - coreCapabilities: Schema.Array( - ApplicationCompositionVersionedIdentitySchema - ), + coreCapabilities: Schema.Array(ApplicationCompositionVersionedIdentitySchema), sharedSingletons: Schema.Array(ApplicationCompositionSingletonSchema), }), }); export type ApplicationComposition = typeof ApplicationCompositionSchema.Type; -export type ApplicationCompositionModule = - typeof ApplicationCompositionModuleSchema.Type; -export type ApplicationCompositionVersionedIdentity = - typeof ApplicationCompositionVersionedIdentitySchema.Type; +export type ApplicationCompositionModule = typeof ApplicationCompositionModuleSchema.Type; +export type ApplicationCompositionVersionedIdentity = typeof ApplicationCompositionVersionedIdentitySchema.Type; const observedContractSchema = Schema.Struct({ contractUrl: artifactUrl, - contributionKeys: - ApplicationCompositionModuleSchema.fields.allowedContributions, + contributionKeys: ApplicationCompositionModuleSchema.fields.allowedContributions, deployment: OntosDeploymentIdentitySchema, - federationExposes: - ApplicationCompositionModuleSchema.fields.federation.fields.exposes, + federationExposes: ApplicationCompositionModuleSchema.fields.federation.fields.exposes, mfBoundaryId: OntosComponentContractSchema.fields.mfBoundaryId, moduleId: OntosModuleIdSchema, publicContract: ApplicationCompositionModuleSchema.fields.publicContract, @@ -117,43 +101,30 @@ const candidateEvidenceSchema = Schema.Struct({ runtime: ApplicationCompositionSchema.fields.shell, }); -export type ObservedApplicationCompositionContract = - typeof observedContractSchema.Type; -export type ObservedModuleFederationManifest = - typeof observedFederationSchema.Type; -export type ApplicationCompositionCandidateEvidence = - typeof candidateEvidenceSchema.Type; +export type ObservedApplicationCompositionContract = typeof observedContractSchema.Type; +export type ObservedModuleFederationManifest = typeof observedFederationSchema.Type; +export type ApplicationCompositionCandidateEvidence = typeof candidateEvidenceSchema.Type; export class ApplicationCompositionValidationError extends Schema.TaggedError()( 'ApplicationCompositionValidationError', { code: Schema.tag('application_composition_invalid'), reason: Schema.String, - } + }, ) {} -const identityKey = ( - identity: ApplicationCompositionVersionedIdentity -): string => `${identity.id}@${identity.version}`; +const identityKey = (identity: ApplicationCompositionVersionedIdentity): string => `${identity.id}@${identity.version}`; const identityOrder = Order.mapInput(Order.String, identityKey); -const moduleOrder = Order.mapInput( - Order.String, - (module: ApplicationCompositionModule) => module.moduleId -); +const moduleOrder = Order.mapInput(Order.String, (module: ApplicationCompositionModule) => module.moduleId); const singletonOrder = Order.Struct({ packageName: Order.String, version: Order.String, }); const sameDeployment = Schema.toEquivalence(OntosDeploymentIdentitySchema); -const samePublicContract = Schema.toEquivalence( - ApplicationCompositionModuleSchema.fields.publicContract -); +const samePublicContract = Schema.toEquivalence(ApplicationCompositionModuleSchema.fields.publicContract); -const sameUniqueStrings = ( - left: readonly string[], - right: readonly string[] -): boolean => { +const sameUniqueStrings = (left: readonly string[], right: readonly string[]): boolean => { const leftSet = new Set(left); const rightSet = new Set(right); return ( @@ -167,29 +138,19 @@ const sameUniqueStrings = ( const sameVersionClaims = ( left: readonly Value[], right: readonly Value[], - key: (value: Value) => string + key: (value: Value) => string, ): boolean => { - const leftVersions = new Map( - left.map((value) => [key(value), value.version]) - ); - const rightVersions = new Map( - right.map((value) => [key(value), value.version]) - ); + const leftVersions = new Map(left.map((value) => [key(value), value.version])); + const rightVersions = new Map(right.map((value) => [key(value), value.version])); return ( leftVersions.size === left.length && rightVersions.size === right.length && leftVersions.size === rightVersions.size && - [...leftVersions].every( - ([claim, claimVersion]) => rightVersions.get(claim) === claimVersion - ) + [...leftVersions].every(([claim, claimVersion]) => rightVersions.get(claim) === claimVersion) ); }; -const claim = Effect.fnUntraced(function* claimUnique( - claims: Set, - value: string, - label: string -) { +const claim = Effect.fnUntraced(function* claimUnique(claims: Set, value: string, label: string) { if (claims.has(value)) { return yield* new ApplicationCompositionValidationError({ reason: `duplicate ${label} ${value}`, @@ -200,17 +161,13 @@ const claim = Effect.fnUntraced(function* claimUnique( }); const assertAcyclicDependencies = Effect.fnUntraced(function* checkCycles( - modules: readonly ApplicationCompositionModule[] + modules: readonly ApplicationCompositionModule[], ) { - const dependencies = new Map( - modules.map((module) => [module.moduleId, module.dependencies]) - ); + const dependencies = new Map(modules.map((module) => [module.moduleId, module.dependencies])); const visiting = new Set(); const visited = new Set(); - const visit: ( - moduleId: string - ) => Effect.Effect = Effect.fn( - 'assertAcyclicDependencies.visit' + const visit: (moduleId: string) => Effect.Effect = Effect.fn( + 'assertAcyclicDependencies.visit', )(function* visitDependency(moduleId) { if (visiting.has(moduleId)) { return yield* new ApplicationCompositionValidationError({ @@ -237,11 +194,7 @@ const assertAcyclicDependencies = Effect.fnUntraced(function* checkCycles( }); const freeze = (value: Value): Value => { - if ( - !Predicate.isObjectKeyword(value) || - value === null || - Object.isFrozen(value) - ) { + if (!Predicate.isObjectKeyword(value) || value === null || Object.isFrozen(value)) { return value; } for (const nested of Object.values(value)) { @@ -252,17 +205,13 @@ const freeze = (value: Value): Value => { const assertDependenciesPresent = Effect.fnUntraced(function* checkDependencies( module: ApplicationCompositionModule, - moduleIds: ReadonlySet + moduleIds: ReadonlySet, ) { const dependencies = new Set(); yield* Effect.forEach( module.dependencies, Effect.fnUntraced(function* checkDependency(dependency) { - yield* claim( - dependencies, - dependency, - `dependency in module ${module.moduleId}` - ); + yield* claim(dependencies, dependency, `dependency in module ${module.moduleId}`); if (!moduleIds.has(dependency)) { return yield* new ApplicationCompositionValidationError({ reason: `module ${module.moduleId} requires missing dependency ${dependency}`, @@ -270,7 +219,7 @@ const assertDependenciesPresent = Effect.fnUntraced(function* checkDependencies( } return yield* Effect.void; }), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); return yield* Effect.void; }); @@ -279,11 +228,9 @@ const assertShellCompatibility = Effect.fnUntraced(function* checkCompatibility( module: ApplicationCompositionModule, shell: ApplicationComposition['shell'], availableCapabilities: ReadonlySet, - availableSingletons: ReadonlyMap + availableSingletons: ReadonlyMap, ) { - if ( - identityKey(module.requiredShellAbi) !== identityKey(shell.contributionAbi) - ) { + if (identityKey(module.requiredShellAbi) !== identityKey(shell.contributionAbi)) { return yield* new ApplicationCompositionValidationError({ reason: `module ${module.moduleId} requires an incompatible Shell contribution ABI`, }); @@ -300,34 +247,28 @@ const assertShellCompatibility = Effect.fnUntraced(function* checkCompatibility( } return yield* Effect.void; }), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); const singletonPackages = new Set(); yield* Effect.forEach( module.sharedSingletons, Effect.fnUntraced(function* checkSingleton(singleton) { - yield* claim( - singletonPackages, - singleton.packageName, - 'required shared singleton' - ); - if ( - availableSingletons.get(singleton.packageName) !== singleton.version - ) { + yield* claim(singletonPackages, singleton.packageName, 'required shared singleton'); + if (availableSingletons.get(singleton.packageName) !== singleton.version) { return yield* new ApplicationCompositionValidationError({ reason: `module ${module.moduleId} requires incompatible shared singleton ${singleton.packageName}`, }); } return yield* Effect.void; }), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); return yield* Effect.void; }); const matchesObservedArtifact = ( module: ApplicationCompositionModule, - contract: ObservedApplicationCompositionContract + contract: ObservedApplicationCompositionContract, ): boolean => contract.contractUrl === module.contract.url && contract.sha256 === module.contract.sha256 && @@ -335,7 +276,7 @@ const matchesObservedArtifact = ( const assertObservedDeployment = Effect.fnUntraced(function* checkDeployment( module: ApplicationCompositionModule, - contract: ObservedApplicationCompositionContract | undefined + contract: ObservedApplicationCompositionContract | undefined, ) { if ( contract === undefined || @@ -344,10 +285,7 @@ const assertObservedDeployment = Effect.fnUntraced(function* checkDeployment( contract.mfBoundaryId !== module.federation.remoteName || !samePublicContract(contract.publicContract, module.publicContract) || module.publicContract.id !== module.moduleId || - !sameUniqueStrings( - module.allowedContributions, - contract.contributionKeys - ) || + !sameUniqueStrings(module.allowedContributions, contract.contributionKeys) || !sameUniqueStrings(module.federation.exposes, contract.federationExposes) ) { return yield* new ApplicationCompositionValidationError({ @@ -357,66 +295,46 @@ const assertObservedDeployment = Effect.fnUntraced(function* checkDeployment( return yield* Effect.void; }); -const assertObservedFederationManifest = Effect.fnUntraced( - function* checkFederation( - module: ApplicationCompositionModule, - manifest: ObservedModuleFederationManifest | undefined +const assertObservedFederationManifest = Effect.fnUntraced(function* checkFederation( + module: ApplicationCompositionModule, + manifest: ObservedModuleFederationManifest | undefined, +) { + if ( + manifest === undefined || + manifest.remoteName !== module.federation.remoteName || + manifest.sha256 !== module.federation.manifest.sha256 || + !sameUniqueStrings(module.federation.exposes, manifest.exposes) || + !sameVersionClaims(module.sharedSingletons, manifest.sharedSingletons, ({ packageName }) => packageName) ) { - if ( - manifest === undefined || - manifest.remoteName !== module.federation.remoteName || - manifest.sha256 !== module.federation.manifest.sha256 || - !sameUniqueStrings(module.federation.exposes, manifest.exposes) || - !sameVersionClaims( - module.sharedSingletons, - manifest.sharedSingletons, - ({ packageName }) => packageName - ) - ) { - return yield* new ApplicationCompositionValidationError({ - reason: `module ${module.moduleId} does not match its observed Module Federation manifest`, - }); - } - return yield* Effect.void; + return yield* new ApplicationCompositionValidationError({ + reason: `module ${module.moduleId} does not match its observed Module Federation manifest`, + }); } -); + return yield* Effect.void; +}); const assertObservedRuntime = Effect.fnUntraced(function* checkRuntime( shell: ApplicationComposition['shell'], - runtime: ApplicationCompositionCandidateEvidence['runtime'] + runtime: ApplicationCompositionCandidateEvidence['runtime'], ) { if ( - identityKey(shell.contributionAbi) !== - identityKey(runtime.contributionAbi) || - !sameVersionClaims( - shell.coreCapabilities, - runtime.coreCapabilities, - ({ id }) => id - ) || - !sameVersionClaims( - shell.sharedSingletons, - runtime.sharedSingletons, - ({ packageName }) => packageName - ) + identityKey(shell.contributionAbi) !== identityKey(runtime.contributionAbi) || + !sameVersionClaims(shell.coreCapabilities, runtime.coreCapabilities, ({ id }) => id) || + !sameVersionClaims(shell.sharedSingletons, runtime.sharedSingletons, ({ packageName }) => packageName) ) { return yield* new ApplicationCompositionValidationError({ - reason: - 'Shell and Core claims do not match the observed runtime contract', + reason: 'Shell and Core claims do not match the observed runtime contract', }); } return yield* Effect.void; }); -const compositionJsonSchema = Schema.fromJsonString( - ApplicationCompositionSchema -); +const compositionJsonSchema = Schema.fromJsonString(ApplicationCompositionSchema); const encodeCompositionJson = (composition: ApplicationComposition): string => Result.getOrThrow(Schema.encodeResult(compositionJsonSchema)(composition)); -export const canonicalizeApplicationComposition = ( - composition: ApplicationComposition -): string => +export const canonicalizeApplicationComposition = (composition: ApplicationComposition): string => encodeCompositionJson({ ...composition, modules: composition.modules @@ -428,141 +346,100 @@ export const canonicalizeApplicationComposition = ( ...module.federation, exposes: module.federation.exposes.toSorted(), }, - requiredCoreCapabilities: - module.requiredCoreCapabilities.toSorted(identityOrder), + requiredCoreCapabilities: module.requiredCoreCapabilities.toSorted(identityOrder), sharedSingletons: module.sharedSingletons.toSorted(singletonOrder), })) .toSorted(moduleOrder), shell: { ...composition.shell, - coreCapabilities: - composition.shell.coreCapabilities.toSorted(identityOrder), - sharedSingletons: - composition.shell.sharedSingletons.toSorted(singletonOrder), + coreCapabilities: composition.shell.coreCapabilities.toSorted(identityOrder), + sharedSingletons: composition.shell.sharedSingletons.toSorted(singletonOrder), }, }); -export const validateApplicationCompositionCandidate = Effect.fnUntraced( - function* validate( - input: Input, - evidence: ApplicationCompositionCandidateEvidence - ) { - const composition = yield* Schema.decodeUnknownEffect( - ApplicationCompositionSchema, - { - onExcessProperty: 'error', +export const validateApplicationCompositionCandidate = Effect.fnUntraced(function* validate( + input: Input, + evidence: ApplicationCompositionCandidateEvidence, +) { + const composition = yield* Schema.decodeUnknownEffect(ApplicationCompositionSchema, { + onExcessProperty: 'error', + })(input).pipe( + Effect.catchTag('SchemaError', () => + Effect.fail( + new ApplicationCompositionValidationError({ + reason: 'candidate does not match the supported Application Composition schema', + }), + ), + ), + ); + const observed = yield* Schema.decodeEffect(candidateEvidenceSchema)(evidence).pipe( + Effect.catchTag('SchemaError', () => + Effect.fail( + new ApplicationCompositionValidationError({ + reason: 'candidate evidence does not match the supported observation schema', + }), + ), + ), + ); + const moduleIds = new Set(composition.modules.map(({ moduleId }) => moduleId)); + const appIds = new Set(); + const artifactUrls = new Set(); + const claimedModuleIds = new Set(); + const contributionKeys = new Set(); + const remoteNames = new Set(); + const shellCapabilities = new Set(composition.shell.coreCapabilities.map(identityKey)); + const shellSingletons = new Map( + composition.shell.sharedSingletons.map(({ packageName, version: singletonVersion }) => [ + packageName, + singletonVersion, + ]), + ); + const shellCapabilityIds = new Set(); + const shellSingletonPackages = new Set(); + yield* Effect.forEach( + composition.shell.coreCapabilities, + ({ id }) => claim(shellCapabilityIds, id, 'Core capability'), + { concurrency: 1, discard: true }, + ); + yield* Effect.forEach( + composition.shell.sharedSingletons, + ({ packageName }) => claim(shellSingletonPackages, packageName, 'shared singleton'), + { concurrency: 1, discard: true }, + ); + yield* assertObservedRuntime(composition.shell, observed.runtime); + + yield* Effect.forEach( + composition.modules, + Effect.fnUntraced(function* validateModule(module) { + const manifestUrl = module.federation.manifest.url; + if ( + observed.environment !== 'development' && + [module.contract.url, manifestUrl].some((url) => new URL(url).protocol !== 'https:') + ) { + return yield* new ApplicationCompositionValidationError({ + reason: 'artifact URLs must use HTTPS outside development', + }); } - )(input).pipe( - Effect.catchTag('SchemaError', () => - Effect.fail( - new ApplicationCompositionValidationError({ - reason: - 'candidate does not match the supported Application Composition schema', - }) - ) - ) - ); - const observed = yield* Schema.decodeEffect(candidateEvidenceSchema)( - evidence - ).pipe( - Effect.catchTag('SchemaError', () => - Effect.fail( - new ApplicationCompositionValidationError({ - reason: - 'candidate evidence does not match the supported observation schema', - }) - ) - ) - ); - const moduleIds = new Set( - composition.modules.map(({ moduleId }) => moduleId) - ); - const appIds = new Set(); - const artifactUrls = new Set(); - const claimedModuleIds = new Set(); - const contributionKeys = new Set(); - const remoteNames = new Set(); - const shellCapabilities = new Set( - composition.shell.coreCapabilities.map(identityKey) - ); - const shellSingletons = new Map( - composition.shell.sharedSingletons.map( - ({ packageName, version: singletonVersion }) => [ - packageName, - singletonVersion, - ] - ) - ); - const shellCapabilityIds = new Set(); - const shellSingletonPackages = new Set(); - yield* Effect.forEach( - composition.shell.coreCapabilities, - ({ id }) => claim(shellCapabilityIds, id, 'Core capability'), - { concurrency: 1, discard: true } - ); - yield* Effect.forEach( - composition.shell.sharedSingletons, - ({ packageName }) => - claim(shellSingletonPackages, packageName, 'shared singleton'), - { concurrency: 1, discard: true } - ); - yield* assertObservedRuntime(composition.shell, observed.runtime); - - yield* Effect.forEach( - composition.modules, - Effect.fnUntraced(function* validateModule(module) { - const manifestUrl = module.federation.manifest.url; - if ( - observed.environment !== 'development' && - [module.contract.url, manifestUrl].some( - (url) => new URL(url).protocol !== 'https:' - ) - ) { - return yield* new ApplicationCompositionValidationError({ - reason: 'artifact URLs must use HTTPS outside development', - }); - } - yield* claim(appIds, module.deployment.appId, 'deployment app ID'); - yield* claim( - artifactUrls, - new URL(module.contract.url).href, - 'artifact URL' - ); - yield* claim(artifactUrls, new URL(manifestUrl).href, 'artifact URL'); - yield* claim(claimedModuleIds, module.moduleId, 'module ID'); - yield* claim( - remoteNames, - module.federation.remoteName, - 'Module Federation remote' - ); - yield* Effect.forEach( - module.allowedContributions, - (contributionKey) => - claim(contributionKeys, contributionKey, 'Shell contribution'), - { concurrency: 1, discard: true } - ); - yield* assertDependenciesPresent(module, moduleIds); - yield* assertShellCompatibility( - module, - composition.shell, - shellCapabilities, - shellSingletons - ); - yield* assertObservedDeployment( - module, - observed.contracts[module.deployment.appId] - ); - yield* assertObservedFederationManifest( - module, - observed.federationManifests[manifestUrl] - ); - return yield* Effect.void; - }), - { concurrency: 1, discard: true } - ); - - yield* assertAcyclicDependencies(composition.modules); - - return freeze(composition); - } -); + yield* claim(appIds, module.deployment.appId, 'deployment app ID'); + yield* claim(artifactUrls, new URL(module.contract.url).href, 'artifact URL'); + yield* claim(artifactUrls, new URL(manifestUrl).href, 'artifact URL'); + yield* claim(claimedModuleIds, module.moduleId, 'module ID'); + yield* claim(remoteNames, module.federation.remoteName, 'Module Federation remote'); + yield* Effect.forEach( + module.allowedContributions, + (contributionKey) => claim(contributionKeys, contributionKey, 'Shell contribution'), + { concurrency: 1, discard: true }, + ); + yield* assertDependenciesPresent(module, moduleIds); + yield* assertShellCompatibility(module, composition.shell, shellCapabilities, shellSingletons); + yield* assertObservedDeployment(module, observed.contracts[module.deployment.appId]); + yield* assertObservedFederationManifest(module, observed.federationManifests[manifestUrl]); + return yield* Effect.void; + }), + { concurrency: 1, discard: true }, + ); + + yield* assertAcyclicDependencies(composition.modules); + + return freeze(composition); +}); diff --git a/app/packages/core-runtime/src/modules/catalog.ts b/app/packages/core-runtime/src/modules/catalog.ts index 59af624f9..14edf502b 100644 --- a/app/packages/core-runtime/src/modules/catalog.ts +++ b/app/packages/core-runtime/src/modules/catalog.ts @@ -11,26 +11,20 @@ import { decodeOntosModuleDeploymentContract } from './manifest.ts'; import { validateShellContributions } from './shell-contribution.ts'; import type { TenantModuleStateValidationUnavailableError } from './tenant-module-state-errors.ts'; -const OntosModuleCatalogValidationErrorContract = Schema.TaggedStruct( +const OntosModuleCatalogValidationErrorContract = Schema.TaggedStruct('OntosModuleCatalogValidationError', { + code: Schema.Literal('ontos_module_catalog_invalid'), + reason: Schema.String, +}); +type OntosModuleCatalogValidationErrorSelf = typeof OntosModuleCatalogValidationErrorContract.Type & + Cause.YieldableError; +const OntosModuleCatalogValidationErrorValue = Schema.TaggedError()( 'OntosModuleCatalogValidationError', { code: Schema.Literal('ontos_module_catalog_invalid'), reason: Schema.String, - } + }, ); -type OntosModuleCatalogValidationErrorSelf = - typeof OntosModuleCatalogValidationErrorContract.Type & Cause.YieldableError; -const OntosModuleCatalogValidationErrorValue = - Schema.TaggedError()( - 'OntosModuleCatalogValidationError', - { - code: Schema.Literal('ontos_module_catalog_invalid'), - reason: Schema.String, - } - ); -export type OntosModuleCatalogValidationError = InstanceType< - typeof OntosModuleCatalogValidationErrorValue ->; +export type OntosModuleCatalogValidationError = InstanceType; export { OntosModuleCatalogValidationErrorValue as OntosModuleCatalogValidationError }; export interface InstalledDeploymentContractInput { @@ -38,13 +32,8 @@ export interface InstalledDeploymentContractInput { readonly expectedAppId: OntosDeploymentAppId; } -const InstalledDeploymentFailureReasonSchema = Schema.Literals([ - 'incompatible', - 'timeout', - 'unavailable', -]); -export type InstalledDeploymentFailureReason = - typeof InstalledDeploymentFailureReasonSchema.Type; +const InstalledDeploymentFailureReasonSchema = Schema.Literals(['incompatible', 'timeout', 'unavailable']); +export type InstalledDeploymentFailureReason = typeof InstalledDeploymentFailureReasonSchema.Type; export type InstalledDeploymentStatus = | { @@ -83,21 +72,14 @@ export interface InstalledModuleCatalog { readonly contracts: readonly OntosModuleDeploymentContract[]; readonly deploymentAppIds: readonly OntosDeploymentAppId[]; readonly deploymentStatuses: readonly InstalledDeploymentStatus[]; - readonly getByDeploymentAppId: ( - appId: OntosDeploymentAppId - ) => OntosModuleDeploymentContract | undefined; - readonly getByModuleId: ( - moduleId: OntosModuleId - ) => OntosModuleDeploymentContract | undefined; + readonly getByDeploymentAppId: (appId: OntosDeploymentAppId) => OntosModuleDeploymentContract | undefined; + readonly getByModuleId: (moduleId: OntosModuleId) => OntosModuleDeploymentContract | undefined; readonly moduleIds: readonly OntosModuleId[]; readonly outboxSubscriptions: readonly OntosOutboxSubscriptionContract[]; } export interface InstalledModuleCatalogServiceContract { - readonly load: Effect.Effect< - InstalledModuleCatalog, - TenantModuleStateValidationUnavailableError - >; + readonly load: Effect.Effect; } export class InstalledModuleCatalogService extends Context.Service< @@ -111,21 +93,15 @@ const invalid = (reason: string): OntosModuleCatalogValidationError => reason, }); -const decodeContract = ( - input: InstalledDeploymentContractInput -): OntosModuleDeploymentContract => { +const decodeContract = (input: InstalledDeploymentContractInput): OntosModuleDeploymentContract => { let contract: OntosModuleDeploymentContract; try { contract = decodeOntosModuleDeploymentContract(input.contract); } catch { - throw invalid( - 'an installed deployment returned an invalid or unsupported module contract' - ); + throw invalid('an installed deployment returned an invalid or unsupported module contract'); } if (contract.deployment.appId !== input.expectedAppId) { - throw invalid( - 'deployment contract app ID does not match its allowlisted topology app ID' - ); + throw invalid('deployment contract app ID does not match its allowlisted topology app ID'); } if (contract.manifest.module.kind !== 'business_module') { throw invalid('V0 deployments may claim only one business module'); @@ -133,49 +109,35 @@ const decodeContract = ( const { publicSurface } = contract.manifest; try { validateShellContributions(publicSurface.shellContributions, { - actionKeys: new Set( - publicSurface.actions.map(({ actionKey }) => actionKey) - ), + actionKeys: new Set(publicSurface.actions.map(({ actionKey }) => actionKey)), apiKeys: new Set(publicSurface.api.map(({ key }) => key)), componentKeys: new Set(publicSurface.components.map(({ key }) => key)), moduleId: contract.manifest.module.id, reportKeys: new Set(publicSurface.reports.map(({ key }) => key)), - resourceTypeKeys: new Set( - publicSurface.resourceTypes.map(({ key }) => key) - ), + resourceTypeKeys: new Set(publicSurface.resourceTypes.map(({ key }) => key)), searchKeys: new Set(publicSurface.search.map(({ key }) => key)), }); } catch { - throw invalid( - 'deployment contract contains invalid Shell contribution references' - ); + throw invalid('deployment contract contains invalid Shell contribution references'); } return contract; }; -const validateOwnedOutboxSubscriptions = ( - contract: OntosModuleDeploymentContract -): void => { +const validateOwnedOutboxSubscriptions = (contract: OntosModuleDeploymentContract): void => { const moduleId = contract.manifest.module.id; const workerKeys = new Set(); for (const subscription of contract.runtime.outboxSubscriptions) { if (subscription.consumerModuleKey !== moduleId) { - throw invalid( - 'an Outbox subscription consumer must match its deployment module' - ); + throw invalid('an Outbox subscription consumer must match its deployment module'); } if ( subscription.entrypoint.moduleKey !== moduleId || subscription.entrypoint.entrypointKey !== subscription.workerKey ) { - throw invalid( - 'an Outbox subscription entrypoint must match its consumer and worker' - ); + throw invalid('an Outbox subscription entrypoint must match its consumer and worker'); } if (workerKeys.has(subscription.workerKey)) { - throw invalid( - 'an Outbox worker key may appear only once in the installed catalog' - ); + throw invalid('an Outbox worker key may appear only once in the installed catalog'); } workerKeys.add(subscription.workerKey); } @@ -183,18 +145,10 @@ const validateOwnedOutboxSubscriptions = ( const assembleInstalledModuleCatalog = ( contractsInput: readonly OntosModuleDeploymentContract[], - deploymentStatuses: readonly InstalledDeploymentStatus[] + deploymentStatuses: readonly InstalledDeploymentStatus[], ): InstalledModuleCatalog => { - const byAppId = new Map( - contractsInput.map( - (contract) => [contract.deployment.appId, contract] as const - ) - ); - const byModuleId = new Map( - contractsInput.map( - (contract) => [contract.manifest.module.id, contract] as const - ) - ); + const byAppId = new Map(contractsInput.map((contract) => [contract.deployment.appId, contract] as const)); + const byModuleId = new Map(contractsInput.map((contract) => [contract.manifest.module.id, contract] as const)); const outboxSubscriptions = Object.freeze( contractsInput .flatMap(({ runtime }) => @@ -207,29 +161,23 @@ const assembleInstalledModuleCatalog = ( ...subscription.entrypoint.authorization, }), }), - }) - ) + }), + ), ) - .toSorted((left, right) => left.workerKey.localeCompare(right.workerKey)) + .toSorted((left, right) => left.workerKey.localeCompare(right.workerKey)), ); const contracts = Object.freeze( - [...contractsInput].toSorted((left, right) => - left.manifest.module.id.localeCompare(right.manifest.module.id) - ) - ); - const deploymentAppIds = Object.freeze( - [...byAppId.keys()].toSorted((left, right) => left.localeCompare(right)) - ); - const moduleIds = Object.freeze( - [...byModuleId.keys()].toSorted((left, right) => left.localeCompare(right)) + [...contractsInput].toSorted((left, right) => left.manifest.module.id.localeCompare(right.manifest.module.id)), ); + const deploymentAppIds = Object.freeze([...byAppId.keys()].toSorted((left, right) => left.localeCompare(right))); + const moduleIds = Object.freeze([...byModuleId.keys()].toSorted((left, right) => left.localeCompare(right))); return Object.freeze({ contracts, deploymentAppIds, deploymentStatuses: Object.freeze( deploymentStatuses .map((status) => Object.freeze({ ...status })) - .toSorted((left, right) => left.appId.localeCompare(right.appId)) + .toSorted((left, right) => left.appId.localeCompare(right.appId)), ), getByDeploymentAppId: (appId: OntosDeploymentAppId) => byAppId.get(appId), getByModuleId: (moduleId: OntosModuleId) => byModuleId.get(moduleId), @@ -239,25 +187,16 @@ const assembleInstalledModuleCatalog = ( }; const collectAuthoritativeDeploymentStatuses = ( - inputs: readonly InstalledDeploymentResolutionInput[] -): ReadonlyMap< - OntosDeploymentAppId, - AuthoritativeInstalledDeploymentStatus -> => { - const statuses = new Map< - OntosDeploymentAppId, - AuthoritativeInstalledDeploymentStatus - >(); + inputs: readonly InstalledDeploymentResolutionInput[], +): ReadonlyMap => { + const statuses = new Map(); for (const input of inputs) { if (input.outcome === 'revoked') { statuses.set(input.expectedAppId, { appId: input.expectedAppId, status: 'revoked', }); - } else if ( - input.outcome === 'disabled' && - statuses.get(input.expectedAppId)?.status !== 'revoked' - ) { + } else if (input.outcome === 'disabled' && statuses.get(input.expectedAppId)?.status !== 'revoked') { statuses.set(input.expectedAppId, { appId: input.expectedAppId, status: 'disabled', @@ -269,7 +208,7 @@ const collectAuthoritativeDeploymentStatuses = ( /** Pure, all-or-nothing aggregation of already fetched deployment documents. */ export const buildInstalledModuleCatalog = ( - inputs: readonly InstalledDeploymentContractInput[] + inputs: readonly InstalledDeploymentContractInput[], ): InstalledModuleCatalog => { const byAppId = new Map(); const byModuleId = new Map(); @@ -283,14 +222,10 @@ export const buildInstalledModuleCatalog = ( }, } = contract; if (byAppId.has(appId)) { - throw invalid( - 'one deployment app ID may appear only once in the installed catalog' - ); + throw invalid('one deployment app ID may appear only once in the installed catalog'); } if (byModuleId.has(moduleId)) { - throw invalid( - 'one OntOS module ID may be claimed by only one deployment' - ); + throw invalid('one OntOS module ID may be claimed by only one deployment'); } byAppId.set(appId, contract); byModuleId.set(moduleId, contract); @@ -299,9 +234,7 @@ export const buildInstalledModuleCatalog = ( for (const contract of byModuleId.values()) { for (const { workerKey } of contract.runtime.outboxSubscriptions) { if (workerKeys.has(workerKey)) { - throw invalid( - 'an Outbox worker key may appear only once in the installed catalog' - ); + throw invalid('an Outbox worker key may appear only once in the installed catalog'); } workerKeys.add(workerKey); } @@ -312,18 +245,15 @@ export const buildInstalledModuleCatalog = ( appId: contract.deployment.appId, moduleId: contract.manifest.module.id, status: 'available', - })) + })), ); }; const findConflictingDeploymentAppIds = ( - candidates: readonly OntosModuleDeploymentContract[] + candidates: readonly OntosModuleDeploymentContract[], ): ReadonlySet => { const conflictingAppIds = new Set(); - const byAppId = new Map< - OntosDeploymentAppId, - OntosModuleDeploymentContract[] - >(); + const byAppId = new Map(); const byModuleId = new Map(); const byWorkerKey = new Map(); for (const contract of candidates) { @@ -336,17 +266,10 @@ const findConflictingDeploymentAppIds = ( byAppId.set(appId, [...(byAppId.get(appId) ?? []), contract]); byModuleId.set(moduleId, [...(byModuleId.get(moduleId) ?? []), contract]); for (const { workerKey } of contract.runtime.outboxSubscriptions) { - byWorkerKey.set(workerKey, [ - ...(byWorkerKey.get(workerKey) ?? []), - contract, - ]); + byWorkerKey.set(workerKey, [...(byWorkerKey.get(workerKey) ?? []), contract]); } } - for (const conflicts of [ - ...byAppId.values(), - ...byModuleId.values(), - ...byWorkerKey.values(), - ]) { + for (const conflicts of [...byAppId.values(), ...byModuleId.values(), ...byWorkerKey.values()]) { if (conflicts.length > 1) { for (const contract of conflicts) { conflictingAppIds.add(contract.deployment.appId); @@ -359,11 +282,8 @@ const findConflictingDeploymentAppIds = ( const collectDeploymentCandidates = ( inputs: readonly InstalledDeploymentResolutionInput[], - authoritativeStatuses: ReadonlyMap< - OntosDeploymentAppId, - AuthoritativeInstalledDeploymentStatus - >, - statuses: Map + authoritativeStatuses: ReadonlyMap, + statuses: Map, ): OntosModuleDeploymentContract[] => { const candidates: OntosModuleDeploymentContract[] = []; for (const input of inputs) { @@ -391,7 +311,7 @@ const collectDeploymentCandidates = ( reason: input.reason, status: 'unavailable', } - : { appId: input.expectedAppId, status: input.outcome } + : { appId: input.expectedAppId, status: input.outcome }, ); } } @@ -401,20 +321,14 @@ const collectDeploymentCandidates = ( /** Resolves each installed deployment independently while excluding contradictory candidates. */ export const resolveInstalledModuleCatalog = ( - inputs: readonly InstalledDeploymentResolutionInput[] + inputs: readonly InstalledDeploymentResolutionInput[], ): InstalledModuleCatalog => { const authoritativeStatuses = collectAuthoritativeDeploymentStatuses(inputs); const statuses = new Map(); - const candidates = collectDeploymentCandidates( - inputs, - authoritativeStatuses, - statuses - ); + const candidates = collectDeploymentCandidates(inputs, authoritativeStatuses, statuses); const conflictingAppIds = findConflictingDeploymentAppIds(candidates); - const healthy = candidates.filter( - (contract) => !conflictingAppIds.has(contract.deployment.appId) - ); + const healthy = candidates.filter((contract) => !conflictingAppIds.has(contract.deployment.appId)); for (const contract of candidates) { const { deployment: { appId }, @@ -423,7 +337,7 @@ export const resolveInstalledModuleCatalog = ( appId, conflictingAppIds.has(appId) ? { appId, reason: 'incompatible', status: 'unavailable' } - : { appId, moduleId: contract.manifest.module.id, status: 'available' } + : { appId, moduleId: contract.manifest.module.id, status: 'available' }, ); } return assembleInstalledModuleCatalog(healthy, [...statuses.values()]); diff --git a/app/packages/core-runtime/src/modules/manifest.ts b/app/packages/core-runtime/src/modules/manifest.ts index 595cd9209..c48c5a81b 100644 --- a/app/packages/core-runtime/src/modules/manifest.ts +++ b/app/packages/core-runtime/src/modules/manifest.ts @@ -5,36 +5,29 @@ import type { AnyActionRegistration } from '../actions/definition.ts'; import { isActionRegistration } from '../actions/definition.ts'; import { TENANT_PERMISSION_KEYS } from '../permissions/context-access.ts'; import { ModuleEntrypointSchema } from './module-entrypoint.ts'; -import { - OntosShellContributionsSchema, - validateShellContributions, -} from './shell-contribution.ts'; +import { OntosShellContributionsSchema, validateShellContributions } from './shell-contribution.ts'; import type { OntosShellContributions } from './shell-contribution.ts'; export const ONTOS_MODULE_CONTRACT_SCHEMA_VERSION = '2' as const; -export const ONTOS_MODULE_CONTRACT_PATH = - '/.well-known/ontos-module-manifest.json' as const; +export const ONTOS_MODULE_CONTRACT_PATH = '/.well-known/ontos-module-manifest.json' as const; export const ONTOS_MODULE_CONTRACT_MAX_BYTES = 1024 * 1024; export const ONTOS_MODULE_CONTRACT_TIMEOUT_MS = 5000; -const dottedIdentifierPattern = - /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; +const dottedIdentifierPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; const deploymentIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; const moduleFederationBoundaryPattern = /^[A-Za-z][A-Za-z0-9]*$/u; const schemaVersionPattern = /^[0-9]+$/u; const nonEmptyString = Schema.String.check(Schema.isMinLength(1)); -export const OntosModuleIdSchema = Schema.String.check( - Schema.isPattern(dottedIdentifierPattern) -).pipe(Schema.brand('OntosModuleId'), Schema.decodeTo(Schema.String)); -export const OntosDeploymentAppIdSchema = Schema.String.check( - Schema.isPattern(deploymentIdPattern) -).pipe(Schema.brand('OntosDeploymentAppId'), Schema.decodeTo(Schema.String)); -export const OntosModuleKindSchema = Schema.Literals([ - 'business_module', - 'foundational_module', - 'system_module', -]); +export const OntosModuleIdSchema = Schema.String.check(Schema.isPattern(dottedIdentifierPattern)).pipe( + Schema.brand('OntosModuleId'), + Schema.decodeTo(Schema.String), +); +export const OntosDeploymentAppIdSchema = Schema.String.check(Schema.isPattern(deploymentIdPattern)).pipe( + Schema.brand('OntosDeploymentAppId'), + Schema.decodeTo(Schema.String), +); +export const OntosModuleKindSchema = Schema.Literals(['business_module', 'foundational_module', 'system_module']); export const OntosModuleActivationStateSchema = Schema.Literals([ 'inactive', 'active', @@ -47,24 +40,13 @@ export const OntosModuleActivationStateSchema = Schema.Literals([ export type OntosModuleId = typeof OntosModuleIdSchema.Type; export type OntosDeploymentAppId = typeof OntosDeploymentAppIdSchema.Type; export type OntosModuleKind = typeof OntosModuleKindSchema.Type; -export type OntosModuleActivationState = - typeof OntosModuleActivationStateSchema.Type; +export type OntosModuleActivationState = typeof OntosModuleActivationStateSchema.Type; -const OntosAccessFilteringSchema = Schema.Literals([ - 'legal_entity_scope', - 'resource_permission', - 'tenant_scope', -]); -const OntosOperationKeySchema = nonEmptyString.pipe( - Schema.brand('OntosOperationKey'), - Schema.decodeTo(Schema.String) -); +const OntosAccessFilteringSchema = Schema.Literals(['legal_entity_scope', 'resource_permission', 'tenant_scope']); +const OntosOperationKeySchema = nonEmptyString.pipe(Schema.brand('OntosOperationKey'), Schema.decodeTo(Schema.String)); const OntosModuleFederationBoundaryIdSchema = Schema.String.check( - Schema.isPattern(moduleFederationBoundaryPattern) -).pipe( - Schema.brand('OntosModuleFederationBoundaryId'), - Schema.decodeTo(Schema.String) -); + Schema.isPattern(moduleFederationBoundaryPattern), +).pipe(Schema.brand('OntosModuleFederationBoundaryId'), Schema.decodeTo(Schema.String)); export const OntosModuleIdentitySchema = Schema.Struct({ description: nonEmptyString, @@ -129,9 +111,7 @@ export const OntosSearchDescriptorSchema = Schema.Struct({ accessFiltering: OntosAccessFilteringSchema, key: OntosModuleIdSchema, owningModuleId: OntosModuleIdSchema, - requestFilters: Schema.optionalKey( - Schema.Array(Schema.Literals(['includeArchived', 'role'])) - ), + requestFilters: Schema.optionalKey(Schema.Array(Schema.Literals(['includeArchived', 'role']))), resourceType: OntosModuleIdSchema, tenantPermission: Schema.optionalKey(Schema.Literals(TENANT_PERMISSION_KEYS)), }); @@ -199,16 +179,12 @@ export type OntosActionContract = typeof OntosActionContractSchema.Type; export type OntosApiContract = typeof OntosApiContractSchema.Type; export type OntosComponentContract = typeof OntosComponentContractSchema.Type; export type OntosResourceType = typeof OntosResourceTypeSchema.Type; -export type OntosPublicEventContract = - typeof OntosPublicEventContractSchema.Type; +export type OntosPublicEventContract = typeof OntosPublicEventContractSchema.Type; export type OntosSearchDescriptor = typeof OntosSearchDescriptorSchema.Type; export type OntosReportDescriptor = typeof OntosReportDescriptorSchema.Type; -export type OntosOutboxSubscriptionContract = - typeof OntosOutboxSubscriptionContractSchema.Type; -export type OntosSerializedModuleManifest = - typeof OntosSerializedModuleManifestSchema.Type; -export type OntosModuleDeploymentContract = - typeof OntosModuleDeploymentContractSchema.Type; +export type OntosOutboxSubscriptionContract = typeof OntosOutboxSubscriptionContractSchema.Type; +export type OntosSerializedModuleManifest = typeof OntosSerializedModuleManifestSchema.Type; +export type OntosModuleDeploymentContract = typeof OntosModuleDeploymentContractSchema.Type; export type OntosManifestActionValue = AnyActionRegistration; @@ -216,8 +192,7 @@ export type OntosManifestActionValue = AnyActionRegistration; export type OntosManifestComponentValue = (...arguments_: never[]) => void; export interface OntosAuthoredPublicEvent< - PayloadSchema extends Schema.ConstraintDecoder = - Schema.ConstraintDecoder, + PayloadSchema extends Schema.ConstraintDecoder = Schema.ConstraintDecoder, > extends Omit { readonly payloadSchema: PayloadSchema; } @@ -239,37 +214,24 @@ export interface OntosModuleManifestInput { readonly publicSurface: OntosAuthoredPublicSurface; } -export type OntosModuleManifest< - Input extends OntosModuleManifestInput = OntosModuleManifestInput, -> = Readonly; +export type OntosModuleManifest = Readonly; class OntosModuleManifestValidationError extends Schema.TaggedError()( 'OntosModuleManifestValidationError', - { message: Schema.String } + { message: Schema.String }, ) {} const invalidManifest = (message: string): OntosModuleManifestValidationError => new OntosModuleManifestValidationError({ message }); -const exactDecode = , Value>( - schema: S, - value: Value -): S['Type'] => - Result.getOrThrow( - Schema.decodeUnknownResult(schema, { onExcessProperty: 'error' })(value) - ); +const exactDecode = , Value>(schema: S, value: Value): S['Type'] => + Result.getOrThrow(Schema.decodeUnknownResult(schema, { onExcessProperty: 'error' })(value)); -const assertExactKeys = ( - value: Value, - keys: readonly string[], - label: string -): void => { +const assertExactKeys = (value: Value, keys: readonly string[], label: string): void => { const allowed = new Set(keys); for (const key of Reflect.ownKeys(value)) { if (!Predicate.isString(key) || !allowed.has(key)) { - throw invalidManifest( - `${label} contains unsupported private field ${String(key)}` - ); + throw invalidManifest(`${label} contains unsupported private field ${String(key)}`); } } }; @@ -302,96 +264,60 @@ const assertUnique = (values: readonly string[], label: string): void => { const assertOwner = (owner: string, expected: string, label: string): void => { if (owner !== expected) { - throw invalidManifest( - `${label} must be owned by manifest module ${expected}` - ); + throw invalidManifest(`${label} must be owned by manifest module ${expected}`); } }; -export const validateOntosModuleManifestFields = < - Input extends object, - PublicSurface extends object, ->( +export const validateOntosModuleManifestFields = ( input: Input, - publicSurface: PublicSurface + publicSurface: PublicSurface, ): void => { assertExactKeys(input, ['activation', 'module', 'publicSurface'], 'manifest'); assertExactKeys( publicSurface, - [ - 'actions', - 'api', - 'components', - 'events', - 'reports', - 'resourceTypes', - 'search', - 'shellContributions', - ], - 'manifest public surface' + ['actions', 'api', 'components', 'events', 'reports', 'resourceTypes', 'search', 'shellContributions'], + 'manifest public surface', ); }; -export const validateOntosModuleExecutableReferences = < - ActionValue, - ApiValue, - ComponentValue, - EventPayloadSchema, ->( +export const validateOntosModuleExecutableReferences = ( actions: readonly ActionValue[], apiValues: readonly ApiValue[], componentValues: readonly ComponentValue[], eventPayloadSchemas: readonly EventPayloadSchema[], - moduleId: string + moduleId: string, ): void => { for (const action of actions) { if (!isActionRegistration(action)) { - throw invalidManifest( - 'manifest Actions must be real values created by defineAction' - ); + throw invalidManifest('manifest Actions must be real values created by defineAction'); } assertOwner(action.descriptor.owningModuleKey, moduleId, 'Action'); if (!action.descriptor.actionKey.startsWith(`${moduleId}.`)) { - throw invalidManifest( - 'Action key must be prefixed by its owning module ID' - ); + throw invalidManifest('Action key must be prefixed by its owning module ID'); } } if (apiValues.some((value) => !HttpApi.isHttpApi(value))) { - throw invalidManifest( - 'public API entries must reference real Effect HttpApi values' - ); + throw invalidManifest('public API entries must reference real Effect HttpApi values'); } if (componentValues.some((value) => !Predicate.isFunction(value))) { - throw invalidManifest( - 'public component entries must reference callable component values' - ); + throw invalidManifest('public component entries must reference callable component values'); } if (eventPayloadSchemas.some((value) => !Schema.isSchema(value))) { - throw invalidManifest( - 'public event payloadSchema must be an Effect Schema value' - ); + throw invalidManifest('public event payloadSchema must be an Effect Schema value'); } }; const validateSearchDescriptorReferences = ( descriptor: typeof OntosSearchDescriptorSchema.Type, moduleId: string, - resourceSet: ReadonlySet + resourceSet: ReadonlySet, ): void => { assertOwner(descriptor.owningModuleId, moduleId, 'search descriptor'); if (!resourceSet.has(descriptor.resourceType)) { - throw invalidManifest( - `search descriptor references undeclared resource type ${descriptor.resourceType}` - ); + throw invalidManifest(`search descriptor references undeclared resource type ${descriptor.resourceType}`); } - if ( - (descriptor.accessFiltering === 'tenant_scope') !== - (descriptor.tenantPermission !== undefined) - ) { - throw invalidManifest( - 'tenant-scoped search requires exactly one explicit Tenant permission declaration' - ); + if ((descriptor.accessFiltering === 'tenant_scope') !== (descriptor.tenantPermission !== undefined)) { + throw invalidManifest('tenant-scoped search requires exactly one explicit Tenant permission declaration'); } if ( descriptor.requestFilters !== undefined && @@ -405,25 +331,17 @@ const validateSearchDescriptorReferences = ( * Defines the owner-authored contract. Executable values remain direct references in this * in-process value and are never part of the serializable deployment contract. */ -export const defineOntosModuleManifest = < - const Input extends OntosModuleManifestInput, ->( - input: Input +export const defineOntosModuleManifest = ( + input: Input, ): OntosModuleManifest => { validateOntosModuleManifestFields(input, input.publicSurface); exactDecode(OntosModuleIdentitySchema, input.module); exactDecode(OntosModuleActivationSchema, input.activation); if (input.module.kind !== 'business_module') { - throw invalidManifest( - 'V0 MicroVertical deployments may define only one business_module' - ); + throw invalidManifest('V0 MicroVertical deployments may define only one business_module'); } - if ( - !input.activation.supportedStates.includes(input.activation.defaultState) - ) { - throw invalidManifest( - 'activation defaultState must be included in supportedStates' - ); + if (!input.activation.supportedStates.includes(input.activation.defaultState)) { + throw invalidManifest('activation defaultState must be included in supportedStates'); } assertUnique(input.activation.supportedStates, 'activation state'); @@ -432,16 +350,12 @@ export const defineOntosModuleManifest = < Object.values(input.publicSurface.api), Object.values(input.publicSurface.components), input.publicSurface.events.map(({ payloadSchema }) => payloadSchema), - input.module.id - ); - const actionKeys = input.publicSurface.actions.map( - ({ descriptor }) => descriptor.actionKey + input.module.id, ); + const actionKeys = input.publicSurface.actions.map(({ descriptor }) => descriptor.actionKey); assertUnique(actionKeys, 'Action key'); - const resources = input.publicSurface.resourceTypes.map((resource) => - exactDecode(OntosResourceTypeSchema, resource) - ); + const resources = input.publicSurface.resourceTypes.map((resource) => exactDecode(OntosResourceTypeSchema, resource)); const resourceKeys = resources.map(({ key }) => key); assertUnique(resourceKeys, 'resource type key'); for (const resource of resources) { @@ -452,15 +366,8 @@ export const defineOntosModuleManifest = < const events = input.publicSurface.events.map((event) => { assertExactKeys( event, - [ - 'key', - 'owningModuleId', - 'payloadSchema', - 'referencesResourceTypes', - 'tense', - 'visibility', - ], - 'public event' + ['key', 'owningModuleId', 'payloadSchema', 'referencesResourceTypes', 'tense', 'visibility'], + 'public event', ); const descriptor = exactDecode(OntosPublicEventContractSchema, { key: event.key, @@ -473,84 +380,55 @@ export const defineOntosModuleManifest = < assertOwner(descriptor.owningModuleId, input.module.id, 'public event'); for (const resourceType of descriptor.referencesResourceTypes) { if (!resourceSet.has(resourceType)) { - throw invalidManifest( - `public event references undeclared resource type ${resourceType}` - ); + throw invalidManifest(`public event references undeclared resource type ${resourceType}`); } } return Object.freeze({ ...event, - referencesResourceTypes: Object.freeze([ - ...event.referencesResourceTypes, - ]), + referencesResourceTypes: Object.freeze([...event.referencesResourceTypes]), }); }); assertUnique( events.map(({ key }) => key), - 'public event key' + 'public event key', ); - const search = input.publicSurface.search.map((descriptor) => - exactDecode(OntosSearchDescriptorSchema, descriptor) - ); + const search = input.publicSurface.search.map((descriptor) => exactDecode(OntosSearchDescriptorSchema, descriptor)); assertUnique( search.map(({ key }) => key), - 'search descriptor key' + 'search descriptor key', ); for (const descriptor of search) { - validateSearchDescriptorReferences( - descriptor, - input.module.id, - resourceSet - ); + validateSearchDescriptorReferences(descriptor, input.module.id, resourceSet); } - const reports = input.publicSurface.reports.map((descriptor) => - exactDecode(OntosReportDescriptorSchema, descriptor) - ); + const reports = input.publicSurface.reports.map((descriptor) => exactDecode(OntosReportDescriptorSchema, descriptor)); assertUnique( reports.map(({ key }) => key), - 'report descriptor key' + 'report descriptor key', ); for (const descriptor of reports) { - assertOwner( - descriptor.owningModuleId, - input.module.id, - 'report descriptor' - ); + assertOwner(descriptor.owningModuleId, input.module.id, 'report descriptor'); for (const resourceType of descriptor.resourceTypes) { if (!resourceSet.has(resourceType)) { - throw invalidManifest( - `report descriptor references undeclared resource type ${resourceType}` - ); + throw invalidManifest(`report descriptor references undeclared resource type ${resourceType}`); } } } assertUnique(Object.keys(input.publicSurface.api), 'API key'); assertUnique(Object.keys(input.publicSurface.components), 'component key'); - const componentKeys = new Set( - Object.keys(input.publicSurface.components).map( - (key) => `${input.module.id}.${key}` - ) - ); - const apiKeys = new Set( - Object.keys(input.publicSurface.api).map( - (key) => `${input.module.id}.${key}` - ) - ); - const shellContributions = validateShellContributions( - input.publicSurface.shellContributions, - { - actionKeys: new Set(actionKeys), - apiKeys, - componentKeys, - moduleId: input.module.id, - reportKeys: new Set(reports.map(({ key }) => key)), - resourceTypeKeys: resourceSet, - searchKeys: new Set(search.map(({ key }) => key)), - } - ); + const componentKeys = new Set(Object.keys(input.publicSurface.components).map((key) => `${input.module.id}.${key}`)); + const apiKeys = new Set(Object.keys(input.publicSurface.api).map((key) => `${input.module.id}.${key}`)); + const shellContributions = validateShellContributions(input.publicSurface.shellContributions, { + actionKeys: new Set(actionKeys), + apiKeys, + componentKeys, + moduleId: input.module.id, + reportKeys: new Set(reports.map(({ key }) => key)), + resourceTypeKeys: resourceSet, + searchKeys: new Set(search.map(({ key }) => key)), + }); const manifest = { ...input, @@ -571,13 +449,11 @@ export const defineOntosModuleManifest = < ...value, dimensions: [...value.dimensions], resourceTypes: [...value.resourceTypes], - }) - ) + }), + ), ), resourceTypes: Object.freeze( - resources.map((value) => - freezePlain({ ...value, capabilities: { ...value.capabilities } }) - ) + resources.map((value) => freezePlain({ ...value, capabilities: { ...value.capabilities } })), ), search: Object.freeze(search.map((value) => freezePlain({ ...value }))), shellContributions: freezePlain({ @@ -595,7 +471,5 @@ export const defineOntosModuleManifest = < return Object.freeze(manifest); }; -export const decodeOntosModuleDeploymentContract = ( - value: Value -): OntosModuleDeploymentContract => +export const decodeOntosModuleDeploymentContract = (value: Value): OntosModuleDeploymentContract => exactDecode(OntosModuleDeploymentContractSchema, value); diff --git a/app/packages/core-runtime/src/modules/module-entrypoint-gateway.ts b/app/packages/core-runtime/src/modules/module-entrypoint-gateway.ts index 832328fd8..e3867a7f3 100644 --- a/app/packages/core-runtime/src/modules/module-entrypoint-gateway.ts +++ b/app/packages/core-runtime/src/modules/module-entrypoint-gateway.ts @@ -6,10 +6,7 @@ import type { ModuleEntrypointDescriptor } from './module-entrypoint.ts'; import { ModuleStateCheckUnavailableError } from './module-state-gate-errors.ts'; import type { ModuleStateGateError } from './module-state-gate-errors.ts'; import { ModuleStateGate } from './module-state-gate.ts'; -import type { - ModuleStateGateService, - ModuleStateSnapshot, -} from './module-state-gate.ts'; +import type { ModuleStateGateService, ModuleStateSnapshot } from './module-state-gate.ts'; const unavailable = (cause?: unknown) => { const error = new ModuleStateCheckUnavailableError({ @@ -24,12 +21,7 @@ const unavailable = (cause?: unknown) => { }); }; -export interface RunGatedModuleEntrypointInput< - Value, - AuthorizationError, - LoadError, - Requirements, -> { +export interface RunGatedModuleEntrypointInput { readonly authorize: Effect.Effect; readonly entrypoint: ModuleEntrypointDescriptor; readonly load: Effect.Effect; @@ -40,60 +32,39 @@ export interface ModuleEntrypointGatewayService { readonly check: ModuleStateGateService['check']; readonly prepareSnapshot: ( context: Readonly, - entrypoints: readonly ModuleEntrypointDescriptor[] + entrypoints: readonly ModuleEntrypointDescriptor[], ) => Effect.Effect; readonly prepareSnapshotInput: ( context: Input, - entrypoints: readonly ModuleEntrypointDescriptor[] + entrypoints: readonly ModuleEntrypointDescriptor[], ) => Effect.Effect; readonly run: ( - input: RunGatedModuleEntrypointInput< - Value, - AuthorizationError, - LoadError, - Requirements - > - ) => Effect.Effect< - Value, - AuthorizationError | LoadError | ModuleStateGateError, - Requirements - >; + input: RunGatedModuleEntrypointInput, + ) => Effect.Effect; } -export const makeModuleEntrypointGateway = < - Gate extends ModuleStateGateService, ->( - gate: Gate +export const makeModuleEntrypointGateway = ( + gate: Gate, ): ModuleEntrypointGatewayService => { - const prepareSnapshotInput = ( - context: Input, - entrypoints: readonly ModuleEntrypointDescriptor[] - ) => + const prepareSnapshotInput = (context: Input, entrypoints: readonly ModuleEntrypointDescriptor[]) => decodeTrustedPrincipalContext(context).pipe( Effect.mapError(unavailable), - Effect.flatMap((trustedContext) => - gate.prepareSnapshot(trustedContext.tenantId, entrypoints) - ) + Effect.flatMap((trustedContext) => gate.prepareSnapshot(trustedContext.tenantId, entrypoints)), ); return { check: gate.check, prepareSnapshot: prepareSnapshotInput, prepareSnapshotInput, run: (input) => - gate - .check(input.snapshot, input.entrypoint) - .pipe(Effect.andThen(input.authorize), Effect.andThen(input.load)), + gate.check(input.snapshot, input.entrypoint).pipe(Effect.andThen(input.authorize), Effect.andThen(input.load)), }; }; -export class ModuleEntrypointGateway extends Context.Service< - ModuleEntrypointGateway, - ModuleEntrypointGatewayService ->()( - '@app/core-runtime/modules/module-entrypoint-gateway/ModuleEntrypointGateway' +export class ModuleEntrypointGateway extends Context.Service()( + '@app/core-runtime/modules/module-entrypoint-gateway/ModuleEntrypointGateway', ) {} export const ModuleEntrypointGatewayLive = Layer.effect( ModuleEntrypointGateway, - ModuleStateGate.pipe(Effect.map(makeModuleEntrypointGateway)) + ModuleStateGate.pipe(Effect.map(makeModuleEntrypointGateway)), ); diff --git a/app/packages/core-runtime/src/modules/module-entrypoint.ts b/app/packages/core-runtime/src/modules/module-entrypoint.ts index 2fdbb3391..78850b3d1 100644 --- a/app/packages/core-runtime/src/modules/module-entrypoint.ts +++ b/app/packages/core-runtime/src/modules/module-entrypoint.ts @@ -12,37 +12,20 @@ export const MODULE_ENTRYPOINT_ROLES = [ 'report', 'worker', ] as const; -export const MODULE_ENTRYPOINT_ACCESSES = [ - 'read', - 'historical_read', - 'write', - 'background', -] as const; +export const MODULE_ENTRYPOINT_ACCESSES = ['read', 'historical_read', 'write', 'background'] as const; export const MODULE_ENTRYPOINT_SCOPES = ['tenant', 'system'] as const; -export const ModuleEntrypointRoleSchema = Schema.Literals( - MODULE_ENTRYPOINT_ROLES -); -export const ModuleEntrypointAccessSchema = Schema.Literals( - MODULE_ENTRYPOINT_ACCESSES -); -export const ModuleEntrypointScopeSchema = Schema.Literals( - MODULE_ENTRYPOINT_SCOPES -); -export type ModuleEntrypointRole = Schema.Schema.Type< - typeof ModuleEntrypointRoleSchema ->; -export type ModuleEntrypointAccess = Schema.Schema.Type< - typeof ModuleEntrypointAccessSchema ->; -export type ModuleEntrypointScope = Schema.Schema.Type< - typeof ModuleEntrypointScopeSchema ->; +export const ModuleEntrypointRoleSchema = Schema.Literals(MODULE_ENTRYPOINT_ROLES); +export const ModuleEntrypointAccessSchema = Schema.Literals(MODULE_ENTRYPOINT_ACCESSES); +export const ModuleEntrypointScopeSchema = Schema.Literals(MODULE_ENTRYPOINT_SCOPES); +export type ModuleEntrypointRole = Schema.Schema.Type; +export type ModuleEntrypointAccess = Schema.Schema.Type; +export type ModuleEntrypointScope = Schema.Schema.Type; const stableKeySchema = Schema.String.check( Schema.isMinLength(3), Schema.isMaxLength(200), - Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u) + Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u), ).pipe(Schema.brand('StableKey')); export const ModuleEntrypointSchema = Schema.Struct({ @@ -54,14 +37,13 @@ export const ModuleEntrypointSchema = Schema.Struct({ scope: ModuleEntrypointScopeSchema, }); -export type EntrypointAccessForRole = - Role extends 'action' - ? 'write' - : Role extends 'worker' - ? 'background' - : Role extends 'api' | 'report' - ? 'historical_read' | 'read' | 'write' - : 'historical_read' | 'read'; +export type EntrypointAccessForRole = Role extends 'action' + ? 'write' + : Role extends 'worker' + ? 'background' + : Role extends 'api' | 'report' + ? 'historical_read' | 'read' | 'write' + : 'historical_read' | 'read'; export interface ModuleEntrypointDescriptor< Role extends ModuleEntrypointRole = ModuleEntrypointRole, @@ -89,51 +71,31 @@ export type SystemModuleEntrypoint< ModuleKey extends string = string, > = ModuleEntrypointDescriptor; -const roleAllowsAccess = ( - role: ModuleEntrypointRole, - access: ModuleEntrypointAccess -): boolean => +const roleAllowsAccess = (role: ModuleEntrypointRole, access: ModuleEntrypointAccess): boolean => Match.value(role).pipe( Match.when('action', () => access === 'write'), Match.when('worker', () => access === 'background'), - Match.whenOr( - 'api', - 'report', - () => - access === 'read' || access === 'historical_read' || access === 'write' - ), - Match.whenOr( - 'page', - 'public_component', - 'search', - () => access === 'read' || access === 'historical_read' - ), - Match.exhaustive + Match.whenOr('api', 'report', () => access === 'read' || access === 'historical_read' || access === 'write'), + Match.whenOr('page', 'public_component', 'search', () => access === 'read' || access === 'historical_read'), + Match.exhaustive, ); -const ModuleEntrypointInvariantError = Schema.TaggedError()( - 'ModuleEntrypointInvariantError', - { message: Schema.String } -); +const ModuleEntrypointInvariantError = Schema.TaggedError()('ModuleEntrypointInvariantError', { + message: Schema.String, +}); const failModuleEntrypointInvariant = (message: string): never => { throw new ModuleEntrypointInvariantError({ message }); }; -const roleAllowsAuthorization = ( - role: ModuleEntrypointRole, - authorization: EntrypointAuthorization -): boolean => { +const roleAllowsAuthorization = (role: ModuleEntrypointRole, authorization: EntrypointAuthorization): boolean => { if (role === 'action') { return authorization.kind === 'action_execution'; } if (role === 'worker') { return authorization.kind === 'owner_local_background'; } - if ( - authorization.kind === 'action_execution' || - authorization.kind === 'owner_local_background' - ) { + if (authorization.kind === 'action_execution' || authorization.kind === 'owner_local_background') { return false; } return authorization.kind !== 'capability_issuance' || role === 'api'; @@ -145,36 +107,23 @@ const defineEntrypoint = < const ModuleKey extends string, const Scope extends ModuleEntrypointScope, >( - input: Omit< - ModuleEntrypointDescriptor, - 'scope' - >, - scope: Scope + input: Omit, 'scope'>, + scope: Scope, ): ModuleEntrypointDescriptor => { - const descriptor: ModuleEntrypointDescriptor = - { - ...input, - scope, - }; + const descriptor: ModuleEntrypointDescriptor = { + ...input, + scope, + }; const validatedDescriptor = Result.getOrThrow( Schema.decodeUnknownResult(ModuleEntrypointSchema, { onExcessProperty: 'error', - })(descriptor) + })(descriptor), ); if (!roleAllowsAccess(validatedDescriptor.role, validatedDescriptor.access)) { - return failModuleEntrypointInvariant( - 'Module entrypoint role and access are inconsistent' - ); + return failModuleEntrypointInvariant('Module entrypoint role and access are inconsistent'); } - if ( - !roleAllowsAuthorization( - validatedDescriptor.role, - validatedDescriptor.authorization - ) - ) { - return failModuleEntrypointInvariant( - 'Module entrypoint role and authorization are inconsistent' - ); + if (!roleAllowsAuthorization(validatedDescriptor.role, validatedDescriptor.authorization)) { + return failModuleEntrypointInvariant('Module entrypoint role and authorization are inconsistent'); } return Object.freeze({ ...descriptor, @@ -182,25 +131,21 @@ const defineEntrypoint = < }); }; -export const decodeTenantModuleEntrypoint = ( - input: Input -): TenantModuleEntrypoint => { +export const decodeTenantModuleEntrypoint = (input: Input): TenantModuleEntrypoint => { const descriptor = Result.getOrThrow( Schema.decodeUnknownResult(ModuleEntrypointSchema, { onExcessProperty: 'error', })({ ...input, scope: 'tenant', - }) + }), ); if ( descriptor.scope !== 'tenant' || !roleAllowsAccess(descriptor.role, descriptor.access) || !roleAllowsAuthorization(descriptor.role, descriptor.authorization) ) { - return failModuleEntrypointInvariant( - 'Module entrypoint role and access are inconsistent' - ); + return failModuleEntrypointInvariant('Module entrypoint role and access are inconsistent'); } return Object.freeze({ access: descriptor.access, @@ -217,15 +162,13 @@ export const defineTenantModuleEntrypoint = < const Access extends EntrypointAccessForRole, const ModuleKey extends string, >( - input: Omit, 'scope'> -): TenantModuleEntrypoint => - defineEntrypoint(input, 'tenant'); + input: Omit, 'scope'>, +): TenantModuleEntrypoint => defineEntrypoint(input, 'tenant'); export const defineSystemModuleEntrypoint = < const Role extends ModuleEntrypointRole, const Access extends EntrypointAccessForRole, const ModuleKey extends string, >( - input: Omit, 'scope'> -): SystemModuleEntrypoint => - defineEntrypoint(input, 'system'); + input: Omit, 'scope'>, +): SystemModuleEntrypoint => defineEntrypoint(input, 'system'); diff --git a/app/packages/core-runtime/src/modules/module-state-check-unavailable-error.ts b/app/packages/core-runtime/src/modules/module-state-check-unavailable-error.ts index be19fc3f7..522605158 100644 --- a/app/packages/core-runtime/src/modules/module-state-check-unavailable-error.ts +++ b/app/packages/core-runtime/src/modules/module-state-check-unavailable-error.ts @@ -5,5 +5,5 @@ export class ModuleStateCheckUnavailableError extends Schema.TaggedError()( - 'ModuleStateDeniedError', - { code: Schema.Literal('module_state_denied'), reason: Schema.String } -) {} +export class ModuleStateDeniedError extends Schema.TaggedError()('ModuleStateDeniedError', { + code: Schema.Literal('module_state_denied'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/modules/module-state-gate-errors.ts b/app/packages/core-runtime/src/modules/module-state-gate-errors.ts index 6f23837f6..0bbe0ce5d 100644 --- a/app/packages/core-runtime/src/modules/module-state-gate-errors.ts +++ b/app/packages/core-runtime/src/modules/module-state-gate-errors.ts @@ -4,6 +4,4 @@ import type { ModuleStateDeniedError } from './module-state-denied-error.ts'; export { ModuleStateCheckUnavailableError } from './module-state-check-unavailable-error.ts'; export { ModuleStateDeniedError } from './module-state-denied-error.ts'; -export type ModuleStateGateError = - | ModuleStateCheckUnavailableError - | ModuleStateDeniedError; +export type ModuleStateGateError = ModuleStateCheckUnavailableError | ModuleStateDeniedError; diff --git a/app/packages/core-runtime/src/modules/module-state-gate.ts b/app/packages/core-runtime/src/modules/module-state-gate.ts index f1c6b6b4d..0818d62e2 100644 --- a/app/packages/core-runtime/src/modules/module-state-gate.ts +++ b/app/packages/core-runtime/src/modules/module-state-gate.ts @@ -9,16 +9,10 @@ import type { TenantModuleEntrypoint, } from './module-entrypoint.ts'; import type { ModuleStateGateError } from './module-state-gate-errors.ts'; -import { - ModuleStateCheckUnavailableError, - ModuleStateDeniedError, -} from './module-state-gate-errors.ts'; +import { ModuleStateCheckUnavailableError, ModuleStateDeniedError } from './module-state-gate-errors.ts'; import type { ModuleStateSnapshot } from './module-state-snapshot.ts'; import { ModuleStateSnapshotValue } from './module-state-snapshot.ts'; -import type { - TenantModuleState, - TenantModuleStateServiceContract, -} from './tenant-module-state-service.ts'; +import type { TenantModuleState, TenantModuleStateServiceContract } from './tenant-module-state-service.ts'; import { TENANT_MODULE_STATES, TenantModuleStateSchema, @@ -30,15 +24,8 @@ export type { ModuleStateSnapshot } from './module-state-snapshot.ts'; const ModuleStateDecisionSchema = Schema.Literals(['allow', 'deny']); export type ModuleStateDecision = typeof ModuleStateDecisionSchema.Type; -const allowedAccessByState: Readonly< - Record> -> = Object.freeze({ - active: new Set([ - 'background', - 'historical_read', - 'read', - 'write', - ]), +const allowedAccessByState: Readonly>> = Object.freeze({ + active: new Set(['background', 'historical_read', 'read', 'write']), archived: new Set(['historical_read']), deprecated: new Set(['historical_read', 'read']), inactive: new Set(['historical_read']), @@ -49,34 +36,20 @@ const allowedAccessByState: Readonly< export const decideModuleStateAccess = ( state: TenantModuleState | null, - access: ModuleEntrypointAccess -): ModuleStateDecision => - state !== null && allowedAccessByState[state].has(access) ? 'allow' : 'deny'; + access: ModuleEntrypointAccess, +): ModuleStateDecision => (state !== null && allowedAccessByState[state].has(access) ? 'allow' : 'deny'); -export const tenantStatesAllowingAccess = ( - access: ModuleEntrypointAccess -): readonly TenantModuleState[] => +export const tenantStatesAllowingAccess = (access: ModuleEntrypointAccess): readonly TenantModuleState[] => Object.freeze( - TENANT_MODULE_STATES.flatMap((state) => - allowedAccessByState[state].has(access) ? [state] : [] - ).toSorted() + TENANT_MODULE_STATES.flatMap((state) => (allowedAccessByState[state].has(access) ? [state] : [])).toSorted(), ); -const ModuleStateSnapshotInvariantError = Schema.TaggedError()( - 'ModuleStateSnapshotInvariantError', - { reason: Schema.String } -); +const ModuleStateSnapshotInvariantError = Schema.TaggedError()('ModuleStateSnapshotInvariantError', { + reason: Schema.String, +}); -const entrypointFingerprint = ( - entrypoint: ModuleEntrypointDescriptor -): string => - [ - entrypoint.scope, - entrypoint.moduleKey, - entrypoint.entrypointKey, - entrypoint.role, - entrypoint.access, - ].join('\u0000'); +const entrypointFingerprint = (entrypoint: ModuleEntrypointDescriptor): string => + [entrypoint.scope, entrypoint.moduleKey, entrypoint.entrypointKey, entrypoint.role, entrypoint.access].join('\u0000'); const unavailable = (cause?: unknown) => { const error = new ModuleStateCheckUnavailableError({ @@ -106,22 +79,17 @@ export const makeModuleStateSnapshot = ( ] ): ModuleStateSnapshot => { const entrypointKeys = Object.freeze( - [ - ...new Set(entrypoints.map((entrypoint) => entrypoint.entrypointKey)), - ].toSorted() + [...new Set(entrypoints.map((entrypoint) => entrypoint.entrypointKey))].toSorted(), ); const declaredEntrypoints = new Set(entrypoints.map(entrypointFingerprint)); - const moduleKeys = entrypoints.flatMap((entrypoint) => - entrypoint.scope === 'tenant' ? [entrypoint.moduleKey] : [] - ); + const moduleKeys = entrypoints.flatMap((entrypoint) => (entrypoint.scope === 'tenant' ? [entrypoint.moduleKey] : [])); const declaredKeys = Object.freeze([...new Set(moduleKeys)].toSorted()); const declaredSet = new Set(declaredKeys); const states = new Map(); for (const record of records) { if (!declaredSet.has(record.moduleKey) || states.has(record.moduleKey)) { throw new ModuleStateSnapshotInvariantError({ - reason: - 'Module state snapshot records do not match the declared module keys', + reason: 'Module state snapshot records do not match the declared module keys', }); } states.set(record.moduleKey, record.state); @@ -133,9 +101,7 @@ export const makeModuleStateSnapshot = ( }); }; -type ModuleStateSpanAttributes = Readonly< - Record ->; +type ModuleStateSpanAttributes = Readonly>; const annotateCurrentSpan = (attributes: ModuleStateSpanAttributes) => Effect.currentSpan.pipe( @@ -144,90 +110,76 @@ const annotateCurrentSpan = (attributes: ModuleStateSpanAttributes) => for (const [key, value] of Object.entries(attributes)) { span.attribute(key, value); } - }) + }), ), Effect.asVoid, - Effect.ignore + Effect.ignore, ); -const recordAcquisitionTelemetry = Effect.fn( - 'ModuleStateGate.recordAcquisitionTelemetry' -)(function* recordAcquisitionTelemetryEffect( - startedAt: number, - batchSize: number, - outcome: 'available' | 'unavailable' -) { - const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; - yield* Effect.annotateLogs( - Effect.logDebug('Module state snapshot acquisition completed'), - { +const recordAcquisitionTelemetry = Effect.fn('ModuleStateGate.recordAcquisitionTelemetry')( + function* recordAcquisitionTelemetryEffect( + startedAt: number, + batchSize: number, + outcome: 'available' | 'unavailable', + ) { + const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; + yield* Effect.annotateLogs(Effect.logDebug('Module state snapshot acquisition completed'), { batchSize, elapsedMs, outcome, - } - ); - return { batchSize, elapsedMs, outcome } satisfies ModuleStateSpanAttributes; -}); + }); + return { batchSize, elapsedMs, outcome } satisfies ModuleStateSpanAttributes; + }, +); -export const prepareModuleStateSnapshot = Effect.fn( - 'ModuleStateGate.prepareModuleStateSnapshot' -)(function* prepareSnapshotEffect( - stateService: TenantModuleStateServiceContract, - tenantId: string, - entrypoints: readonly ModuleEntrypointDescriptor[] -): Effect.fn.Return { - const moduleKeys = entrypoints.flatMap((entrypoint) => - entrypoint.scope === 'tenant' ? [entrypoint.moduleKey] : [] - ); - const distinctKeys = [...new Set(moduleKeys)].toSorted(); - const startedAt = yield* Clock.currentTimeMillis; - let acquisition: Effect.Effect< - ModuleStateSnapshot, - ModuleStateCheckUnavailableError - >; - if (distinctKeys.length === 0) { - acquisition = Effect.succeed( - makeModuleStateSnapshot(tenantId, entrypoints, []) +export const prepareModuleStateSnapshot = Effect.fn('ModuleStateGate.prepareModuleStateSnapshot')( + function* prepareSnapshotEffect( + stateService: TenantModuleStateServiceContract, + tenantId: string, + entrypoints: readonly ModuleEntrypointDescriptor[], + ): Effect.fn.Return { + const moduleKeys = entrypoints.flatMap((entrypoint) => + entrypoint.scope === 'tenant' ? [entrypoint.moduleKey] : [], ); - } else if (tenantId.length === 0) { - acquisition = Effect.fail(unavailable()); - } else { - acquisition = stateService - .getTenantModuleStates(tenantId, distinctKeys) - .pipe( + const distinctKeys = [...new Set(moduleKeys)].toSorted(); + const startedAt = yield* Clock.currentTimeMillis; + let acquisition: Effect.Effect; + if (distinctKeys.length === 0) { + acquisition = Effect.succeed(makeModuleStateSnapshot(tenantId, entrypoints, [])); + } else if (tenantId.length === 0) { + acquisition = Effect.fail(unavailable()); + } else { + acquisition = stateService.getTenantModuleStates(tenantId, distinctKeys).pipe( Effect.mapError(unavailable), Effect.flatMap((records) => Effect.try({ catch: unavailable, try: () => makeModuleStateSnapshot(tenantId, entrypoints, records), - }) - ) + }), + ), ); - } - return yield* acquisition.pipe( - Effect.tap(() => - recordAcquisitionTelemetry( - startedAt, - distinctKeys.length, - 'available' - ).pipe(Effect.flatMap(annotateCurrentSpan)) - ), - Effect.tapError(() => - recordAcquisitionTelemetry( - startedAt, - distinctKeys.length, - 'unavailable' - ).pipe(Effect.flatMap(annotateCurrentSpan)) - ), - Effect.withSpan('ModuleStateGate.acquire', { - attributes: { batchSize: distinctKeys.length }, - }) - ); -}); + } + return yield* acquisition.pipe( + Effect.tap(() => + recordAcquisitionTelemetry(startedAt, distinctKeys.length, 'available').pipe( + Effect.flatMap(annotateCurrentSpan), + ), + ), + Effect.tapError(() => + recordAcquisitionTelemetry(startedAt, distinctKeys.length, 'unavailable').pipe( + Effect.flatMap(annotateCurrentSpan), + ), + ), + Effect.withSpan('ModuleStateGate.acquire', { + attributes: { batchSize: distinctKeys.length }, + }), + ); + }, +); export const checkModuleEntrypoint = ( snapshot: ModuleStateSnapshot, - entrypoint: ModuleEntrypointDescriptor + entrypoint: ModuleEntrypointDescriptor, ): Effect.Effect => { const data = ModuleStateSnapshotValue.dataOf(snapshot); const fingerprint = entrypointFingerprint(entrypoint); @@ -240,7 +192,7 @@ export const checkModuleEntrypoint = ( scope: entrypoint.scope, snapshotReuse: false, }, - }) + }), ); } const snapshotReuse = data.evaluatedEntrypoints.has(fingerprint); @@ -254,13 +206,10 @@ export const checkModuleEntrypoint = ( scope: 'system', snapshotReuse, }, - }) + }), ); } - if ( - snapshot.tenantId.length === 0 || - !snapshot.moduleKeys.includes(entrypoint.moduleKey) - ) { + if (snapshot.tenantId.length === 0 || !snapshot.moduleKeys.includes(entrypoint.moduleKey)) { return Effect.fail(unavailable()).pipe( Effect.withSpan('ModuleStateGate.evaluate', { attributes: { @@ -269,13 +218,10 @@ export const checkModuleEntrypoint = ( scope: 'tenant', snapshotReuse, }, - }) + }), ); } - const outcome = decideModuleStateAccess( - data.states.get(entrypoint.moduleKey) ?? null, - entrypoint.access - ); + const outcome = decideModuleStateAccess(data.states.get(entrypoint.moduleKey) ?? null, entrypoint.access); return (outcome === 'allow' ? Effect.void : Effect.fail(denied())).pipe( Effect.withSpan('ModuleStateGate.evaluate', { attributes: { @@ -284,37 +230,31 @@ export const checkModuleEntrypoint = ( scope: 'tenant', snapshotReuse, }, - }) + }), ); }; export interface ModuleStateGateService { readonly check: ( snapshot: ModuleStateSnapshot, - entrypoint: ModuleEntrypointDescriptor + entrypoint: ModuleEntrypointDescriptor, ) => Effect.Effect; readonly prepareSnapshot: ( tenantId: string, - entrypoints: readonly ModuleEntrypointDescriptor[] + entrypoints: readonly ModuleEntrypointDescriptor[], ) => Effect.Effect; readonly recheckWrite: ( transaction: CoreTransaction, tenantId: string, - entrypoint: TenantModuleEntrypoint< - ModuleEntrypointDescriptor['role'], - 'write' - > + entrypoint: TenantModuleEntrypoint, ) => Effect.Effect; } const isModuleStateDenied = Schema.is(ModuleStateDeniedError); -export const makeModuleStateGate = ( - stateService: TenantModuleStateServiceContract -): ModuleStateGateService => ({ +export const makeModuleStateGate = (stateService: TenantModuleStateServiceContract): ModuleStateGateService => ({ check: checkModuleEntrypoint, - prepareSnapshot: (tenantId, entrypoints) => - prepareModuleStateSnapshot(stateService, tenantId, entrypoints), + prepareSnapshot: (tenantId, entrypoints) => prepareModuleStateSnapshot(stateService, tenantId, entrypoints), recheckWrite: (transaction, tenantId, entrypoint) => { const recheck = Effect.gen(function* recheckWriteEffect() { const tenantRows = yield* transaction @@ -329,19 +269,14 @@ export const makeModuleStateGate = ( const rows = yield* transaction .select({ state: tenantModuleStates.state }) .from(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, entrypoint.moduleKey) - ) - ) + .where(and(eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, entrypoint.moduleKey))) .pipe(Effect.mapError(unavailable)); const state = rows[0] === undefined ? null - : yield* Schema.decodeUnknownEffect(TenantModuleStateSchema)( - rows[0].state - ).pipe(Effect.mapError(unavailable)); + : yield* Schema.decodeUnknownEffect(TenantModuleStateSchema)(rows[0].state).pipe( + Effect.mapError(unavailable), + ); if (decideModuleStateAccess(state, 'write') === 'deny') { return yield* denied(); } @@ -355,17 +290,16 @@ export const makeModuleStateGate = ( Effect.tapError(annotateFailure), Effect.withSpan('ModuleStateGate.recheckWrite', { attributes: { access: 'write', scope: 'tenant' }, - }) + }), ); }, }); -export class ModuleStateGate extends Context.Service< - ModuleStateGate, - ModuleStateGateService ->()('@app/core-runtime/modules/module-state-gate/ModuleStateGate') {} +export class ModuleStateGate extends Context.Service()( + '@app/core-runtime/modules/module-state-gate/ModuleStateGate', +) {} export const ModuleStateGateLive = Layer.effect( ModuleStateGate, - TenantModuleStateService.pipe(Effect.map(makeModuleStateGate)) + TenantModuleStateService.pipe(Effect.map(makeModuleStateGate)), ); diff --git a/app/packages/core-runtime/src/modules/module-state-snapshot.ts b/app/packages/core-runtime/src/modules/module-state-snapshot.ts index 2869901c4..e4401c664 100644 --- a/app/packages/core-runtime/src/modules/module-state-snapshot.ts +++ b/app/packages/core-runtime/src/modules/module-state-snapshot.ts @@ -22,7 +22,7 @@ export class ModuleStateSnapshotValue implements ModuleStateSnapshot { tenantId: string, entrypointKeys: readonly string[], moduleKeys: readonly string[], - data: ModuleStateSnapshotData + data: ModuleStateSnapshotData, ) { this.#data = data; this.entrypointKeys = entrypointKeys; @@ -31,9 +31,7 @@ export class ModuleStateSnapshotValue implements ModuleStateSnapshot { Object.freeze(this); } - static dataOf( - snapshot: ModuleStateSnapshot - ): ModuleStateSnapshotData | undefined { + static dataOf(snapshot: ModuleStateSnapshot): ModuleStateSnapshotData | undefined { return #data in snapshot ? snapshot.#data : undefined; } } diff --git a/app/packages/core-runtime/src/modules/runtime-registration.ts b/app/packages/core-runtime/src/modules/runtime-registration.ts index 840019715..74aad4ff9 100644 --- a/app/packages/core-runtime/src/modules/runtime-registration.ts +++ b/app/packages/core-runtime/src/modules/runtime-registration.ts @@ -13,9 +13,7 @@ import type { import { OntosActionContractSchema } from './manifest.ts'; import type { OntosShellContributions } from './shell-contribution.ts'; -const runtimeRegistrationBrand: unique symbol = Symbol( - '@app/core-runtime/modules/runtime-registration' -); +const runtimeRegistrationBrand: unique symbol = Symbol('@app/core-runtime/modules/runtime-registration'); interface PrivateVerticalRuntime { readonly actions: readonly OntosManifestActionValue[]; @@ -29,9 +27,7 @@ export interface VerticalRuntimeRegistration { readonly [runtimeRegistrationBrand]: true; } -class VerticalRuntimeRegistrationValue< - ModuleId extends string, -> implements VerticalRuntimeRegistration { +class VerticalRuntimeRegistrationValue implements VerticalRuntimeRegistration { readonly #runtime: PrivateVerticalRuntime; readonly [runtimeRegistrationBrand] = true as const; readonly moduleId: ModuleId; @@ -42,25 +38,21 @@ class VerticalRuntimeRegistrationValue< Object.freeze(this); } - static runtimeOf( - registration: VerticalRuntimeRegistration - ): PrivateVerticalRuntime | undefined { + static runtimeOf(registration: VerticalRuntimeRegistration): PrivateVerticalRuntime | undefined { return #runtime in registration ? registration.#runtime : undefined; } } const VerticalRuntimeRegistrationInvariantError = Schema.TaggedError()( 'VerticalRuntimeRegistrationInvariantError', - { message: Schema.String } + { message: Schema.String }, ); const failRuntimeRegistration = (message: string): never => { throw new VerticalRuntimeRegistrationInvariantError({ message }); }; -export interface VerticalRuntimeRegistrationInput< - Manifest extends OntosModuleManifest = OntosModuleManifest, -> { +export interface VerticalRuntimeRegistrationInput { readonly actions: readonly OntosManifestActionValue[]; readonly entrypoints?: VerticalRuntimeEntrypointBindings; readonly manifest: Manifest; @@ -68,8 +60,7 @@ export interface VerticalRuntimeRegistrationInput< } type EntrypointImportBoundary = Parameters>[0]; -export type VerticalRuntimeEntrypointThunk = - () => ReturnType; +export type VerticalRuntimeEntrypointThunk = () => ReturnType; export interface VerticalRuntimeEntrypointBindings { readonly api: Readonly>; @@ -97,79 +88,50 @@ const assertUnique = (values: readonly string[], label: string): void => { } }; -const validateRuntimeActions = ( - input: VerticalRuntimeRegistrationInput -): void => { - const allowed = new Set([ - 'actions', - 'entrypoints', - 'manifest', - 'outboxWorkers', - ]); +const validateRuntimeActions = (input: VerticalRuntimeRegistrationInput): void => { + const allowed = new Set(['actions', 'entrypoints', 'manifest', 'outboxWorkers']); for (const key of Reflect.ownKeys(input)) { if (!Predicate.isString(key) || !allowed.has(key)) { - failRuntimeRegistration( - `runtime registration contains unsupported field ${String(key)}` - ); + failRuntimeRegistration(`runtime registration contains unsupported field ${String(key)}`); } } const manifestActions = new Set(input.manifest.publicSurface.actions); assertUnique( input.actions.map(({ descriptor }) => descriptor.actionKey), - 'runtime Action' + 'runtime Action', ); for (const action of input.actions) { if (action.descriptor.owningModuleKey !== input.manifest.module.id) { - failRuntimeRegistration( - 'runtime Action owner must match the manifest module ID' - ); + failRuntimeRegistration('runtime Action owner must match the manifest module ID'); } if (!manifestActions.has(action)) { - failRuntimeRegistration( - 'runtime Action must be the same value published by the manifest' - ); + failRuntimeRegistration('runtime Action must be the same value published by the manifest'); } } }; -const validateRuntimeEntrypoints = ( - entrypoints: VerticalRuntimeEntrypointBindings -): void => { - const entrypointCategories = new Set([ - 'api', - 'components', - 'pages', - 'reports', - 'search', - ]); +const validateRuntimeEntrypoints = (entrypoints: VerticalRuntimeEntrypointBindings): void => { + const entrypointCategories = new Set(['api', 'components', 'pages', 'reports', 'search']); for (const key of Reflect.ownKeys(entrypoints)) { if (!Predicate.isString(key) || !entrypointCategories.has(key)) { - failRuntimeRegistration( - `runtime entrypoints contain unsupported field ${String(key)}` - ); + failRuntimeRegistration(`runtime entrypoints contain unsupported field ${String(key)}`); } } for (const [category, bindings] of Object.entries(entrypoints)) { if (Object.values(bindings).some((value) => !Predicate.isFunction(value))) { - failRuntimeRegistration( - `runtime ${category} entrypoints must be lazy thunks` - ); + failRuntimeRegistration(`runtime ${category} entrypoints must be lazy thunks`); } } }; -export const defineVerticalRuntimeRegistration = < - const Manifest extends OntosModuleManifest, ->( - input: VerticalRuntimeRegistrationInput +export const defineVerticalRuntimeRegistration = ( + input: VerticalRuntimeRegistrationInput, ): VerticalRuntimeRegistration => { validateRuntimeActions(input); const workers = validateOutboxWorkerRegistrations(input.outboxWorkers); for (const worker of workers) { if (worker.descriptor.consumerModuleKey !== input.manifest.module.id) { - failRuntimeRegistration( - 'runtime Outbox Worker owner must match the manifest module ID' - ); + failRuntimeRegistration('runtime Outbox Worker owner must match the manifest module ID'); } } const entrypoints = input.entrypoints ?? emptyEntrypoints(); @@ -188,9 +150,7 @@ export const defineVerticalRuntimeRegistration = < }); }; -const requirePrivateRuntime = ( - registration: VerticalRuntimeRegistration -): PrivateVerticalRuntime => { +const requirePrivateRuntime = (registration: VerticalRuntimeRegistration): PrivateVerticalRuntime => { const value = VerticalRuntimeRegistrationValue.runtimeOf(registration); if (value === undefined || !registration[runtimeRegistrationBrand]) { return failRuntimeRegistration('invalid Vertical Runtime Registration'); @@ -200,21 +160,18 @@ const requirePrivateRuntime = ( /** Owner-local runtime seam; executable values never appear on the registration object. */ export const getVerticalRuntimeActions = ( - registration: VerticalRuntimeRegistration -): readonly OntosManifestActionValue[] => - requirePrivateRuntime(registration).actions; + registration: VerticalRuntimeRegistration, +): readonly OntosManifestActionValue[] => requirePrivateRuntime(registration).actions; /** Owner-local runtime seam; executable values never appear on the registration object. */ export const getVerticalRuntimeOutboxWorkers = ( - registration: VerticalRuntimeRegistration -): readonly AnyOutboxWorkerRegistration[] => - requirePrivateRuntime(registration).outboxWorkers; + registration: VerticalRuntimeRegistration, +): readonly AnyOutboxWorkerRegistration[] => requirePrivateRuntime(registration).outboxWorkers; /** Owner-local runtime seam; lazy executable values never appear on the registration object. */ export const getVerticalRuntimeEntrypoints = ( - registration: VerticalRuntimeRegistration -): VerticalRuntimeEntrypointBindings => - requirePrivateRuntime(registration).entrypoints; + registration: VerticalRuntimeRegistration, +): VerticalRuntimeEntrypointBindings => requirePrivateRuntime(registration).entrypoints; export interface VerticalRuntimeSafeDescriptors { readonly actions: readonly OntosActionContract[]; @@ -225,7 +182,7 @@ export interface VerticalRuntimeSafeDescriptors { /** Build-tool seam. Returns copied, frozen data and never returns a handler or Schema value. */ export const extractVerticalRuntimeSafeDescriptors = ( - registration: VerticalRuntimeRegistration + registration: VerticalRuntimeRegistration, ): VerticalRuntimeSafeDescriptors => { const runtime = requirePrivateRuntime(registration); return Object.freeze({ @@ -242,13 +199,11 @@ export const extractVerticalRuntimeSafeDescriptors = ( legalEntityScope: descriptor.legalEntityScope, owningModuleId: descriptor.owningModuleKey, schemaVersion: descriptor.schemaVersion, - }) - ) - ) - ) - .toSorted((left, right) => - left.actionKey.localeCompare(right.actionKey) + }), + ), + ), ) + .toSorted((left, right) => left.actionKey.localeCompare(right.actionKey)), ), moduleId: registration.moduleId, outboxSubscriptions: Object.freeze( @@ -265,11 +220,9 @@ export const extractVerticalRuntimeSafeDescriptors = ( producerModuleKey: descriptor.producerModuleKey, topic: descriptor.topic, workerKey: descriptor.workerKey, - }) - ) - .toSorted((left, right) => - left.workerKey.localeCompare(right.workerKey) + }), ) + .toSorted((left, right) => left.workerKey.localeCompare(right.workerKey)), ), shellContributions: runtime.shellContributions, }); diff --git a/app/packages/core-runtime/src/modules/shell-contribution.ts b/app/packages/core-runtime/src/modules/shell-contribution.ts index 9496a91bd..fe4498035 100644 --- a/app/packages/core-runtime/src/modules/shell-contribution.ts +++ b/app/packages/core-runtime/src/modules/shell-contribution.ts @@ -5,7 +5,7 @@ import { ModuleEntrypointSchema } from './module-entrypoint.ts'; const stableKey = Schema.String.check( Schema.isMinLength(3), Schema.isMaxLength(200), - Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u) + Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u), ); const actionKey = stableKey.pipe(Schema.brand('ActionKey')); const apiKey = stableKey.pipe(Schema.brand('ApiKey')); @@ -15,18 +15,15 @@ const groupKey = stableKey.pipe(Schema.brand('GroupKey')); const pageKey = stableKey.pipe(Schema.brand('PageKey')); const reportKey = stableKey.pipe(Schema.brand('ReportKey')); const searchKey = stableKey.pipe(Schema.brand('SearchKey')); -const order = Schema.Finite.check( - Schema.isInt(), - Schema.isBetween({ maximum: 10_000, minimum: 0 }) -); +const order = Schema.Finite.check(Schema.isInt(), Schema.isBetween({ maximum: 10_000, minimum: 0 })); const routeParameterPattern = /^:(?[a-z][A-Za-z0-9]*)$/u; const routeLocalePrefixPattern = /^[a-z]{2}(?:-[a-z]{2})?$/u; const routePath = Schema.String.check( Schema.isMinLength(2), Schema.isMaxLength(200), Schema.isPattern( - /^\/(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|:[a-z][A-Za-z0-9]*)(?:\/(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|:[a-z][A-Za-z0-9]*))*$/u - ) + /^\/(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|:[a-z][A-Za-z0-9]*)(?:\/(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|:[a-z][A-Za-z0-9]*))*$/u, + ), ).pipe( Schema.check( Schema.makeFilter((value) => { @@ -41,78 +38,65 @@ const routePath = Schema.String.check( return new Set(parameterNames).size === parameterNames.length ? undefined : 'page contribution routePath must not repeat a parameter name'; - }) - ) + }), + ), ); -const allowsRead = (access: string): boolean => - access === 'read' || access === 'historical_read'; +const allowsRead = (access: string): boolean => access === 'read' || access === 'historical_read'; const pageEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'page' && - allowsRead(entrypoint.access) + entrypoint.scope === 'tenant' && entrypoint.role === 'page' && allowsRead(entrypoint.access) ? undefined - : 'page contribution entrypoint must be a readable tenant page' - ) - ) + : 'page contribution entrypoint must be a readable tenant page', + ), + ), ); const componentEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'public_component' && - allowsRead(entrypoint.access) + entrypoint.scope === 'tenant' && entrypoint.role === 'public_component' && allowsRead(entrypoint.access) ? undefined - : 'component contribution entrypoint must be a readable tenant public component' - ) - ) + : 'component contribution entrypoint must be a readable tenant public component', + ), + ), ); const searchEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'search' && - allowsRead(entrypoint.access) + entrypoint.scope === 'tenant' && entrypoint.role === 'search' && allowsRead(entrypoint.access) ? undefined - : 'search contribution entrypoint must be a readable tenant search entrypoint' - ) - ) + : 'search contribution entrypoint must be a readable tenant search entrypoint', + ), + ), ); const reportEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'report' && - entrypoint.access !== 'background' + entrypoint.scope === 'tenant' && entrypoint.role === 'report' && entrypoint.access !== 'background' ? undefined - : 'report contribution entrypoint must be a tenant report with compatible access' - ) - ) + : 'report contribution entrypoint must be a tenant report with compatible access', + ), + ), ); const readableApiEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'api' && - allowsRead(entrypoint.access) + entrypoint.scope === 'tenant' && entrypoint.role === 'api' && allowsRead(entrypoint.access) ? undefined - : 'resource contribution entrypoint must be a readable tenant API' - ) - ) + : 'resource contribution entrypoint must be a readable tenant API', + ), + ), ); const writableApiEntrypoint = ModuleEntrypointSchema.pipe( Schema.check( Schema.makeFilter((entrypoint) => - entrypoint.scope === 'tenant' && - entrypoint.role === 'api' && - entrypoint.access === 'write' + entrypoint.scope === 'tenant' && entrypoint.role === 'api' && entrypoint.access === 'write' ? undefined - : 'media contribution entrypoint must be a writable tenant API' - ) - ) + : 'media contribution entrypoint must be a writable tenant API', + ), + ), ); export const ShellNavigationContributionSchema = Schema.Struct({ @@ -181,9 +165,7 @@ export const OntosShellContributionsSchema = Schema.Struct({ timelines: Schema.Array(ShellTimelineContributionSchema), }); -export type OntosShellContributions = Schema.Schema.Type< - typeof OntosShellContributionsSchema ->; +export type OntosShellContributions = Schema.Schema.Type; export interface ShellContributionReferenceSets { readonly actionKeys: ReadonlySet; @@ -195,41 +177,22 @@ export interface ShellContributionReferenceSets { readonly searchKeys: ReadonlySet; } -const referenceIssue = ( - set: ReadonlySet, - key: string, - label: string -): string | undefined => - set.has(key) - ? undefined - : `${label} references undeclared manifest key ${key}`; +const referenceIssue = (set: ReadonlySet, key: string, label: string): string | undefined => + set.has(key) ? undefined : `${label} references undeclared manifest key ${key}`; const validatePageReferences = ( contributions: OntosShellContributions, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): string | undefined => { - const pageKeys = new Set( - contributions.pages.map(({ contributionKey: key }) => key) - ); + const pageKeys = new Set(contributions.pages.map(({ contributionKey: key }) => key)); for (const contribution of contributions.navigation) { - const issue = referenceIssue( - pageKeys, - contribution.pageKey, - 'navigation contribution' - ); + const issue = referenceIssue(pageKeys, contribution.pageKey, 'navigation contribution'); if (issue !== undefined) { return issue; } } - for (const contribution of [ - ...contributions.pages, - ...contributions.publicComponents, - ]) { - const issue = referenceIssue( - references.componentKeys, - contribution.componentKey, - 'component contribution' - ); + for (const contribution of [...contributions.pages, ...contributions.publicComponents]) { + const issue = referenceIssue(references.componentKeys, contribution.componentKey, 'component contribution'); if (issue !== undefined) { return issue; } @@ -240,24 +203,16 @@ const validatePageReferences = ( const validateDiscoveryReferences = ( contributions: OntosShellContributions, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): string | undefined => { for (const contribution of contributions.search) { - const issue = referenceIssue( - references.searchKeys, - contribution.searchKey, - 'search contribution' - ); + const issue = referenceIssue(references.searchKeys, contribution.searchKey, 'search contribution'); if (issue !== undefined) { return issue; } } for (const contribution of contributions.reports) { - const issue = referenceIssue( - references.reportKeys, - contribution.reportKey, - 'report contribution' - ); + const issue = referenceIssue(references.reportKeys, contribution.reportKey, 'report contribution'); if (issue !== undefined) { return issue; } @@ -268,24 +223,17 @@ const validateDiscoveryReferences = ( const validateResourceReferences = ( contributions: OntosShellContributions, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): string | undefined => { - for (const contribution of [ - ...contributions.resourceDetails, - ...contributions.timelines, - ]) { - const apiIssue = referenceIssue( - references.apiKeys, - contribution.apiKey, - 'resource contribution' - ); + for (const contribution of [...contributions.resourceDetails, ...contributions.timelines]) { + const apiIssue = referenceIssue(references.apiKeys, contribution.apiKey, 'resource contribution'); if (apiIssue !== undefined) { return apiIssue; } const resourceIssue = referenceIssue( references.resourceTypeKeys, contribution.resourceType, - 'resource contribution' + 'resource contribution', ); if (resourceIssue !== undefined) { return resourceIssue; @@ -297,30 +245,18 @@ const validateResourceReferences = ( const validateMediaReferences = ( contributions: OntosShellContributions, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): string | undefined => { for (const contribution of contributions.mediaAttachments) { - const actionIssue = referenceIssue( - references.actionKeys, - contribution.actionKey, - 'media contribution' - ); + const actionIssue = referenceIssue(references.actionKeys, contribution.actionKey, 'media contribution'); if (actionIssue !== undefined) { return actionIssue; } - const apiIssue = referenceIssue( - references.apiKeys, - contribution.apiKey, - 'media contribution' - ); + const apiIssue = referenceIssue(references.apiKeys, contribution.apiKey, 'media contribution'); if (apiIssue !== undefined) { return apiIssue; } - const resourceIssue = referenceIssue( - references.resourceTypeKeys, - contribution.resourceType, - 'media contribution' - ); + const resourceIssue = referenceIssue(references.resourceTypeKeys, contribution.resourceType, 'media contribution'); if (resourceIssue !== undefined) { return resourceIssue; } @@ -330,7 +266,7 @@ const validateMediaReferences = ( const validateReferences = ( contributions: OntosShellContributions, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): string | undefined => { const all = [ ...contributions.mediaAttachments, @@ -349,9 +285,7 @@ const validateReferences = ( for (const contribution of all) { if ( contribution.entrypoint.moduleKey !== references.moduleId || - !contribution.entrypoint.entrypointKey.startsWith( - `${references.moduleId}.` - ) + !contribution.entrypoint.entrypointKey.startsWith(`${references.moduleId}.`) ) { return 'Shell contribution entrypoint owner must match the manifest module'; } @@ -366,16 +300,10 @@ const validateReferences = ( export const validateShellContributions = ( input: Input, - references: ShellContributionReferenceSets + references: ShellContributionReferenceSets, ): OntosShellContributions => { const schema = OntosShellContributionsSchema.pipe( - Schema.check( - Schema.makeFilter((contributions) => - validateReferences(contributions, references) - ) - ) - ); - return Result.getOrThrow( - Schema.decodeUnknownResult(schema, { onExcessProperty: 'error' })(input) + Schema.check(Schema.makeFilter((contributions) => validateReferences(contributions, references))), ); + return Result.getOrThrow(Schema.decodeUnknownResult(schema, { onExcessProperty: 'error' })(input)); }; diff --git a/app/packages/core-runtime/src/modules/tenant-module-state-concurrent-change-error.ts b/app/packages/core-runtime/src/modules/tenant-module-state-concurrent-change-error.ts index d5f429d54..b46c0900b 100644 --- a/app/packages/core-runtime/src/modules/tenant-module-state-concurrent-change-error.ts +++ b/app/packages/core-runtime/src/modules/tenant-module-state-concurrent-change-error.ts @@ -5,5 +5,5 @@ export class TenantModuleStateConcurrentChangeError extends Schema.TaggedError( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); export const TENANT_MODULE_STATES = [ 'inactive', @@ -50,42 +40,28 @@ export const TENANT_MODULE_STATES = [ ] as const; export const TenantModuleStateSchema = Schema.Literals(TENANT_MODULE_STATES); -export type TenantModuleState = Schema.Schema.Type< - typeof TenantModuleStateSchema ->; +export type TenantModuleState = Schema.Schema.Type; export const ActiveTenantModuleSchema = Schema.Struct({ moduleKey: OntosModuleIdSchema, state: Schema.Literal('active'), }); -export type ActiveTenantModule = Schema.Schema.Type< - typeof ActiveTenantModuleSchema ->; +export type ActiveTenantModule = Schema.Schema.Type; export const TenantModuleStateRecordSchema = Schema.Struct({ moduleKey: OntosModuleIdSchema, state: TenantModuleStateSchema, }); -export type TenantModuleStateRecord = Schema.Schema.Type< - typeof TenantModuleStateRecordSchema ->; +export type TenantModuleStateRecord = Schema.Schema.Type; -const TenantModuleStateChangeSourceSchema = Schema.Literals([ - 'support', - 'system', - 'user', -]); -export type TenantModuleStateChangeSource = - typeof TenantModuleStateChangeSourceSchema.Type; +const TenantModuleStateChangeSourceSchema = Schema.Literals(['support', 'system', 'user']); +export type TenantModuleStateChangeSource = typeof TenantModuleStateChangeSourceSchema.Type; export const validateTenantModuleStateTransition = ( catalog: InstalledModuleCatalog, moduleKey: OntosModuleId, - newState: TenantModuleState -): Effect.Effect< - void, - TenantModuleStateUnknownModuleError | TenantModuleStateUnsupportedStateError -> => { + newState: TenantModuleState, +): Effect.Effect => { if (newState === 'inactive') { return Effect.void; } @@ -95,7 +71,7 @@ export const validateTenantModuleStateTransition = ( new TenantModuleStateUnknownModuleError({ code: 'tenant_module_state_module_unknown', reason: 'The requested OntOS module is not installed', - }) + }), ); } if (!contract.manifest.activation.supportedStates.includes(newState)) { @@ -103,77 +79,60 @@ export const validateTenantModuleStateTransition = ( new TenantModuleStateUnsupportedStateError({ code: 'tenant_module_state_unsupported', reason: 'The requested state is not supported by the installed module', - }) + }), ); } return Effect.void; }; export const resolveTenantModuleStateChangeSource = ( - authMethod: ActionAuthMethod -): Effect.Effect< - TenantModuleStateChangeSource, - TenantModuleStateUnsupportedChangeSourceError -> => + authMethod: ActionAuthMethod, +): Effect.Effect => Match.value(authMethod).pipe( Match.when('session', () => Effect.succeed('user' as const)), - Match.when('support_impersonation', () => - Effect.succeed('support' as const) - ), + Match.when('support_impersonation', () => Effect.succeed('support' as const)), Match.when('system', () => Effect.succeed('system' as const)), Match.when('api_key', () => Effect.fail( new TenantModuleStateUnsupportedChangeSourceError({ code: 'tenant_module_state_change_source_unsupported', - reason: - 'This authentication method cannot change tenant module state', - }) - ) + reason: 'This authentication method cannot change tenant module state', + }), + ), ), - Match.exhaustive + Match.exhaustive, ); export const rejectUnchangedTenantModuleState = ( previousState: TenantModuleState | null, - newState: TenantModuleState + newState: TenantModuleState, ): Effect.Effect => previousState === newState ? Effect.fail( new TenantModuleStateUnchangedError({ code: 'tenant_module_state_unchanged', reason: 'The tenant module already has the requested state', - }) + }), ) : Effect.void; export interface TenantModuleStateServiceContract { readonly getTenantModuleStates: ( tenantId: string, - moduleKeys: readonly string[] - ) => Effect.Effect< - readonly TenantModuleStateRecord[], - TenantModuleStateReadUnavailableError - >; + moduleKeys: readonly string[], + ) => Effect.Effect; readonly listActiveTenantModules: ( - tenantId: string - ) => Effect.Effect< - readonly ActiveTenantModule[], - TenantModuleStateReadUnavailableError - >; + tenantId: string, + ) => Effect.Effect; readonly listTenantModuleStates: ( - tenantId: string - ) => Effect.Effect< - readonly TenantModuleStateRecord[], - TenantModuleStateReadUnavailableError - >; + tenantId: string, + ) => Effect.Effect; } export class TenantModuleStateService extends Context.Service< TenantModuleStateService, TenantModuleStateServiceContract ->()( - '@app/core-runtime/modules/tenant-module-state-service/TenantModuleStateService' -) {} +>()('@app/core-runtime/modules/tenant-module-state-service/TenantModuleStateService') {} const tenantModuleStateReadUnavailable = (cause?: unknown) => { const error = new TenantModuleStateReadUnavailableError({ @@ -189,19 +148,15 @@ const tenantModuleStateReadUnavailable = (cause?: unknown) => { export const makeTenantModuleStateService = (database: { readonly executor: Pick; }): TenantModuleStateServiceContract => { - const decodeRows = ( - rows: readonly { readonly moduleKey: string; readonly state: unknown }[] - ) => + const decodeRows = (rows: readonly { readonly moduleKey: string; readonly state: unknown }[]) => Effect.forEach( rows, (row: (typeof rows)[number]) => Schema.decodeUnknownEffect(TenantModuleStateSchema)(row.state).pipe( - Effect.map((state) => - Object.freeze({ moduleKey: row.moduleKey, state }) - ), - Effect.mapError(tenantModuleStateReadUnavailable) + Effect.map((state) => Object.freeze({ moduleKey: row.moduleKey, state })), + Effect.mapError(tenantModuleStateReadUnavailable), ), - { concurrency: 1 } + { concurrency: 1 }, ).pipe(Effect.map((records) => Object.freeze(records))); const listTenantModuleStates = (tenantId: string) => @@ -213,10 +168,7 @@ export const makeTenantModuleStateService = (database: { .from(tenantModuleStates) .where(eq(tenantModuleStates.tenantId, tenantId)) .orderBy(asc(tenantModuleStates.moduleKey)) - .pipe( - Effect.mapError(tenantModuleStateReadUnavailable), - Effect.flatMap(decodeRows) - ); + .pipe(Effect.mapError(tenantModuleStateReadUnavailable), Effect.flatMap(decodeRows)); return { getTenantModuleStates: (tenantId, moduleKeys) => { @@ -230,27 +182,17 @@ export const makeTenantModuleStateService = (database: { state: tenantModuleStates.state, }) .from(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - inArray(tenantModuleStates.moduleKey, distinctKeys) - ) - ) + .where(and(eq(tenantModuleStates.tenantId, tenantId), inArray(tenantModuleStates.moduleKey, distinctKeys))) .orderBy(asc(tenantModuleStates.moduleKey)) - .pipe( - Effect.mapError(tenantModuleStateReadUnavailable), - Effect.flatMap(decodeRows) - ); + .pipe(Effect.mapError(tenantModuleStateReadUnavailable), Effect.flatMap(decodeRows)); }, listActiveTenantModules: (tenantId) => listTenantModuleStates(tenantId).pipe( Effect.map((rows) => rows.flatMap((row) => - row.state === 'active' - ? [{ moduleKey: row.moduleKey, state: 'active' as const }] - : [] - ) - ) + row.state === 'active' ? [{ moduleKey: row.moduleKey, state: 'active' as const }] : [], + ), + ), ), listTenantModuleStates, }; @@ -258,7 +200,7 @@ export const makeTenantModuleStateService = (database: { export const TenantModuleStateServiceLive = Layer.effect( TenantModuleStateService, - CoreDatabase.pipe(Effect.map(makeTenantModuleStateService)) + CoreDatabase.pipe(Effect.map(makeTenantModuleStateService)), ); export interface PersistTenantModuleStateChangeInput { @@ -289,17 +231,12 @@ const persistenceUnavailable = (cause?: unknown) => { return error; }; -export const persistTenantModuleStateChange = Effect.fn( - 'TenantModuleStateService.persistTenantModuleStateChange' -)(function* persistTenantModuleStateChangeEffect( - transaction: ScopedTransactionExecutor, - input: PersistTenantModuleStateChangeInput -): Effect.fn.Return< - PersistTenantModuleStateChangeResult, - TenantModuleStateTransitionError -> { - const { changeSource, tenantRows } = - yield* resolveTenantModuleStateChangeSource(input.authMethod).pipe( +export const persistTenantModuleStateChange = Effect.fn('TenantModuleStateService.persistTenantModuleStateChange')( + function* persistTenantModuleStateChangeEffect( + transaction: ScopedTransactionExecutor, + input: PersistTenantModuleStateChangeInput, + ): Effect.fn.Return { + const { changeSource, tenantRows } = yield* resolveTenantModuleStateChangeSource(input.authMethod).pipe( Effect.flatMap((resolvedChangeSource) => transaction .select({ tenantId: tenants.tenantId }) @@ -311,123 +248,114 @@ export const persistTenantModuleStateChange = Effect.fn( Effect.map((lockedTenantRows) => ({ changeSource: resolvedChangeSource, tenantRows: lockedTenantRows, - })) - ) - ) + })), + ), + ), ); - const [tenant] = tenantRows; - if (tenant === undefined) { - return yield* new TenantModuleStateTenantMissingError({ - code: 'tenant_module_state_tenant_missing', - reason: 'The tenant required for this state change does not exist', - }); - } - - const currentRows = yield* transaction - .select({ - state: tenantModuleStates.state, - tenantModuleStateId: tenantModuleStates.tenantModuleStateId, - }) - .from(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, input.tenantId), - eq(tenantModuleStates.moduleKey, input.moduleKey) - ) - ) - .pipe(Effect.mapError(persistenceUnavailable)); - const [current] = currentRows; - const previousState = - current === undefined - ? null - : yield* Schema.decodeUnknownEffect(TenantModuleStateSchema)( - current.state - ).pipe(Effect.mapError(persistenceUnavailable)); - const effectivePreviousState = previousState ?? 'inactive'; - if ( - input.expectedState !== undefined && - input.expectedState !== effectivePreviousState - ) { - return yield* new TenantModuleStateConcurrentChangeError({ - code: 'tenant_module_state_changed_concurrently', - reason: 'The tenant module state changed after it was read', - }); - } - yield* rejectUnchangedTenantModuleState(previousState, input.newState); - const currentTimeMillis = yield* Clock.currentTimeMillis; - const changedAt = DateTime.toDateUtc(DateTime.makeUnsafe(currentTimeMillis)); - - const historyRows = yield* transaction - .insert(tenantModuleStateChanges) - .values( - withOptionalProperty( - { - actionInvocationId: input.actionInvocationId, - changedByPrincipalId: input.principalId, - changeSource, - moduleKey: input.moduleKey, - newState: input.newState, - occurredAt: changedAt, - previousState, - }, - input.reason !== undefined, - 'reason', - input.reason, - { - tenantId: input.tenantId, - } - ) - ) - .returning({ - moduleStateChangeId: tenantModuleStateChanges.moduleStateChangeId, - }) - .pipe(Effect.mapError(persistenceUnavailable)); - const [history] = historyRows; - if (history === undefined) { - return yield* persistenceUnavailable(); - } + const [tenant] = tenantRows; + if (tenant === undefined) { + return yield* new TenantModuleStateTenantMissingError({ + code: 'tenant_module_state_tenant_missing', + reason: 'The tenant required for this state change does not exist', + }); + } - if (current === undefined) { - const inserted = yield* transaction - .insert(tenantModuleStates) - .values({ - lastChangeId: history.moduleStateChangeId, - moduleKey: input.moduleKey, - state: input.newState, - tenantId: input.tenantId, - updatedAt: changedAt, - }) - .returning({ + const currentRows = yield* transaction + .select({ + state: tenantModuleStates.state, tenantModuleStateId: tenantModuleStates.tenantModuleStateId, }) + .from(tenantModuleStates) + .where(and(eq(tenantModuleStates.tenantId, input.tenantId), eq(tenantModuleStates.moduleKey, input.moduleKey))) .pipe(Effect.mapError(persistenceUnavailable)); - const [insertedState] = inserted; - if (insertedState === undefined) { - return yield* persistenceUnavailable(); + const [current] = currentRows; + const previousState = + current === undefined + ? null + : yield* Schema.decodeUnknownEffect(TenantModuleStateSchema)(current.state).pipe( + Effect.mapError(persistenceUnavailable), + ); + const effectivePreviousState = previousState ?? 'inactive'; + if (input.expectedState !== undefined && input.expectedState !== effectivePreviousState) { + return yield* new TenantModuleStateConcurrentChangeError({ + code: 'tenant_module_state_changed_concurrently', + reason: 'The tenant module state changed after it was read', + }); } - } else { - const updated = yield* transaction - .update(tenantModuleStates) - .set({ - lastChangeId: history.moduleStateChangeId, - state: input.newState, - updatedAt: changedAt, - }) - .where( - eq(tenantModuleStates.tenantModuleStateId, current.tenantModuleStateId) + yield* rejectUnchangedTenantModuleState(previousState, input.newState); + const currentTimeMillis = yield* Clock.currentTimeMillis; + const changedAt = DateTime.toDateUtc(DateTime.makeUnsafe(currentTimeMillis)); + + const historyRows = yield* transaction + .insert(tenantModuleStateChanges) + .values( + withOptionalProperty( + { + actionInvocationId: input.actionInvocationId, + changedByPrincipalId: input.principalId, + changeSource, + moduleKey: input.moduleKey, + newState: input.newState, + occurredAt: changedAt, + previousState, + }, + input.reason !== undefined, + 'reason', + input.reason, + { + tenantId: input.tenantId, + }, + ), ) .returning({ - tenantModuleStateId: tenantModuleStates.tenantModuleStateId, + moduleStateChangeId: tenantModuleStateChanges.moduleStateChangeId, }) .pipe(Effect.mapError(persistenceUnavailable)); - if (updated[0] === undefined) { + const [history] = historyRows; + if (history === undefined) { return yield* persistenceUnavailable(); } - } - return { - moduleKey: input.moduleKey, - newState: input.newState, - previousState, - }; -}); + if (current === undefined) { + const inserted = yield* transaction + .insert(tenantModuleStates) + .values({ + lastChangeId: history.moduleStateChangeId, + moduleKey: input.moduleKey, + state: input.newState, + tenantId: input.tenantId, + updatedAt: changedAt, + }) + .returning({ + tenantModuleStateId: tenantModuleStates.tenantModuleStateId, + }) + .pipe(Effect.mapError(persistenceUnavailable)); + const [insertedState] = inserted; + if (insertedState === undefined) { + return yield* persistenceUnavailable(); + } + } else { + const updated = yield* transaction + .update(tenantModuleStates) + .set({ + lastChangeId: history.moduleStateChangeId, + state: input.newState, + updatedAt: changedAt, + }) + .where(eq(tenantModuleStates.tenantModuleStateId, current.tenantModuleStateId)) + .returning({ + tenantModuleStateId: tenantModuleStates.tenantModuleStateId, + }) + .pipe(Effect.mapError(persistenceUnavailable)); + if (updated[0] === undefined) { + return yield* persistenceUnavailable(); + } + } + + return { + moduleKey: input.moduleKey, + newState: input.newState, + previousState, + }; + }, +); diff --git a/app/packages/core-runtime/src/modules/tenant-module-state-tenant-missing-error.ts b/app/packages/core-runtime/src/modules/tenant-module-state-tenant-missing-error.ts index 75afe8cf0..bc6794984 100644 --- a/app/packages/core-runtime/src/modules/tenant-module-state-tenant-missing-error.ts +++ b/app/packages/core-runtime/src/modules/tenant-module-state-tenant-missing-error.ts @@ -5,5 +5,5 @@ export class TenantModuleStateTenantMissingError extends Schema.TaggedError( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); -export const LEGAL_ENTITY_SCOPES = [ - 'required', - 'optional', - 'forbidden', -] as const; +export const LEGAL_ENTITY_SCOPES = ['required', 'optional', 'forbidden'] as const; export type LegalEntityScope = (typeof LEGAL_ENTITY_SCOPES)[number]; export interface OperationalScopeRequest { @@ -59,15 +41,9 @@ export interface OperationalScopeRequest { readonly traceId?: string; } -export interface OperationalScope - extends - Readonly, - Readonly {} +export interface OperationalScope extends Readonly, Readonly {} -export type LegalEntityScopeAccess = Pick< - ContextAccessService, - 'legalEntities' -> & +export type LegalEntityScopeAccess = Pick & Partial>; export interface ResolveOperationalScopeInput extends Readonly { @@ -76,9 +52,7 @@ export interface ResolveOperationalScopeInput extends Readonly Effect.Effect; + readonly resolve: (input: ResolveOperationalScopeInput) => Effect.Effect; } export class OperationalScopeResolver extends Context.Service< @@ -123,52 +97,34 @@ export const makeOperationalScopeRepository = (database: { .from(tenants) .innerJoin( principals, - and( - eq(principals.tenantId, tenants.tenantId), - eq(principals.principalId, principal.principalId) - ) + and(eq(principals.tenantId, tenants.tenantId), eq(principals.principalId, principal.principalId)), ) .leftJoin( impersonators, principal.impersonatedByPrincipalId === undefined - ? eq( - impersonators.principalId, - '00000000-0000-0000-0000-000000000000' - ) + ? eq(impersonators.principalId, '00000000-0000-0000-0000-000000000000') : and( eq(impersonators.tenantId, principal.tenantId), - eq( - impersonators.principalId, - principal.impersonatedByPrincipalId - ) - ) + eq(impersonators.principalId, principal.impersonatedByPrincipalId), + ), ) .leftJoin( principalAuthBindings, principal.authBindingId === undefined - ? eq( - principalAuthBindings.principalAuthBindingId, - '00000000-0000-0000-0000-000000000000' - ) + ? eq(principalAuthBindings.principalAuthBindingId, '00000000-0000-0000-0000-000000000000') : and( eq(principalAuthBindings.tenantId, principal.tenantId), - eq( - principalAuthBindings.principalAuthBindingId, - principal.authBindingId - ) - ) + eq(principalAuthBindings.principalAuthBindingId, principal.authBindingId), + ), ) .leftJoin( legalEntities, principal.legalEntityId === undefined - ? eq( - legalEntities.legalEntityId, - '00000000-0000-0000-0000-000000000000' - ) + ? eq(legalEntities.legalEntityId, '00000000-0000-0000-0000-000000000000') : and( eq(legalEntities.tenantId, principal.tenantId), - eq(legalEntities.legalEntityId, principal.legalEntityId) - ) + eq(legalEntities.legalEntityId, principal.legalEntityId), + ), ) .where(eq(tenants.tenantId, principal.tenantId)) .limit(1); @@ -188,15 +144,15 @@ export const makeOperationalScopeRepository = (database: { principalStatus: null, principalTenantId: null, tenantStatus: null, - } - ) + }, + ), ), }); const validateRequestedScope = ( request: OperationalScopeRequest, legalEntityScope: LegalEntityScope, - principal: TrustedPrincipalContext + principal: TrustedPrincipalContext, ): OperationContextError | undefined => { if (request.correlationId.length === 0) { return new OperationContextInvalid({ @@ -204,37 +160,25 @@ const validateRequestedScope = ( reason: 'The operation context is incomplete', }); } - if ( - legalEntityScope === 'required' && - principal.legalEntityId === undefined - ) { + if (legalEntityScope === 'required' && principal.legalEntityId === undefined) { return new OperationContextDenied({ code: 'operation_context_denied', reason: 'An active legal entity is required for this operation', }); } - if ( - legalEntityScope === 'forbidden' && - principal.legalEntityId !== undefined - ) { + if (legalEntityScope === 'forbidden' && principal.legalEntityId !== undefined) { return new OperationContextInvalid({ code: 'operation_context_invalid', reason: 'This operation does not accept legal-entity context', }); } - if ( - principal.authMethod === 'system' && - !isTrustedSystemPrincipalContext(principal) - ) { + if (principal.authMethod === 'system' && !isTrustedSystemPrincipalContext(principal)) { return new OperationAuthenticationRequired({ code: 'operation_authentication_required', reason: 'The system principal context is not trusted', }); } - if ( - principal.authMethod !== 'system' && - principal.authBindingId === undefined - ) { + if (principal.authMethod !== 'system' && principal.authBindingId === undefined) { return new OperationAuthenticationRequired({ code: 'operation_authentication_required', reason: 'The authenticated principal binding is unavailable', @@ -246,35 +190,28 @@ const validateRequestedScope = ( const hasInvalidPersistedBinding = ( principal: TrustedPrincipalContext, persisted: PersistedScopeRecord, - supportRecovery: boolean + supportRecovery: boolean, ): boolean => persisted.bindingTenantId !== principal.tenantId || persisted.bindingPrincipalId !== principal.principalId || - (!supportRecovery && - (persisted.bindingStatus !== 'active' || - persisted.bindingRevokedAt !== null)); + (!supportRecovery && (persisted.bindingStatus !== 'active' || persisted.bindingRevokedAt !== null)); const validatePersistedPrincipal = ( principal: TrustedPrincipalContext, persisted: PersistedScopeRecord, - supportRecovery: boolean + supportRecovery: boolean, ): OperationContextError | undefined => { if ( persisted.principalTenantId !== principal.tenantId || persisted.tenantStatus === null || - (!supportRecovery && - (persisted.tenantStatus !== 'active' || - persisted.principalStatus !== 'active')) + (!supportRecovery && (persisted.tenantStatus !== 'active' || persisted.principalStatus !== 'active')) ) { return new OperationContextDenied({ code: 'operation_context_denied', reason: 'The tenant or principal is not active in this operation scope', }); } - if ( - principal.authBindingId !== undefined && - hasInvalidPersistedBinding(principal, persisted, supportRecovery) - ) { + if (principal.authBindingId !== undefined && hasInvalidPersistedBinding(principal, persisted, supportRecovery)) { return new OperationAuthenticationRequired({ code: 'operation_authentication_required', reason: 'The authenticated principal binding is no longer valid', @@ -283,149 +220,135 @@ const validatePersistedPrincipal = ( return undefined; }; -const validateSupportImpersonation = Effect.fn( - 'OperationalScopeResolver.validateSupportImpersonation' -)(function* validateSupportImpersonationEffect( - contextAccess: LegalEntityScopeAccess, - principal: TrustedPrincipalContext, - persisted: PersistedScopeRecord -) { - if (principal.authMethod !== 'support_impersonation') { - return yield* Effect.void; - } - if ( - principal.impersonatedByPrincipalId === undefined || - persisted.impersonatorStatus !== 'active' || - persisted.impersonatorTenantId !== principal.tenantId +const validateSupportImpersonation = Effect.fn('OperationalScopeResolver.validateSupportImpersonation')( + function* validateSupportImpersonationEffect( + contextAccess: LegalEntityScopeAccess, + principal: TrustedPrincipalContext, + persisted: PersistedScopeRecord, ) { - return yield* new OperationContextDenied({ - code: 'operation_context_denied', - reason: 'The support administrator is no longer active in this tenant', - }); - } + if (principal.authMethod !== 'support_impersonation') { + return yield* Effect.void; + } + if ( + principal.impersonatedByPrincipalId === undefined || + persisted.impersonatorStatus !== 'active' || + persisted.impersonatorTenantId !== principal.tenantId + ) { + return yield* new OperationContextDenied({ + code: 'operation_context_denied', + reason: 'The support administrator is no longer active in this tenant', + }); + } - if (contextAccess.tenants === undefined) { - return yield* new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'Support authorization is temporarily unavailable', - }); - } - const [supportDecision] = yield* contextAccess.tenants({ - permission: 'impersonate', - principalId: principal.impersonatedByPrincipalId, - tenantIds: [principal.tenantId], - }); - if (supportDecision?.decision === 'denied') { - return yield* new OperationContextDenied({ - code: 'operation_context_denied', - reason: 'Support impersonation permission was revoked', - }); - } - if (supportDecision?.decision !== 'allowed') { - return yield* new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'Support authorization is temporarily unavailable', + if (contextAccess.tenants === undefined) { + return yield* new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'Support authorization is temporarily unavailable', + }); + } + const [supportDecision] = yield* contextAccess.tenants({ + permission: 'impersonate', + principalId: principal.impersonatedByPrincipalId, + tenantIds: [principal.tenantId], }); - } - return yield* Effect.void; -}); - -const validateLegalEntity = Effect.fn( - 'OperationalScopeResolver.validateLegalEntity' -)(function* validateLegalEntityEffect( - contextAccess: LegalEntityScopeAccess, - principal: TrustedPrincipalContext, - persisted: PersistedScopeRecord -) { - if (principal.legalEntityId === undefined) { + if (supportDecision?.decision === 'denied') { + return yield* new OperationContextDenied({ + code: 'operation_context_denied', + reason: 'Support impersonation permission was revoked', + }); + } + if (supportDecision?.decision !== 'allowed') { + return yield* new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'Support authorization is temporarily unavailable', + }); + } return yield* Effect.void; - } - if ( - persisted.legalEntityStatus !== 'active' || - persisted.legalEntityTenantId !== principal.tenantId + }, +); + +const validateLegalEntity = Effect.fn('OperationalScopeResolver.validateLegalEntity')( + function* validateLegalEntityEffect( + contextAccess: LegalEntityScopeAccess, + principal: TrustedPrincipalContext, + persisted: PersistedScopeRecord, ) { - return yield* new OperationContextDenied({ - code: 'operation_context_denied', - reason: 'The selected legal entity is unavailable in this tenant', - }); - } + if (principal.legalEntityId === undefined) { + return yield* Effect.void; + } + if (persisted.legalEntityStatus !== 'active' || persisted.legalEntityTenantId !== principal.tenantId) { + return yield* new OperationContextDenied({ + code: 'operation_context_denied', + reason: 'The selected legal entity is unavailable in this tenant', + }); + } - const [decision] = yield* contextAccess.legalEntities({ - legalEntityIds: [principal.legalEntityId], - principalId: principal.principalId, - tenantId: principal.tenantId, - }); - if (decision?.decision === 'denied') { - return yield* new OperationContextDenied({ - code: 'operation_context_denied', - reason: 'The principal cannot access the selected legal entity', - }); - } - if (decision?.decision !== 'allowed') { - return yield* new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'Legal-entity authorization is temporarily unavailable', + const [decision] = yield* contextAccess.legalEntities({ + legalEntityIds: [principal.legalEntityId], + principalId: principal.principalId, + tenantId: principal.tenantId, }); - } - return yield* Effect.void; -}); + if (decision?.decision === 'denied') { + return yield* new OperationContextDenied({ + code: 'operation_context_denied', + reason: 'The principal cannot access the selected legal entity', + }); + } + if (decision?.decision !== 'allowed') { + return yield* new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'Legal-entity authorization is temporarily unavailable', + }); + } + return yield* Effect.void; + }, +); export const makeOperationalScopeResolver = ( repository: Pick, - contextAccess: LegalEntityScopeAccess + contextAccess: LegalEntityScopeAccess, ): OperationalScopeResolverService => { - const resolveOperationalScope = Effect.fn('OperationalScopeResolver.resolve')( - function* resolveOperationalScopeEffect( - input: ResolveOperationalScopeInput - ) { - const { principal } = input; - const requestFailure = validateRequestedScope( - input, - input.legalEntityScope, - principal - ); - if (requestFailure !== undefined) { - return yield* requestFailure; - } - - const persisted = yield* repository.load(principal); - const supportRecovery = - isTrustedSupportRecoveryPrincipalContext(principal); - const persistedFailure = validatePersistedPrincipal( - principal, - persisted, - supportRecovery - ); - if (persistedFailure !== undefined) { - return yield* persistedFailure; - } + const resolveOperationalScope = Effect.fn('OperationalScopeResolver.resolve')(function* resolveOperationalScopeEffect( + input: ResolveOperationalScopeInput, + ) { + const { principal } = input; + const requestFailure = validateRequestedScope(input, input.legalEntityScope, principal); + if (requestFailure !== undefined) { + return yield* requestFailure; + } - yield* validateSupportImpersonation(contextAccess, principal, persisted); - yield* validateLegalEntity(contextAccess, principal, persisted); - return preserveSystemPrincipalContextTrust( - principal, - Object.freeze( - withOptionalProperty( - { - ...principal, - correlationId: input.correlationId, - }, - input.traceId !== undefined, - 'traceId', - input.traceId, - {} - ) - ) - ); + const persisted = yield* repository.load(principal); + const supportRecovery = isTrustedSupportRecoveryPrincipalContext(principal); + const persistedFailure = validatePersistedPrincipal(principal, persisted, supportRecovery); + if (persistedFailure !== undefined) { + return yield* persistedFailure; } - ); + + yield* validateSupportImpersonation(contextAccess, principal, persisted); + yield* validateLegalEntity(contextAccess, principal, persisted); + return preserveSystemPrincipalContextTrust( + principal, + Object.freeze( + withOptionalProperty( + { + ...principal, + correlationId: input.correlationId, + }, + input.traceId !== undefined, + 'traceId', + input.traceId, + {}, + ), + ), + ); + }); return { resolve: resolveOperationalScope }; }; export const OperationalScopeRepositoryLive = Layer.effect( OperationalScopeRepositoryContext, - CoreDatabase.pipe(Effect.map(makeOperationalScopeRepository)) + CoreDatabase.pipe(Effect.map(makeOperationalScopeRepository)), ); export const OperationalScopeResolverFromRepositoryLive = Layer.effect( @@ -434,7 +357,7 @@ export const OperationalScopeResolverFromRepositoryLive = Layer.effect( const repository = yield* OperationalScopeRepositoryContext; const contextAccess = yield* ContextAccess; return makeOperationalScopeResolver(repository, contextAccess); - }) + }), ); export const OperationalScopeResolverLive = Layer.effect( @@ -442,9 +365,6 @@ export const OperationalScopeResolverLive = Layer.effect( Effect.gen(function* createLiveOperationalScopeResolverService() { const database = yield* CoreDatabase; const contextAccess = yield* ContextAccess; - return makeOperationalScopeResolver( - makeOperationalScopeRepository(database), - contextAccess - ); - }) + return makeOperationalScopeResolver(makeOperationalScopeRepository(database), contextAccess); + }), ); diff --git a/app/packages/core-runtime/src/operations/operation-authentication-required.ts b/app/packages/core-runtime/src/operations/operation-authentication-required.ts index 362e94342..63bfb39fe 100644 --- a/app/packages/core-runtime/src/operations/operation-authentication-required.ts +++ b/app/packages/core-runtime/src/operations/operation-authentication-required.ts @@ -5,5 +5,5 @@ export class OperationAuthenticationRequired extends Schema.TaggedError()( - 'OperationContextDenied', - { code: Schema.Literal('operation_context_denied'), reason: Schema.String } -) {} +export class OperationContextDenied extends Schema.TaggedError()('OperationContextDenied', { + code: Schema.Literal('operation_context_denied'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/operations/operation-context-invalid.ts b/app/packages/core-runtime/src/operations/operation-context-invalid.ts index 99ee8f389..c2055e26a 100644 --- a/app/packages/core-runtime/src/operations/operation-context-invalid.ts +++ b/app/packages/core-runtime/src/operations/operation-context-invalid.ts @@ -1,6 +1,6 @@ import { Schema } from 'effect'; -export class OperationContextInvalid extends Schema.TaggedError()( - 'OperationContextInvalid', - { code: Schema.Literal('operation_context_invalid'), reason: Schema.String } -) {} +export class OperationContextInvalid extends Schema.TaggedError()('OperationContextInvalid', { + code: Schema.Literal('operation_context_invalid'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/operations/operation-context-unavailable.ts b/app/packages/core-runtime/src/operations/operation-context-unavailable.ts index f140b9dac..84178aad0 100644 --- a/app/packages/core-runtime/src/operations/operation-context-unavailable.ts +++ b/app/packages/core-runtime/src/operations/operation-context-unavailable.ts @@ -5,5 +5,5 @@ export class OperationContextUnavailable extends Schema.TaggedError Effect.Effect; } export class OperationalScopeRepositoryContext extends Context.Service< OperationalScopeRepositoryContext, OperationalScopeRepository ->()( - '@app/core-runtime/operations/repository-context/OperationalScopeRepositoryContext' -) {} +>()('@app/core-runtime/operations/repository-context/OperationalScopeRepositoryContext') {} diff --git a/app/packages/core-runtime/src/outbox/definition.ts b/app/packages/core-runtime/src/outbox/definition.ts index 9969981f0..81d4cdbed 100644 --- a/app/packages/core-runtime/src/outbox/definition.ts +++ b/app/packages/core-runtime/src/outbox/definition.ts @@ -4,11 +4,8 @@ import type { Effect } from 'effect'; import type { TenantModuleEntrypoint } from '../modules/module-entrypoint.ts'; import { OutboxWorkerDescriptorError } from './errors.ts'; -const outboxWorkerRegistration: unique symbol = Symbol( - '@app/core-runtime/outbox/worker-registration' -); -const verifiedOutboxWorkerHandlerContext = - '__verifiedOutboxWorkerHandlerContext' as const; +const outboxWorkerRegistration: unique symbol = Symbol('@app/core-runtime/outbox/worker-registration'); +const verifiedOutboxWorkerHandlerContext = '__verifiedOutboxWorkerHandlerContext' as const; export interface OutboxWorkerRetryPolicy { readonly initialBackoffMs: number; @@ -17,9 +14,7 @@ export interface OutboxWorkerRetryPolicy { readonly multiplier: number; } -export interface OutboxWorkerHandlerContext extends Readonly< - Partial> -> { +export interface OutboxWorkerHandlerContext extends Readonly>> { readonly attemptNumber: number; readonly claimId: string; readonly deliveryId: string; @@ -37,9 +32,7 @@ const VerifiedOutboxWorkerHandlerContextSchema = Schema.Struct({ }); /** Core-private construction seam: caller-created context objects are not trusted worker claims. */ -export const attestOutboxWorkerHandlerContext = ( - context: OutboxWorkerHandlerContext -): OutboxWorkerHandlerContext => { +export const attestOutboxWorkerHandlerContext = (context: OutboxWorkerHandlerContext): OutboxWorkerHandlerContext => { const verified = { ...context }; Object.defineProperty(verified, verifiedOutboxWorkerHandlerContext, { enumerable: false, @@ -48,9 +41,8 @@ export const attestOutboxWorkerHandlerContext = ( return Object.freeze(verified); }; -export const isVerifiedOutboxWorkerHandlerContext = ( - context: OutboxWorkerHandlerContext -): boolean => Schema.is(VerifiedOutboxWorkerHandlerContextSchema)(context); +export const isVerifiedOutboxWorkerHandlerContext = (context: OutboxWorkerHandlerContext): boolean => + Schema.is(VerifiedOutboxWorkerHandlerContextSchema)(context); export interface OutboxWorkerDescriptor< PayloadSchema extends Schema.ConstraintDecoder, @@ -70,21 +62,16 @@ export interface OutboxWorkerDescriptor< export type OutboxWorkerSubscription = Readonly< Pick< OutboxWorkerDescriptor, string, string>, - | 'consumerModuleKey' - | 'entrypoint' - | 'producerModuleKey' - | 'topic' - | 'workerKey' + 'consumerModuleKey' | 'entrypoint' | 'producerModuleKey' | 'topic' | 'workerKey' > >; export type OutboxWorkerHandler = ( payload: Payload, - context: OutboxWorkerHandlerContext + context: OutboxWorkerHandlerContext, ) => Effect.Effect; -type BivariantOutboxWorkerHandler = - OutboxWorkerHandler; +type BivariantOutboxWorkerHandler = OutboxWorkerHandler; export interface OutboxWorkerRegistration< PayloadSchema extends Schema.ConstraintDecoder, @@ -95,9 +82,7 @@ export interface OutboxWorkerRegistration< > { readonly _handlerError?: HandlerError; readonly _handlerRequirements?: HandlerRequirements; - readonly descriptor: Readonly< - OutboxWorkerDescriptor - >; + readonly descriptor: Readonly>; readonly [outboxWorkerRegistration]: true; } @@ -107,49 +92,25 @@ class OutboxWorkerRegistrationValue< Producer extends string, HandlerError, HandlerRequirements, -> implements OutboxWorkerRegistration< - PayloadSchema, - Consumer, - Producer, - HandlerError, - HandlerRequirements -> { +> implements OutboxWorkerRegistration { readonly [outboxWorkerRegistration] = true; - readonly #handler: BivariantOutboxWorkerHandler< - PayloadSchema['Type'], - HandlerError, - HandlerRequirements - >; - readonly descriptor: Readonly< - OutboxWorkerDescriptor - >; + readonly #handler: BivariantOutboxWorkerHandler; + readonly descriptor: Readonly>; constructor( - descriptor: Readonly< - OutboxWorkerDescriptor - >, - handler: BivariantOutboxWorkerHandler< - PayloadSchema['Type'], - HandlerError, - HandlerRequirements - > + descriptor: Readonly>, + handler: BivariantOutboxWorkerHandler, ) { this.descriptor = descriptor; this.#handler = handler; } - resolveHandler(): BivariantOutboxWorkerHandler< - PayloadSchema['Type'], - HandlerError, - HandlerRequirements - > { + resolveHandler(): BivariantOutboxWorkerHandler { return this.#handler; } } -const isOutboxWorkerRegistrationValue = Schema.is( - Schema.instanceOf(OutboxWorkerRegistrationValue) -); +const isOutboxWorkerRegistrationValue = Schema.is(Schema.instanceOf(OutboxWorkerRegistrationValue)); export type AnyOutboxWorkerRegistration = OutboxWorkerRegistration< Schema.ConstraintDecoder, @@ -161,7 +122,7 @@ export type AnyOutboxWorkerRegistration = OutboxWorkerRegistration< /** Derive the schema-free deployment catalog without importing an owner's unrelated entrypoints. */ export const extractOutboxWorkerSubscriptions = ( - registrations: readonly AnyOutboxWorkerRegistration[] + registrations: readonly AnyOutboxWorkerRegistration[], ): readonly OutboxWorkerSubscription[] => Object.freeze( registrations @@ -172,14 +133,12 @@ export const extractOutboxWorkerSubscriptions = ( producerModuleKey: descriptor.producerModuleKey, topic: descriptor.topic, workerKey: descriptor.workerKey, - }) + }), ) - .toSorted((left, right) => left.workerKey.localeCompare(right.workerKey)) + .toSorted((left, right) => left.workerKey.localeCompare(right.workerKey)), ); -export type OutboxWorkerRequirements< - Registration extends AnyOutboxWorkerRegistration, -> = +export type OutboxWorkerRequirements = Registration extends OutboxWorkerRegistration< Schema.ConstraintDecoder, string, @@ -190,9 +149,7 @@ export type OutboxWorkerRequirements< ? Requirements : never; -type OutboxWorkerHandlerError< - Registration extends AnyOutboxWorkerRegistration, -> = +type OutboxWorkerHandlerError = Registration extends OutboxWorkerRegistration< Schema.ConstraintDecoder, string, @@ -205,8 +162,7 @@ type OutboxWorkerHandlerError< const moduleKeyPattern = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u; const workerSlugPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; -const topicPattern = - /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; +const topicPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; const descriptorError = (reason: string): OutboxWorkerDescriptorError => new OutboxWorkerDescriptorError({ @@ -214,23 +170,13 @@ const descriptorError = (reason: string): OutboxWorkerDescriptorError => reason, }); -const assertFiniteInteger = ( - value: number, - minimum: number, - maximum: number, - label: string -): void => { +const assertFiniteInteger = (value: number, minimum: number, maximum: number, label: string): void => { if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { - throw descriptorError( - `${label} must be an integer from ${minimum} through ${maximum}` - ); + throw descriptorError(`${label} must be an integer from ${minimum} through ${maximum}`); } }; -const assertWorkerEntrypoint = ( - descriptor: OutboxWorkerSubscription, - reason: string -): void => { +const assertWorkerEntrypoint = (descriptor: OutboxWorkerSubscription, reason: string): void => { if ( descriptor.entrypoint.scope !== 'tenant' || descriptor.entrypoint.role !== 'worker' || @@ -243,10 +189,7 @@ const assertWorkerEntrypoint = ( } }; -const assertWorkerSubscription = ( - descriptor: OutboxWorkerSubscription, - entrypointError: string -): void => { +const assertWorkerSubscription = (descriptor: OutboxWorkerSubscription, entrypointError: string): void => { if (!moduleKeyPattern.test(descriptor.consumerModuleKey)) { throw descriptorError('consumerModuleKey must be a stable module key'); } @@ -255,19 +198,12 @@ const assertWorkerSubscription = ( throw descriptorError('producerModuleKey must be a stable module key'); } if (!topicPattern.test(descriptor.topic)) { - throw descriptorError( - 'topic must be an exact lowercase dot-separated identifier' - ); + throw descriptorError('topic must be an exact lowercase dot-separated identifier'); } const expectedWorkerPrefix = `${descriptor.consumerModuleKey}.`; const workerSlug = descriptor.workerKey.slice(expectedWorkerPrefix.length); - if ( - !descriptor.workerKey.startsWith(expectedWorkerPrefix) || - !workerSlugPattern.test(workerSlug) - ) { - throw descriptorError( - 'workerKey must be owned by consumerModuleKey and end in lower-kebab-case' - ); + if (!descriptor.workerKey.startsWith(expectedWorkerPrefix) || !workerSlugPattern.test(workerSlug)) { + throw descriptorError('workerKey must be owned by consumerModuleKey and end in lower-kebab-case'); } }; @@ -279,54 +215,27 @@ export const defineOutboxWorker = < HandlerRequirements, >( descriptor: OutboxWorkerDescriptor, - handler: OutboxWorkerHandler< - PayloadSchema['Type'], - HandlerError, - HandlerRequirements - > -): OutboxWorkerRegistration< - PayloadSchema, - Consumer, - Producer, - HandlerError, - HandlerRequirements -> => { + handler: OutboxWorkerHandler, +): OutboxWorkerRegistration => { assertWorkerSubscription( descriptor, - 'Worker entrypoint must be an immutable tenant worker/background descriptor owned by consumerModuleKey' - ); - assertFiniteInteger( - descriptor.leaseDurationMs, - 1000, - 3_600_000, - 'leaseDurationMs' - ); - assertFiniteInteger( - descriptor.retryPolicy.maxAttempts, - 1, - 100, - 'retryPolicy.maxAttempts' - ); - assertFiniteInteger( - descriptor.retryPolicy.initialBackoffMs, - 0, - 86_400_000, - 'retryPolicy.initialBackoffMs' + 'Worker entrypoint must be an immutable tenant worker/background descriptor owned by consumerModuleKey', ); + assertFiniteInteger(descriptor.leaseDurationMs, 1000, 3_600_000, 'leaseDurationMs'); + assertFiniteInteger(descriptor.retryPolicy.maxAttempts, 1, 100, 'retryPolicy.maxAttempts'); + assertFiniteInteger(descriptor.retryPolicy.initialBackoffMs, 0, 86_400_000, 'retryPolicy.initialBackoffMs'); assertFiniteInteger( descriptor.retryPolicy.maxBackoffMs, descriptor.retryPolicy.initialBackoffMs, 86_400_000, - 'retryPolicy.maxBackoffMs' + 'retryPolicy.maxBackoffMs', ); if ( !Number.isFinite(descriptor.retryPolicy.multiplier) || descriptor.retryPolicy.multiplier < 1 || descriptor.retryPolicy.multiplier > 100 ) { - throw descriptorError( - 'retryPolicy.multiplier must be a finite number from 1 through 100' - ); + throw descriptorError('retryPolicy.multiplier must be a finite number from 1 through 100'); } if (!Predicate.isFunction(handler)) { throw descriptorError('handler must be an Effect function'); @@ -338,15 +247,13 @@ export const defineOutboxWorker = < entrypoint: descriptor.entrypoint, retryPolicy: Object.freeze({ ...descriptor.retryPolicy }), }), - handler + handler, ); return Object.freeze(registration); }; -export const validateOutboxWorkerRegistrations = < - Registration extends AnyOutboxWorkerRegistration, ->( - registrations: readonly Registration[] +export const validateOutboxWorkerRegistrations = ( + registrations: readonly Registration[], ): readonly Registration[] => { const workerKeys = new Set(); for (const registration of registrations) { @@ -355,9 +262,7 @@ export const validateOutboxWorkerRegistrations = < !registration[outboxWorkerRegistration] || !Object.isFrozen(registration) ) { - throw descriptorError( - 'every Outbox Worker must be created by defineOutboxWorker' - ); + throw descriptorError('every Outbox Worker must be created by defineOutboxWorker'); } const { workerKey } = registration.descriptor; if (workerKeys.has(workerKey)) { @@ -369,55 +274,36 @@ export const validateOutboxWorkerRegistrations = < }; export const validateOutboxWorkerSubscriptions = ( - subscriptions: readonly OutboxWorkerSubscription[] + subscriptions: readonly OutboxWorkerSubscription[], ): readonly OutboxWorkerSubscription[] => { const workerKeys = new Set(); for (const subscription of subscriptions) { - assertWorkerSubscription( - subscription, - 'installed Worker entrypoint is inconsistent with its subscription owner' - ); + assertWorkerSubscription(subscription, 'installed Worker entrypoint is inconsistent with its subscription owner'); if (workerKeys.has(subscription.workerKey)) { - throw descriptorError( - `duplicate Outbox Worker key ${subscription.workerKey}` - ); + throw descriptorError(`duplicate Outbox Worker key ${subscription.workerKey}`); } workerKeys.add(subscription.workerKey); } - return Object.freeze( - subscriptions.map((subscription) => Object.freeze({ ...subscription })) - ); + return Object.freeze(subscriptions.map((subscription) => Object.freeze({ ...subscription }))); }; /** Internal Core seam. Worker handlers are absent from public registrations. */ -export function getOutboxWorkerHandler< - Registration extends AnyOutboxWorkerRegistration, ->( - registration: Registration +export function getOutboxWorkerHandler( + registration: Registration, ): OutboxWorkerHandler< Registration['descriptor']['payloadSchema']['Type'], OutboxWorkerHandlerError, OutboxWorkerRequirements >; -export function getOutboxWorkerHandler( - registration: AnyOutboxWorkerRegistration -) { +export function getOutboxWorkerHandler(registration: AnyOutboxWorkerRegistration) { if (!isOutboxWorkerRegistrationValue(registration)) { - throw descriptorError( - 'every Outbox Worker must be created by defineOutboxWorker' - ); + throw descriptorError('every Outbox Worker must be created by defineOutboxWorker'); } return registration.resolveHandler(); } -export const retryBackoffMs = ( - policy: OutboxWorkerRetryPolicy, - completedAttempts: number -): number => +export const retryBackoffMs = (policy: OutboxWorkerRetryPolicy, completedAttempts: number): number => Math.min( policy.maxBackoffMs, - Math.round( - policy.initialBackoffMs * - policy.multiplier ** Math.max(0, completedAttempts - 1) - ) + Math.round(policy.initialBackoffMs * policy.multiplier ** Math.max(0, completedAttempts - 1)), ); diff --git a/app/packages/core-runtime/src/outbox/errors.ts b/app/packages/core-runtime/src/outbox/errors.ts index 989dfafb6..f615095ca 100644 --- a/app/packages/core-runtime/src/outbox/errors.ts +++ b/app/packages/core-runtime/src/outbox/errors.ts @@ -6,16 +6,14 @@ export { OutboxPayloadDecodeError } from './outbox-payload-decode-error.ts'; export { OutboxPollerConfigError } from './outbox-poller-config-error.ts'; export { OutboxWorkerDescriptorError } from './outbox-worker-descriptor-error.ts'; -export class OutboxPersistenceError extends Schema.TaggedError()( - 'OutboxPersistenceError', - { code: Schema.Literal('outbox_persistence_failed'), reason: Schema.String } -) {} +export class OutboxPersistenceError extends Schema.TaggedError()('OutboxPersistenceError', { + code: Schema.Literal('outbox_persistence_failed'), + reason: Schema.String, +}) {} const PERSISTENCE_CAUSE_PROPERTY = 'ontosOutboxPersistenceCause'; -export const outboxPersistenceError = ( - cause: FailureCause -): OutboxPersistenceError => { +export const outboxPersistenceError = (cause: FailureCause): OutboxPersistenceError => { const failure = new OutboxPersistenceError({ code: 'outbox_persistence_failed', reason: 'The Outbox Worker persistence operation failed', diff --git a/app/packages/core-runtime/src/outbox/health.ts b/app/packages/core-runtime/src/outbox/health.ts index 5b958a1e2..335d0109b 100644 --- a/app/packages/core-runtime/src/outbox/health.ts +++ b/app/packages/core-runtime/src/outbox/health.ts @@ -21,49 +21,46 @@ export interface CreateOutboxWorkerHealthOptions { readonly staleAfterMs: number; } -const makeOutboxWorkerHealth = Effect.fn('OutboxWorkerHealth.make')( - function* makeOutboxWorkerHealthEffect( - staleAfterMs: number, - now: Effect.Effect - ) { - const state = yield* Ref.make({ +const makeOutboxWorkerHealth = Effect.fn('OutboxWorkerHealth.make')(function* makeOutboxWorkerHealthEffect( + staleAfterMs: number, + now: Effect.Effect, +) { + const state = yield* Ref.make({ + lastSuccessfulCycleAt: Option.none(), + running: true, + }); + return { + cycleFailed: Ref.update(state, (current) => ({ + ...current, lastSuccessfulCycleAt: Option.none(), - running: true, - }); - return { - cycleFailed: Ref.update(state, (current) => ({ - ...current, - lastSuccessfulCycleAt: Option.none(), - })), - cycleSucceeded: now.pipe( - Effect.flatMap((lastSuccessfulCycleAt) => - Ref.update(state, (current) => ({ - ...current, - lastSuccessfulCycleAt: Option.some(lastSuccessfulCycleAt), - })) - ) + })), + cycleSucceeded: now.pipe( + Effect.flatMap((lastSuccessfulCycleAt) => + Ref.update(state, (current) => ({ + ...current, + lastSuccessfulCycleAt: Option.some(lastSuccessfulCycleAt), + })), ), - isReady: Effect.all([Ref.get(state), now], { concurrency: 1 }).pipe( - Effect.map( - ([current, currentTime]) => - current.running && - Option.isSome(current.lastSuccessfulCycleAt) && - currentTime - current.lastSuccessfulCycleAt.value <= staleAfterMs - ) + ), + isReady: Effect.all([Ref.get(state), now], { concurrency: 1 }).pipe( + Effect.map( + ([current, currentTime]) => + current.running && + Option.isSome(current.lastSuccessfulCycleAt) && + currentTime - current.lastSuccessfulCycleAt.value <= staleAfterMs, ), - shuttingDown: Ref.set(state, { - lastSuccessfulCycleAt: Option.none(), - running: false, - }), - }; - } -); + ), + shuttingDown: Ref.set(state, { + lastSuccessfulCycleAt: Option.none(), + running: false, + }), + }; +}); export const createOutboxWorkerHealth = ({ now = Clock.currentTimeMillis, staleAfterMs, -}: CreateOutboxWorkerHealthOptions): Effect.Effect => - makeOutboxWorkerHealth(staleAfterMs, now); +}: CreateOutboxWorkerHealthOptions): Effect.Effect => makeOutboxWorkerHealth(staleAfterMs, now); export interface OutboxWorkerHealthServer { readonly hostname: string; @@ -71,43 +68,32 @@ export interface OutboxWorkerHealthServer { } // Node supplies only the server constructor; the Effect adapter owns all socket I/O and cleanup. -const createNodeHealthServer = () => - process.getBuiltinModule('http').createServer(); +const createNodeHealthServer = () => process.getBuiltinModule('http').createServer(); export const serveOutboxWorkerHealth: ( health: OutboxWorkerHealth, - options: { readonly port: number } -) => Effect.Effect = - Effect.fn('OutboxWorkerHealth.serve')( - function* serveOutboxWorkerHealthEffect(health, options) { - const server = yield* NodeHttpServer.make(createNodeHealthServer, { - host: '0.0.0.0', - port: options.port, - }); - const healthApplication = HttpServerRequest.HttpServerRequest.use( - (request) => { - if (request.url !== '/ready') { - return Effect.succeed(HttpServerResponse.empty({ status: 404 })); - } - return health.isReady.pipe( - Effect.map((ready) => - HttpServerResponse.jsonUnsafe( - { ready }, - { status: ready ? 200 : 503 } - ) - ) - ); - } + options: { readonly port: number }, +) => Effect.Effect = Effect.fn('OutboxWorkerHealth.serve')( + function* serveOutboxWorkerHealthEffect(health, options) { + const server = yield* NodeHttpServer.make(createNodeHealthServer, { + host: '0.0.0.0', + port: options.port, + }); + const healthApplication = HttpServerRequest.HttpServerRequest.use((request) => { + if (request.url !== '/ready') { + return Effect.succeed(HttpServerResponse.empty({ status: 404 })); + } + return health.isReady.pipe( + Effect.map((ready) => HttpServerResponse.jsonUnsafe({ ready }, { status: ready ? 200 : 503 })), ); - yield* server.serve(healthApplication); + }); + yield* server.serve(healthApplication); - const address = yield* Match.value(server.address).pipe( - Match.tag('TcpAddress', (tcpAddress) => Effect.succeed(tcpAddress)), - Match.orElse(() => - Effect.die('Outbox health server did not bind to TCP') - ) - ); - yield* Effect.addFinalizer(() => health.shuttingDown); - return { hostname: address.hostname, port: address.port }; - } - ); + const address = yield* Match.value(server.address).pipe( + Match.tag('TcpAddress', (tcpAddress) => Effect.succeed(tcpAddress)), + Match.orElse(() => Effect.die('Outbox health server did not bind to TCP')), + ); + yield* Effect.addFinalizer(() => health.shuttingDown); + return { hostname: address.hostname, port: address.port }; + }, +); diff --git a/app/packages/core-runtime/src/outbox/outbox-claim-lost-error.ts b/app/packages/core-runtime/src/outbox/outbox-claim-lost-error.ts index e0aa33256..afd5dc9eb 100644 --- a/app/packages/core-runtime/src/outbox/outbox-claim-lost-error.ts +++ b/app/packages/core-runtime/src/outbox/outbox-claim-lost-error.ts @@ -1,6 +1,6 @@ import { Schema } from 'effect'; -export class OutboxClaimLostError extends Schema.TaggedError()( - 'OutboxClaimLostError', - { code: Schema.Literal('outbox_claim_lost'), reason: Schema.String } -) {} +export class OutboxClaimLostError extends Schema.TaggedError()('OutboxClaimLostError', { + code: Schema.Literal('outbox_claim_lost'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/outbox/outbox-handler-execution-error.ts b/app/packages/core-runtime/src/outbox/outbox-handler-execution-error.ts index 4c09eacc9..1c420309c 100644 --- a/app/packages/core-runtime/src/outbox/outbox-handler-execution-error.ts +++ b/app/packages/core-runtime/src/outbox/outbox-handler-execution-error.ts @@ -5,5 +5,5 @@ export class OutboxHandlerExecutionError extends Schema.TaggedError()( 'OutboxPayloadDecodeError', - { code: Schema.Literal('outbox_payload_invalid'), reason: Schema.String } + { code: Schema.Literal('outbox_payload_invalid'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/outbox/outbox-poller-config-error.ts b/app/packages/core-runtime/src/outbox/outbox-poller-config-error.ts index 7ceab02da..5bfffe0f1 100644 --- a/app/packages/core-runtime/src/outbox/outbox-poller-config-error.ts +++ b/app/packages/core-runtime/src/outbox/outbox-poller-config-error.ts @@ -1,9 +1,6 @@ import { Schema } from 'effect'; -export class OutboxPollerConfigError extends Schema.TaggedError()( - 'OutboxPollerConfigError', - { - code: Schema.Literal('outbox_poller_config_invalid'), - reason: Schema.String, - } -) {} +export class OutboxPollerConfigError extends Schema.TaggedError()('OutboxPollerConfigError', { + code: Schema.Literal('outbox_poller_config_invalid'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/outbox/outbox-worker-descriptor-error.ts b/app/packages/core-runtime/src/outbox/outbox-worker-descriptor-error.ts index 793c25de3..22d7df701 100644 --- a/app/packages/core-runtime/src/outbox/outbox-worker-descriptor-error.ts +++ b/app/packages/core-runtime/src/outbox/outbox-worker-descriptor-error.ts @@ -5,5 +5,5 @@ export class OutboxWorkerDescriptorError extends Schema.TaggedError { readonly config: OutboxPollingConfig; readonly health?: Pick; @@ -53,133 +36,78 @@ export interface RunOutboxPollingLoopInput< } export type OutboxCycleRunner< - Registration extends AnyOutboxWorkerRegistration = - AnyOutboxWorkerRegistration, + Registration extends AnyOutboxWorkerRegistration = AnyOutboxWorkerRegistration, RunnerRequirements = OutboxRuntime, > = ( - input: RunOutboxCycleInput -) => Effect.Effect< - OutboxCycleResult, - OutboxCycleError, - RunnerRequirements | OutboxWorkerRequirements ->; + input: RunOutboxCycleInput, +) => Effect.Effect>; const configError = (reason: string): OutboxPollerConfigError => new OutboxPollerConfigError({ code: 'outbox_poller_config_invalid', reason }); const EmptyConfigValue = Schema.Trim.pipe(Schema.decodeTo(Schema.Literal(''))); const ClaimOwnerOverride = Schema.Trim.check(Schema.isMaxLength(200)); -const ClaimOwner = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(200) -); - -const boundedIntegerConfig = ( - key: string, - fallback: number, - minimum: number, - maximum: number -): Config.Config => +const ClaimOwner = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200)); + +const boundedIntegerConfig = (key: string, fallback: number, minimum: number, maximum: number): Config.Config => Config.schema( Schema.Union([ EmptyConfigValue, Schema.Trim.check(Schema.isPattern(/^\d+$/u)).pipe( Schema.decodeTo(Schema.FiniteFromString), Schema.check(Schema.isInt()), - Schema.check(Schema.isBetween({ maximum, minimum })) + Schema.check(Schema.isBetween({ maximum, minimum })), ), ]), - key + key, ).pipe( Config.withDefault(fallback), - Config.map((value) => (value === '' ? fallback : value)) + Config.map((value) => (value === '' ? fallback : value)), ); const pollingConfig = (defaultClaimOwner: string) => Config.all({ - claimOwner: Config.schema( - ClaimOwnerOverride, - 'OUTBOX_WORKER_CLAIM_OWNER' - ).pipe( + claimOwner: Config.schema(ClaimOwnerOverride, 'OUTBOX_WORKER_CLAIM_OWNER').pipe( Config.withDefault(defaultClaimOwner), - Config.map((value) => (value === '' ? defaultClaimOwner : value)) - ), - maxDeliveries: boundedIntegerConfig( - 'OUTBOX_WORKER_MAX_DELIVERIES', - DEFAULT_MAX_DELIVERIES, - 1, - 1000 - ), - pollIntervalMs: boundedIntegerConfig( - 'OUTBOX_WORKER_POLL_INTERVAL_MS', - DEFAULT_POLL_INTERVAL_MS, - 10, - 3_600_000 + Config.map((value) => (value === '' ? defaultClaimOwner : value)), ), + maxDeliveries: boundedIntegerConfig('OUTBOX_WORKER_MAX_DELIVERIES', DEFAULT_MAX_DELIVERIES, 1, 1000), + pollIntervalMs: boundedIntegerConfig('OUTBOX_WORKER_POLL_INTERVAL_MS', DEFAULT_POLL_INTERVAL_MS, 10, 3_600_000), }); -const pollingConfigFailure = ({ message }: { readonly message: string }) => - configError(message); +const pollingConfigFailure = ({ message }: { readonly message: string }) => configError(message); export const parseOutboxPollingConfig = ({ defaultClaimOwner, environment, -}: ParseOutboxPollingConfigInput): Effect.Effect< - OutboxPollingConfig, - OutboxPollerConfigError -> => { +}: ParseOutboxPollingConfigInput): Effect.Effect => { const config = pollingConfig(defaultClaimOwner); - const decoded = - environment === undefined - ? config - : config.parse(ConfigProvider.fromUnknown(environment)); + const decoded = environment === undefined ? config : config.parse(ConfigProvider.fromUnknown(environment)); return decoded.pipe( Effect.flatMap((value) => Schema.decodeEffect(ClaimOwner)(value.claimOwner).pipe( - Effect.map((claimOwner) => Object.freeze({ ...value, claimOwner })) - ) + Effect.map((claimOwner) => Object.freeze({ ...value, claimOwner })), + ), ), - Effect.mapError(pollingConfigFailure) + Effect.mapError(pollingConfigFailure), ); }; const hasActivity = (result: OutboxCycleResult): boolean => - result.messagesMatched > 0 || - result.deliveriesCreated > 0 || - result.claimed > 0; - -export function runOutboxPollingLoop< - Registration extends AnyOutboxWorkerRegistration, ->( - input: RunOutboxPollingLoopInput -): Effect.Effect< - void, - never, - OutboxRuntime | OutboxWorkerRequirements ->; -export function runOutboxPollingLoop< - Registration extends AnyOutboxWorkerRegistration, - RunnerRequirements, ->( + result.messagesMatched > 0 || result.deliveriesCreated > 0 || result.claimed > 0; + +export function runOutboxPollingLoop( input: RunOutboxPollingLoopInput, - runCycle: OutboxCycleRunner -): Effect.Effect< - void, - never, - RunnerRequirements | OutboxWorkerRequirements ->; -export function runOutboxPollingLoop< - Registration extends AnyOutboxWorkerRegistration, - RunnerRequirements, ->( +): Effect.Effect>; +export function runOutboxPollingLoop( input: RunOutboxPollingLoopInput, - runCycle?: OutboxCycleRunner -): Effect.Effect< - void, - never, - OutboxRuntime | RunnerRequirements | OutboxWorkerRequirements -> { + runCycle: OutboxCycleRunner, +): Effect.Effect>; +export function runOutboxPollingLoop( + input: RunOutboxPollingLoopInput, + runCycle?: OutboxCycleRunner, +): Effect.Effect> { const cycleInput = { claimOwner: input.config.claimOwner, maxDeliveries: input.config.maxDeliveries, @@ -190,48 +118,36 @@ export function runOutboxPollingLoop< OutboxCycleResult, OutboxCycleError, OutboxRuntime | RunnerRequirements | OutboxWorkerRequirements - > = - runCycle === undefined ? runOutboxCycle(cycleInput) : runCycle(cycleInput); + > = runCycle === undefined ? runOutboxCycle(cycleInput) : runCycle(cycleInput); const tick = cycle.pipe( Effect.tap(() => input.health?.cycleSucceeded ?? Effect.void), Effect.tap((result) => hasActivity(result) - ? Effect.annotateLogs( - Effect.logInfo('Outbox polling cycle completed'), - { - claimed: result.claimed, - dead: result.dead, - deliveriesCreated: result.deliveriesCreated, - failed: result.failed, - messagesMatched: result.messagesMatched, - retried: result.retried, - succeeded: result.succeeded, - } - ) - : Effect.void + ? Effect.annotateLogs(Effect.logInfo('Outbox polling cycle completed'), { + claimed: result.claimed, + dead: result.dead, + deliveriesCreated: result.deliveriesCreated, + failed: result.failed, + messagesMatched: result.messagesMatched, + retried: result.retried, + succeeded: result.succeeded, + }) + : Effect.void, ), Effect.matchEffect({ onFailure: (error) => Effect.all( [ input.health?.cycleFailed ?? Effect.void, - Effect.annotateLogs( - Effect.logError('Outbox polling cycle failed'), - { - errorTag: error._tag, - } - ), + Effect.annotateLogs(Effect.logError('Outbox polling cycle failed'), { + errorTag: error._tag, + }), ], - { concurrency: 1 } + { concurrency: 1 }, ), onSuccess: () => Effect.void, - }) + }), ); - return tick.pipe( - Effect.repeat( - Schedule.spaced(Duration.millis(input.config.pollIntervalMs)) - ), - Effect.asVoid - ); + return tick.pipe(Effect.repeat(Schedule.spaced(Duration.millis(input.config.pollIntervalMs))), Effect.asVoid); } diff --git a/app/packages/core-runtime/src/outbox/process.ts b/app/packages/core-runtime/src/outbox/process.ts index 63153c24e..e974c749d 100644 --- a/app/packages/core-runtime/src/outbox/process.ts +++ b/app/packages/core-runtime/src/outbox/process.ts @@ -13,15 +13,8 @@ import { } from 'effect'; import type { Layer } from 'effect'; -import type { - AnyOutboxWorkerRegistration, - OutboxWorkerRequirements, - OutboxWorkerSubscription, -} from './definition.ts'; -import type { - createOutboxWorkerHealth, - serveOutboxWorkerHealth, -} from './health.ts'; +import type { AnyOutboxWorkerRegistration, OutboxWorkerRequirements, OutboxWorkerSubscription } from './definition.ts'; +import type { createOutboxWorkerHealth, serveOutboxWorkerHealth } from './health.ts'; import { parseOutboxPollingConfig, runOutboxPollingLoop } from './poller.ts'; import type { RunOutboxPollingLoopInput } from './poller.ts'; import type { OutboxRuntime } from './runtime.ts'; @@ -30,8 +23,7 @@ const ShutdownSignalSchema = Schema.Literals(['SIGINT', 'SIGTERM']); export type ShutdownSignal = typeof ShutdownSignalSchema.Type; export interface RunOutboxWorkerProcessInput< - Registration extends AnyOutboxWorkerRegistration = - AnyOutboxWorkerRegistration, + Registration extends AnyOutboxWorkerRegistration = AnyOutboxWorkerRegistration, > { readonly claimOwnerPrefix: string; readonly health?: boolean; @@ -43,15 +35,11 @@ export interface StartOutboxWorkerProcessInput< Registration extends AnyOutboxWorkerRegistration, LayerError, > extends RunOutboxWorkerProcessInput { - readonly layer: Layer.Layer< - OutboxRuntime | OutboxWorkerRequirements, - LayerError - >; + readonly layer: Layer.Layer, LayerError>; } const waitForShutdownSignal = Effect.callback((resume) => { - const onSignal = (signal: ShutdownSignal) => (): void => - resume(Effect.succeed(signal)); + const onSignal = (signal: ShutdownSignal) => (): void => resume(Effect.succeed(signal)); const onSigint = onSignal('SIGINT'); const onSigterm = onSignal('SIGTERM'); process.on('SIGINT', onSigint); @@ -63,9 +51,7 @@ const waitForShutdownSignal = Effect.callback((resume) => { }); }); -const healthPortConfig = Config.option( - Config.port('OUTBOX_WORKER_HEALTH_PORT') -); +const healthPortConfig = Config.option(Config.port('OUTBOX_WORKER_HEALTH_PORT')); export { OutboxRuntimeLive as OutboxWorkerInfrastructureLive } from './runtime.ts'; @@ -80,16 +66,11 @@ interface OutboxWorkerHealthApi { const loadOutboxWorkerHealthApi = Effect.suspend(() => { const healthApi: Promise = import('./health.ts'); - return Effect.promise(Fn.constant(healthApi)).pipe( - Effect.timeout('30 seconds'), - Effect.orDie - ); + return Effect.promise(Fn.constant(healthApi)).pipe(Effect.timeout('30 seconds'), Effect.orDie); }); -export const runOutboxWorkerProcess = < - Registration extends AnyOutboxWorkerRegistration, ->( - input: RunOutboxWorkerProcessInput +export const runOutboxWorkerProcess = ( + input: RunOutboxWorkerProcessInput, ) => Effect.scoped( Effect.gen(function* runOutboxWorkerProcessEffect() { @@ -97,8 +78,7 @@ export const runOutboxWorkerProcess = < const config = yield* parseOutboxPollingConfig({ defaultClaimOwner: `${input.claimOwnerPrefix}:${process.pid}:${processNonce}`, }); - const healthApi = - input.health === true ? yield* loadOutboxWorkerHealthApi : undefined; + const healthApi = input.health === true ? yield* loadOutboxWorkerHealthApi : undefined; const health = healthApi === undefined ? undefined @@ -113,15 +93,12 @@ export const runOutboxWorkerProcess = < }); } } - yield* Effect.annotateLogs( - Effect.logInfo('Outbox Worker process started'), - { - claimOwner: config.claimOwner, - maxDeliveries: config.maxDeliveries, - pollIntervalMs: config.pollIntervalMs, - registrations: input.registrations.length, - } - ); + yield* Effect.annotateLogs(Effect.logInfo('Outbox Worker process started'), { + claimOwner: config.claimOwner, + maxDeliveries: config.maxDeliveries, + pollIntervalMs: config.pollIntervalMs, + registrations: input.registrations.length, + }); let pollingInput: RunOutboxPollingLoopInput = { config, @@ -132,23 +109,14 @@ export const runOutboxWorkerProcess = < pollingInput = { ...pollingInput, health }; } const signal = yield* waitForShutdownSignal.pipe( - Effect.raceFirst( - runOutboxPollingLoop(pollingInput).pipe( - Effect.as('SIGTERM') - ) - ) - ); - yield* Effect.logInfo( - `Outbox Worker process received ${signal}; shutting down` + Effect.raceFirst(runOutboxPollingLoop(pollingInput).pipe(Effect.as('SIGTERM'))), ); - }) + yield* Effect.logInfo(`Outbox Worker process received ${signal}; shutting down`); + }), ); -export const startOutboxWorkerProcess = < - Registration extends AnyOutboxWorkerRegistration, - LayerError, ->( - input: StartOutboxWorkerProcessInput +export const startOutboxWorkerProcess = ( + input: StartOutboxWorkerProcessInput, ): void => { let processInput: RunOutboxWorkerProcessInput = { claimOwnerPrefix: input.claimOwnerPrefix, @@ -164,12 +132,12 @@ export const startOutboxWorkerProcess = < Effect.withLogger(Logger.defaultLogger), Effect.withTracer(processTracer), Effect.provideService(References.MinimumLogLevel, 'Info'), - Effect.ensuring(runtime.disposeEffect) + Effect.ensuring(runtime.disposeEffect), ), { onExit: (exit) => { process.exitCode = Exit.isSuccess(exit) ? 0 : 1; }, - } + }, ); }; diff --git a/app/packages/core-runtime/src/outbox/repository.ts b/app/packages/core-runtime/src/outbox/repository.ts index 121da3f82..6ebbd92ea 100644 --- a/app/packages/core-runtime/src/outbox/repository.ts +++ b/app/packages/core-runtime/src/outbox/repository.ts @@ -18,32 +18,18 @@ import { } from '../db/schema.ts'; import type { CoreTransaction, CoreDatabaseExecutor } from '../db/types.ts'; import { tenantStatesAllowingAccess } from '../modules/module-state-gate.ts'; -import type { - AnyOutboxWorkerRegistration, - OutboxWorkerRetryPolicy, - OutboxWorkerSubscription, -} from './definition.ts'; +import type { AnyOutboxWorkerRegistration, OutboxWorkerRetryPolicy, OutboxWorkerSubscription } from './definition.ts'; import { retryBackoffMs } from './definition.ts'; import type { OutboxPersistenceError } from './errors.ts'; -import { - OutboxClaimLostError, - outboxPersistenceError, - sanitizeOutboxErrorMessage, -} from './errors.ts'; +import { OutboxClaimLostError, outboxPersistenceError, sanitizeOutboxErrorMessage } from './errors.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const BACKGROUND_ELIGIBLE_STATES = tenantStatesAllowingAccess('background'); interface OutboxMatchResult { readonly deliveriesCreated: number; @@ -72,82 +58,61 @@ export interface OutboxRepositoryService { readonly claimNext: ( registrations: readonly AnyOutboxWorkerRegistration[], claimOwner: string, - now: Date + now: Date, ) => Effect.Effect, OutboxPersistenceError>; readonly complete: ( claim: OutboxClaim, - now: Date + now: Date, ) => Effect.Effect; readonly fail: ( claim: OutboxClaim, safeErrorMessage: string, - now: Date - ) => Effect.Effect< - OutboxFailureStatus, - OutboxClaimLostError | OutboxPersistenceError - >; + now: Date, + ) => Effect.Effect; readonly matchUnmatched: ( subscriptions: readonly OutboxWorkerSubscription[], - now: Date + now: Date, ) => Effect.Effect; } -export class OutboxRepository extends Context.Service< - OutboxRepository, - OutboxRepositoryService ->()('@app/core-runtime/outbox/repository/OutboxRepository') {} +export class OutboxRepository extends Context.Service()( + '@app/core-runtime/outbox/repository/OutboxRepository', +) {} const claimLostOrPersistenceError = (error: Failure) => - Schema.is(OutboxClaimLostError)(error) - ? error - : outboxPersistenceError(error); -const OutboxRepositoryInvariantError = Schema.TaggedError()( - 'OutboxRepositoryInvariantError', - { reason: Schema.String } -); + Schema.is(OutboxClaimLostError)(error) ? error : outboxPersistenceError(error); +const OutboxRepositoryInvariantError = Schema.TaggedError()('OutboxRepositoryInvariantError', { + reason: Schema.String, +}); const claimLost = (): OutboxClaimLostError => new OutboxClaimLostError({ code: 'outbox_claim_lost', reason: 'The Outbox delivery claim is no longer owned by this runtime', }); -const streamKeyFor = (producerModuleKey: string, topic: string): string => - `${producerModuleKey}:${topic}`; +const streamKeyFor = (producerModuleKey: string, topic: string): string => `${producerModuleKey}:${topic}`; const addMilliseconds = (date: Date, milliseconds: number): Date => - DateTime.toDateUtc( - DateTime.addDuration(DateTime.makeUnsafe(date), milliseconds) - ); -const loadClaimCorrelationId = Effect.fnUntraced( - function* loadClaimCorrelationId( - transaction: CoreTransaction, - actionInvocationId: string | null - ) { - if (actionInvocationId === null) { - return null; - } - const [invocation] = yield* transaction - .select({ correlationId: actionInvocations.correlationId }) - .from(actionInvocations) - .where(eq(actionInvocations.actionInvocationId, actionInvocationId)); - return invocation?.correlationId; + DateTime.toDateUtc(DateTime.addDuration(DateTime.makeUnsafe(date), milliseconds)); +const loadClaimCorrelationId = Effect.fnUntraced(function* loadClaimCorrelationId( + transaction: CoreTransaction, + actionInvocationId: string | null, +) { + if (actionInvocationId === null) { + return null; } -); + const [invocation] = yield* transaction + .select({ correlationId: actionInvocations.correlationId }) + .from(actionInvocations) + .where(eq(actionInvocations.actionInvocationId, actionInvocationId)); + return invocation?.correlationId; +}); -export const makeOutboxRepository = ( - executor: CoreDatabaseExecutor -): OutboxRepositoryService => ({ +export const makeOutboxRepository = (executor: CoreDatabaseExecutor): OutboxRepositoryService => ({ claimNext: (registrations, claimOwner, now) => { if (registrations.length === 0) { return Effect.succeedNone; } - const byWorkerKey = new Map( - registrations.map((registration) => [ - registration.descriptor.workerKey, - registration, - ]) - ); + const byWorkerKey = new Map(registrations.map((registration) => [registration.descriptor.workerKey, registration])); return executor .transaction( - Effect.fn('claimNextEffect')(function* claimNextEffect( - transaction: CoreTransaction - ) { + Effect.fn('claimNextEffect')(function* claimNextEffect(transaction: CoreTransaction) { const candidates = yield* transaction .select({ actionInvocationId: domainEvents.actionInvocationId, @@ -165,47 +130,29 @@ export const makeOutboxRepository = ( workerKey: outboxDeliveries.workerKey, }) .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq( - outboxMessages.outboxMessageId, - outboxDeliveries.outboxMessageId - ) - ) - .innerJoin( - domainEvents, - eq(domainEvents.domainEventId, outboxMessages.domainEventId) - ) + .innerJoin(outboxMessages, eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId)) + .innerJoin(domainEvents, eq(domainEvents.domainEventId, outboxMessages.domainEventId)) .innerJoin( tenantModuleStates, and( eq(tenantModuleStates.tenantId, outboxMessages.tenantId), - eq( - tenantModuleStates.moduleKey, - outboxDeliveries.consumerModuleKey - ), - inArray(tenantModuleStates.state, BACKGROUND_ELIGIBLE_STATES) - ) + eq(tenantModuleStates.moduleKey, outboxDeliveries.consumerModuleKey), + inArray(tenantModuleStates.state, BACKGROUND_ELIGIBLE_STATES), + ), ) .where( and( inArray(outboxDeliveries.workerKey, [...byWorkerKey.keys()]), or( - and( - eq(outboxDeliveries.status, 'pending'), - lte(outboxDeliveries.availableAt, now) - ), - and( - eq(outboxDeliveries.status, 'processing'), - lte(outboxDeliveries.claimExpiresAt, now) - ) - ) - ) + and(eq(outboxDeliveries.status, 'pending'), lte(outboxDeliveries.availableAt, now)), + and(eq(outboxDeliveries.status, 'processing'), lte(outboxDeliveries.claimExpiresAt, now)), + ), + ), ) .orderBy( asc(outboxDeliveries.availableAt), asc(domainEvents.tenantSequenceNo), - asc(outboxDeliveries.outboxDeliveryId) + asc(outboxDeliveries.outboxDeliveryId), ) .limit(1) .for('update', { skipLocked: true }); @@ -224,17 +171,9 @@ export const makeOutboxRepository = ( errorMessage: 'Outbox Worker lease expired before completion', finishedAt: now, }) - .where( - and( - eq(outboxAttempts.outboxDeliveryId, candidate.deliveryId), - isNull(outboxAttempts.finishedAt) - ) - ); + .where(and(eq(outboxAttempts.outboxDeliveryId, candidate.deliveryId), isNull(outboxAttempts.finishedAt))); } - if ( - candidate.attemptsCount >= - registration.descriptor.retryPolicy.maxAttempts - ) { + if (candidate.attemptsCount >= registration.descriptor.retryPolicy.maxAttempts) { yield* transaction .update(outboxDeliveries) .set({ @@ -244,16 +183,11 @@ export const makeOutboxRepository = ( status: 'dead', updatedAt: now, }) - .where( - eq(outboxDeliveries.outboxDeliveryId, candidate.deliveryId) - ); + .where(eq(outboxDeliveries.outboxDeliveryId, candidate.deliveryId)); return Option.none(); } const claimId = `${claimOwner}:${randomUUID()}`; - const claimExpiresAt = addMilliseconds( - now, - registration.descriptor.leaseDurationMs - ); + const claimExpiresAt = addMilliseconds(now, registration.descriptor.leaseDurationMs); const [claimed] = yield* transaction .update(outboxDeliveries) .set({ @@ -280,10 +214,7 @@ export const makeOutboxRepository = ( reason: 'Attempt insert returned no row', }); } - const correlationId = yield* loadClaimCorrelationId( - transaction, - candidate.actionInvocationId - ); + const correlationId = yield* loadClaimCorrelationId(transaction, candidate.actionInvocationId); return Option.some( withOptionalProperty( { @@ -306,24 +237,20 @@ export const makeOutboxRepository = ( tenantSequenceNo: candidate.tenantSequenceNo, topic: candidate.topic, workerKey: candidate.workerKey, - } - ) satisfies OutboxClaim + }, + ) satisfies OutboxClaim, ); - }) + }), ) .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.mapError(outboxPersistenceError) + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.mapError(outboxPersistenceError), ); }, complete: (claim, now) => executor .transaction( - Effect.fn('completeEffect')(function* completeEffect( - transaction: CoreTransaction - ) { + Effect.fn('completeEffect')(function* completeEffect(transaction: CoreTransaction) { yield* transaction .select({ tenantId: tenants.tenantId }) .from(tenants) @@ -336,8 +263,8 @@ export const makeOutboxRepository = ( and( eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId), eq(outboxDeliveries.status, 'processing'), - eq(outboxDeliveries.claimedBy, claim.claimId) - ) + eq(outboxDeliveries.claimedBy, claim.claimId), + ), ) .for('update'); if (owned === undefined) { @@ -346,12 +273,7 @@ export const makeOutboxRepository = ( const finishedAttempts = yield* transaction .update(outboxAttempts) .set({ finishedAt: now }) - .where( - and( - eq(outboxAttempts.outboxAttemptId, claim.attemptId), - isNull(outboxAttempts.finishedAt) - ) - ) + .where(and(eq(outboxAttempts.outboxAttemptId, claim.attemptId), isNull(outboxAttempts.finishedAt))) .returning({ attemptId: outboxAttempts.outboxAttemptId }); if (finishedAttempts.length !== 1) { return yield* claimLost(); @@ -369,8 +291,8 @@ export const makeOutboxRepository = ( and( eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId), eq(outboxDeliveries.status, 'processing'), - eq(outboxDeliveries.claimedBy, claim.claimId) - ) + eq(outboxDeliveries.claimedBy, claim.claimId), + ), ) .returning({ deliveryId: outboxDeliveries.outboxDeliveryId }); if (completed.length !== 1) { @@ -386,8 +308,8 @@ export const makeOutboxRepository = ( and( eq(workerCheckpoints.tenantId, claim.tenantId), eq(workerCheckpoints.consumerName, claim.workerKey), - eq(workerCheckpoints.streamKey, streamKey) - ) + eq(workerCheckpoints.streamKey, streamKey), + ), ) .for('update'); const previous = checkpoint?.lastTenantSequenceNo ?? 0n; @@ -397,25 +319,16 @@ export const makeOutboxRepository = ( tenantSequenceNo: domainEvents.tenantSequenceNo, }) .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq( - outboxMessages.outboxMessageId, - outboxDeliveries.outboxMessageId - ) - ) - .innerJoin( - domainEvents, - eq(domainEvents.domainEventId, outboxMessages.domainEventId) - ) + .innerJoin(outboxMessages, eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId)) + .innerJoin(domainEvents, eq(domainEvents.domainEventId, outboxMessages.domainEventId)) .where( and( eq(outboxMessages.tenantId, claim.tenantId), eq(outboxDeliveries.workerKey, claim.workerKey), eq(outboxMessages.producerModuleKey, claim.producerModuleKey), eq(outboxMessages.topic, claim.topic), - gt(domainEvents.tenantSequenceNo, previous) - ) + gt(domainEvents.tenantSequenceNo, previous), + ), ) .orderBy(asc(domainEvents.tenantSequenceNo)); let nextCheckpoint = previous; @@ -442,28 +355,20 @@ export const makeOutboxRepository = ( lastTenantSequenceNo: nextCheckpoint, updatedAt: now, }, - target: [ - workerCheckpoints.tenantId, - workerCheckpoints.consumerName, - workerCheckpoints.streamKey, - ], + target: [workerCheckpoints.tenantId, workerCheckpoints.consumerName, workerCheckpoints.streamKey], }); } return yield* Effect.void; - }) + }), ) .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.mapError(claimLostOrPersistenceError) + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.mapError(claimLostOrPersistenceError), ), fail: (claim, safeErrorMessage, now) => executor .transaction( - Effect.fn('failEffect')(function* failEffect( - transaction: CoreTransaction - ) { + Effect.fn('failEffect')(function* failEffect(transaction: CoreTransaction) { const [owned] = yield* transaction .select({ deliveryId: outboxDeliveries.outboxDeliveryId }) .from(outboxDeliveries) @@ -471,8 +376,8 @@ export const makeOutboxRepository = ( and( eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId), eq(outboxDeliveries.status, 'processing'), - eq(outboxDeliveries.claimedBy, claim.claimId) - ) + eq(outboxDeliveries.claimedBy, claim.claimId), + ), ) .for('update'); if (owned === undefined) { @@ -484,27 +389,14 @@ export const makeOutboxRepository = ( errorMessage: sanitizeOutboxErrorMessage(safeErrorMessage), finishedAt: now, }) - .where( - and( - eq(outboxAttempts.outboxAttemptId, claim.attemptId), - isNull(outboxAttempts.finishedAt) - ) - ) + .where(and(eq(outboxAttempts.outboxAttemptId, claim.attemptId), isNull(outboxAttempts.finishedAt))) .returning({ attemptId: outboxAttempts.outboxAttemptId }); if (finishedAttempts.length !== 1) { return yield* claimLost(); } - const status: OutboxFailureStatus = - claim.attemptNumber >= claim.retryPolicy.maxAttempts - ? 'dead' - : 'pending'; + const status: OutboxFailureStatus = claim.attemptNumber >= claim.retryPolicy.maxAttempts ? 'dead' : 'pending'; const availableAt = - status === 'dead' - ? now - : addMilliseconds( - now, - retryBackoffMs(claim.retryPolicy, claim.attemptNumber) - ); + status === 'dead' ? now : addMilliseconds(now, retryBackoffMs(claim.retryPolicy, claim.attemptNumber)); const updated = yield* transaction .update(outboxDeliveries) .set({ @@ -519,28 +411,24 @@ export const makeOutboxRepository = ( and( eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId), eq(outboxDeliveries.status, 'processing'), - eq(outboxDeliveries.claimedBy, claim.claimId) - ) + eq(outboxDeliveries.claimedBy, claim.claimId), + ), ) .returning({ deliveryId: outboxDeliveries.outboxDeliveryId }); if (updated.length !== 1) { return yield* claimLost(); } return status; - }) + }), ) .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.mapError(claimLostOrPersistenceError) + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.mapError(claimLostOrPersistenceError), ), matchUnmatched: (subscriptions, now) => executor .transaction( - Effect.fn('matchUnmatchedEffect')(function* matchUnmatchedEffect( - transaction: CoreTransaction - ) { + Effect.fn('matchUnmatchedEffect')(function* matchUnmatchedEffect(transaction: CoreTransaction) { const messages = yield* transaction .select({ messageId: outboxMessages.outboxMessageId, @@ -549,66 +437,49 @@ export const makeOutboxRepository = ( }) .from(outboxMessages) .where(isNull(outboxMessages.matchedAt)) - .orderBy( - asc(outboxMessages.createdAt), - asc(outboxMessages.outboxMessageId) - ) + .orderBy(asc(outboxMessages.createdAt), asc(outboxMessages.outboxMessageId)) .limit(100) .for('update', { skipLocked: true }); - const matchMessage = Effect.fn('OutboxRepository.matchMessage')( - function* matchNextMessage( - messageIndex: number, - deliveriesCreated: number - ): Effect.fn.Return { - const message = messages[messageIndex]; - if (message === undefined) { - return deliveriesCreated; - } - const matches = subscriptions.filter( - (subscription) => - subscription.producerModuleKey === - message.producerModuleKey && - subscription.topic === message.topic - ); - let nextDeliveriesCreated = deliveriesCreated; - if (matches.length > 0) { - const inserted = yield* transaction - .insert(outboxDeliveries) - .values( - matches.map((subscription) => ({ - consumerModuleKey: subscription.consumerModuleKey, - outboxMessageId: message.messageId, - workerKey: subscription.workerKey, - })) - ) - .onConflictDoNothing() - .returning({ deliveryId: outboxDeliveries.outboxDeliveryId }); - nextDeliveriesCreated += inserted.length; - } - yield* transaction - .update(outboxMessages) - .set({ matchedAt: now }) - .where( - and( - eq(outboxMessages.outboxMessageId, message.messageId), - isNull(outboxMessages.matchedAt) - ) - ); - return yield* matchMessage( - messageIndex + 1, - nextDeliveriesCreated - ); + const matchMessage = Effect.fn('OutboxRepository.matchMessage')(function* matchNextMessage( + messageIndex: number, + deliveriesCreated: number, + ): Effect.fn.Return { + const message = messages[messageIndex]; + if (message === undefined) { + return deliveriesCreated; } - ); + const matches = subscriptions.filter( + (subscription) => + subscription.producerModuleKey === message.producerModuleKey && subscription.topic === message.topic, + ); + let nextDeliveriesCreated = deliveriesCreated; + if (matches.length > 0) { + const inserted = yield* transaction + .insert(outboxDeliveries) + .values( + matches.map((subscription) => ({ + consumerModuleKey: subscription.consumerModuleKey, + outboxMessageId: message.messageId, + workerKey: subscription.workerKey, + })), + ) + .onConflictDoNothing() + .returning({ deliveryId: outboxDeliveries.outboxDeliveryId }); + nextDeliveriesCreated += inserted.length; + } + yield* transaction + .update(outboxMessages) + .set({ matchedAt: now }) + .where(and(eq(outboxMessages.outboxMessageId, message.messageId), isNull(outboxMessages.matchedAt))); + return yield* matchMessage(messageIndex + 1, nextDeliveriesCreated); + }); const deliveriesCreated = yield* matchMessage(0, 0); return { deliveriesCreated, messagesMatched: messages.length }; - }) + }), ) .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.mapError(outboxPersistenceError) + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.mapError(outboxPersistenceError), ), }); export const OutboxRepositoryLive = Layer.effect( @@ -616,5 +487,5 @@ export const OutboxRepositoryLive = Layer.effect( Effect.gen(function* makeOutboxRepositoryService() { const database = yield* CoreDatabase; return makeOutboxRepository(database.executor); - }) + }), ); diff --git a/app/packages/core-runtime/src/outbox/runtime.ts b/app/packages/core-runtime/src/outbox/runtime.ts index 36886f12d..51d458b54 100644 --- a/app/packages/core-runtime/src/outbox/runtime.ts +++ b/app/packages/core-runtime/src/outbox/runtime.ts @@ -15,36 +15,20 @@ import { validateOutboxWorkerRegistrations, validateOutboxWorkerSubscriptions, } from './definition.ts'; -import { - OutboxHandlerExecutionError, - OutboxPayloadDecodeError, - OutboxWorkerDescriptorError, -} from './errors.ts'; +import { OutboxHandlerExecutionError, OutboxPayloadDecodeError, OutboxWorkerDescriptorError } from './errors.ts'; import type { OutboxClaimLostError, OutboxPersistenceError } from './errors.ts'; import { OutboxRepository } from './repository.ts'; -import type { - OutboxClaim, - OutboxRepositoryService as OutboxRepositoryPort, -} from './repository.ts'; - -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +import type { OutboxClaim, OutboxRepositoryService as OutboxRepositoryPort } from './repository.ts'; + +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; - -export interface RunOutboxCycleInput< - Registration extends AnyOutboxWorkerRegistration = - AnyOutboxWorkerRegistration, -> { + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); + +export interface RunOutboxCycleInput { readonly claimOwner: string; readonly maxDeliveries?: number; readonly now?: Date; @@ -72,25 +56,15 @@ export interface OutboxCycleResult { readonly succeeded: number; } -export type OutboxCycleError = - | OutboxClaimLostError - | OutboxPersistenceError - | OutboxWorkerDescriptorError; +export type OutboxCycleError = OutboxClaimLostError | OutboxPersistenceError | OutboxWorkerDescriptorError; export interface OutboxRuntimeService { readonly matchMessages: ( - input: MatchOutboxMessagesInput - ) => Effect.Effect< - OutboxMatchResult, - OutboxPersistenceError | OutboxWorkerDescriptorError - >; + input: MatchOutboxMessagesInput, + ) => Effect.Effect; readonly runCycle: ( - input: RunOutboxCycleInput - ) => Effect.Effect< - OutboxCycleResult, - OutboxCycleError, - OutboxWorkerRequirements - >; + input: RunOutboxCycleInput, + ) => Effect.Effect>; } const descriptorFailure = (reason: string): OutboxWorkerDescriptorError => @@ -99,44 +73,34 @@ const descriptorFailure = (reason: string): OutboxWorkerDescriptorError => reason, }); -const validateCycleInput = Effect.fn('OutboxRuntime.validateCycleInput')( - function* validateCycleInputEffect< - Registration extends AnyOutboxWorkerRegistration, - >(input: RunOutboxCycleInput) { - if (input.claimOwner.trim().length === 0 || input.claimOwner.length > 200) { - return yield* descriptorFailure( - 'claimOwner must be a non-empty stable runtime identity' - ); - } - const maxDeliveries = input.maxDeliveries ?? 100; - if ( - !Number.isSafeInteger(maxDeliveries) || - maxDeliveries < 1 || - maxDeliveries > 1000 - ) { - return yield* descriptorFailure( - 'maxDeliveries must be an integer from 1 through 1000' - ); - } - const now = input.now ?? (yield* DateTime.nowAsDate); - if (Number.isNaN(now.getTime())) { - return yield* descriptorFailure('now must be a valid timestamp'); - } - const registrations = yield* Effect.try({ - catch: (error) => - Schema.is(OutboxWorkerDescriptorError)(error) - ? error - : descriptorFailure('The Outbox Worker descriptor set is invalid'), - try: () => validateOutboxWorkerRegistrations(input.registrations), - }); - return { - claimOwner: input.claimOwner, - maxDeliveries, - now, - registrations, - }; +const validateCycleInput = Effect.fn('OutboxRuntime.validateCycleInput')(function* validateCycleInputEffect< + Registration extends AnyOutboxWorkerRegistration, +>(input: RunOutboxCycleInput) { + if (input.claimOwner.trim().length === 0 || input.claimOwner.length > 200) { + return yield* descriptorFailure('claimOwner must be a non-empty stable runtime identity'); } -); + const maxDeliveries = input.maxDeliveries ?? 100; + if (!Number.isSafeInteger(maxDeliveries) || maxDeliveries < 1 || maxDeliveries > 1000) { + return yield* descriptorFailure('maxDeliveries must be an integer from 1 through 1000'); + } + const now = input.now ?? (yield* DateTime.nowAsDate); + if (Number.isNaN(now.getTime())) { + return yield* descriptorFailure('now must be a valid timestamp'); + } + const registrations = yield* Effect.try({ + catch: (error) => + Schema.is(OutboxWorkerDescriptorError)(error) + ? error + : descriptorFailure('The Outbox Worker descriptor set is invalid'), + try: () => validateOutboxWorkerRegistrations(input.registrations), + }); + return { + claimOwner: input.claimOwner, + maxDeliveries, + now, + registrations, + }; +}); const claimAnnotations = (claim: OutboxClaim, outcome?: string) => withOptionalProperty( @@ -152,7 +116,7 @@ const claimAnnotations = (claim: OutboxClaim, outcome?: string) => { deliveryId: claim.deliveryId, messageId: claim.messageId, - } + }, ), outcome !== undefined, 'outcome', @@ -162,26 +126,24 @@ const claimAnnotations = (claim: OutboxClaim, outcome?: string) => tenantId: claim.tenantId, topic: claim.topic, workerKey: claim.workerKey, - } + }, ); const logUnexpectedPersistence = (claim?: OutboxClaim) => Effect.annotateLogs( Effect.logError('Unexpected Outbox persistence failure'), - claim === undefined - ? { outcome: 'persistence_failure' } - : claimAnnotations(claim, 'persistence_failure') + claim === undefined ? { outcome: 'persistence_failure' } : claimAnnotations(claim, 'persistence_failure'), ); const withOutcomeSpan = ( effect: Effect.Effect, claim: OutboxClaim, - outcome: string + outcome: string, ): Effect.Effect => effect.pipe( Effect.withSpan('OutboxWorker.finalize', { attributes: claimAnnotations(claim, outcome), - }) + }), ); const handlerContext = (claim: OutboxClaim): OutboxWorkerHandlerContext => @@ -203,56 +165,44 @@ const handlerContext = (claim: OutboxClaim): OutboxWorkerHandlerContext => tenantSequenceNo: claim.tenantSequenceNo, topic: claim.topic, workerKey: claim.workerKey, - } - ) + }, + ), ); const subscriptionMatchesRegistration = ( subscription: OutboxWorkerSubscription | undefined, - registration: AnyOutboxWorkerRegistration + registration: AnyOutboxWorkerRegistration, ): boolean => subscription !== undefined && - subscription.consumerModuleKey === - registration.descriptor.consumerModuleKey && - subscription.entrypoint.entrypointKey === - registration.descriptor.entrypoint.entrypointKey && - subscription.entrypoint.moduleKey === - registration.descriptor.entrypoint.moduleKey && + subscription.consumerModuleKey === registration.descriptor.consumerModuleKey && + subscription.entrypoint.entrypointKey === registration.descriptor.entrypoint.entrypointKey && + subscription.entrypoint.moduleKey === registration.descriptor.entrypoint.moduleKey && subscription.entrypoint.role === registration.descriptor.entrypoint.role && - subscription.entrypoint.access === - registration.descriptor.entrypoint.access && + subscription.entrypoint.access === registration.descriptor.entrypoint.access && subscription.entrypoint.scope === registration.descriptor.entrypoint.scope && - subscription.producerModuleKey === - registration.descriptor.producerModuleKey && + subscription.producerModuleKey === registration.descriptor.producerModuleKey && subscription.topic === registration.descriptor.topic; -const validateDeployedRegistrationSnapshot = Effect.fn( - 'OutboxRuntime.validateDeployedRegistrationSnapshot' -)(function* validateDeployedRegistrationSnapshotEffect( - registrations: readonly AnyOutboxWorkerRegistration[], - subscriptions: readonly OutboxWorkerSubscription[] -) { - const subscriptionsByKey = new Map( - subscriptions.map((subscription) => [subscription.workerKey, subscription]) - ); - for (const registration of registrations) { - if ( - !subscriptionMatchesRegistration( - subscriptionsByKey.get(registration.descriptor.workerKey), - registration - ) - ) { +const validateDeployedRegistrationSnapshot = Effect.fn('OutboxRuntime.validateDeployedRegistrationSnapshot')( + function* validateDeployedRegistrationSnapshotEffect( + registrations: readonly AnyOutboxWorkerRegistration[], + subscriptions: readonly OutboxWorkerSubscription[], + ) { + const subscriptionsByKey = new Map(subscriptions.map((subscription) => [subscription.workerKey, subscription])); + for (const registration of registrations) { + if (!subscriptionMatchesRegistration(subscriptionsByKey.get(registration.descriptor.workerKey), registration)) { + return yield* descriptorFailure( + `worker ${registration.descriptor.workerKey} is absent from the installed subscription catalog`, + ); + } + } + if (subscriptions.length !== registrations.length) { return yield* descriptorFailure( - `worker ${registration.descriptor.workerKey} is absent from the installed subscription catalog` + 'the owner-local worker registration set contradicts its deployed descriptor snapshot', ); } - } - if (subscriptions.length !== registrations.length) { - return yield* descriptorFailure( - 'the owner-local worker registration set contradicts its deployed descriptor snapshot' - ); - } -}); + }, +); interface OutboxCycleProgress { readonly claimed: number; @@ -272,251 +222,201 @@ const initialCycleProgress = (): OutboxCycleProgress => ({ succeeded: 0, }); -interface OutboxCycleExecution< - Registration extends AnyOutboxWorkerRegistration, -> { +interface OutboxCycleExecution { readonly claimOwner: string; readonly now: Date; readonly registrations: readonly Registration[]; readonly registrationsByKey: ReadonlyMap; } -const matchMessagesWithRepository = Effect.fn( - 'makeOutboxRuntime.matchMessages' -)(function* matchMessagesWithRepositoryEffect( +const matchMessagesWithRepository = Effect.fn('makeOutboxRuntime.matchMessages')( + function* matchMessagesWithRepositoryEffect(repository: OutboxRepositoryPort, input: MatchOutboxMessagesInput) { + const subscriptions = yield* Effect.try({ + catch: (error) => { + void error; + return descriptorFailure('The installed subscription snapshot is invalid'); + }, + try: () => validateOutboxWorkerSubscriptions(input.subscriptions), + }); + const now = input.now ?? (yield* DateTime.nowAsDate); + if (Number.isNaN(now.getTime())) { + return yield* descriptorFailure('now must be a valid timestamp'); + } + return yield* repository.matchUnmatched(subscriptions, now).pipe(Effect.tapError(() => logUnexpectedPersistence())); + }, +); + +const failOutboxDelivery = Effect.fn('OutboxRuntime.failDelivery')(function* failOutboxDeliveryEffect( repository: OutboxRepositoryPort, - input: MatchOutboxMessagesInput + claim: OutboxClaim, + now: Date, + state: OutboxCycleProgress, + reason: string, + outcome: string, ) { - const subscriptions = yield* Effect.try({ - catch: (error) => { - void error; - return descriptorFailure( - 'The installed subscription snapshot is invalid' - ); - }, - try: () => validateOutboxWorkerSubscriptions(input.subscriptions), - }); - const now = input.now ?? (yield* DateTime.nowAsDate); - if (Number.isNaN(now.getTime())) { - return yield* descriptorFailure('now must be a valid timestamp'); - } - return yield* repository - .matchUnmatched(subscriptions, now) - .pipe(Effect.tapError(() => logUnexpectedPersistence())); + const status = yield* repository.fail(claim, reason, now).pipe( + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), + (effect) => withOutcomeSpan(effect, claim, outcome), + ); + return { + ...state, + dead: state.dead + (status === 'dead' ? 1 : 0), + failed: state.failed + 1, + retried: state.retried + (status === 'pending' ? 1 : 0), + }; }); -const failOutboxDelivery = Effect.fn('OutboxRuntime.failDelivery')( - function* failOutboxDeliveryEffect( +const processNextOutboxDelivery = Effect.fn('makeOutboxRuntime.processNextDelivery')( + function* processNextOutboxDeliveryEffect( repository: OutboxRepositoryPort, - claim: OutboxClaim, - now: Date, + execution: OutboxCycleExecution, state: OutboxCycleProgress, - reason: string, - outcome: string ) { - const status = yield* repository.fail(claim, reason, now).pipe( - Effect.tapErrorTag('OutboxPersistenceError', () => - logUnexpectedPersistence(claim) + const claimOption = yield* repository + .claimNext(execution.registrations, execution.claimOwner, execution.now) + .pipe(Effect.tapError(() => logUnexpectedPersistence())); + if (Option.isNone(claimOption)) { + return { ...state, stopped: true }; + } + const claim = claimOption.value; + const claimedState = { ...state, claimed: state.claimed + 1 }; + const registration = execution.registrationsByKey.get(claim.workerKey); + if (registration === undefined) { + return yield* descriptorFailure(`claimed delivery references unknown worker ${claim.workerKey}`); + } + const decoded = yield* Effect.exit( + Schema.decodeUnknownEffect(registration.descriptor.payloadSchema)(claim.payloadJson), + ); + if (Exit.isFailure(decoded)) { + const decodeError = new OutboxPayloadDecodeError({ + code: 'outbox_payload_invalid', + reason: 'The Outbox Message payload does not match its published schema', + }); + return yield* failOutboxDelivery( + repository, + claim, + execution.now, + claimedState, + decodeError.reason, + 'payload_decode_failure', + ); + } + + const handler = getOutboxWorkerHandler(registration); + const handlerExit = yield* Effect.exit( + Effect.suspend(() => handler(decoded.value, handlerContext(claim))).pipe( + Effect.match({ + onFailure: (error) => { + void error; + return 'declared_failure' as const; + }, + onSuccess: () => 'success' as const, + }), + Effect.withSpan('OutboxWorker.handle', { + attributes: claimAnnotations(claim), + }), ), - (effect) => withOutcomeSpan(effect, claim, outcome) ); - return { - ...state, - dead: state.dead + (status === 'dead' ? 1 : 0), - failed: state.failed + 1, - retried: state.retried + (status === 'pending' ? 1 : 0), - }; - } + if (Exit.isFailure(handlerExit)) { + yield* Effect.annotateLogs( + Effect.logError('Unexpected Outbox Worker handler defect'), + claimAnnotations(claim, 'handler_defect'), + ); + } + if (Exit.isFailure(handlerExit) || handlerExit.value === 'declared_failure') { + const executionError = new OutboxHandlerExecutionError({ + code: 'outbox_handler_execution_failed', + reason: Exit.isFailure(handlerExit) + ? 'The Outbox Worker handler failed unexpectedly' + : 'The Outbox Worker handler returned a declared failure', + }); + return yield* failOutboxDelivery( + repository, + claim, + execution.now, + claimedState, + executionError.reason, + 'handler_failure', + ); + } + + yield* repository.complete(claim, execution.now).pipe( + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), + (effect) => withOutcomeSpan(effect, claim, 'success'), + ); + return { ...claimedState, succeeded: claimedState.succeeded + 1 }; + }, ); -const processNextOutboxDelivery = Effect.fn( - 'makeOutboxRuntime.processNextDelivery' -)(function* processNextOutboxDeliveryEffect< +const runCycleWithRepository = Effect.fn('makeOutboxRuntime.runCycle')(function* runCycleWithRepositoryEffect< Registration extends AnyOutboxWorkerRegistration, ->( - repository: OutboxRepositoryPort, - execution: OutboxCycleExecution, - state: OutboxCycleProgress -) { - const claimOption = yield* repository - .claimNext(execution.registrations, execution.claimOwner, execution.now) - .pipe(Effect.tapError(() => logUnexpectedPersistence())); - if (Option.isNone(claimOption)) { - return { ...state, stopped: true }; - } - const claim = claimOption.value; - const claimedState = { ...state, claimed: state.claimed + 1 }; - const registration = execution.registrationsByKey.get(claim.workerKey); - if (registration === undefined) { - return yield* descriptorFailure( - `claimed delivery references unknown worker ${claim.workerKey}` - ); - } - const decoded = yield* Effect.exit( - Schema.decodeUnknownEffect(registration.descriptor.payloadSchema)( - claim.payloadJson - ) +>(repository: OutboxRepositoryPort, input: RunOutboxCycleInput) { + const validated = yield* validateCycleInput(input); + const deployedSubscriptions = yield* Effect.try({ + catch: (error) => { + void error; + return descriptorFailure('The deployed subscription snapshot is invalid'); + }, + try: () => validateOutboxWorkerSubscriptions(input.subscriptions), + }); + yield* validateDeployedRegistrationSnapshot(validated.registrations, deployedSubscriptions); + const registrationsByKey = new Map( + validated.registrations.map((registration) => [registration.descriptor.workerKey, registration] as const), ); - if (Exit.isFailure(decoded)) { - const decodeError = new OutboxPayloadDecodeError({ - code: 'outbox_payload_invalid', - reason: 'The Outbox Message payload does not match its published schema', - }); - return yield* failOutboxDelivery( - repository, - claim, - execution.now, - claimedState, - decodeError.reason, - 'payload_decode_failure' - ); - } - - const handler = getOutboxWorkerHandler(registration); - const handlerExit = yield* Effect.exit( - Effect.suspend(() => handler(decoded.value, handlerContext(claim))).pipe( - Effect.match({ - onFailure: (error) => { - void error; - return 'declared_failure' as const; - }, - onSuccess: () => 'success' as const, - }), - Effect.withSpan('OutboxWorker.handle', { - attributes: claimAnnotations(claim), - }) - ) + const execution: OutboxCycleExecution = { + claimOwner: validated.claimOwner, + now: validated.now, + registrations: validated.registrations, + registrationsByKey, + }; + const progress = yield* Effect.reduce( + Array.from({ length: validated.maxDeliveries }), + initialCycleProgress, + (state) => (state.stopped ? Effect.succeed(state) : processNextOutboxDelivery(repository, execution, state)), ); - if (Exit.isFailure(handlerExit)) { - yield* Effect.annotateLogs( - Effect.logError('Unexpected Outbox Worker handler defect'), - claimAnnotations(claim, 'handler_defect') - ); - } - if (Exit.isFailure(handlerExit) || handlerExit.value === 'declared_failure') { - const executionError = new OutboxHandlerExecutionError({ - code: 'outbox_handler_execution_failed', - reason: Exit.isFailure(handlerExit) - ? 'The Outbox Worker handler failed unexpectedly' - : 'The Outbox Worker handler returned a declared failure', - }); - return yield* failOutboxDelivery( - repository, - claim, - execution.now, - claimedState, - executionError.reason, - 'handler_failure' - ); - } - yield* repository.complete(claim, execution.now).pipe( - Effect.tapErrorTag('OutboxPersistenceError', () => - logUnexpectedPersistence(claim) - ), - (effect) => withOutcomeSpan(effect, claim, 'success') - ); - return { ...claimedState, succeeded: claimedState.succeeded + 1 }; + return Object.freeze({ + claimed: progress.claimed, + dead: progress.dead, + deliveriesCreated: 0, + failed: progress.failed, + messagesMatched: 0, + retried: progress.retried, + succeeded: progress.succeeded, + }); }); -const runCycleWithRepository = Effect.fn('makeOutboxRuntime.runCycle')( - function* runCycleWithRepositoryEffect< - Registration extends AnyOutboxWorkerRegistration, - >( - repository: OutboxRepositoryPort, - input: RunOutboxCycleInput - ) { - const validated = yield* validateCycleInput(input); - const deployedSubscriptions = yield* Effect.try({ - catch: (error) => { - void error; - return descriptorFailure( - 'The deployed subscription snapshot is invalid' - ); - }, - try: () => validateOutboxWorkerSubscriptions(input.subscriptions), - }); - yield* validateDeployedRegistrationSnapshot( - validated.registrations, - deployedSubscriptions - ); - const registrationsByKey = new Map( - validated.registrations.map( - (registration) => - [registration.descriptor.workerKey, registration] as const - ) - ); - const execution: OutboxCycleExecution = { - claimOwner: validated.claimOwner, - now: validated.now, - registrations: validated.registrations, - registrationsByKey, - }; - const progress = yield* Effect.reduce( - Array.from({ length: validated.maxDeliveries }), - initialCycleProgress, - (state) => - state.stopped - ? Effect.succeed(state) - : processNextOutboxDelivery(repository, execution, state) - ); - - return Object.freeze({ - claimed: progress.claimed, - dead: progress.dead, - deliveriesCreated: 0, - failed: progress.failed, - messagesMatched: 0, - retried: progress.retried, - succeeded: progress.succeeded, - }); - } -); - -export const makeOutboxRuntime = ( - repository: OutboxRepositoryPort -): OutboxRuntimeService => { +export const makeOutboxRuntime = (repository: OutboxRepositoryPort): OutboxRuntimeService => { const matchMessages: OutboxRuntimeService['matchMessages'] = (input) => - matchMessagesWithRepository(repository, input).pipe( - Effect.withSpan('OutboxMatcher.matchMessages') - ); + matchMessagesWithRepository(repository, input).pipe(Effect.withSpan('OutboxMatcher.matchMessages')); const runCycle: OutboxRuntimeService['runCycle'] = (input) => runCycleWithRepository(repository, input).pipe( Effect.withSpan('OutboxWorker.runCycle', { attributes: { claimOwner: input.claimOwner }, - }) + }), ); return Object.freeze({ matchMessages, runCycle }); }; -export class OutboxRuntime extends Context.Service< - OutboxRuntime, - OutboxRuntimeService ->()('@app/core-runtime/outbox/runtime/OutboxRuntime') {} +export class OutboxRuntime extends Context.Service()( + '@app/core-runtime/outbox/runtime/OutboxRuntime', +) {} export const OutboxRuntimeLive = Layer.effect( OutboxRuntime, Effect.gen(function* makeOutboxRuntimeService() { const repository = yield* OutboxRepository; return makeOutboxRuntime(repository); - }) + }), ); -export const runOutboxCycle = < - Registration extends AnyOutboxWorkerRegistration, ->( - input: RunOutboxCycleInput -): Effect.Effect< - OutboxCycleResult, - OutboxCycleError, - OutboxRuntime | OutboxWorkerRequirements -> => Effect.flatMap(OutboxRuntime, (runtime) => runtime.runCycle(input)); +export const runOutboxCycle = ( + input: RunOutboxCycleInput, +): Effect.Effect> => + Effect.flatMap(OutboxRuntime, (runtime) => runtime.runCycle(input)); export const matchOutboxMessages = ( - input: MatchOutboxMessagesInput -): Effect.Effect< - OutboxMatchResult, - OutboxPersistenceError | OutboxWorkerDescriptorError, - OutboxRuntime -> => Effect.flatMap(OutboxRuntime, (runtime) => runtime.matchMessages(input)); + input: MatchOutboxMessagesInput, +): Effect.Effect => + Effect.flatMap(OutboxRuntime, (runtime) => runtime.matchMessages(input)); diff --git a/app/packages/core-runtime/src/outbox/worker-entrypoint.ts b/app/packages/core-runtime/src/outbox/worker-entrypoint.ts index 1eda855de..5deebc19e 100644 --- a/app/packages/core-runtime/src/outbox/worker-entrypoint.ts +++ b/app/packages/core-runtime/src/outbox/worker-entrypoint.ts @@ -1,38 +1,20 @@ /** Focused server-only entrypoint used to bundle independently deployed Outbox Worker hosts. */ export { defineTenantModuleEntrypoint } from '../modules/module-entrypoint.ts'; -export { - tenantLegalEntityRlsPolicies, - tenantRlsPolicies, -} from '../db/scoped-transaction.ts'; +export { tenantLegalEntityRlsPolicies, tenantRlsPolicies } from '../db/scoped-transaction.ts'; export { DatabaseConfigLive } from '../db/config.ts'; export { CorePersistenceLive } from '../runtime-infrastructure.ts'; -export { - CoreSearchIngestion, - CoreSearchIngestionLive, -} from '../search/ingestion.ts'; +export { CoreSearchIngestion, CoreSearchIngestionLive } from '../search/ingestion.ts'; export { CoreSearchProjectionStoreLive } from '../search/persistence.ts'; export { CoreSearchProjectionDocumentSchema, CoreSearchProjectionMutationSchema, CoreSearchProjectionStore, } from '../search/projection.ts'; -export { - CoreSearchWorkerSnapshot, - CoreSearchWorkerSnapshotLive, -} from '../search/worker-snapshot.ts'; -export { - defineOutboxWorker, - extractOutboxWorkerSubscriptions, -} from './definition.ts'; -export { - OutboxWorkerInfrastructureLive, - startOutboxWorkerProcess, -} from './process.ts'; +export { CoreSearchWorkerSnapshot, CoreSearchWorkerSnapshotLive } from '../search/worker-snapshot.ts'; +export { defineOutboxWorker, extractOutboxWorkerSubscriptions } from './definition.ts'; +export { OutboxWorkerInfrastructureLive, startOutboxWorkerProcess } from './process.ts'; export { OutboxRepositoryLive } from './repository.ts'; -export type { - AnyOutboxWorkerRegistration, - OutboxWorkerHandlerContext, -} from './definition.ts'; +export type { AnyOutboxWorkerRegistration, OutboxWorkerHandlerContext } from './definition.ts'; export type { CoreSearchIngestionService } from '../search/ingestion.ts'; export type { CoreSearchProjectionDocument, diff --git a/app/packages/core-runtime/src/permissions/client.ts b/app/packages/core-runtime/src/permissions/client.ts index c41d5bc88..c88dc2d12 100644 --- a/app/packages/core-runtime/src/permissions/client.ts +++ b/app/packages/core-runtime/src/permissions/client.ts @@ -21,76 +21,55 @@ export interface CloseableSpiceDbClient { export class SpiceDbPermissionClientError extends Schema.TaggedError()( 'SpiceDbPermissionClientError', - { reason: Schema.String } + { reason: Schema.String }, ) {} -const attachCause = ( - failure: Failure, - cause: unknown -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); +const attachCause = (failure: Failure, cause: unknown): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); -export const spiceDbPermissionClientError = ( - cause?: unknown -): SpiceDbPermissionClientError => +export const spiceDbPermissionClientError = (cause?: unknown): SpiceDbPermissionClientError => attachCause( new SpiceDbPermissionClientError({ reason: 'The SpiceDB client operation did not complete safely', }), - cause + cause, ); export interface SpiceDbPermissionClient extends CloseableSpiceDbClient { readonly checkBulkPermissions: ( - request: v1.CheckBulkPermissionsRequest - ) => Effect.Effect< - v1.CheckBulkPermissionsResponse, - SpiceDbPermissionClientError - >; + request: v1.CheckBulkPermissionsRequest, + ) => Effect.Effect; readonly checkPermission: ( - request: v1.CheckPermissionRequest + request: v1.CheckPermissionRequest, ) => Effect.Effect; } const permissionTimeout = Effect.timeoutOrElse({ duration: Duration.millis(SPICEDB_CHECK_TIMEOUT_MS), - orElse: () => - Effect.fail( - spiceDbPermissionClientError( - new Cause.TimeoutError('SpiceDB client operation timed out') - ) - ), + orElse: () => Effect.fail(spiceDbPermissionClientError(new Cause.TimeoutError('SpiceDB client operation timed out'))), }); export const spiceDbClientSecurity = ( - configuration: Pick< - SpiceDbConfigValue, - 'deploymentEnvironment' | 'endpoint' | 'insecureLocal' - > + configuration: Pick, ): v1.ClientSecurity => { if (!allowsInsecureSpiceDbTransport(configuration)) { throw new SpiceDbConfigError({ - reason: - 'Insecure SpiceDB client credentials are not allowed for this endpoint', + reason: 'Insecure SpiceDB client credentials are not allowed for this endpoint', }); } - return configuration.insecureLocal - ? v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS - : v1.ClientSecurity.SECURE; + return configuration.insecureLocal ? v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS : v1.ClientSecurity.SECURE; }; export const createSpiceDbPermissionClient = ( configuration: SpiceDbConfigValue, - timeoutMilliseconds: number + timeoutMilliseconds: number, ): SpiceDbPermissionClient => { const client = v1.NewClient( configuration.preSharedKey, configuration.endpoint, spiceDbClientSecurity(configuration), undefined, - { interceptors: [deadlineInterceptor(timeoutMilliseconds)] } + { interceptors: [deadlineInterceptor(timeoutMilliseconds)] }, ); return { checkBulkPermissions: (request) => @@ -109,14 +88,8 @@ export const createSpiceDbPermissionClient = ( }; }; -export const acquireSpiceDbClientResource = < - Client extends CloseableSpiceDbClient, - Error, ->( +export const acquireSpiceDbClientResource = ( acquire: () => Client, - onFailure: (cause: unknown) => Error + onFailure: (cause: unknown) => Error, ): Effect.Effect => - Effect.acquireRelease( - Effect.try({ catch: onFailure, try: acquire }), - (client) => Effect.sync(() => client.close()) - ); + Effect.acquireRelease(Effect.try({ catch: onFailure, try: acquire }), (client) => Effect.sync(() => client.close())); diff --git a/app/packages/core-runtime/src/permissions/config-error.ts b/app/packages/core-runtime/src/permissions/config-error.ts index bbbee89a2..869018a26 100644 --- a/app/packages/core-runtime/src/permissions/config-error.ts +++ b/app/packages/core-runtime/src/permissions/config-error.ts @@ -1,8 +1,5 @@ import { Schema } from 'effect'; -export class SpiceDbConfigError extends Schema.TaggedError()( - 'SpiceDbConfigError', - { - reason: Schema.String, - } -) {} +export class SpiceDbConfigError extends Schema.TaggedError()('SpiceDbConfigError', { + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/permissions/config.ts b/app/packages/core-runtime/src/permissions/config.ts index 9a1b9a2a5..916139838 100644 --- a/app/packages/core-runtime/src/permissions/config.ts +++ b/app/packages/core-runtime/src/permissions/config.ts @@ -1,11 +1,4 @@ -import { - Config, - ConfigProvider, - Effect, - Option, - Redacted, - Schema, -} from 'effect'; +import { Config, ConfigProvider, Effect, Option, Redacted, Schema } from 'effect'; import { loadDotEnvProvider } from '../environment/dotenv-provider.ts'; import { APP_ENV_PATH } from '../environment/workspace-environment.ts'; @@ -31,7 +24,7 @@ const makeSpiceDbConfigValue = (settings: { : Object.freeze( Object.assign(base, { deploymentEnvironment: settings.deploymentEnvironment, - }) + }), ); }; @@ -41,10 +34,7 @@ export type SpiceDbConfigValue = ReturnType & export type SpiceDbEnvironment = Readonly< Partial< Record< - | 'SPICEDB_ENDPOINT' - | 'SPICEDB_INSECURE' - | 'SPICEDB_PRESHARED_KEY' - | 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT', + 'SPICEDB_ENDPOINT' | 'SPICEDB_INSECURE' | 'SPICEDB_PRESHARED_KEY' | 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT', string > > @@ -79,23 +69,15 @@ const isLocalhostEndpoint = (endpoint: string): boolean => { } }; -const isStagePrivateEndpoint = ( - endpoint: string, - deploymentEnvironment?: string -): boolean => deploymentEnvironment === 'stage' && endpoint === 'spicedb:50051'; +const isStagePrivateEndpoint = (endpoint: string, deploymentEnvironment?: string): boolean => + deploymentEnvironment === 'stage' && endpoint === 'spicedb:50051'; export const allowsInsecureSpiceDbTransport = ( - configuration: Pick< - SpiceDbConfigValue, - 'deploymentEnvironment' | 'endpoint' | 'insecureLocal' - > + configuration: Pick, ): boolean => !configuration.insecureLocal || isLocalhostEndpoint(configuration.endpoint) || - isStagePrivateEndpoint( - configuration.endpoint, - configuration.deploymentEnvironment - ); + isStagePrivateEndpoint(configuration.endpoint, configuration.deploymentEnvironment); const isValidEndpoint = (endpoint: string): boolean => { try { @@ -113,101 +95,72 @@ const isValidEndpoint = (endpoint: string): boolean => { } }; -const parseSpiceDbConfigWith = Effect.fn('Config.parseSpiceDbConfigWith')( - function* parseConfig(provider: ConfigProvider.ConfigProvider) { - const { deploymentEnvironment, endpoint, insecureFlag, preSharedKey } = - yield* Effect.all( - { - deploymentEnvironment: Config.schema( - Schema.Trim, - 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT' - ) - .pipe(Config.option, Config.map(Option.getOrUndefined)) - .parse(provider) - .pipe( - Effect.mapError((error) => - configFailureWithCause( - 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT must be a string', - error - ) - ) - ), - endpoint: Config.schema(Schema.Trim, 'SPICEDB_ENDPOINT') - .parse(provider) - .pipe( - Effect.mapError((error) => - configFailureWithCause('SPICEDB_ENDPOINT is required', error) - ) - ), - insecureFlag: Config.schema(Schema.Trim, 'SPICEDB_INSECURE') - .pipe(Config.map((value) => value.toLowerCase())) - .parse(provider) - .pipe( - Effect.mapError((error) => - configFailureWithCause( - 'SPICEDB_INSECURE must be explicitly true or false', - error - ) - ) - ), - preSharedKey: Config.redacted('SPICEDB_PRESHARED_KEY') - .pipe( - Config.map((value) => Redacted.make(Redacted.value(value).trim())) - ) - .parse(provider) - .pipe( - Effect.mapError((error) => - configFailureWithCause( - 'SPICEDB_PRESHARED_KEY is required', - error - ) - ) - ), - }, - { concurrency: 4 } - ); - - if (endpoint.length === 0) { - return yield* configFailure('SPICEDB_ENDPOINT is required'); - } - if (!isValidEndpoint(endpoint)) { - return yield* configFailure( - 'SPICEDB_ENDPOINT must be a valid host and optional port' - ); - } - if (Redacted.value(preSharedKey).length === 0) { - return yield* configFailure('SPICEDB_PRESHARED_KEY is required'); - } - if (insecureFlag !== 'true' && insecureFlag !== 'false') { - return yield* configFailure( - 'SPICEDB_INSECURE must be explicitly true or false' - ); - } - const configuration = makeSpiceDbConfigValue({ - deploymentEnvironment, - endpoint, - insecureLocal: insecureFlag === 'true', - preSharedKey, - }); - if (!allowsInsecureSpiceDbTransport(configuration)) { - return yield* configFailure( - 'Insecure SpiceDB transport is allowed only for an explicit localhost port or the stage private endpoint' - ); - } +const parseSpiceDbConfigWith = Effect.fn('Config.parseSpiceDbConfigWith')(function* parseConfig( + provider: ConfigProvider.ConfigProvider, +) { + const { deploymentEnvironment, endpoint, insecureFlag, preSharedKey } = yield* Effect.all( + { + deploymentEnvironment: Config.schema(Schema.Trim, 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT') + .pipe(Config.option, Config.map(Option.getOrUndefined)) + .parse(provider) + .pipe( + Effect.mapError((error) => + configFailureWithCause('ULTRAMODERN_DEPLOYMENT_ENVIRONMENT must be a string', error), + ), + ), + endpoint: Config.schema(Schema.Trim, 'SPICEDB_ENDPOINT') + .parse(provider) + .pipe(Effect.mapError((error) => configFailureWithCause('SPICEDB_ENDPOINT is required', error))), + insecureFlag: Config.schema(Schema.Trim, 'SPICEDB_INSECURE') + .pipe(Config.map((value) => value.toLowerCase())) + .parse(provider) + .pipe( + Effect.mapError((error) => + configFailureWithCause('SPICEDB_INSECURE must be explicitly true or false', error), + ), + ), + preSharedKey: Config.redacted('SPICEDB_PRESHARED_KEY') + .pipe(Config.map((value) => Redacted.make(Redacted.value(value).trim()))) + .parse(provider) + .pipe(Effect.mapError((error) => configFailureWithCause('SPICEDB_PRESHARED_KEY is required', error))), + }, + { concurrency: 4 }, + ); - return configuration; + if (endpoint.length === 0) { + return yield* configFailure('SPICEDB_ENDPOINT is required'); + } + if (!isValidEndpoint(endpoint)) { + return yield* configFailure('SPICEDB_ENDPOINT must be a valid host and optional port'); + } + if (Redacted.value(preSharedKey).length === 0) { + return yield* configFailure('SPICEDB_PRESHARED_KEY is required'); + } + if (insecureFlag !== 'true' && insecureFlag !== 'false') { + return yield* configFailure('SPICEDB_INSECURE must be explicitly true or false'); + } + const configuration = makeSpiceDbConfigValue({ + deploymentEnvironment, + endpoint, + insecureLocal: insecureFlag === 'true', + preSharedKey, + }); + if (!allowsInsecureSpiceDbTransport(configuration)) { + return yield* configFailure( + 'Insecure SpiceDB transport is allowed only for an explicit localhost port or the stage private endpoint', + ); } -); + + return configuration; +}); export const parseSpiceDbConfig = ( - environment: SpiceDbEnvironment + environment: SpiceDbEnvironment, ): Effect.Effect => - parseSpiceDbConfigWith( - ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true }) - ); + parseSpiceDbConfigWith(ConfigProvider.fromUnknown(environment, { preserveEmptyStrings: true })); export const loadSpiceDbConfig = ( - options: LoadSpiceDbConfigOptions = {} + options: LoadSpiceDbConfigOptions = {}, ): Effect.Effect => { const environmentProvider = options.environment === undefined @@ -219,10 +172,6 @@ export const loadSpiceDbConfig = ( return loadDotEnvProvider(envPath, configFailureWithCause).pipe( Effect.withSpan('Config.loadFileConfigProvider'), - Effect.flatMap((fileProvider) => - parseSpiceDbConfigWith( - ConfigProvider.orElse(environmentProvider, fileProvider) - ) - ) + Effect.flatMap((fileProvider) => parseSpiceDbConfigWith(ConfigProvider.orElse(environmentProvider, fileProvider))), ); }; diff --git a/app/packages/core-runtime/src/permissions/context-access.ts b/app/packages/core-runtime/src/permissions/context-access.ts index e486a357c..794e217cc 100644 --- a/app/packages/core-runtime/src/permissions/context-access.ts +++ b/app/packages/core-runtime/src/permissions/context-access.ts @@ -14,11 +14,7 @@ import type { SpiceDbConfigError } from './config-error.ts'; import { loadSpiceDbConfig } from './config.ts'; import type { SpiceDbConfigValue } from './config.ts'; -const ContextAccessDecisionSchema = Schema.Literals([ - 'allowed', - 'denied', - 'unavailable', -]); +const ContextAccessDecisionSchema = Schema.Literals(['allowed', 'denied', 'unavailable']); export type ContextAccessDecision = typeof ContextAccessDecisionSchema.Type; export const TENANT_PERMISSION_KEYS = [ @@ -32,13 +28,8 @@ export const TENANT_PERMISSION_KEYS = [ 'review_party_identity', ] as const; export type TenantPermissionKey = (typeof TENANT_PERMISSION_KEYS)[number]; -export const LEGAL_ENTITY_PERMISSION_KEYS = [ - 'access', - 'manage_counterparty', - 'read_counterparty', -] as const; -export type LegalEntityPermissionKey = - (typeof LEGAL_ENTITY_PERMISSION_KEYS)[number]; +export const LEGAL_ENTITY_PERMISSION_KEYS = ['access', 'manage_counterparty', 'read_counterparty'] as const; +export type LegalEntityPermissionKey = (typeof LEGAL_ENTITY_PERMISSION_KEYS)[number]; export interface ContextAccessResult { readonly decision: ContextAccessDecision; @@ -78,14 +69,13 @@ export interface ContextAccessService { }) => Effect.Effect; } -export class ContextAccess extends Context.Service< - ContextAccess, - ContextAccessService ->()('@app/core-runtime/permissions/context-access/ContextAccess') {} +export class ContextAccess extends Context.Service()( + '@app/core-runtime/permissions/context-access/ContextAccess', +) {} export type ContextAccessClientFactory = ( configuration: SpiceDbConfigValue, - timeoutMilliseconds: number + timeoutMilliseconds: number, ) => SpiceDbPermissionClient; interface BatchItem { @@ -95,12 +85,8 @@ interface BatchItem { readonly resourceType: string; } -const ContextAccessObjectIdParts = Schema.fromJsonString( - Schema.Array(Schema.String) -); -const encodeContextAccessObjectIdParts = Schema.encodeResult( - ContextAccessObjectIdParts -); +const ContextAccessObjectIdParts = Schema.fromJsonString(Schema.Array(Schema.String)); +const encodeContextAccessObjectIdParts = Schema.encodeResult(ContextAccessObjectIdParts); const principalReference = (principalId: string) => v1.SubjectReference.create({ @@ -114,55 +100,36 @@ const encodeObjectId = (parts: readonly string[]): string | undefined => { if (parts.some((part) => part.length === 0)) { return undefined; } - const encodedParts = Result.getOrThrow( - encodeContextAccessObjectIdParts(parts) - ); + const encodedParts = Result.getOrThrow(encodeContextAccessObjectIdParts(parts)); const encoded = `ctx_${Buffer.from(encodedParts, 'utf-8').toString('base64url')}`; return encoded.length <= 1024 ? encoded : undefined; }; -export const toLegalEntityAccessObjectId = ( - tenantId: string, - legalEntityId: string -): string | undefined => encodeObjectId([tenantId, legalEntityId]); +export const toLegalEntityAccessObjectId = (tenantId: string, legalEntityId: string): string | undefined => + encodeObjectId([tenantId, legalEntityId]); -export const toModuleAccessObjectId = ( - tenantId: string, - legalEntityId: string, - moduleId: string -): string | undefined => encodeObjectId([tenantId, legalEntityId, moduleId]); +export const toModuleAccessObjectId = (tenantId: string, legalEntityId: string, moduleId: string): string | undefined => + encodeObjectId([tenantId, legalEntityId, moduleId]); export const toResourceAccessObjectId = ( tenantId: string, legalEntityId: string, - resource: ResourceAccessTarget + resource: ResourceAccessTarget, ): string | undefined => - encodeObjectId([ - tenantId, - legalEntityId, - resource.moduleId, - resource.resourceType, - resource.resourceId, - ]); + encodeObjectId([tenantId, legalEntityId, resource.moduleId, resource.resourceType, resource.resourceId]); const unavailable = (keys: readonly string[]): readonly ContextAccessResult[] => keys.map((key) => ({ decision: 'unavailable' as const, key })); -const classifyPair = ( - pair: v1.CheckBulkPermissionsPair -): ContextAccessDecision => { +const classifyPair = (pair: v1.CheckBulkPermissionsPair): ContextAccessDecision => { if (pair.response.oneofKind !== 'item') { return 'unavailable'; } const { permissionship } = pair.response.item; - if ( - permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION - ) { + if (permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION) { return 'allowed'; } - if ( - permissionship === v1.CheckPermissionResponse_Permissionship.NO_PERMISSION - ) { + if (permissionship === v1.CheckPermissionResponse_Permissionship.NO_PERMISSION) { return 'denied'; } return 'unavailable'; @@ -180,25 +147,21 @@ const makeRequestItem = (item: BatchItem, principalId: string) => const sameObjectReference = ( expected: v1.ObjectReference | undefined, - actual: v1.ObjectReference | undefined -): boolean => - actual?.objectId === expected?.objectId && - actual?.objectType === expected?.objectType; + actual: v1.ObjectReference | undefined, +): boolean => actual?.objectId === expected?.objectId && actual?.objectType === expected?.objectType; const sameRequest = ( expected: v1.CheckBulkPermissionsRequestItem, - actual: v1.CheckBulkPermissionsRequestItem | undefined + actual: v1.CheckBulkPermissionsRequestItem | undefined, ): boolean => actual?.permission === expected.permission && sameObjectReference(expected.resource, actual.resource) && sameObjectReference(expected.subject?.object, actual.subject?.object); -export const makeContextAccess = ( - client: SpiceDbPermissionClient -): ContextAccessService => { +export const makeContextAccess = (client: SpiceDbPermissionClient): ContextAccessService => { const checkBatch = ( items: readonly BatchItem[], - principalId: string + principalId: string, ): Effect.Effect => { const keys = items.map(({ key }) => key); if ( @@ -218,7 +181,7 @@ export const makeContextAccess = ( consistency: fullyConsistent, items: requests, withTracing: false, - }) + }), ) .pipe( Effect.map((response) => { @@ -229,73 +192,50 @@ export const makeContextAccess = ( const decisions = response.pairs.map((pair, index) => { const expected = requests[index]; const key = keys[index]; - if ( - expected === undefined || - key === undefined || - !sameRequest(expected, pair.request) || - seen.has(key) - ) { + if (expected === undefined || key === undefined || !sameRequest(expected, pair.request) || seen.has(key)) { return null; } seen.add(key); return { decision: classifyPair(pair), key }; }); - return decisions.every( - (decision): decision is ContextAccessResult => decision !== null - ) + return decisions.every((decision): decision is ContextAccessResult => decision !== null) ? decisions : unavailable(keys); }), - Effect.catchTag('SpiceDbPermissionClientError', () => - Effect.succeed(unavailable(keys)) - ) + Effect.catchTag('SpiceDbPermissionClientError', () => Effect.succeed(unavailable(keys))), ); }; const service: ContextAccessService = { - legalEntities: ({ - legalEntityIds, - permission = 'access', - principalId, - tenantId, - }) => + legalEntities: ({ legalEntityIds, permission = 'access', principalId, tenantId }) => checkBatch( legalEntityIds.map((legalEntityId) => ({ key: legalEntityId, permission, - resourceId: - toLegalEntityAccessObjectId(tenantId, legalEntityId) ?? '', + resourceId: toLegalEntityAccessObjectId(tenantId, legalEntityId) ?? '', resourceType: 'legal_entity', })), - principalId + principalId, ), modules: ({ legalEntityId, moduleIds, principalId, tenantId }) => checkBatch( moduleIds.map((moduleId) => ({ key: moduleId, permission: 'access', - resourceId: - toModuleAccessObjectId(tenantId, legalEntityId, moduleId) ?? '', + resourceId: toModuleAccessObjectId(tenantId, legalEntityId, moduleId) ?? '', resourceType: 'module_access', })), - principalId + principalId, ), - resources: ({ - legalEntityId, - permission = 'read', - principalId, - resources, - tenantId, - }) => + resources: ({ legalEntityId, permission = 'read', principalId, resources, tenantId }) => checkBatch( resources.map((resource) => ({ key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, permission, - resourceId: - toResourceAccessObjectId(tenantId, legalEntityId, resource) ?? '', + resourceId: toResourceAccessObjectId(tenantId, legalEntityId, resource) ?? '', resourceType: 'resource', })), - principalId + principalId, ), tenants: ({ permission, principalId, tenantIds }) => checkBatch( @@ -305,7 +245,7 @@ export const makeContextAccess = ( resourceId: tenantId, resourceType: 'tenant', })), - principalId + principalId, ), }; return Object.freeze(service); @@ -313,17 +253,13 @@ export const makeContextAccess = ( const unavailableContextAccess = (): ContextAccessService => { const service: ContextAccessService = { - legalEntities: ({ legalEntityIds }) => - Effect.succeed(unavailable(legalEntityIds)), + legalEntities: ({ legalEntityIds }) => Effect.succeed(unavailable(legalEntityIds)), modules: ({ moduleIds }) => Effect.succeed(unavailable(moduleIds)), resources: ({ resources }) => Effect.succeed( unavailable( - resources.map( - ({ moduleId, resourceId, resourceType }) => - `${moduleId}:${resourceType}:${resourceId}` - ) - ) + resources.map(({ moduleId, resourceId, resourceType }) => `${moduleId}:${resourceType}:${resourceId}`), + ), ), tenants: ({ tenantIds }) => Effect.succeed(unavailable(tenantIds)), }; @@ -332,29 +268,19 @@ const unavailableContextAccess = (): ContextAccessService => { export const makeContextAccessLive = ( clientFactory: ContextAccessClientFactory = createSpiceDbPermissionClient, - loadConfiguration: () => Effect.Effect< - SpiceDbConfigValue, - SpiceDbConfigError - > = loadSpiceDbConfig + loadConfiguration: () => Effect.Effect = loadSpiceDbConfig, ): Effect.Effect => loadConfiguration().pipe( Effect.flatMap((configuration) => acquireSpiceDbClientResource( () => clientFactory(configuration, SPICEDB_CHECK_TIMEOUT_MS), - spiceDbPermissionClientError + spiceDbPermissionClientError, ).pipe( Effect.map(makeContextAccess), - Effect.catchTag('SpiceDbPermissionClientError', () => - Effect.succeed(unavailableContextAccess()) - ) - ) + Effect.catchTag('SpiceDbPermissionClientError', () => Effect.succeed(unavailableContextAccess())), + ), ), - Effect.catchTag('SpiceDbConfigError', () => - Effect.succeed(unavailableContextAccess()) - ) + Effect.catchTag('SpiceDbConfigError', () => Effect.succeed(unavailableContextAccess())), ); -export const ContextAccessLive = Layer.effect( - ContextAccess, - makeContextAccessLive() -); +export const ContextAccessLive = Layer.effect(ContextAccess, makeContextAccessLive()); diff --git a/app/packages/core-runtime/src/permissions/service.ts b/app/packages/core-runtime/src/permissions/service.ts index 98ea7f788..8cbef1ad7 100644 --- a/app/packages/core-runtime/src/permissions/service.ts +++ b/app/packages/core-runtime/src/permissions/service.ts @@ -32,8 +32,7 @@ export const toSpiceDbActionObjectId = (actionKey: string): string => `ak_${Buffer.from(actionKey, 'utf-8').toString('base64url')}`; const ActionPermissionDecisionSchema = Schema.Literals(['allowed', 'denied']); -export type ActionPermissionDecision = - typeof ActionPermissionDecisionSchema.Type; +export type ActionPermissionDecision = typeof ActionPermissionDecisionSchema.Type; interface ActionPermissionTargetInput { readonly actionKey: string; @@ -46,27 +45,19 @@ export type CheckActionPermissionInput = Readonly< export interface ActionPermissionService { readonly checkActionPermission: ( - input: CheckActionPermissionInput + input: CheckActionPermissionInput, ) => Effect.Effect; } -export type PermissionCheckClient = Pick< - SpiceDbPermissionClient, - 'checkPermission' | 'close' ->; +export type PermissionCheckClient = Pick; export type PermissionClientFactory = ( configuration: SpiceDbConfigValue, - timeoutMilliseconds: number + timeoutMilliseconds: number, ) => PermissionCheckClient; -const attachCause = ( - failure: Failure, - cause: unknown -): Failure => - cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); +const attachCause = (failure: Failure, cause: unknown): Failure => + cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); const checkFailure = (cause?: unknown): ActionPermissionCheckError => attachCause( @@ -74,25 +65,16 @@ const checkFailure = (cause?: unknown): ActionPermissionCheckError => code: 'action_permission_check_failed', reason: 'The authorization service could not determine permission safely', }), - cause + cause, ); -export const createPermissionCheckClient: PermissionClientFactory = ( - configuration, - timeoutMilliseconds -) => - createSpiceDbPermissionClient( - configuration, - timeoutMilliseconds - ) satisfies SpiceDbPermissionClient; +export const createPermissionCheckClient: PermissionClientFactory = (configuration, timeoutMilliseconds) => + createSpiceDbPermissionClient(configuration, timeoutMilliseconds) satisfies SpiceDbPermissionClient; export const acquirePermissionClientResource = ( - acquire: () => PermissionCheckClient -): Effect.Effect< - PermissionCheckClient, - ActionPermissionCheckError, - Scope.Scope -> => acquireSpiceDbClientResource(acquire, checkFailure); + acquire: () => PermissionCheckClient, +): Effect.Effect => + acquireSpiceDbClientResource(acquire, checkFailure); const actionReference = (actionKey: string) => v1.ObjectReference.create({ @@ -125,25 +107,17 @@ const restrictionRequest = (actionKey: string, principalId: string) => }); const classifyPermissionship = ( - response: Response + response: Response, ): Effect.Effect<'has' | 'none', ActionPermissionCheckError> => { - if ( - !Predicate.isObjectKeyword(response) || - response === null || - !('permissionship' in response) - ) { + if (!Predicate.isObjectKeyword(response) || response === null || !('permissionship' in response)) { return Effect.fail(checkFailure()); } const { permissionship } = response; - if ( - permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION - ) { + if (permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION) { return Effect.succeed('has'); } - if ( - permissionship === v1.CheckPermissionResponse_Permissionship.NO_PERMISSION - ) { + if (permissionship === v1.CheckPermissionResponse_Permissionship.NO_PERMISSION) { return Effect.succeed('none'); } return Effect.fail(checkFailure()); @@ -151,14 +125,9 @@ const classifyPermissionship = ( const runCheck = ( client: PermissionCheckClient, - request: v1.CheckPermissionRequest + request: v1.CheckPermissionRequest, ): Effect.Effect<'has' | 'none', ActionPermissionCheckError> => - client - .checkPermission(request) - .pipe( - Effect.mapError(checkFailure), - Effect.flatMap(classifyPermissionship) - ); + client.checkPermission(request).pipe(Effect.mapError(checkFailure), Effect.flatMap(classifyPermissionship)); interface ActionPermissionRolloutOptions { readonly emit: (event: AuthorizationWouldDenyEvent) => void; @@ -170,87 +139,68 @@ type PermissionRollout = ActionPermissionRolloutOptions; const actionPermissionService = ( client: PermissionCheckClient, - rolloutOptions?: PermissionRollout + rolloutOptions?: PermissionRollout, ): ActionPermissionService => Object.freeze({ - checkActionPermission: Effect.fn('ActionPermission.checkActionPermission')( - function* checkActionPermissionEffect(input: CheckActionPermissionInput) { - const execution = yield* runCheck( - client, - executionRequest(input.actionKey, input.principalId) - ); - if (execution === 'has') { - return 'allowed' as const; - } - if (rolloutOptions === undefined) { - return 'denied' as const; - } - const restricted = yield* runCheck( - client, - restrictionRequest(input.actionKey, input.principalId) - ); - if (restricted === 'has') { - return 'denied' as const; - } - return yield* Effect.try({ - catch: (cause) => checkFailure(cause), - try: () => - decideAuthorizationRollout( - { - candidate: 'denied', - current: 'allowed', - denialReason: 'missing_policy', - entrypointKey: input.actionKey, - nowEpochMs: rolloutOptions.nowEpochMs(), - policyClass: 'action_execution', - surface: 'action', - }, - { contract: rolloutOptions.rollout, emit: rolloutOptions.emit } - ), - }); + checkActionPermission: Effect.fn('ActionPermission.checkActionPermission')(function* checkActionPermissionEffect( + input: CheckActionPermissionInput, + ) { + const execution = yield* runCheck(client, executionRequest(input.actionKey, input.principalId)); + if (execution === 'has') { + return 'allowed' as const; } - ), + if (rolloutOptions === undefined) { + return 'denied' as const; + } + const restricted = yield* runCheck(client, restrictionRequest(input.actionKey, input.principalId)); + if (restricted === 'has') { + return 'denied' as const; + } + return yield* Effect.try({ + catch: (cause) => checkFailure(cause), + try: () => + decideAuthorizationRollout( + { + candidate: 'denied', + current: 'allowed', + denialReason: 'missing_policy', + entrypointKey: input.actionKey, + nowEpochMs: rolloutOptions.nowEpochMs(), + policyClass: 'action_execution', + surface: 'action', + }, + { contract: rolloutOptions.rollout, emit: rolloutOptions.emit }, + ), + }); + }), }); export const makeActionPermissionService = actionPermissionService; -const unavailablePermissionService = ( - cause?: unknown -): ActionPermissionService => +const unavailablePermissionService = (cause?: unknown): ActionPermissionService => Object.freeze({ checkActionPermission: Effect.fn('ActionPermission.checkActionPermission')( function* unavailableCheckActionPermissionEffect() { return yield* checkFailure(cause); - } + }, ), }); -export class ActionPermission extends Context.Service< - ActionPermission, - ActionPermissionService ->()('@app/core-runtime/permissions/service/ActionPermission') {} +export class ActionPermission extends Context.Service()( + '@app/core-runtime/permissions/service/ActionPermission', +) {} export const makeActionPermissionLive = ( clientFactory: PermissionClientFactory = createPermissionCheckClient, - loadConfiguration: () => Effect.Effect< - SpiceDbConfigValue, - SpiceDbConfigError - > = loadSpiceDbConfig + loadConfiguration: () => Effect.Effect = loadSpiceDbConfig, ): Effect.Effect => Effect.matchEffect(loadConfiguration(), { onFailure: (cause) => Effect.succeed(unavailablePermissionService(cause)), onSuccess: (configuration) => - acquirePermissionClientResource(() => - clientFactory(configuration, SPICEDB_CHECK_TIMEOUT_MS) - ).pipe( + acquirePermissionClientResource(() => clientFactory(configuration, SPICEDB_CHECK_TIMEOUT_MS)).pipe( Effect.map(makeActionPermissionService), - Effect.catchTag('ActionPermissionCheckError', (cause) => - Effect.succeed(unavailablePermissionService(cause)) - ) + Effect.catchTag('ActionPermissionCheckError', (cause) => Effect.succeed(unavailablePermissionService(cause))), ), }); -export const ActionPermissionLive = Layer.effect( - ActionPermission, - makeActionPermissionLive() -); +export const ActionPermissionLive = Layer.effect(ActionPermission, makeActionPermissionLive()); diff --git a/app/packages/core-runtime/src/reads/context.ts b/app/packages/core-runtime/src/reads/context.ts index 1722b4df3..0cb84250d 100644 --- a/app/packages/core-runtime/src/reads/context.ts +++ b/app/packages/core-runtime/src/reads/context.ts @@ -4,19 +4,13 @@ import type { OperationalScope } from '../operations/context.ts'; import type { ReadEvidenceCaptureMode } from './definition.ts'; import { ReadEvidenceValidationError } from './errors.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); export interface ReadEvidenceMetadata { readonly queryHash?: string; @@ -37,12 +31,7 @@ export interface ReadHandlerResult { } const sha256 = /^[\da-f]{64}$/u; -const evidenceKeys = new Set([ - 'queryHash', - 'resultCount', - 'resultFingerprintHash', - 'resultFingerprintSchema', -]); +const evidenceKeys = new Set(['queryHash', 'resultCount', 'resultFingerprintHash', 'resultFingerprintSchema']); const invalidEvidence = (cause?: unknown): ReadEvidenceValidationError => { const failure = new ReadEvidenceValidationError({ code: 'read_evidence_invalid', @@ -67,33 +56,24 @@ const ReadEvidenceCandidateSchema = Schema.Struct({ type ReadEvidenceCandidate = typeof ReadEvidenceCandidateSchema.Type; const isValidResultCount = Schema.is( - Schema.Finite.check( - Schema.isInt(), - Schema.isBetween({ maximum: 2_147_483_647, minimum: 0 }) - ) + Schema.Finite.check(Schema.isInt(), Schema.isBetween({ maximum: 2_147_483_647, minimum: 0 })), ); -const hasInvalidFingerprintHash = ( - value: ReadEvidenceCandidate['resultFingerprintHash'] -): boolean => +const hasInvalidFingerprintHash = (value: ReadEvidenceCandidate['resultFingerprintHash']): boolean => value !== undefined && (!Predicate.isString(value) || !sha256.test(value)); -const hasInvalidFingerprintSchema = ( - value: ReadEvidenceCandidate['resultFingerprintSchema'] -): boolean => - value !== undefined && - (!Predicate.isString(value) || value.length === 0 || value.length > 300); +const hasInvalidFingerprintSchema = (value: ReadEvidenceCandidate['resultFingerprintSchema']): boolean => + value !== undefined && (!Predicate.isString(value) || value.length === 0 || value.length > 300); const hasInvalidHashEvidence = (record: ReadEvidenceCandidate): boolean => record.queryHash !== undefined || - (record.resultFingerprintHash === undefined) !== - (record.resultFingerprintSchema === undefined) || + (record.resultFingerprintHash === undefined) !== (record.resultFingerprintSchema === undefined) || hasInvalidFingerprintHash(record.resultFingerprintHash) || hasInvalidFingerprintSchema(record.resultFingerprintSchema); export const validateReadEvidenceMetadata = ( captureMode: ReadEvidenceCaptureMode, - value: Value + value: Value, ): Effect.Effect, ReadEvidenceValidationError> => Schema.decodeUnknownEffect(ReadEvidenceCandidateSchema, { onExcessProperty: 'error', @@ -106,17 +86,12 @@ export const validateReadEvidenceMetadata = ( resultFingerprintHash: fingerprintHash, resultFingerprintSchema: fingerprintSchema, } = record; - if ( - Object.keys(record).some((key) => !evidenceKeys.has(key)) || - !isValidResultCount(resultCount) - ) { + if (Object.keys(record).some((key) => !evidenceKeys.has(key)) || !isValidResultCount(resultCount)) { return Effect.fail(invalidEvidence()); } if ( captureMode === 'metadata_only' && - (queryHash !== undefined || - fingerprintHash !== undefined || - fingerprintSchema !== undefined) + (queryHash !== undefined || fingerprintHash !== undefined || fingerprintSchema !== undefined) ) { return Effect.fail(invalidEvidence()); } @@ -127,26 +102,20 @@ export const validateReadEvidenceMetadata = ( Object.freeze( withOptionalProperty( withOptionalProperty( - withOptionalProperty( - {}, - Predicate.isString(queryHash), - 'queryHash', - queryHash, - { - resultCount, - } - ), + withOptionalProperty({}, Predicate.isString(queryHash), 'queryHash', queryHash, { + resultCount, + }), Predicate.isString(fingerprintHash), 'resultFingerprintHash', fingerprintHash, - {} + {}, ), Predicate.isString(fingerprintSchema), 'resultFingerprintSchema', fingerprintSchema, - {} - ) - ) + {}, + ), + ), ); - }) + }), ); diff --git a/app/packages/core-runtime/src/reads/definition.ts b/app/packages/core-runtime/src/reads/definition.ts index dfc993cb6..7706f64ba 100644 --- a/app/packages/core-runtime/src/reads/definition.ts +++ b/app/packages/core-runtime/src/reads/definition.ts @@ -4,15 +4,9 @@ import type { Effect } from 'effect'; import type { ActionPolicy } from '../actions/policy.ts'; import { isActionPolicy } from '../actions/policy.ts'; import type { ScopedTransactionExecutor } from '../db/scoped-transaction.ts'; -import type { - ModuleEntrypointDescriptor, - ModuleEntrypointRole, -} from '../modules/module-entrypoint.ts'; +import type { ModuleEntrypointDescriptor, ModuleEntrypointRole } from '../modules/module-entrypoint.ts'; import { LEGAL_ENTITY_SCOPES } from '../operations/context.ts'; -import type { - LegalEntityScope, - OperationalScope, -} from '../operations/context.ts'; +import type { LegalEntityScope, OperationalScope } from '../operations/context.ts'; import type { OperationContextUnavailable } from '../operations/errors.ts'; import type { LegalEntityPermissionKey, @@ -21,9 +15,7 @@ import type { } from '../permissions/context-access.ts'; import type { ReadHandlerContext, ReadHandlerResult } from './context.ts'; -const registrationMarker: unique symbol = Symbol( - '@app/core-runtime/reads/registration' -); +const registrationMarker: unique symbol = Symbol('@app/core-runtime/reads/registration'); class ReadPrivateStorage { declare readonly [registrationMarker]?: true; @@ -36,7 +28,7 @@ class ReadPrivateStorage { static create( value: Value, - publicFields: PublicFields + publicFields: PublicFields, ): ReadPrivateStorage & Readonly { const storage = Object.assign(new ReadPrivateStorage(value), publicFields); Object.freeze(storage); @@ -48,37 +40,18 @@ class ReadPrivateStorage { } } -export const READ_ACCESS_KINDS = [ - 'detail', - 'download', - 'export', - 'list', - 'report', - 'search', -] as const; +export const READ_ACCESS_KINDS = ['detail', 'download', 'export', 'list', 'report', 'search'] as const; export type ReadAccessKind = (typeof READ_ACCESS_KINDS)[number]; -export const READ_EVIDENCE_CAPTURE_MODES = [ - 'hash_only', - 'metadata_only', -] as const; -export type ReadEvidenceCaptureMode = - (typeof READ_EVIDENCE_CAPTURE_MODES)[number]; -export const READ_PERMISSION_TARGETS = [ - 'legal_entity', - 'module', - 'resource', - 'tenant', -] as const; +export const READ_EVIDENCE_CAPTURE_MODES = ['hash_only', 'metadata_only'] as const; +export type ReadEvidenceCaptureMode = (typeof READ_EVIDENCE_CAPTURE_MODES)[number]; +export const READ_PERMISSION_TARGETS = ['legal_entity', 'module', 'resource', 'tenant'] as const; export type ReadPermissionTarget = (typeof READ_PERMISSION_TARGETS)[number]; export type ReadPermissionDenialStatus = 409 | 422; export interface ReadPolicyDescriptor { readonly denialStatus: ReadPermissionDenialStatus; readonly policyKey: string; } -export type ReadAlternativeTenantPermission = Exclude< - TenantPermissionKey, - 'access' | 'impersonate' ->; +export type ReadAlternativeTenantPermission = Exclude; export type AtomicResolvedReadPermissionTarget = | Readonly<{ readonly kind: 'legal_entity'; @@ -118,17 +91,15 @@ export type ResolvedReadPermissionTarget = }>; export type ReadPermissionTargetResolver = ( input: Input, - scope: OperationalScope + scope: OperationalScope, ) => ResolvedReadPermissionTarget; export type ReadResultPermissionTargetResolver = ( result: Result, - scope: OperationalScope + scope: OperationalScope, ) => readonly ResourceAccessTarget[]; -const isOwnerCompatiblePolicy = ( - policy: ActionPolicy, - owner: string -): boolean => policy.scope === 'global' || policy.owningModuleKey === owner; +const isOwnerCompatiblePolicy = (policy: ActionPolicy, owner: string): boolean => + policy.scope === 'global' || policy.owningModuleKey === owner; export interface ReadDescriptor< InputSchema extends Schema.ConstraintDecoder, @@ -157,7 +128,7 @@ export interface ReadDescriptor< export type ReadServiceFactory = ( transaction: ScopedTransactionExecutor, - scope: OperationalScope + scope: OperationalScope, ) => Effect.Effect; export type ReadHandler< @@ -168,12 +139,8 @@ export type ReadHandler< Requirements = never, > = ( input: InputSchema['Type'], - context: ReadHandlerContext -) => Effect.Effect< - ReadHandlerResult, - Error, - Requirements ->; + context: ReadHandlerContext, +) => Effect.Effect, Error, Requirements>; export interface ReadDescriptorValidationInput { readonly entrypoint: ModuleEntrypointDescriptor; @@ -181,37 +148,25 @@ export interface ReadDescriptorValidationInput { readonly owningModuleKey: string; } -const ReadDefinitionInvariantError = Schema.TaggedError()( - 'ReadDefinitionInvariantError', - { - message: Schema.String, - } -); +const ReadDefinitionInvariantError = Schema.TaggedError()('ReadDefinitionInvariantError', { + message: Schema.String, +}); const failReadDefinition = (message: string): never => { throw new ReadDefinitionInvariantError({ message }); }; -export const validateReadDescriptorInput = ( - descriptor: ReadDescriptorValidationInput -): void => { +export const validateReadDescriptorInput = (descriptor: ReadDescriptorValidationInput): void => { if ( descriptor.entrypoint.moduleKey !== descriptor.owningModuleKey || - descriptor.entrypoint.scope !== - (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') || + descriptor.entrypoint.scope !== (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') || !['read', 'historical_read'].includes(descriptor.entrypoint.access) || !Object.isFrozen(descriptor.entrypoint) ) { - return failReadDefinition( - 'Read entrypoint must be immutable, read-only, and owner-scoped' - ); + return failReadDefinition('Read entrypoint must be immutable, read-only, and owner-scoped'); } - if ( - !LEGAL_ENTITY_SCOPES.some((scope) => scope === descriptor.legalEntityScope) - ) { - return failReadDefinition( - 'Read legal-entity scope must be required, optional, or forbidden' - ); + if (!LEGAL_ENTITY_SCOPES.some((scope) => scope === descriptor.legalEntityScope)) { + return failReadDefinition('Read legal-entity scope must be required, optional, or forbidden'); } }; @@ -223,20 +178,10 @@ type ReadRegistrationPrivateValue< Error, Requirements = never, > = Readonly<{ - readonly handler: ReadHandler< - InputSchema, - ResultSchema, - Services, - Error, - Requirements - >; - readonly permissionTargetResolver: ReadPermissionTargetResolver< - InputSchema['Type'] - >; + readonly handler: ReadHandler; + readonly permissionTargetResolver: ReadPermissionTargetResolver; readonly policies: readonly ActionPolicy[]; - readonly resultPermissionTargetResolver?: ReadResultPermissionTargetResolver< - ResultSchema['Type'] - >; + readonly resultPermissionTargetResolver?: ReadResultPermissionTargetResolver; readonly serviceFactory: ReadServiceFactory; }>; @@ -248,51 +193,31 @@ export type ReadRegistration< Error, Requirements = never, > = ReadPrivateStorage< - ReadRegistrationPrivateValue< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > + ReadRegistrationPrivateValue > & { readonly _error?: Error; readonly _requirements?: Requirements; readonly _services?: Services; - readonly descriptor: Readonly< - ReadDescriptor - >; + readonly descriptor: Readonly>; readonly [registrationMarker]: true; }; const validateReadVocabulary = ( - descriptor: ReadDescriptor< - Schema.ConstraintDecoder, - Schema.ConstraintDecoder, - string - >, + descriptor: ReadDescriptor, Schema.ConstraintDecoder, string>, permissionTargetResolver: ReadPermissionTargetResolver, - resultPermissionTargetResolver: - | ReadResultPermissionTargetResolver - | undefined + resultPermissionTargetResolver: ReadResultPermissionTargetResolver | undefined, ): void => { if ( !READ_ACCESS_KINDS.includes(descriptor.accessKind) || - !READ_EVIDENCE_CAPTURE_MODES.includes( - descriptor.evidencePolicy.captureMode - ) || + !READ_EVIDENCE_CAPTURE_MODES.includes(descriptor.evidencePolicy.captureMode) || !READ_PERMISSION_TARGETS.includes(descriptor.permissionTarget) || - (descriptor.accessKind === 'search' && - !Predicate.isFunction(resultPermissionTargetResolver)) || + (descriptor.accessKind === 'search' && !Predicate.isFunction(resultPermissionTargetResolver)) || !Predicate.isFunction(permissionTargetResolver) || descriptor.evidencePolicy.policyKey.length === 0 || descriptor.readKey.length === 0 || descriptor.schemaVersion.length === 0 ) { - return failReadDefinition( - 'Read metadata must use the closed governed-read vocabulary' - ); + return failReadDefinition('Read metadata must use the closed governed-read vocabulary'); } }; @@ -306,69 +231,37 @@ export const defineRead = < >( descriptor: ReadDescriptor, ...definition: readonly [ - handler: ReadHandler< - InputSchema, - ResultSchema, - Services, - Error, - Requirements - >, + handler: ReadHandler, serviceFactory: ReadServiceFactory, permissionTargetResolver: ReadPermissionTargetResolver, - resultPermissionTargetResolver?: ReadResultPermissionTargetResolver< - ResultSchema['Type'] - >, - executablePolicies?: readonly ActionPolicy< - InputSchema['Type'], - NoInfer - >[], + resultPermissionTargetResolver?: ReadResultPermissionTargetResolver, + executablePolicies?: readonly ActionPolicy>[], ] -): ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements -> => { - const [ - handler, - serviceFactory, - permissionTargetResolver, - resultPermissionTargetResolver, - executablePolicies = [], - ] = definition; +): ReadRegistration => { + const [handler, serviceFactory, permissionTargetResolver, resultPermissionTargetResolver, executablePolicies = []] = + definition; validateReadDescriptorInput(descriptor); - validateReadVocabulary( - descriptor, - permissionTargetResolver, - resultPermissionTargetResolver - ); + validateReadVocabulary(descriptor, permissionTargetResolver, resultPermissionTargetResolver); if ( !Array.isArray(descriptor.policies) || descriptor.policies.some( - ({ denialStatus, policyKey }) => - ![409, 422].includes(denialStatus) || policyKey.length === 0 + ({ denialStatus, policyKey }) => ![409, 422].includes(denialStatus) || policyKey.length === 0, ) || descriptor.policies.length !== executablePolicies.length || executablePolicies.some( (policy, index) => !isActionPolicy(policy) || descriptor.policies[index]?.policyKey !== policy.policyKey || - !isOwnerCompatiblePolicy(policy, descriptor.owningModuleKey) + !isOwnerCompatiblePolicy(policy, descriptor.owningModuleKey), ) ) { - return failReadDefinition( - 'Read policies must be an explicit array of Policy references' - ); + return failReadDefinition('Read policies must be an explicit array of Policy references'); } const frozenDescriptor = Object.freeze({ ...descriptor, entrypoint: descriptor.entrypoint, evidencePolicy: Object.freeze({ ...descriptor.evidencePolicy }), - policies: Object.freeze( - descriptor.policies.map((reference) => Object.freeze({ ...reference })) - ), + policies: Object.freeze(descriptor.policies.map((reference) => Object.freeze({ ...reference }))), }); const privateValue = { handler, @@ -383,10 +276,7 @@ export const defineRead = < if (resultPermissionTargetResolver === undefined) { return ReadPrivateStorage.create(Object.freeze(privateValue), publicFields); } - return ReadPrivateStorage.create( - Object.freeze({ ...privateValue, resultPermissionTargetResolver }), - publicFields - ); + return ReadPrivateStorage.create(Object.freeze({ ...privateValue, resultPermissionTargetResolver }), publicFields); }; export const getReadPolicyImplementations = < @@ -397,16 +287,8 @@ export const getReadPolicyImplementations = < Error, Requirements, >( - registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > -): readonly ActionPolicy[] => - ReadPrivateStorage.getValue(registration).policies; + registration: ReadRegistration, +): readonly ActionPolicy[] => ReadPrivateStorage.getValue(registration).policies; export const getReadResultPermissionTargetResolver = < InputSchema extends Schema.ConstraintDecoder, @@ -416,14 +298,7 @@ export const getReadResultPermissionTargetResolver = < Error, Requirements, >( - registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > + registration: ReadRegistration, ): ReadResultPermissionTargetResolver | undefined => ReadPrivateStorage.getValue(registration).resultPermissionTargetResolver; @@ -435,14 +310,7 @@ export const getReadPermissionTargetResolver = < Error, Requirements, >( - registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > + registration: ReadRegistration, ): ReadPermissionTargetResolver => ReadPrivateStorage.getValue(registration).permissionTargetResolver; @@ -454,14 +322,7 @@ export const getReadHandler = < Error, Requirements, >( - registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > + registration: ReadRegistration, ): ReadHandler => ReadPrivateStorage.getValue(registration).handler; @@ -473,13 +334,5 @@ export const getReadServiceFactory = < Error, Requirements, >( - registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - Error, - Requirements - > -): ReadServiceFactory => - ReadPrivateStorage.getValue(registration).serviceFactory; + registration: ReadRegistration, +): ReadServiceFactory => ReadPrivateStorage.getValue(registration).serviceFactory; diff --git a/app/packages/core-runtime/src/reads/read-evidence-persistence-error.ts b/app/packages/core-runtime/src/reads/read-evidence-persistence-error.ts index 130ce1644..c143fc55d 100644 --- a/app/packages/core-runtime/src/reads/read-evidence-persistence-error.ts +++ b/app/packages/core-runtime/src/reads/read-evidence-persistence-error.ts @@ -5,5 +5,5 @@ export class ReadEvidencePersistenceError extends Schema.TaggedError()( 'ReadEvidenceValidationError', - { code: Schema.Literal('read_evidence_invalid'), reason: Schema.String } + { code: Schema.Literal('read_evidence_invalid'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/reads/read-handler-execution-error.ts b/app/packages/core-runtime/src/reads/read-handler-execution-error.ts index 8f894a2e8..8eefcf14b 100644 --- a/app/packages/core-runtime/src/reads/read-handler-execution-error.ts +++ b/app/packages/core-runtime/src/reads/read-handler-execution-error.ts @@ -5,5 +5,5 @@ export class ReadHandlerExecutionError extends Schema.TaggedError()( - 'ReadHandlerNotFound', - { code: Schema.Literal('read_handler_not_found'), reason: Schema.String } -) {} +export class ReadHandlerNotFound extends Schema.TaggedError()('ReadHandlerNotFound', { + code: Schema.Literal('read_handler_not_found'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/reads/read-handler-unavailable.ts b/app/packages/core-runtime/src/reads/read-handler-unavailable.ts index f015bc2a1..320bf7809 100644 --- a/app/packages/core-runtime/src/reads/read-handler-unavailable.ts +++ b/app/packages/core-runtime/src/reads/read-handler-unavailable.ts @@ -1,6 +1,6 @@ import { Schema } from 'effect'; -export class ReadHandlerUnavailable extends Schema.TaggedError()( - 'ReadHandlerUnavailable', - { code: Schema.Literal('read_handler_unavailable'), reason: Schema.String } -) {} +export class ReadHandlerUnavailable extends Schema.TaggedError()('ReadHandlerUnavailable', { + code: Schema.Literal('read_handler_unavailable'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/reads/read-input-validation-error.ts b/app/packages/core-runtime/src/reads/read-input-validation-error.ts index d838ad111..6925cfd18 100644 --- a/app/packages/core-runtime/src/reads/read-input-validation-error.ts +++ b/app/packages/core-runtime/src/reads/read-input-validation-error.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class ReadInputValidationError extends Schema.TaggedError()( 'ReadInputValidationError', - { code: Schema.Literal('read_input_invalid'), reason: Schema.String } + { code: Schema.Literal('read_input_invalid'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/reads/read-permission-denied.ts b/app/packages/core-runtime/src/reads/read-permission-denied.ts index 958314fea..dfcad5ca0 100644 --- a/app/packages/core-runtime/src/reads/read-permission-denied.ts +++ b/app/packages/core-runtime/src/reads/read-permission-denied.ts @@ -1,6 +1,6 @@ import { Schema } from 'effect'; -export class ReadPermissionDenied extends Schema.TaggedError()( - 'ReadPermissionDenied', - { code: Schema.Literal('read_permission_denied'), reason: Schema.String } -) {} +export class ReadPermissionDenied extends Schema.TaggedError()('ReadPermissionDenied', { + code: Schema.Literal('read_permission_denied'), + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/reads/read-permission-unavailable.ts b/app/packages/core-runtime/src/reads/read-permission-unavailable.ts index eda7ebdd8..3a0a19c22 100644 --- a/app/packages/core-runtime/src/reads/read-permission-unavailable.ts +++ b/app/packages/core-runtime/src/reads/read-permission-unavailable.ts @@ -2,5 +2,5 @@ import { Schema } from 'effect'; export class ReadPermissionUnavailable extends Schema.TaggedError()( 'ReadPermissionUnavailable', - { code: Schema.Literal('read_permission_unavailable'), reason: Schema.String } + { code: Schema.Literal('read_permission_unavailable'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/reads/read-policy-denied.ts b/app/packages/core-runtime/src/reads/read-policy-denied.ts index e337706d1..2d046d10d 100644 --- a/app/packages/core-runtime/src/reads/read-policy-denied.ts +++ b/app/packages/core-runtime/src/reads/read-policy-denied.ts @@ -1,11 +1,8 @@ import { Schema } from 'effect'; -export class ReadPolicyDenied extends Schema.TaggedError()( - 'ReadPolicyDenied', - { - code: Schema.Literal('read_policy_denied'), - httpStatus: Schema.Literals([409, 422]), - policyReasonCode: Schema.String, - reason: Schema.String, - } -) {} +export class ReadPolicyDenied extends Schema.TaggedError()('ReadPolicyDenied', { + code: Schema.Literal('read_policy_denied'), + httpStatus: Schema.Literals([409, 422]), + policyReasonCode: Schema.String, + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/src/reads/read-policy-evaluation-error.ts b/app/packages/core-runtime/src/reads/read-policy-evaluation-error.ts index b6881372e..fd33f851e 100644 --- a/app/packages/core-runtime/src/reads/read-policy-evaluation-error.ts +++ b/app/packages/core-runtime/src/reads/read-policy-evaluation-error.ts @@ -5,5 +5,5 @@ export class ReadPolicyEvaluationError extends Schema.TaggedError()( 'ReadResultValidationError', - { code: Schema.Literal('read_result_invalid'), reason: Schema.String } + { code: Schema.Literal('read_result_invalid'), reason: Schema.String }, ) {} diff --git a/app/packages/core-runtime/src/reads/repository.ts b/app/packages/core-runtime/src/reads/repository.ts index a10d9e2f1..2f001f036 100644 --- a/app/packages/core-runtime/src/reads/repository.ts +++ b/app/packages/core-runtime/src/reads/repository.ts @@ -25,16 +25,12 @@ export interface PersistReadEvidenceInput { readonly targetResourceType?: string; } -const accessKind = ( - kind: ReadAccessKind -): 'download' | 'export' | 'list' | 'read' | 'search' => +const accessKind = (kind: ReadAccessKind): 'download' | 'export' | 'list' | 'read' | 'search' => kind === 'detail' || kind === 'report' ? 'read' : kind; const READ_EVIDENCE_PERSISTENCE_TIMEOUT = Duration.seconds(30); -const readEvidencePersistenceFailure = ( - cause: unknown -): ReadEvidencePersistenceError => { +const readEvidencePersistenceFailure = (cause: unknown): ReadEvidencePersistenceError => { const failure = new ReadEvidencePersistenceError({ code: 'read_evidence_persistence_failed', reason: 'Required read evidence could not be persisted', @@ -47,7 +43,7 @@ const readEvidencePersistenceFailure = ( export const persistReadEvidence = ( executor: CoreDbExecutor, - input: PersistReadEvidenceInput + input: PersistReadEvidenceInput, ): Effect.Effect => executor .insert(dataAccessEvents) @@ -78,12 +74,7 @@ export const persistReadEvidence = ( Effect.mapError(readEvidencePersistenceFailure), Effect.timeoutOrElse({ duration: READ_EVIDENCE_PERSISTENCE_TIMEOUT, - orElse: () => - Effect.fail( - readEvidencePersistenceFailure( - 'Read evidence persistence timed out' - ) - ), + orElse: () => Effect.fail(readEvidencePersistenceFailure('Read evidence persistence timed out')), }), - Effect.asVoid + Effect.asVoid, ); diff --git a/app/packages/core-runtime/src/reads/runtime.ts b/app/packages/core-runtime/src/reads/runtime.ts index a9ee18ebd..82305ce1e 100644 --- a/app/packages/core-runtime/src/reads/runtime.ts +++ b/app/packages/core-runtime/src/reads/runtime.ts @@ -13,15 +13,9 @@ import { installOperationalScope } from '../db/scoped-transaction.ts'; import type { CoreTransaction } from '../db/types.ts'; import type { ModuleEntrypointGatewayService } from '../modules/module-entrypoint-gateway.ts'; import { ModuleEntrypointGateway } from '../modules/module-entrypoint-gateway.ts'; -import type { - OperationalScope, - OperationalScopeResolverService, -} from '../operations/context.ts'; +import type { OperationalScope, OperationalScopeResolverService } from '../operations/context.ts'; import { OperationalScopeResolver } from '../operations/context.ts'; -import { - ContextAccess, - LEGAL_ENTITY_PERMISSION_KEYS, -} from '../permissions/context-access.ts'; +import { ContextAccess, LEGAL_ENTITY_PERMISSION_KEYS } from '../permissions/context-access.ts'; import { validateReadEvidenceMetadata } from './context.ts'; import type { AtomicResolvedReadPermissionTarget, @@ -59,25 +53,16 @@ const withOptionalProperty = < condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); -const CorrelationIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe( - Schema.brand('ReadCorrelationId') -); -const TargetModuleKeySchema = Schema.String.check(Schema.isMinLength(1)).pipe( - Schema.brand('ReadTargetModuleKey') -); -const TargetResourceIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe( - Schema.brand('ReadTargetResourceId') -); -const TargetResourceTypeSchema = Schema.String.check( - Schema.isMinLength(1) -).pipe(Schema.brand('ReadTargetResourceType')); -const TraceIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe( - Schema.brand('ReadTraceId') +const CorrelationIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand('ReadCorrelationId')); +const TargetModuleKeySchema = Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand('ReadTargetModuleKey')); +const TargetResourceIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand('ReadTargetResourceId')); +const TargetResourceTypeSchema = Schema.String.check(Schema.isMinLength(1)).pipe( + Schema.brand('ReadTargetResourceType'), ); +const TraceIdSchema = Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand('ReadTraceId')); const ReadTransportSchema = Schema.Struct({ correlationId: CorrelationIdSchema, @@ -104,26 +89,17 @@ export interface ReadRuntimeOptions { readonly onStage?: (stage: ReadRuntimeStage) => void; } -const stableTargetKey = (value: string): boolean => - value.length > 0 && value.length <= 300; -const PermissionDecisionSchema = Schema.Literals([ - 'allowed', - 'denied', - 'unavailable', -]); +const stableTargetKey = (value: string): boolean => value.length > 0 && value.length <= 300; +const PermissionDecisionSchema = Schema.Literals(['allowed', 'denied', 'unavailable']); type PermissionDecision = typeof PermissionDecisionSchema.Type; -const atomicTargetIsValid = ( - target: AtomicResolvedReadPermissionTarget -): boolean => { +const atomicTargetIsValid = (target: AtomicResolvedReadPermissionTarget): boolean => { if (target.kind === 'tenant') { return true; } if (target.kind === 'legal_entity') { return ( target.permission === undefined || - LEGAL_ENTITY_PERMISSION_KEYS.some( - (permission) => permission === target.permission - ) + LEGAL_ENTITY_PERMISSION_KEYS.some((permission) => permission === target.permission) ); } if (target.kind === 'module') { @@ -136,20 +112,15 @@ const atomicTargetIsValid = ( ); }; -const usesForbiddenAlternativeTenantPermission = ( - target: AtomicResolvedReadPermissionTarget -): boolean => - target.kind === 'tenant' && - (target.permission === 'access' || target.permission === 'impersonate'); +const usesForbiddenAlternativeTenantPermission = (target: AtomicResolvedReadPermissionTarget): boolean => + target.kind === 'tenant' && (target.permission === 'access' || target.permission === 'impersonate'); -const canonicalPermissionTarget = ( - target: ResolvedReadPermissionTarget -): AtomicResolvedReadPermissionTarget => +const canonicalPermissionTarget = (target: ResolvedReadPermissionTarget): AtomicResolvedReadPermissionTarget => target.kind === 'any_of' ? target.targets[0] : target; const targetIsValid = ( declared: 'legal_entity' | 'module' | 'resource' | 'tenant', - target: ResolvedReadPermissionTarget + target: ResolvedReadPermissionTarget, ): boolean => { const canonical = canonicalPermissionTarget(target); if (canonical.kind !== declared || !atomicTargetIsValid(canonical)) { @@ -162,9 +133,7 @@ const targetIsValid = ( target.targets.length >= 2 && target.targets.length <= 5 && target.targets.every( - (candidate) => - atomicTargetIsValid(candidate) && - !usesForbiddenAlternativeTenantPermission(candidate) + (candidate) => atomicTargetIsValid(candidate) && !usesForbiddenAlternativeTenantPermission(candidate), ) ); }; @@ -189,21 +158,17 @@ const decisionFor = ( readonly decision: PermissionDecision; readonly key: string; }[], - expectedKey: string + expectedKey: string, ): PermissionDecision => { const [decision, ...unexpected] = decisions; - return unexpected.length === 0 && decision?.key === expectedKey - ? decision.decision - : 'unavailable'; + return unexpected.length === 0 && decision?.key === expectedKey ? decision.decision : 'unavailable'; }; -const checkAtomicPermissionTarget = < - AccessValue extends (typeof ContextAccess)['Service'], ->( +const checkAtomicPermissionTarget = ( contextAccess: AccessValue, scope: OperationalScope, target: AtomicResolvedReadPermissionTarget, - allowMissingLegalEntity: boolean + allowMissingLegalEntity: boolean, ): Effect.Effect => { if (target.kind === 'tenant') { return contextAccess @@ -232,9 +197,7 @@ const checkAtomicPermissionTarget = < principalId: scope.principalId, tenantId: scope.tenantId, }); - return decision.pipe( - Effect.map((decisions) => decisionFor(decisions, legalEntityId)) - ); + return decision.pipe(Effect.map((decisions) => decisionFor(decisions, legalEntityId))); } if (target.kind === 'module') { return contextAccess @@ -257,34 +220,26 @@ const checkAtomicPermissionTarget = < .pipe(Effect.map((decisions) => decisionFor(decisions, expectedKey))); }; -const checkPermissionTarget = < - AccessValue extends (typeof ContextAccess)['Service'], ->( +const checkPermissionTarget = ( contextAccess: AccessValue, scope: OperationalScope, target: ResolvedReadPermissionTarget, - allowMissingLegalEntity: boolean + allowMissingLegalEntity: boolean, ): Effect.Effect => { const targets = target.kind === 'any_of' ? target.targets : [target]; - const mayAuthorizeWithoutLegalEntity = - allowMissingLegalEntity && target.kind !== 'any_of'; + const mayAuthorizeWithoutLegalEntity = allowMissingLegalEntity && target.kind !== 'any_of'; return Effect.all( targets.map((candidate) => - checkAtomicPermissionTarget( - contextAccess, - scope, - candidate, - mayAuthorizeWithoutLegalEntity - ) + checkAtomicPermissionTarget(contextAccess, scope, candidate, mayAuthorizeWithoutLegalEntity), ), - { concurrency: 5 } + { concurrency: 5 }, ).pipe( Effect.map((decisions) => { if (decisions.includes('allowed')) { return 'allowed'; } return decisions.includes('unavailable') ? 'unavailable' : 'denied'; - }) + }), ); }; @@ -298,10 +253,7 @@ const sanitizeReadHandlerFailure = (failure: Failure) => reason: 'The read handler failed unexpectedly', }); -const preserveFailureCause = ( - failure: Failure, - cause: unknown -): Failure => +const preserveFailureCause = (failure: Failure, cause: unknown): Failure => Object.defineProperty(failure, 'cause', { configurable: false, enumerable: false, @@ -309,128 +261,113 @@ const preserveFailureCause = ( writable: false, }); -const checkTenantResultPermission = Effect.fnUntraced( - function* checkTenantResultPermission< - AccessValue extends (typeof ContextAccess)['Service'], - >( - contextAccess: AccessValue, - scope: OperationalScope, - permissionTarget: Extract - ) { - const decisions = yield* contextAccess.tenants({ - permission: permissionTarget.permission, - principalId: scope.principalId, - tenantIds: [scope.tenantId], +const checkTenantResultPermission = Effect.fnUntraced(function* checkTenantResultPermission< + AccessValue extends (typeof ContextAccess)['Service'], +>( + contextAccess: AccessValue, + scope: OperationalScope, + permissionTarget: Extract, +) { + const decisions = yield* contextAccess.tenants({ + permission: permissionTarget.permission, + principalId: scope.principalId, + tenantIds: [scope.tenantId], + }); + const decision = decisionFor(decisions, scope.tenantId); + if (decision === 'unavailable') { + return yield* new ReadPermissionUnavailable({ + code: 'read_permission_unavailable', + reason: 'Read result authorization is temporarily unavailable', }); - const decision = decisionFor(decisions, scope.tenantId); - if (decision === 'unavailable') { - return yield* new ReadPermissionUnavailable({ - code: 'read_permission_unavailable', - reason: 'Read result authorization is temporarily unavailable', - }); - } - if (decision === 'denied') { - return yield* new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'The read result contains a forbidden resource', - }); - } - return yield* Effect.void; } -); + if (decision === 'denied') { + return yield* new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'The read result contains a forbidden resource', + }); + } + return yield* Effect.void; +}); -const checkResultPermissions = Effect.fn('ReadRuntime.checkResultPermissions')( - function* checkResultPermissionsEffect< - Result, - AccessValue extends (typeof ContextAccess)['Service'], - >( - contextAccess: AccessValue, +const checkResultPermissions = Effect.fn('ReadRuntime.checkResultPermissions')(function* checkResultPermissionsEffect< + Result, + AccessValue extends (typeof ContextAccess)['Service'], +>( + contextAccess: AccessValue, + result: Result, + scope: OperationalScope, + permissionTarget: ResolvedReadPermissionTarget, + resolver: ( result: Result, scope: OperationalScope, - permissionTarget: ResolvedReadPermissionTarget, - resolver: ( - result: Result, - scope: OperationalScope - ) => readonly { - readonly moduleId: string; - readonly resourceId: string; - readonly resourceType: string; - }[] + ) => readonly { + readonly moduleId: string; + readonly resourceId: string; + readonly resourceType: string; + }[], +) { + const resultTargets = yield* Effect.try({ + catch: (resolverDefect) => + preserveFailureCause( + new ReadHandlerExecutionError({ + code: 'read_handler_execution_failed', + reason: 'The read result permission targets are invalid', + }), + resolverDefect, + ), + try: () => resolver(result, scope), + }); + if ( + resultTargets.some( + (target) => + !stableTargetKey(target.moduleId) || + !stableTargetKey(target.resourceId) || + !stableTargetKey(target.resourceType), + ) ) { - const resultTargets = yield* Effect.try({ - catch: (resolverDefect) => - preserveFailureCause( - new ReadHandlerExecutionError({ - code: 'read_handler_execution_failed', - reason: 'The read result permission targets are invalid', - }), - resolverDefect - ), - try: () => resolver(result, scope), - }); - if ( - resultTargets.some( - (target) => - !stableTargetKey(target.moduleId) || - !stableTargetKey(target.resourceId) || - !stableTargetKey(target.resourceType) - ) - ) { - return yield* new ReadHandlerExecutionError({ - code: 'read_handler_execution_failed', - reason: 'The read result permission targets are invalid', - }); - } - if (resultTargets.length === 0) { - return yield* Effect.void; - } - if (permissionTarget.kind === 'tenant') { - return yield* checkTenantResultPermission( - contextAccess, - scope, - permissionTarget - ); - } - if (scope.legalEntityId === undefined) { - return yield* new ReadHandlerExecutionError({ - code: 'read_handler_execution_failed', - reason: 'The read result permission targets are invalid', - }); - } - const decisions = yield* contextAccess.resources({ - legalEntityId: scope.legalEntityId, - principalId: scope.principalId, - resources: resultTargets, - tenantId: scope.tenantId, + return yield* new ReadHandlerExecutionError({ + code: 'read_handler_execution_failed', + reason: 'The read result permission targets are invalid', }); - const malformed = - decisions.length !== resultTargets.length || - decisions.some(({ key }, index) => { - const target = resultTargets[index]; - return ( - target === undefined || - key !== - `${target.moduleId}:${target.resourceType}:${target.resourceId}` - ); - }); - if ( - malformed || - decisions.some(({ decision }) => decision === 'unavailable') - ) { - return yield* new ReadPermissionUnavailable({ - code: 'read_permission_unavailable', - reason: 'Read result authorization is temporarily unavailable', - }); - } - if (decisions.some(({ decision }) => decision === 'denied')) { - return yield* new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'The read result contains a forbidden resource', - }); - } + } + if (resultTargets.length === 0) { return yield* Effect.void; } -); + if (permissionTarget.kind === 'tenant') { + return yield* checkTenantResultPermission(contextAccess, scope, permissionTarget); + } + if (scope.legalEntityId === undefined) { + return yield* new ReadHandlerExecutionError({ + code: 'read_handler_execution_failed', + reason: 'The read result permission targets are invalid', + }); + } + const decisions = yield* contextAccess.resources({ + legalEntityId: scope.legalEntityId, + principalId: scope.principalId, + resources: resultTargets, + tenantId: scope.tenantId, + }); + const malformed = + decisions.length !== resultTargets.length || + decisions.some(({ key }, index) => { + const target = resultTargets[index]; + return target === undefined || key !== `${target.moduleId}:${target.resourceType}:${target.resourceId}`; + }); + if (malformed || decisions.some(({ decision }) => decision === 'unavailable')) { + return yield* new ReadPermissionUnavailable({ + code: 'read_permission_unavailable', + reason: 'Read result authorization is temporarily unavailable', + }); + } + if (decisions.some(({ decision }) => decision === 'denied')) { + return yield* new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'The read result contains a forbidden resource', + }); + } + return yield* Effect.void; +}); const readRuntimeFromDependencies = < DatabaseValue extends (typeof CoreDatabase)['Service'], @@ -442,7 +379,7 @@ const readRuntimeFromDependencies = < gateway: GatewayValue, scopeResolver: ScopeValue, contextAccess: AccessValue, - options: ReadRuntimeOptions = {} + options: ReadRuntimeOptions = {}, ) => { const stage = (value: ReadRuntimeStage): void => options.onStage?.(value); @@ -456,28 +393,19 @@ const readRuntimeFromDependencies = < >(input: { readonly input: unknown; readonly principal: unknown; - readonly registration: ReadRegistration< - InputSchema, - ResultSchema, - Owner, - Services, - HandlerError, - Requirements - >; + readonly registration: ReadRegistration; readonly transport: unknown; }) { - const decodedInput = yield* Schema.decodeUnknownEffect( - input.registration.descriptor.inputSchema - )(input.input).pipe( + const decodedInput = yield* Schema.decodeUnknownEffect(input.registration.descriptor.inputSchema)(input.input).pipe( Effect.mapError((parseIssue) => preserveFailureCause( new ReadInputValidationError({ code: 'read_input_invalid', reason: 'The read input does not match its declared schema', }), - parseIssue - ) - ) + parseIssue, + ), + ), ); const queryHash = input.registration.descriptor.evidencePolicy.captureMode === 'hash_only' @@ -488,21 +416,19 @@ const readRuntimeFromDependencies = < code: 'read_input_invalid', reason: 'The read input cannot be normalized safely', }), - normalizationDefect + normalizationDefect, ), try: () => computeCanonicalValueHash(decodedInput), }) : undefined; - const principal = yield* decodeTrustedPrincipalContext( - input.principal - ).pipe( + const principal = yield* decodeTrustedPrincipalContext(input.principal).pipe( Effect.filterOrFail( (context) => !isTrustedSupportRecoveryPrincipalContext(context), () => new ReadInputValidationError({ code: 'read_input_invalid', reason: 'Support recovery context is not valid for Reads', - }) + }), ), Effect.mapError((principalFailure) => preserveFailureCause( @@ -510,22 +436,20 @@ const readRuntimeFromDependencies = < code: 'read_input_invalid', reason: 'The trusted read identity is invalid', }), - principalFailure - ) - ) + principalFailure, + ), + ), ); - const transport = yield* Schema.decodeUnknownEffect(ReadTransportSchema)( - input.transport - ).pipe( + const transport = yield* Schema.decodeUnknownEffect(ReadTransportSchema)(input.transport).pipe( Effect.mapError((parseIssue) => preserveFailureCause( new ReadInputValidationError({ code: 'read_input_invalid', reason: 'The read transport metadata is invalid', }), - parseIssue - ) - ) + parseIssue, + ), + ), ); stage('input_decoded'); const scope = yield* scopeResolver.resolve( @@ -538,8 +462,8 @@ const readRuntimeFromDependencies = < transport.traceId !== undefined, 'traceId', transport.traceId, - {} - ) + {}, + ), ); stage('scope_validated'); const permissionTarget = yield* Effect.try({ @@ -549,22 +473,13 @@ const readRuntimeFromDependencies = < code: 'read_handler_execution_failed', reason: 'The declared read permission target could not be resolved', }), - resolverDefect - ), - try: () => - getReadPermissionTargetResolver(input.registration)( - decodedInput, - scope + resolverDefect, ), + try: () => getReadPermissionTargetResolver(input.registration)(decodedInput, scope), }); if ( - !targetIsValid( - input.registration.descriptor.permissionTarget, - permissionTarget - ) || - (getReadResultPermissionTargetResolver(input.registration) !== - undefined && - permissionTarget.kind === 'any_of') + !targetIsValid(input.registration.descriptor.permissionTarget, permissionTarget) || + (getReadResultPermissionTargetResolver(input.registration) !== undefined && permissionTarget.kind === 'any_of') ) { return yield* new ReadHandlerExecutionError({ code: 'read_handler_execution_failed', @@ -572,9 +487,7 @@ const readRuntimeFromDependencies = < }); } const permissionTargetMetadata = targetMetadata(permissionTarget); - const snapshot = yield* gateway.prepareSnapshot(scope, [ - input.registration.descriptor.entrypoint, - ]); + const snapshot = yield* gateway.prepareSnapshot(scope, [input.registration.descriptor.entrypoint]); yield* gateway.check(snapshot, input.registration.descriptor.entrypoint); stage('module_state_checked'); @@ -582,7 +495,7 @@ const readRuntimeFromDependencies = < contextAccess, scope, permissionTarget, - input.registration.descriptor.legalEntityScope === 'forbidden' + input.registration.descriptor.legalEntityScope === 'forbidden', ); stage('permission_checked'); if (permissionDecision === 'denied') { @@ -591,8 +504,7 @@ const readRuntimeFromDependencies = < withOptionalProperty( { accessKind: input.registration.descriptor.accessKind, - captureMode: - input.registration.descriptor.evidencePolicy.captureMode, + captureMode: input.registration.descriptor.evidencePolicy.captureMode, outcome: 'denied', outcomeCode: 'spicedb_permission_denied', outcomeStage: 'authz', @@ -607,8 +519,8 @@ const readRuntimeFromDependencies = < scope, servingModuleKey: input.registration.descriptor.owningModuleKey, ...permissionTargetMetadata, - } - ) + }, + ), ); return yield* new ReadPermissionDenied({ code: 'read_permission_denied', @@ -632,7 +544,7 @@ const readRuntimeFromDependencies = < new ReadPolicyEvaluationError({ code: 'read_policy_evaluation_failed', reason: 'A required read Policy is unavailable', - }) + }), ); } const { descriptor } = input.registration; @@ -651,7 +563,7 @@ const readRuntimeFromDependencies = < transport.traceId !== undefined, 'traceId', transport.traceId, - {} + {}, ), }) .pipe( @@ -675,8 +587,8 @@ const readRuntimeFromDependencies = < resultCount: 0, scope, servingModuleKey: descriptor.owningModuleKey, - } - ) + }, + ), ).pipe( Effect.andThen( Effect.fail( @@ -685,124 +597,102 @@ const readRuntimeFromDependencies = < httpStatus: policyDescriptor.denialStatus, policyReasonCode: failure.reasonCode, reason: failure.reason, - }) - ) - ) - ) - ) + }), + ), + ), + ), + ), ); }, - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); stage('policies_checked'); const transactionResult = database.executor .transaction( - Effect.fn('ReadRuntime.readTransactionBody')( - function* readTransactionBody(transaction: CoreTransaction) { - const scoped = yield* installOperationalScope(transaction, scope); - stage('scope_installed'); - const services = yield* getReadServiceFactory(input.registration)( - scoped, - scope - ); - const handlerResult = yield* Effect.suspend(() => - getReadHandler(input.registration)( - decodedInput, - Object.freeze({ - readKey: input.registration.descriptor.readKey, - scope, - services, - }) - ) - ).pipe(Effect.mapError(sanitizeReadHandlerFailure)); - stage('handler_executed'); - const result = yield* Schema.decodeUnknownEffect( - Schema.toType(input.registration.descriptor.resultSchema) - )(handlerResult.result).pipe( - Effect.mapError((parseIssue) => - preserveFailureCause( - new ReadResultValidationError({ - code: 'read_result_invalid', - reason: - 'The read result does not match its declared schema', - }), - parseIssue - ) - ) - ); - stage('result_decoded'); - const resultPermissionResolver = - getReadResultPermissionTargetResolver(input.registration); - if (resultPermissionResolver !== undefined) { - yield* checkResultPermissions( - contextAccess, - result, + Effect.fn('ReadRuntime.readTransactionBody')(function* readTransactionBody(transaction: CoreTransaction) { + const scoped = yield* installOperationalScope(transaction, scope); + stage('scope_installed'); + const services = yield* getReadServiceFactory(input.registration)(scoped, scope); + const handlerResult = yield* Effect.suspend(() => + getReadHandler(input.registration)( + decodedInput, + Object.freeze({ + readKey: input.registration.descriptor.readKey, scope, - permissionTarget, - resultPermissionResolver - ); - } - const evidence = yield* validateReadEvidenceMetadata( - input.registration.descriptor.evidencePolicy.captureMode, - handlerResult.evidence - ); - yield* persistReadEvidence( - transaction, + services, + }), + ), + ).pipe(Effect.mapError(sanitizeReadHandlerFailure)); + stage('handler_executed'); + const result = yield* Schema.decodeUnknownEffect(Schema.toType(input.registration.descriptor.resultSchema))( + handlerResult.result, + ).pipe( + Effect.mapError((parseIssue) => + preserveFailureCause( + new ReadResultValidationError({ + code: 'read_result_invalid', + reason: 'The read result does not match its declared schema', + }), + parseIssue, + ), + ), + ); + stage('result_decoded'); + const resultPermissionResolver = getReadResultPermissionTargetResolver(input.registration); + if (resultPermissionResolver !== undefined) { + yield* checkResultPermissions(contextAccess, result, scope, permissionTarget, resultPermissionResolver); + } + const evidence = yield* validateReadEvidenceMetadata( + input.registration.descriptor.evidencePolicy.captureMode, + handlerResult.evidence, + ); + yield* persistReadEvidence( + transaction, + withOptionalProperty( withOptionalProperty( withOptionalProperty( - withOptionalProperty( - { - accessKind: input.registration.descriptor.accessKind, - captureMode: - input.registration.descriptor.evidencePolicy - .captureMode, - outcome: 'allowed', - outcomeCode: 'read_allowed', - outcomeStage: 'evidence', - policyKey: - input.registration.descriptor.evidencePolicy.policyKey, - }, - queryHash !== undefined, - 'queryHash', - queryHash, - { - readKey: input.registration.descriptor.readKey, - resultCount: evidence.resultCount, - } - ), - evidence.resultFingerprintHash !== undefined, - 'resultFingerprintHash', - evidence.resultFingerprintHash, - {} + { + accessKind: input.registration.descriptor.accessKind, + captureMode: input.registration.descriptor.evidencePolicy.captureMode, + outcome: 'allowed', + outcomeCode: 'read_allowed', + outcomeStage: 'evidence', + policyKey: input.registration.descriptor.evidencePolicy.policyKey, + }, + queryHash !== undefined, + 'queryHash', + queryHash, + { + readKey: input.registration.descriptor.readKey, + resultCount: evidence.resultCount, + }, ), - evidence.resultFingerprintSchema !== undefined, - 'resultFingerprintSchema', - evidence.resultFingerprintSchema, - { - scope, - servingModuleKey: - input.registration.descriptor.owningModuleKey, - ...permissionTargetMetadata, - } - ) - ); - stage('evidence_persisted'); - return result; - } - ) + evidence.resultFingerprintHash !== undefined, + 'resultFingerprintHash', + evidence.resultFingerprintHash, + {}, + ), + evidence.resultFingerprintSchema !== undefined, + 'resultFingerprintSchema', + evidence.resultFingerprintSchema, + { + scope, + servingModuleKey: input.registration.descriptor.owningModuleKey, + ...permissionTargetMetadata, + }, + ), + ); + stage('evidence_persisted'); + return result; + }), ) .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), Effect.tapError((failure) => Schema.is(SqlError)(failure) - ? Effect.logError( - 'Unexpected governed read transaction failure', - failure - ) - : Effect.void + ? Effect.logError('Unexpected governed read transaction failure', failure) + : Effect.void, ), Effect.mapError((transactionFailure) => Schema.is(SqlError)(transactionFailure) @@ -811,23 +701,17 @@ const readRuntimeFromDependencies = < code: 'read_handler_execution_failed', reason: 'The governed read transaction failed', }), - transactionFailure + transactionFailure, ) - : transactionFailure - ) + : transactionFailure, + ), ); const transactionExit = yield* Effect.exit(transactionResult); if (Exit.isSuccess(transactionExit)) { return transactionExit.value; } const { cause } = transactionExit; - if ( - !cause.reasons.some( - (reason) => - Cause.isFailReason(reason) && - Schema.is(ReadPermissionDenied)(reason.error) - ) - ) { + if (!cause.reasons.some((reason) => Cause.isFailReason(reason) && Schema.is(ReadPermissionDenied)(reason.error))) { return yield* Effect.failCause(cause); } const evidenceExit = yield* Effect.exit( @@ -836,8 +720,7 @@ const readRuntimeFromDependencies = < withOptionalProperty( { accessKind: input.registration.descriptor.accessKind, - captureMode: - input.registration.descriptor.evidencePolicy.captureMode, + captureMode: input.registration.descriptor.evidencePolicy.captureMode, outcome: 'denied', outcomeCode: 'read_permission_denied', outcomeStage: 'authz', @@ -852,15 +735,11 @@ const readRuntimeFromDependencies = < scope, servingModuleKey: input.registration.descriptor.owningModuleKey, ...permissionTargetMetadata, - } - ) - ) - ); - return yield* Effect.failCause( - Exit.isFailure(evidenceExit) - ? Cause.combine(evidenceExit.cause, cause) - : cause + }, + ), + ), ); + return yield* Effect.failCause(Exit.isFailure(evidenceExit) ? Cause.combine(evidenceExit.cause, cause) : cause); }); return Object.freeze({ runRead }); @@ -870,10 +749,9 @@ export { readRuntimeFromDependencies as makeReadRuntime }; export type ReadRuntimeService = ReturnType; -export class ReadRuntime extends Context.Service< - ReadRuntime, - ReadRuntimeService ->()('@app/core-runtime/reads/runtime/ReadRuntime') {} +export class ReadRuntime extends Context.Service()( + '@app/core-runtime/reads/runtime/ReadRuntime', +) {} export const ReadRuntimeLive = Layer.effect( ReadRuntime, @@ -882,11 +760,6 @@ export const ReadRuntimeLive = Layer.effect( const gateway = yield* ModuleEntrypointGateway; const scopeResolver = yield* OperationalScopeResolver; const contextAccess = yield* ContextAccess; - return readRuntimeFromDependencies( - database, - gateway, - scopeResolver, - contextAccess - ); - }) + return readRuntimeFromDependencies(database, gateway, scopeResolver, contextAccess); + }), ); diff --git a/app/packages/core-runtime/src/search/ingestion.ts b/app/packages/core-runtime/src/search/ingestion.ts index 2e9f937f4..80aed5fd7 100644 --- a/app/packages/core-runtime/src/search/ingestion.ts +++ b/app/packages/core-runtime/src/search/ingestion.ts @@ -8,14 +8,9 @@ import { import type { CoreSearchProjectionStoreService } from './projection.ts'; const PARTY_REGISTRY_MODULE_KEY = 'party.registry' as const; -const tenantIdSchema = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('TenantId') -); +const tenantIdSchema = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('TenantId')); type UnparsedCoreSearchIngestionObservation = typeof Schema.Unknown.Type; -type CoreSearchProjectionMutationSink = Pick< - CoreSearchProjectionStoreService, - 'apply' ->; +type CoreSearchProjectionMutationSink = Pick; export const CORE_SEARCH_PARTY_LIFECYCLE_TOPICS = [ 'party.registry.party-created.v1', @@ -34,8 +29,7 @@ export const CORE_SEARCH_PARTY_LIFECYCLE_TOPICS = [ 'party.registry.counterparty-role-ended.v1', 'party.registry.search-rebuild-requested.v1', ] as const; -export type CoreSearchPartyLifecycleTopic = - (typeof CORE_SEARCH_PARTY_LIFECYCLE_TOPICS)[number]; +export type CoreSearchPartyLifecycleTopic = (typeof CORE_SEARCH_PARTY_LIFECYCLE_TOPICS)[number]; export const CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS = [ 'party.registry.project-party-created-to-search', @@ -54,13 +48,10 @@ export const CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS = [ 'party.registry.project-counterparty-role-ended-to-search', 'party.registry.rebuild-search', ] as const; -export type CoreSearchPartyProjectorWorkerKey = - (typeof CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS)[number]; +export type CoreSearchPartyProjectorWorkerKey = (typeof CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS)[number]; const topicSchema = Schema.Literals(CORE_SEARCH_PARTY_LIFECYCLE_TOPICS); -const workerKeySchema = Schema.Literals( - CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS -); +const workerKeySchema = Schema.Literals(CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS); const versionSchema = Schema.String.check(Schema.isPattern(/^[1-9][0-9]*$/u)); export const CoreSearchIngestionObservationSchema = Schema.Struct({ @@ -72,8 +63,7 @@ export const CoreSearchIngestionObservationSchema = Schema.Struct({ topic: topicSchema, workerKey: workerKeySchema, }); -export type CoreSearchIngestionObservation = - typeof CoreSearchIngestionObservationSchema.Type; +export type CoreSearchIngestionObservation = typeof CoreSearchIngestionObservationSchema.Type; export interface CoreSearchIngestionRegistration { readonly consumerModuleKey: typeof PARTY_REGISTRY_MODULE_KEY; @@ -82,27 +72,23 @@ export interface CoreSearchIngestionRegistration { readonly workerKey: CoreSearchPartyProjectorWorkerKey; } -export const CORE_SEARCH_INGESTION_REGISTRATIONS: readonly CoreSearchIngestionRegistration[] = - Object.freeze( - CORE_SEARCH_PARTY_LIFECYCLE_TOPICS.flatMap((topic, index) => { - const workerKey = CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS[index]; - return workerKey === undefined - ? [] - : [ - Object.freeze({ - consumerModuleKey: PARTY_REGISTRY_MODULE_KEY, - producerModuleKey: PARTY_REGISTRY_MODULE_KEY, - topic, - workerKey, - }), - ]; - }) - ); +export const CORE_SEARCH_INGESTION_REGISTRATIONS: readonly CoreSearchIngestionRegistration[] = Object.freeze( + CORE_SEARCH_PARTY_LIFECYCLE_TOPICS.flatMap((topic, index) => { + const workerKey = CORE_SEARCH_PARTY_PROJECTOR_WORKER_KEYS[index]; + return workerKey === undefined + ? [] + : [ + Object.freeze({ + consumerModuleKey: PARTY_REGISTRY_MODULE_KEY, + producerModuleKey: PARTY_REGISTRY_MODULE_KEY, + topic, + workerKey, + }), + ]; + }), +); -const invalid = ( - reason: string, - cause?: unknown -): CoreSearchProjectionInvalid => { +const invalid = (reason: string, cause?: unknown): CoreSearchProjectionInvalid => { if (cause === undefined) { return new CoreSearchProjectionInvalid({ code: 'core_search_projection_invalid', @@ -118,33 +104,26 @@ const invalid = ( export interface CoreSearchIngestionService { readonly ingest: ( - input: UnparsedCoreSearchIngestionObservation + input: UnparsedCoreSearchIngestionObservation, ) => ReturnType; } /** Core-owned consumer seam for post-commit Party lifecycle observations. */ -export class CoreSearchIngestion extends Context.Service< - CoreSearchIngestion, - CoreSearchIngestionService ->()('@app/core-runtime/search/ingestion/CoreSearchIngestion') {} +export class CoreSearchIngestion extends Context.Service()( + '@app/core-runtime/search/ingestion/CoreSearchIngestion', +) {} -export const makeCoreSearchIngestion = ( - store: CoreSearchProjectionMutationSink -): CoreSearchIngestionService => ({ +export const makeCoreSearchIngestion = (store: CoreSearchProjectionMutationSink): CoreSearchIngestionService => ({ ingest: (input) => - Schema.decodeUnknownEffect(CoreSearchIngestionObservationSchema)( - input - ).pipe( - Effect.mapError((cause) => - invalid('Core Search ingestion observation is invalid', cause) - ), + Schema.decodeUnknownEffect(CoreSearchIngestionObservationSchema)(input).pipe( + Effect.mapError((cause) => invalid('Core Search ingestion observation is invalid', cause)), Effect.flatMap((observation) => { const registered = CORE_SEARCH_INGESTION_REGISTRATIONS.some( (registration) => registration.consumerModuleKey === observation.consumerModuleKey && registration.producerModuleKey === observation.producerModuleKey && registration.topic === observation.topic && - registration.workerKey === observation.workerKey + registration.workerKey === observation.workerKey, ); const mutationTenantId = observation.mutation.kind === 'upsert' @@ -164,14 +143,10 @@ export const makeCoreSearchIngestion = ( mutationModuleId !== observation.producerModuleKey || mutationVersion !== observation.projectionVersion ) { - return Effect.fail( - invalid( - 'Core Search ingestion identity does not match its post-commit observation' - ) - ); + return Effect.fail(invalid('Core Search ingestion identity does not match its post-commit observation')); } return store.apply(observation.mutation); - }) + }), ), }); @@ -180,5 +155,5 @@ export const CoreSearchIngestionLive = Layer.effect( Effect.gen(function* makeCoreSearchIngestionLive() { const store = yield* CoreSearchProjectionStore; return makeCoreSearchIngestion(store); - }) + }), ); diff --git a/app/packages/core-runtime/src/search/persistence.ts b/app/packages/core-runtime/src/search/persistence.ts index 6c02528ad..915a78178 100644 --- a/app/packages/core-runtime/src/search/persistence.ts +++ b/app/packages/core-runtime/src/search/persistence.ts @@ -32,9 +32,7 @@ import { } from './projection.ts'; const PersistedDocumentPayloadSchema = Schema.Struct({ - aliases: Schema.optionalKey( - Schema.Array(CoreSearchAliasSchema).check(Schema.isMaxLength(100)) - ), + aliases: Schema.optionalKey(Schema.Array(CoreSearchAliasSchema).check(Schema.isMaxLength(100))), archived: Schema.Boolean, facets: Schema.Array(CoreSearchFacetSchema), matchedRef: Schema.optionalKey(CoreSearchResourceRefSchema), @@ -42,31 +40,21 @@ const PersistedDocumentPayloadSchema = Schema.Struct({ metadata: Schema.Array(CoreSearchMetadataFieldSchema), schemaVersion: Schema.Literal('1'), subjectRef: Schema.optionalKey(CoreSearchResourceRefSchema), - temporalFacets: Schema.optionalKey( - Schema.Array(CoreSearchTemporalFacetSchema) - ), + temporalFacets: Schema.optionalKey(Schema.Array(CoreSearchTemporalFacetSchema)), temporalSearchableText: Schema.optionalKey( - Schema.Array(CoreSearchTemporalSearchableTextSchema).check( - Schema.isMaxLength(100) - ) + Schema.Array(CoreSearchTemporalSearchableTextSchema).check(Schema.isMaxLength(100)), ), }); -const ProjectionUnitKeySchema = Schema.fromJsonString( - Schema.Tuple([Schema.String, Schema.String, Schema.String]) -); +const ProjectionUnitKeySchema = Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String, Schema.String])); type PersistedDocumentPayload = typeof PersistedDocumentPayloadSchema.Type; type MutablePersistedDocumentPayload = { - -readonly [ - Key in keyof PersistedDocumentPayload - ]: PersistedDocumentPayload[Key]; + -readonly [Key in keyof PersistedDocumentPayload]: PersistedDocumentPayload[Key]; }; type CoreSearchPersistenceDatabase = Readonly<{ executor: CoreDatabaseExecutor; }>; -type CoreSearchProjectionInput = Parameters< - CoreSearchProjectionStoreService['apply'] ->[0]; +type CoreSearchProjectionInput = Parameters[0]; type CoreSearchPersistenceCause = typeof Schema.Unknown.Type; type SearchIndexEntry = typeof searchIndexEntries.$inferSelect; interface PersistedDocumentInput { @@ -91,10 +79,7 @@ interface PersistedDocumentInput { title: string; } -const invalid = ( - reason: string, - cause?: CoreSearchPersistenceCause -): CoreSearchProjectionInvalid => +const invalid = (reason: string, cause?: CoreSearchPersistenceCause): CoreSearchProjectionInvalid => cause === undefined ? new CoreSearchProjectionInvalid({ code: 'core_search_projection_invalid', @@ -117,13 +102,10 @@ const unavailable = (cause?: CoreSearchPersistenceCause) => reason: 'Core Search projection is temporarily unavailable', }); -const normalize = (value: string): string => - value.normalize('NFKC').toLocaleLowerCase('und'); +const normalize = (value: string): string => value.normalize('NFKC').toLocaleLowerCase('und'); const bodyText = (document: CoreSearchProjectionDocument): string => [document.title, ...document.searchableText].map(normalize).join('\n'); -const payload = ( - document: CoreSearchProjectionDocument -): PersistedDocumentPayload => { +const payload = (document: CoreSearchProjectionDocument): PersistedDocumentPayload => { const persisted: MutablePersistedDocumentPayload = { archived: document.archived, facets: document.facets, @@ -151,35 +133,22 @@ const payload = ( return persisted; }; -const persistedPayloadEquivalence = Schema.toEquivalence( - PersistedDocumentPayloadSchema -); -const decodePersistedPayload = Schema.decodeUnknownResult( - PersistedDocumentPayloadSchema, - { - onExcessProperty: 'error', - } -); -const encodeProjectionUnitKey = Schema.encodeUnknownResult( - ProjectionUnitKeySchema -); +const persistedPayloadEquivalence = Schema.toEquivalence(PersistedDocumentPayloadSchema); +const decodePersistedPayload = Schema.decodeUnknownResult(PersistedDocumentPayloadSchema, { + onExcessProperty: 'error', +}); +const encodeProjectionUnitKey = Schema.encodeUnknownResult(ProjectionUnitKeySchema); -const projectionUnitKey = ( - tenantId: string, - moduleId: string, - resourceType: string -): string => +const projectionUnitKey = (tenantId: string, moduleId: string, resourceType: string): string => Result.getOrThrow( encodeProjectionUnitKey([tenantId, moduleId, resourceType]).pipe( - Result.mapError((cause) => - invalid('Core Search projection unit is invalid', cause) - ) - ) + Result.mapError((cause) => invalid('Core Search projection unit is invalid', cause)), + ), ); const rowMatchesDocument = ( row: SearchIndexEntry, - document: CoreSearchProjectionDocument + document: CoreSearchProjectionDocument, ): Result.Result => decodePersistedPayload(row.facetsJson).pipe( Result.map( @@ -188,20 +157,16 @@ const rowMatchesDocument = ( row.legalEntityId === (document.selectedLegalEntityId ?? null) && row.title === document.title && row.bodyText === bodyText(document) && - persistedPayloadEquivalence(persistedPayload, payload(document)) + persistedPayloadEquivalence(persistedPayload, payload(document)), ), - Result.mapError((cause) => - invalid('Core Search persisted payload is invalid', cause) - ) + Result.mapError((cause) => invalid('Core Search persisted payload is invalid', cause)), ); const makeTransactionOperations = () => { - const installTenantScope = Effect.fn( - 'CoreSearchPersistence.installTenantScope' - )(function* installTenantScopeEffect( + const installTenantScope = Effect.fn('CoreSearchPersistence.installTenantScope')(function* installTenantScopeEffect( transaction: CoreTransaction, tenantId: string, - legalEntityId?: string + legalEntityId?: string, ) { const result = yield* transaction .execute( @@ -210,14 +175,14 @@ const makeTransactionOperations = () => { set_config('ontos.tenant_id', ${tenantId}, true) as tenant_id, set_config('ontos.legal_entity_id', ${legalEntityId ?? ''}, true) as legal_entity_id `, - 'objects' + 'objects', ) .pipe(Effect.mapError(unavailable)); const verified = Schema.decodeUnknownOption( Schema.Struct({ legal_entity_id: Schema.String, tenant_id: Schema.String, - }) + }), )(result[0]); if ( Option.isNone(verified) || @@ -229,191 +194,153 @@ const makeTransactionOperations = () => { return yield* Effect.void; }); - const lockProjectionUnit = Effect.fn( - 'CoreSearchPersistence.lockProjectionUnit' - )(function* lockProjectionUnitEffect( + const lockProjectionUnit = Effect.fn('CoreSearchPersistence.lockProjectionUnit')(function* lockProjectionUnitEffect( transaction: CoreTransaction, tenantId: string, moduleId: string, - resourceType: string + resourceType: string, ) { yield* transaction .execute( - sql`select pg_advisory_xact_lock(hashtextextended(${projectionUnitKey( - tenantId, - moduleId, - resourceType - )}, 0))` + sql`select pg_advisory_xact_lock(hashtextextended(${projectionUnitKey(tenantId, moduleId, resourceType)}, 0))`, ) .pipe(Effect.mapError(unavailable)); }); - const currentRow = Effect.fn('CoreSearchPersistence.currentRow')( - function* currentRowEffect( - transaction: CoreTransaction, - ref: CoreSearchResourceRef - ) { - const query = transaction.query.searchIndexEntries.findFirst({ - where: { - sourceModuleKey: ref.moduleId, - sourceResourceId: ref.resourceId, - sourceResourceType: ref.resourceType, - tenantId: ref.tenantId, - }, - }); - const row = yield* query.execute - .bind(query)() - .pipe(Effect.mapError(unavailable)); - return Option.fromNullishOr(row); - } - ); - - const currentRebuild = Effect.fn('CoreSearchPersistence.currentRebuild')( - function* currentRebuildEffect( - transaction: CoreTransaction, - unit: Readonly<{ - moduleId: string; - resourceType: string; - tenantId: string; - }> - ) { - const query = transaction.query.searchProjectionRebuilds.findFirst({ - where: { - sourceModuleKey: unit.moduleId, - sourceResourceType: unit.resourceType, - tenantId: unit.tenantId, - }, - }); - const rebuild = yield* query.execute - .bind(query)() - .pipe(Effect.mapError(unavailable)); - return Option.fromNullishOr(rebuild); - } - ); + const currentRow = Effect.fn('CoreSearchPersistence.currentRow')(function* currentRowEffect( + transaction: CoreTransaction, + ref: CoreSearchResourceRef, + ) { + const query = transaction.query.searchIndexEntries.findFirst({ + where: { + sourceModuleKey: ref.moduleId, + sourceResourceId: ref.resourceId, + sourceResourceType: ref.resourceType, + tenantId: ref.tenantId, + }, + }); + const row = yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return Option.fromNullishOr(row); + }); - const persistUpsert = Effect.fn('CoreSearchPersistence.persistUpsert')( - function* persistUpsertEffect( - transaction: CoreTransaction, - document: CoreSearchProjectionDocument, - updatedAt: Date - ) { - const current = yield* currentRow(transaction, document.ref); - const version = BigInt(document.projectionVersion); - if (Option.isSome(current)) { - const existing = current.value; - if (existing.projectionVersion > version) { - return yield* Effect.void; - } - if (existing.projectionVersion === version) { - if (!Result.getOrThrow(rowMatchesDocument(existing, document))) { - return yield* invalid( - 'Core Search mutation reuses a version for different content' - ); - } - return yield* Effect.void; - } - const query = transaction - .update(searchIndexEntries) - .set({ - bodyText: bodyText(document), - deleted: false, - facetsJson: payload(document), - legalEntityId: document.selectedLegalEntityId ?? null, - projectionVersion: version, - title: document.title, - updatedAt, - }) - .where( - eq( - searchIndexEntries.searchIndexEntryId, - existing.searchIndexEntryId - ) - ); - yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); - return yield* Effect.void; - } - const query = transaction.insert(searchIndexEntries).values({ - bodyText: bodyText(document), - deleted: false, - facetsJson: payload(document), - legalEntityId: document.selectedLegalEntityId ?? null, - projectionVersion: version, - sourceModuleKey: document.ref.moduleId, - sourceResourceId: document.ref.resourceId, - sourceResourceType: document.ref.resourceType, - tenantId: document.ref.tenantId, - title: document.title, - }); - yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); - return yield* Effect.void; - } - ); + const currentRebuild = Effect.fn('CoreSearchPersistence.currentRebuild')(function* currentRebuildEffect( + transaction: CoreTransaction, + unit: Readonly<{ + moduleId: string; + resourceType: string; + tenantId: string; + }>, + ) { + const query = transaction.query.searchProjectionRebuilds.findFirst({ + where: { + sourceModuleKey: unit.moduleId, + sourceResourceType: unit.resourceType, + tenantId: unit.tenantId, + }, + }); + const rebuild = yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return Option.fromNullishOr(rebuild); + }); - const persistDelete = Effect.fn('CoreSearchPersistence.persistDelete')( - function* persistDeleteEffect( - transaction: CoreTransaction, - mutation: Extract< - CoreSearchProjectionMutation, - { readonly kind: 'delete' } - >, - updatedAt: Date - ) { - const current = yield* currentRow(transaction, mutation.ref); - const version = BigInt(mutation.projectionVersion); - if (Option.isSome(current) && current.value.projectionVersion > version) { + const persistUpsert = Effect.fn('CoreSearchPersistence.persistUpsert')(function* persistUpsertEffect( + transaction: CoreTransaction, + document: CoreSearchProjectionDocument, + updatedAt: Date, + ) { + const current = yield* currentRow(transaction, document.ref); + const version = BigInt(document.projectionVersion); + if (Option.isSome(current)) { + const existing = current.value; + if (existing.projectionVersion > version) { return yield* Effect.void; } - if ( - Option.isSome(current) && - current.value.projectionVersion === version - ) { - if (!current.value.deleted) { - return yield* invalid( - 'Core Search mutation reuses a version for different content' - ); + if (existing.projectionVersion === version) { + if (!Result.getOrThrow(rowMatchesDocument(existing, document))) { + return yield* invalid('Core Search mutation reuses a version for different content'); } return yield* Effect.void; } - if (Option.isNone(current)) { - const query = transaction.insert(searchIndexEntries).values({ - bodyText: '', - deleted: true, - facetsJson: { schemaVersion: '1' }, - projectionVersion: version, - sourceModuleKey: mutation.ref.moduleId, - sourceResourceId: mutation.ref.resourceId, - sourceResourceType: mutation.ref.resourceType, - tenantId: mutation.ref.tenantId, - title: '', - }); - yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); - return yield* Effect.void; - } const query = transaction .update(searchIndexEntries) .set({ - bodyText: '', - deleted: true, - facetsJson: { schemaVersion: '1' }, - legalEntityId: null, + bodyText: bodyText(document), + deleted: false, + facetsJson: payload(document), + legalEntityId: document.selectedLegalEntityId ?? null, projectionVersion: version, - title: '', + title: document.title, updatedAt, }) - .where( - eq( - searchIndexEntries.searchIndexEntryId, - current.value.searchIndexEntryId - ) - ); + .where(eq(searchIndexEntries.searchIndexEntryId, existing.searchIndexEntryId)); yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); return yield* Effect.void; } - ); + const query = transaction.insert(searchIndexEntries).values({ + bodyText: bodyText(document), + deleted: false, + facetsJson: payload(document), + legalEntityId: document.selectedLegalEntityId ?? null, + projectionVersion: version, + sourceModuleKey: document.ref.moduleId, + sourceResourceId: document.ref.resourceId, + sourceResourceType: document.ref.resourceType, + tenantId: document.ref.tenantId, + title: document.title, + }); + yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return yield* Effect.void; + }); + + const persistDelete = Effect.fn('CoreSearchPersistence.persistDelete')(function* persistDeleteEffect( + transaction: CoreTransaction, + mutation: Extract, + updatedAt: Date, + ) { + const current = yield* currentRow(transaction, mutation.ref); + const version = BigInt(mutation.projectionVersion); + if (Option.isSome(current) && current.value.projectionVersion > version) { + return yield* Effect.void; + } + if (Option.isSome(current) && current.value.projectionVersion === version) { + if (!current.value.deleted) { + return yield* invalid('Core Search mutation reuses a version for different content'); + } + return yield* Effect.void; + } + if (Option.isNone(current)) { + const query = transaction.insert(searchIndexEntries).values({ + bodyText: '', + deleted: true, + facetsJson: { schemaVersion: '1' }, + projectionVersion: version, + sourceModuleKey: mutation.ref.moduleId, + sourceResourceId: mutation.ref.resourceId, + sourceResourceType: mutation.ref.resourceType, + tenantId: mutation.ref.tenantId, + title: '', + }); + yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return yield* Effect.void; + } + const query = transaction + .update(searchIndexEntries) + .set({ + bodyText: '', + deleted: true, + facetsJson: { schemaVersion: '1' }, + legalEntityId: null, + projectionVersion: version, + title: '', + updatedAt, + }) + .where(eq(searchIndexEntries.searchIndexEntryId, current.value.searchIndexEntryId)); + yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return yield* Effect.void; + }); const persistedDeleteMutation = ( row: SearchIndexEntry, - projectionVersion: string + projectionVersion: string, ): Extract => { const mutation = decodeCoreSearchProjectionMutation({ kind: 'delete', @@ -431,125 +358,95 @@ const makeTransactionOperations = () => { return mutation; }; - const persistRebuildFloor = Effect.fn( - 'CoreSearchPersistence.persistRebuildFloor' - )(function* persistRebuildFloorEffect( + const persistRebuildFloor = Effect.fn('CoreSearchPersistence.persistRebuildFloor')( + function* persistRebuildFloorEffect( + transaction: CoreTransaction, + replacement: CoreSearchProjectionReplacement, + fingerprint: string, + updatedAt: Date, + ) { + const rebuildVersion = BigInt(replacement.rebuildVersion); + const query = transaction + .insert(searchProjectionRebuilds) + .values({ + fingerprint, + rebuildVersion, + sourceModuleKey: replacement.moduleId, + sourceResourceType: replacement.resourceType, + tenantId: replacement.tenantId, + }) + .onConflictDoUpdate({ + set: { fingerprint, rebuildVersion, updatedAt }, + target: [ + searchProjectionRebuilds.tenantId, + searchProjectionRebuilds.sourceModuleKey, + searchProjectionRebuilds.sourceResourceType, + ], + }); + yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + }, + ); + + const replacementRows = Effect.fn('CoreSearchPersistence.replacementRows')(function* replacementRowsEffect( transaction: CoreTransaction, replacement: CoreSearchProjectionReplacement, - fingerprint: string, - updatedAt: Date ) { - const rebuildVersion = BigInt(replacement.rebuildVersion); - const query = transaction - .insert(searchProjectionRebuilds) - .values({ - fingerprint, - rebuildVersion, + const current = yield* currentRebuild(transaction, replacement); + const version = BigInt(replacement.rebuildVersion); + const fingerprint = coreSearchReplacementFingerprint(replacement); + if (Option.isSome(current) && version < current.value.rebuildVersion) { + return Option.none(); + } + if (Option.isSome(current) && version === current.value.rebuildVersion) { + const prior = current.value; + if (fingerprint !== prior.fingerprint) { + return yield* invalid('Core Search rebuild reuses a version for different content'); + } + return Option.none(); + } + const query = transaction.query.searchIndexEntries.findMany({ + where: { sourceModuleKey: replacement.moduleId, sourceResourceType: replacement.resourceType, tenantId: replacement.tenantId, - }) - .onConflictDoUpdate({ - set: { fingerprint, rebuildVersion, updatedAt }, - target: [ - searchProjectionRebuilds.tenantId, - searchProjectionRebuilds.sourceModuleKey, - searchProjectionRebuilds.sourceResourceType, - ], - }); - yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + }, + }); + const existing = yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + return Option.some({ existing, fingerprint }); }); - const replacementRows = Effect.fn('CoreSearchPersistence.replacementRows')( - function* replacementRowsEffect( - transaction: CoreTransaction, - replacement: CoreSearchProjectionReplacement - ) { - const current = yield* currentRebuild(transaction, replacement); - const version = BigInt(replacement.rebuildVersion); - const fingerprint = coreSearchReplacementFingerprint(replacement); - if (Option.isSome(current) && version < current.value.rebuildVersion) { - return Option.none(); - } - if (Option.isSome(current) && version === current.value.rebuildVersion) { - const prior = current.value; - if (fingerprint !== prior.fingerprint) { - return yield* invalid( - 'Core Search rebuild reuses a version for different content' - ); - } - return Option.none(); - } - const query = transaction.query.searchIndexEntries.findMany({ - where: { - sourceModuleKey: replacement.moduleId, - sourceResourceType: replacement.resourceType, - tenantId: replacement.tenantId, - }, - }); - const existing = yield* query.execute - .bind(query)() - .pipe(Effect.mapError(unavailable)); - return Option.some({ existing, fingerprint }); - } - ); - - const replaceProjection = Effect.fn( - 'CoreSearchPersistence.replaceProjection' - )(function* replaceProjectionEffect( + const replaceProjection = Effect.fn('CoreSearchPersistence.replaceProjection')(function* replaceProjectionEffect( transaction: CoreTransaction, replacement: CoreSearchProjectionReplacement, - updatedAt: Date + updatedAt: Date, ) { - yield* lockProjectionUnit( - transaction, - replacement.tenantId, - replacement.moduleId, - replacement.resourceType - ); + yield* lockProjectionUnit(transaction, replacement.tenantId, replacement.moduleId, replacement.resourceType); const work = yield* replacementRows(transaction, replacement); if (Option.isNone(work)) { return; } const { existing, fingerprint } = work.value; - yield* Effect.forEach( - replacement.documents, - (document) => persistUpsert(transaction, document, updatedAt), - { concurrency: 1, discard: true } - ); - const nextIds = new Set( - replacement.documents.map(({ ref }) => ref.resourceId) - ); + yield* Effect.forEach(replacement.documents, (document) => persistUpsert(transaction, document, updatedAt), { + concurrency: 1, + discard: true, + }); + const nextIds = new Set(replacement.documents.map(({ ref }) => ref.resourceId)); const staleRows = existing.filter( - (row) => - !nextIds.has(row.sourceResourceId) && - row.projectionVersion < BigInt(replacement.rebuildVersion) + (row) => !nextIds.has(row.sourceResourceId) && row.projectionVersion < BigInt(replacement.rebuildVersion), ); yield* Effect.forEach( staleRows, - (row) => - persistDelete( - transaction, - persistedDeleteMutation(row, replacement.rebuildVersion), - updatedAt - ), - { concurrency: 1, discard: true } - ); - yield* persistRebuildFloor( - transaction, - replacement, - fingerprint, - updatedAt + (row) => persistDelete(transaction, persistedDeleteMutation(row, replacement.rebuildVersion), updatedAt), + { concurrency: 1, discard: true }, ); + yield* persistRebuildFloor(transaction, replacement, fingerprint, updatedAt); }); const decodeRow = (row: SearchIndexEntry): CoreSearchProjectionDocument => { const decoded = Result.getOrThrow( decodePersistedPayload(row.facetsJson).pipe( - Result.mapError((cause) => - invalid('Core Search persisted payload is invalid', cause) - ) - ) + Result.mapError((cause) => invalid('Core Search persisted payload is invalid', cause)), + ), ); const document: PersistedDocumentInput = { archived: decoded.archived, @@ -601,10 +498,7 @@ const makeTransactionOperations = () => { catch: (error) => Schema.is(CoreSearchProjectionInvalid)(error) ? error - : invalid( - 'Core Search mutation does not match its declared contract', - error - ), + : invalid('Core Search mutation does not match its declared contract', error), try: () => decodeCoreSearchProjectionMutation(input), }); @@ -613,90 +507,67 @@ const makeTransactionOperations = () => { catch: (error) => Schema.is(CoreSearchProjectionInvalid)(error) ? error - : invalid( - 'Core Search replacement does not match its declared contract', - error - ), + : invalid('Core Search replacement does not match its declared contract', error), try: () => decodeCoreSearchProjectionReplacement(input), }); - const applyMutationTransaction = Effect.fn( - 'CoreSearchPersistence.applyMutationTransaction' - )(function* applyMutationTransactionEffect( - transaction: CoreTransaction, - mutation: CoreSearchProjectionMutation, - updatedAt: Date - ) { - const ref = - mutation.kind === 'upsert' ? mutation.document.ref : mutation.ref; - yield* installTenantScope(transaction, ref.tenantId); - yield* lockProjectionUnit( - transaction, - ref.tenantId, - ref.moduleId, - ref.resourceType - ); - const rebuild = yield* currentRebuild(transaction, ref); - const version = BigInt( - mutation.kind === 'upsert' - ? mutation.document.projectionVersion - : mutation.projectionVersion - ); - if (Option.isSome(rebuild) && version <= rebuild.value.rebuildVersion) { - return; - } - if (mutation.kind === 'upsert') { - yield* persistUpsert(transaction, mutation.document, updatedAt); - return; - } - yield* persistDelete(transaction, mutation, updatedAt); - }); + const applyMutationTransaction = Effect.fn('CoreSearchPersistence.applyMutationTransaction')( + function* applyMutationTransactionEffect( + transaction: CoreTransaction, + mutation: CoreSearchProjectionMutation, + updatedAt: Date, + ) { + const ref = mutation.kind === 'upsert' ? mutation.document.ref : mutation.ref; + yield* installTenantScope(transaction, ref.tenantId); + yield* lockProjectionUnit(transaction, ref.tenantId, ref.moduleId, ref.resourceType); + const rebuild = yield* currentRebuild(transaction, ref); + const version = BigInt( + mutation.kind === 'upsert' ? mutation.document.projectionVersion : mutation.projectionVersion, + ); + if (Option.isSome(rebuild) && version <= rebuild.value.rebuildVersion) { + return; + } + if (mutation.kind === 'upsert') { + yield* persistUpsert(transaction, mutation.document, updatedAt); + return; + } + yield* persistDelete(transaction, mutation, updatedAt); + }, + ); - const queryCandidatesTransaction = Effect.fn( - 'CoreSearchPersistence.queryCandidatesTransaction' - )(function* queryCandidatesTransactionEffect( - transaction: CoreTransaction, - input: CoreSearchQuery - ) { - yield* installTenantScope( - transaction, - input.tenantId, - input.selectedLegalEntityId - ); - const query = transaction.query.searchIndexEntries.findMany({ - // Match the bounded rebuild unit; never silently truncate before evidence filtering. - limit: 10_001, - orderBy: (table, { asc }) => [ - asc(table.title), - asc(table.sourceResourceId), - ], - where: { - deleted: false, - legalEntityId: input.selectedLegalEntityId ?? { isNull: true }, - sourceModuleKey: input.moduleId, - sourceResourceType: input.resourceType, - tenantId: input.tenantId, - }, - }); - const rows = yield* query.execute - .bind(query)() - .pipe(Effect.mapError(unavailable)); - if (rows.length > 10_000) { - return yield* unavailable(); - } - return rows.map(decodeRow); - }); + const queryCandidatesTransaction = Effect.fn('CoreSearchPersistence.queryCandidatesTransaction')( + function* queryCandidatesTransactionEffect(transaction: CoreTransaction, input: CoreSearchQuery) { + yield* installTenantScope(transaction, input.tenantId, input.selectedLegalEntityId); + const query = transaction.query.searchIndexEntries.findMany({ + // Match the bounded rebuild unit; never silently truncate before evidence filtering. + limit: 10_001, + orderBy: (table, { asc }) => [asc(table.title), asc(table.sourceResourceId)], + where: { + deleted: false, + legalEntityId: input.selectedLegalEntityId ?? { isNull: true }, + sourceModuleKey: input.moduleId, + sourceResourceType: input.resourceType, + tenantId: input.tenantId, + }, + }); + const rows = yield* query.execute.bind(query)().pipe(Effect.mapError(unavailable)); + if (rows.length > 10_000) { + return yield* unavailable(); + } + return rows.map(decodeRow); + }, + ); - const replaceProjectionTransaction = Effect.fn( - 'CoreSearchPersistence.replaceProjectionTransaction' - )(function* replaceProjectionTransactionEffect( - transaction: CoreTransaction, - replacement: CoreSearchProjectionReplacement, - updatedAt: Date - ) { - yield* installTenantScope(transaction, replacement.tenantId); - yield* replaceProjection(transaction, replacement, updatedAt); - }); + const replaceProjectionTransaction = Effect.fn('CoreSearchPersistence.replaceProjectionTransaction')( + function* replaceProjectionTransactionEffect( + transaction: CoreTransaction, + replacement: CoreSearchProjectionReplacement, + updatedAt: Date, + ) { + yield* installTenantScope(transaction, replacement.tenantId); + yield* replaceProjection(transaction, replacement, updatedAt); + }, + ); return Object.freeze({ applyMutationTransaction, @@ -710,56 +581,41 @@ const makeTransactionOperations = () => { const transactionOperations = makeTransactionOperations(); export const makePostgresCoreSearchProjectionStore = ( - database: CoreSearchPersistenceDatabase + database: CoreSearchPersistenceDatabase, ): CoreSearchProjectionStoreService => { - const runTransaction = ( - body: (transaction: CoreTransaction) => Effect.Effect - ) => + const runTransaction = (body: (transaction: CoreTransaction) => Effect.Effect) => database.executor.transaction(body).pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), - Effect.catchTag('SqlError', (failure) => - Effect.fail(unavailable(failure)) - ) + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.catchTag('SqlError', (failure) => Effect.fail(unavailable(failure))), ); - const apply: CoreSearchProjectionStoreService['apply'] = Effect.fn( - 'CoreSearchProjectionStore.applyPostgres' - )(function* applyCoreSearchProjection(input: CoreSearchProjectionInput) { - const mutation = yield* transactionOperations.decodeMutation(input); - const updatedAt = DateTime.toDateUtc(yield* DateTime.now); + const apply: CoreSearchProjectionStoreService['apply'] = Effect.fn('CoreSearchProjectionStore.applyPostgres')( + function* applyCoreSearchProjection(input: CoreSearchProjectionInput) { + const mutation = yield* transactionOperations.decodeMutation(input); + const updatedAt = DateTime.toDateUtc(yield* DateTime.now); + const transactionBody = (transaction: CoreTransaction) => + transactionOperations.applyMutationTransaction(transaction, mutation, updatedAt); + yield* runTransaction(transactionBody); + }, + ); + const queryCandidates: CoreSearchProjectionStoreService['queryCandidates'] = Effect.fn( + 'CoreSearchProjectionStore.queryCandidatesPostgres', + )(function* queryCoreSearchCandidates(input: CoreSearchQuery) { const transactionBody = (transaction: CoreTransaction) => - transactionOperations.applyMutationTransaction( - transaction, - mutation, - updatedAt - ); - yield* runTransaction(transactionBody); - }); - const queryCandidates: CoreSearchProjectionStoreService['queryCandidates'] = - Effect.fn('CoreSearchProjectionStore.queryCandidatesPostgres')( - function* queryCoreSearchCandidates(input: CoreSearchQuery) { - const transactionBody = (transaction: CoreTransaction) => - transactionOperations.queryCandidatesTransaction(transaction, input); - const documents = yield* runTransaction(transactionBody); - return yield* Schema.decodeEffect( - Schema.Array(CoreSearchProjectionDocumentSchema) - )(documents).pipe(Effect.mapError(unavailable)); - } + transactionOperations.queryCandidatesTransaction(transaction, input); + const documents = yield* runTransaction(transactionBody); + return yield* Schema.decodeEffect(Schema.Array(CoreSearchProjectionDocumentSchema))(documents).pipe( + Effect.mapError(unavailable), ); - const replace: CoreSearchProjectionStoreService['replace'] = Effect.fn( - 'CoreSearchProjectionStore.replacePostgres' - )(function* replaceCoreSearchProjection(input: CoreSearchProjectionInput) { - const replacement = yield* transactionOperations.decodeReplacement(input); - const updatedAt = DateTime.toDateUtc(yield* DateTime.now); - const transactionBody = (transaction: CoreTransaction) => - transactionOperations.replaceProjectionTransaction( - transaction, - replacement, - updatedAt - ); - yield* runTransaction(transactionBody); }); + const replace: CoreSearchProjectionStoreService['replace'] = Effect.fn('CoreSearchProjectionStore.replacePostgres')( + function* replaceCoreSearchProjection(input: CoreSearchProjectionInput) { + const replacement = yield* transactionOperations.decodeReplacement(input); + const updatedAt = DateTime.toDateUtc(yield* DateTime.now); + const transactionBody = (transaction: CoreTransaction) => + transactionOperations.replaceProjectionTransaction(transaction, replacement, updatedAt); + yield* runTransaction(transactionBody); + }, + ); return Object.freeze({ apply, queryCandidates, replace }); }; @@ -768,11 +624,8 @@ export const CoreSearchProjectionStoreLive = Layer.effect( Effect.gen(function* makeCoreSearchProjectionStoreLive() { const database = yield* CoreDatabase; return makePostgresCoreSearchProjectionStore(database); - }) + }), ); /** Query layer exposes its store requirement for composition at the application boundary. */ -export const CoreSearchQueryRuntimeLive = Layer.effect( - CoreSearchQueryRuntime, - createCoreSearchQueryRuntime -); +export const CoreSearchQueryRuntimeLive = Layer.effect(CoreSearchQueryRuntime, createCoreSearchQueryRuntime); diff --git a/app/packages/core-runtime/src/search/projection-store.ts b/app/packages/core-runtime/src/search/projection-store.ts index 6ad425fff..7fc8e86a5 100644 --- a/app/packages/core-runtime/src/search/projection-store.ts +++ b/app/packages/core-runtime/src/search/projection-store.ts @@ -9,35 +9,24 @@ import type { } from './projection.ts'; type UnparsedCoreSearchInput = typeof Schema.Unknown.Type; -type CoreSearchProjectionUnavailableInstance = InstanceType< - typeof CoreSearchProjectionUnavailable ->; +type CoreSearchProjectionUnavailableInstance = InstanceType; export interface CoreSearchProjectionStoreService { /** Applies one idempotent versioned lifecycle observation. */ readonly apply: ( - input: UnparsedCoreSearchInput - ) => Effect.Effect< - void, - CoreSearchProjectionInvalid | CoreSearchProjectionUnavailableInstance - >; + input: UnparsedCoreSearchInput, + ) => Effect.Effect; /** Candidate access is Core-private: the query runtime strips searchable evidence before return. */ readonly queryCandidates: ( - input: CoreSearchQuery - ) => Effect.Effect< - readonly CoreSearchProjectionDocument[], - CoreSearchProjectionUnavailableInstance - >; + input: CoreSearchQuery, + ) => Effect.Effect; /** * Replaces one tenant/module/resource projection as one physical rebuild unit. Implementations * must leave the prior unit intact when validation or persistence fails. */ readonly replace: ( - input: UnparsedCoreSearchInput - ) => Effect.Effect< - void, - CoreSearchProjectionInvalid | CoreSearchProjectionUnavailableInstance - >; + input: UnparsedCoreSearchInput, + ) => Effect.Effect; } /** Production persistence implements this Core-owned port; business modules never own an index. */ @@ -47,5 +36,5 @@ export class CoreSearchProjectionStore extends Context.Service< >()( // Preserve the public Context identity after splitting the service into its owning module. // @effect-diagnostics-next-line deterministicKeys:off - '@app/core-runtime/search/projection/CoreSearchProjectionStore' + '@app/core-runtime/search/projection/CoreSearchProjectionStore', ) {} diff --git a/app/packages/core-runtime/src/search/projection.ts b/app/packages/core-runtime/src/search/projection.ts index 1ae855b87..7a734af9a 100644 --- a/app/packages/core-runtime/src/search/projection.ts +++ b/app/packages/core-runtime/src/search/projection.ts @@ -1,14 +1,6 @@ import { createHash } from 'node:crypto'; -import { - Clock, - DateTime, - Effect, - Option, - Predicate, - Result, - Schema, -} from 'effect'; +import { Clock, DateTime, Effect, Option, Predicate, Result, Schema } from 'effect'; import { CoreSearchProjectionStore } from './projection-store.ts'; import type { CoreSearchProjectionStoreService } from './projection-store.ts'; @@ -19,30 +11,18 @@ export { CoreSearchQueryRuntime } from './query-runtime.ts'; export type { CoreSearchProjectionStoreService } from './projection-store.ts'; export type { CoreSearchQueryRuntimeService } from './query-runtime.ts'; -const boundedText = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(300) -); +const boundedText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)); const stableKey = Schema.String.check( Schema.isMinLength(3), Schema.isMaxLength(200), - Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u) + Schema.isPattern(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u), ); const moduleId = stableKey.pipe(Schema.brand('ModuleId')); -const resourceId = Schema.String.check( - Schema.isMinLength(1), - Schema.isMaxLength(300) -).pipe(Schema.brand('ResourceId')); +const resourceId = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(300)).pipe(Schema.brand('ResourceId')); const resourceType = stableKey.pipe(Schema.brand('ResourceType')); -const selectedLegalEntityId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('SelectedLegalEntityId') -); -const tenantId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand('TenantId') -); -const projectionVersion = Schema.String.check( - Schema.isPattern(/^[1-9][0-9]*$/u) -); +const selectedLegalEntityId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('SelectedLegalEntityId')); +const tenantId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand('TenantId')); +const projectionVersion = Schema.String.check(Schema.isPattern(/^[1-9][0-9]*$/u)); type UnparsedCoreSearchInput = typeof Schema.Unknown.Type; export const CoreSearchResourceRefSchema = Schema.Struct({ @@ -92,9 +72,7 @@ export const CoreSearchTemporalSearchableTextSchema = Schema.Struct({ value: boundedText, }); const temporalSearchableText = Schema.optionalKey( - Schema.Array(CoreSearchTemporalSearchableTextSchema).check( - Schema.isMaxLength(100) - ) + Schema.Array(CoreSearchTemporalSearchableTextSchema).check(Schema.isMaxLength(100)), ); export const CoreSearchAliasSchema = Schema.Struct({ @@ -105,53 +83,40 @@ export const CoreSearchAliasSchema = Schema.Struct({ }); export const CoreSearchProjectionDocumentSchema = Schema.Struct({ - aliases: Schema.optionalKey( - Schema.Array(CoreSearchAliasSchema).check(Schema.isMaxLength(100)) - ), + aliases: Schema.optionalKey(Schema.Array(CoreSearchAliasSchema).check(Schema.isMaxLength(100))), archived: Schema.Boolean, facets: Schema.Array(CoreSearchFacetSchema).check(Schema.isMaxLength(50)), matchedRef: Schema.optionalKey(CoreSearchResourceRefSchema), matchedSubjectRef: Schema.optionalKey(CoreSearchResourceRefSchema), - metadata: Schema.Array(CoreSearchMetadataFieldSchema).check( - Schema.isMaxLength(50) - ), + metadata: Schema.Array(CoreSearchMetadataFieldSchema).check(Schema.isMaxLength(50)), projectionVersion, ref: CoreSearchResourceRefSchema, searchableText: Schema.Array(boundedText).check(Schema.isMaxLength(100)), selectedLegalEntityId: Schema.optionalKey(selectedLegalEntityId), subjectRef: Schema.optionalKey(CoreSearchResourceRefSchema), - temporalFacets: Schema.optionalKey( - Schema.Array(CoreSearchTemporalFacetSchema).check(Schema.isMaxLength(100)) - ), + temporalFacets: Schema.optionalKey(Schema.Array(CoreSearchTemporalFacetSchema).check(Schema.isMaxLength(100))), temporalSearchableText, title: boundedText, }); -export type CoreSearchProjectionDocument = - typeof CoreSearchProjectionDocumentSchema.Type; +export type CoreSearchProjectionDocument = typeof CoreSearchProjectionDocumentSchema.Type; export const CoreSearchProjectionHitSchema = Schema.Struct({ archived: Schema.Boolean, facets: Schema.Array(CoreSearchFacetSchema).check(Schema.isMaxLength(50)), matchedRef: Schema.optionalKey(CoreSearchResourceRefSchema), matchedSubjectRef: Schema.optionalKey(CoreSearchResourceRefSchema), - metadata: Schema.Array(CoreSearchMetadataFieldSchema).check( - Schema.isMaxLength(50) - ), + metadata: Schema.Array(CoreSearchMetadataFieldSchema).check(Schema.isMaxLength(50)), ref: CoreSearchResourceRefSchema, selectedLegalEntityId: Schema.optionalKey(selectedLegalEntityId), subjectRef: Schema.optionalKey(CoreSearchResourceRefSchema), - temporalFacets: Schema.optionalKey( - Schema.Array(CoreSearchTemporalFacetSchema).check(Schema.isMaxLength(100)) - ), + temporalFacets: Schema.optionalKey(Schema.Array(CoreSearchTemporalFacetSchema).check(Schema.isMaxLength(100))), title: boundedText, }); export type CoreSearchProjectionHit = typeof CoreSearchProjectionHitSchema.Type; export const CoreSearchQuerySchema = Schema.Struct({ effectiveAt: Schema.optionalKey(Schema.DateTimeUtcFromString), - facets: Schema.optionalKey( - Schema.Array(CoreSearchFacetSchema).check(Schema.isMaxLength(20)) - ), + facets: Schema.optionalKey(Schema.Array(CoreSearchFacetSchema).check(Schema.isMaxLength(20))), includeArchived: Schema.Boolean, moduleId, query: Schema.Trim.check(Schema.isMinLength(1), Schema.isMaxLength(200)), @@ -162,16 +127,13 @@ export const CoreSearchQuerySchema = Schema.Struct({ export type CoreSearchQuery = typeof CoreSearchQuerySchema.Type; export const CoreSearchProjectionReplacementSchema = Schema.Struct({ - documents: Schema.Array(CoreSearchProjectionDocumentSchema).check( - Schema.isMaxLength(10_000) - ), + documents: Schema.Array(CoreSearchProjectionDocumentSchema).check(Schema.isMaxLength(10_000)), moduleId, rebuildVersion: projectionVersion, resourceType, tenantId, }); -export type CoreSearchProjectionReplacement = - typeof CoreSearchProjectionReplacementSchema.Type; +export type CoreSearchProjectionReplacement = typeof CoreSearchProjectionReplacementSchema.Type; export const CoreSearchProjectionMutationSchema = Schema.Union([ Schema.Struct({ @@ -184,8 +146,7 @@ export const CoreSearchProjectionMutationSchema = Schema.Union([ ref: CoreSearchResourceRefSchema, }), ]); -export type CoreSearchProjectionMutation = - typeof CoreSearchProjectionMutationSchema.Type; +export type CoreSearchProjectionMutation = typeof CoreSearchProjectionMutationSchema.Type; export class CoreSearchProjectionInvalid extends Schema.TaggedError()( 'CoreSearchProjectionInvalid', @@ -193,7 +154,7 @@ export class CoreSearchProjectionInvalid extends Schema.TaggedError()( - 'CoreSearchProjectionUnavailable', - projectionUnavailableFields - ); -const projectionUnitKeyCodec = Schema.fromJsonString( - Schema.Tuple([Schema.String, Schema.String, Schema.String]) + projectionUnavailableFields, ); -const encodeProjectionUnitKey = Schema.encodeUnknownResult( - projectionUnitKeyCodec +export type CoreSearchProjectionUnavailableError = typeof CoreSearchProjectionUnavailableSchema.Type; +export const CoreSearchProjectionUnavailable = Schema.TaggedError()( + 'CoreSearchProjectionUnavailable', + projectionUnavailableFields, ); -const projectionUnitKey = ( - tenant: string, - module: string, - type: string -): string => Result.getOrThrow(encodeProjectionUnitKey([tenant, module, type])); -const documentKey = ({ ref }: CoreSearchProjectionDocument): string => - ref.resourceId; -const normalize = (value: string): string => - value.normalize('NFKC').toLocaleLowerCase('und'); - -const invalid = ( - reason: string, - cause?: unknown -): CoreSearchProjectionInvalid => { +const projectionUnitKeyCodec = Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String, Schema.String])); +const encodeProjectionUnitKey = Schema.encodeUnknownResult(projectionUnitKeyCodec); +const projectionUnitKey = (tenant: string, module: string, type: string): string => + Result.getOrThrow(encodeProjectionUnitKey([tenant, module, type])); +const documentKey = ({ ref }: CoreSearchProjectionDocument): string => ref.resourceId; +const normalize = (value: string): string => value.normalize('NFKC').toLocaleLowerCase('und'); + +const invalid = (reason: string, cause?: unknown): CoreSearchProjectionInvalid => { if (cause === undefined) { return new CoreSearchProjectionInvalid({ code: 'core_search_projection_invalid', @@ -249,55 +196,30 @@ const hasUniqueKeys = (values: readonly { readonly key: string }[]): boolean => new Set(values.map(({ key }) => key)).size === values.length; const toEpochMillis = (value: string): number | undefined => - DateTime.make(value).pipe( - Option.map(DateTime.toEpochMillis), - Option.getOrUndefined - ); + DateTime.make(value).pipe(Option.map(DateTime.toEpochMillis), Option.getOrUndefined); -const invalidPeriod = ({ - validFrom, - validTo, -}: Readonly<{ validFrom: string; validTo?: string }>): boolean => { +const invalidPeriod = ({ validFrom, validTo }: Readonly<{ validFrom: string; validTo?: string }>): boolean => { const from = toEpochMillis(validFrom); const to = validTo === undefined ? undefined : toEpochMillis(validTo); - return ( - from === undefined || - (validTo !== undefined && (to === undefined || to <= from)) - ); + return from === undefined || (validTo !== undefined && (to === undefined || to <= from)); }; -const hasForeignDocumentReference = ( - document: CoreSearchProjectionDocument, - tenant: string -): boolean => +const hasForeignDocumentReference = (document: CoreSearchProjectionDocument, tenant: string): boolean => [document.matchedRef, document.subjectRef, document.matchedSubjectRef].some( - (ref) => ref !== undefined && ref.tenantId !== tenant + (ref) => ref !== undefined && ref.tenantId !== tenant, ); -const hasInvalidDocumentFacets = ( - document: CoreSearchProjectionDocument -): boolean => +const hasInvalidDocumentFacets = (document: CoreSearchProjectionDocument): boolean => !hasUniqueKeys(document.facets) || !hasUniqueKeys(document.metadata) || - document.facets.some( - ({ values }) => - values.length === 0 || new Set(values).size !== values.length - ); + document.facets.some(({ values }) => values.length === 0 || new Set(values).size !== values.length); -const hasInvalidDocumentPeriods = ( - document: CoreSearchProjectionDocument -): boolean => - (document.temporalFacets ?? []).some(invalidPeriod) || - (document.temporalSearchableText ?? []).some(invalidPeriod); +const hasInvalidDocumentPeriods = (document: CoreSearchProjectionDocument): boolean => + (document.temporalFacets ?? []).some(invalidPeriod) || (document.temporalSearchableText ?? []).some(invalidPeriod); -const hasInvalidDocumentAliases = ( - document: CoreSearchProjectionDocument, - tenant: string -): boolean => +const hasInvalidDocumentAliases = (document: CoreSearchProjectionDocument, tenant: string): boolean => (document.aliases ?? []).some( - (alias) => - alias.ref.tenantId !== tenant || - (alias.temporalSearchableText ?? []).some(invalidPeriod) + (alias) => alias.ref.tenantId !== tenant || (alias.temporalSearchableText ?? []).some(invalidPeriod), ); const validateDocument = ( @@ -306,7 +228,7 @@ const validateDocument = ( moduleId: string; resourceType: string; tenantId: string; - }> + }>, ): Result.Result => { if ( document.ref.tenantId !== expected.tenantId || @@ -317,19 +239,14 @@ const validateDocument = ( hasInvalidDocumentPeriods(document) || hasInvalidDocumentAliases(document, expected.tenantId) ) { - return Result.fail( - invalid('Core Search replacement contains an inconsistent document') - ); + return Result.fail(invalid('Core Search replacement contains an inconsistent document')); } return Result.succeed(true); }; const validateReplacement = ( - input: typeof CoreSearchProjectionReplacementSchema.Type -): Result.Result< - typeof CoreSearchProjectionReplacementSchema.Type, - CoreSearchProjectionInvalid -> => { + input: typeof CoreSearchProjectionReplacementSchema.Type, +): Result.Result => { const seen = new Set(); const rebuildVersion = BigInt(input.rebuildVersion); for (const document of input.documents) { @@ -338,15 +255,11 @@ const validateReplacement = ( return Result.fail(validity.failure); } if (BigInt(document.projectionVersion) > rebuildVersion) { - return Result.fail( - invalid('Core Search replacement contains an inconsistent document') - ); + return Result.fail(invalid('Core Search replacement contains an inconsistent document')); } const key = documentKey(document); if (seen.has(key)) { - return Result.fail( - invalid('Core Search replacement contains a duplicate resource') - ); + return Result.fail(invalid('Core Search replacement contains a duplicate resource')); } seen.add(key); } @@ -354,11 +267,8 @@ const validateReplacement = ( }; const validateMutation = ( - mutation: typeof CoreSearchProjectionMutationSchema.Type -): Result.Result< - typeof CoreSearchProjectionMutationSchema.Type, - CoreSearchProjectionInvalid -> => { + mutation: typeof CoreSearchProjectionMutationSchema.Type, +): Result.Result => { if (mutation.kind === 'upsert') { const validity = validateDocument(mutation.document, mutation.document.ref); if (Result.isFailure(validity)) { @@ -369,32 +279,28 @@ const validateMutation = ( }; export const decodeCoreSearchProjectionReplacement = ( - input: UnparsedCoreSearchInput + input: UnparsedCoreSearchInput, ): CoreSearchProjectionReplacement => Result.getOrThrow( Result.flatMap( Schema.decodeUnknownResult(CoreSearchProjectionReplacementSchema, { onExcessProperty: 'error', })(input), - validateReplacement - ) + validateReplacement, + ), ); -export const decodeCoreSearchProjectionMutation = ( - input: UnparsedCoreSearchInput -): CoreSearchProjectionMutation => +export const decodeCoreSearchProjectionMutation = (input: UnparsedCoreSearchInput): CoreSearchProjectionMutation => Result.getOrThrow( Result.flatMap( Schema.decodeUnknownResult(CoreSearchProjectionMutationSchema, { onExcessProperty: 'error', })(input), - validateMutation - ) + validateMutation, + ), ); -const encodeJsonValue = Schema.encodeUnknownResult( - Schema.fromJsonString(Schema.Any) -); +const encodeJsonValue = Schema.encodeUnknownResult(Schema.fromJsonString(Schema.Any)); const stableJson = (value: UnparsedCoreSearchInput): string => { if (Array.isArray(value)) { @@ -403,60 +309,41 @@ const stableJson = (value: UnparsedCoreSearchInput): string => { if (Predicate.isObject(value)) { return `{${Object.entries(value) .toSorted(([left], [right]) => left.localeCompare(right)) - .map( - ([key, entry]) => - `${Result.getOrThrow(encodeJsonValue(key))}:${stableJson(entry)}` - ) + .map(([key, entry]) => `${Result.getOrThrow(encodeJsonValue(key))}:${stableJson(entry)}`) .join(',')}}`; } return Result.getOrThrow(encodeJsonValue(value)); }; /** Private Core persistence identity, independent of transport object/document ordering. */ -export const coreSearchReplacementFingerprint = ( - replacement: CoreSearchProjectionReplacement -): string => +export const coreSearchReplacementFingerprint = (replacement: CoreSearchProjectionReplacement): string => createHash('sha256') .update( stableJson({ ...replacement, documents: replacement.documents.toSorted((left, right) => - left.ref.resourceId.localeCompare(right.ref.resourceId) + left.ref.resourceId.localeCompare(right.ref.resourceId), ), - }) + }), ) .digest('hex'); -const projectionDocumentEquivalence = Schema.toEquivalence( - CoreSearchProjectionDocumentSchema -); +const projectionDocumentEquivalence = Schema.toEquivalence(CoreSearchProjectionDocumentSchema); const decodeMutationEffect = (input: UnparsedCoreSearchInput) => Schema.decodeUnknownEffect(CoreSearchProjectionMutationSchema, { onExcessProperty: 'error', })(input).pipe( - Effect.mapError((cause) => - invalid( - 'Core Search mutation does not match its declared contract', - cause - ) - ), - Effect.flatMap((mutation) => Effect.fromResult(validateMutation(mutation))) + Effect.mapError((cause) => invalid('Core Search mutation does not match its declared contract', cause)), + Effect.flatMap((mutation) => Effect.fromResult(validateMutation(mutation))), ); const decodeReplacementEffect = (input: UnparsedCoreSearchInput) => Schema.decodeUnknownEffect(CoreSearchProjectionReplacementSchema, { onExcessProperty: 'error', })(input).pipe( - Effect.mapError((cause) => - invalid( - 'Core Search replacement does not match its declared contract', - cause - ) - ), - Effect.flatMap((replacement) => - Effect.fromResult(validateReplacement(replacement)) - ) + Effect.mapError((cause) => invalid('Core Search replacement does not match its declared contract', cause)), + Effect.flatMap((replacement) => Effect.fromResult(validateReplacement(replacement))), ); type Stored = Readonly<{ @@ -464,24 +351,19 @@ type Stored = Readonly<{ readonly projectionVersion: string; }>; -const sameStoredDocument = ( - current: Stored, - next: CoreSearchProjectionDocument | undefined -): boolean => +const sameStoredDocument = (current: Stored, next: CoreSearchProjectionDocument | undefined): boolean => current.document === undefined ? next === undefined - : next !== undefined && - projectionDocumentEquivalence(current.document, next); + : next !== undefined && projectionDocumentEquivalence(current.document, next); const shouldApplyMutation = ( current: Stored | undefined, - next: Stored + next: Stored, ): Result.Result => { if (current === undefined) { return Result.succeed(true); } - const order = - BigInt(next.projectionVersion) - BigInt(current.projectionVersion); + const order = BigInt(next.projectionVersion) - BigInt(current.projectionVersion); if (order < 0n) { return Result.succeed(false); } @@ -490,15 +372,13 @@ const shouldApplyMutation = ( } return sameStoredDocument(current, next.document) ? Result.succeed(false) - : Result.fail( - invalid('Core Search mutation reuses a version for different content') - ); + : Result.fail(invalid('Core Search mutation reuses a version for different content')); }; const shouldReplaceProjection = ( prior: Readonly<{ fingerprint: string; version: bigint }> | undefined, version: bigint, - fingerprint: string + fingerprint: string, ): Result.Result => { if (prior === undefined || version > prior.version) { return Result.succeed(true); @@ -508,76 +388,45 @@ const shouldReplaceProjection = ( } return fingerprint === prior.fingerprint ? Result.succeed(false) - : Result.fail( - invalid('Core Search rebuild reuses a version for different content') - ); + : Result.fail(invalid('Core Search rebuild reuses a version for different content')); }; const mergeReplacementDocuments = ( current: Map, - documents: readonly CoreSearchProjectionDocument[] + documents: readonly CoreSearchProjectionDocument[], ): Result.Result => { for (const document of documents) { const existing = current.get(document.ref.resourceId); - if ( - existing === undefined || - BigInt(existing.projectionVersion) < BigInt(document.projectionVersion) - ) { + if (existing === undefined || BigInt(existing.projectionVersion) < BigInt(document.projectionVersion)) { current.set(document.ref.resourceId, { document, projectionVersion: document.projectionVersion, }); - } else if ( - existing.projectionVersion === document.projectionVersion && - !sameStoredDocument(existing, document) - ) { - return Result.fail( - invalid('Core Search rebuild reuses a version for different content') - ); + } else if (existing.projectionVersion === document.projectionVersion && !sameStoredDocument(existing, document)) { + return Result.fail(invalid('Core Search rebuild reuses a version for different content')); } } return Result.succeed(true); }; -const retireMissingDocuments = ( - current: Map, - replacement: CoreSearchProjectionReplacement -): void => { - const nextIds = new Set( - replacement.documents.map(({ ref }) => ref.resourceId) - ); +const retireMissingDocuments = (current: Map, replacement: CoreSearchProjectionReplacement): void => { + const nextIds = new Set(replacement.documents.map(({ ref }) => ref.resourceId)); for (const [id, existing] of current) { - if ( - !nextIds.has(id) && - BigInt(existing.projectionVersion) < BigInt(replacement.rebuildVersion) - ) { + if (!nextIds.has(id) && BigInt(existing.projectionVersion) < BigInt(replacement.rebuildVersion)) { current.set(id, { projectionVersion: replacement.rebuildVersion }); } } }; -export const makeInMemoryCoreSearchProjectionStore = - (): CoreSearchProjectionStoreService => { - const units = new Map>(); - const rebuilds = new Map< - string, - { readonly fingerprint: string; readonly version: bigint } - >(); - const apply: CoreSearchProjectionStoreService['apply'] = Effect.fn( - 'CoreSearchProjectionStore.apply' - )(function* applyCoreSearchProjection(input: UnparsedCoreSearchInput) { +export const makeInMemoryCoreSearchProjectionStore = (): CoreSearchProjectionStoreService => { + const units = new Map>(); + const rebuilds = new Map(); + const apply: CoreSearchProjectionStoreService['apply'] = Effect.fn('CoreSearchProjectionStore.apply')( + function* applyCoreSearchProjection(input: UnparsedCoreSearchInput) { const mutation = yield* decodeMutationEffect(input); - const ref = - mutation.kind === 'upsert' ? mutation.document.ref : mutation.ref; - const version = - mutation.kind === 'upsert' - ? mutation.document.projectionVersion - : mutation.projectionVersion; - const unitKey = projectionUnitKey( - ref.tenantId, - ref.moduleId, - ref.resourceType - ); + const ref = mutation.kind === 'upsert' ? mutation.document.ref : mutation.ref; + const version = mutation.kind === 'upsert' ? mutation.document.projectionVersion : mutation.projectionVersion; + const unitKey = projectionUnitKey(ref.tenantId, ref.moduleId, ref.resourceType); const rebuild = rebuilds.get(unitKey); if (rebuild !== undefined && BigInt(version) <= rebuild.version) { return yield* Effect.void; @@ -587,90 +436,56 @@ export const makeInMemoryCoreSearchProjectionStore = mutation.kind === 'upsert' ? { document: mutation.document, projectionVersion: version } : { projectionVersion: version }; - const shouldApply = yield* Effect.fromResult( - shouldApplyMutation(unit.get(ref.resourceId), next) - ); + const shouldApply = yield* Effect.fromResult(shouldApplyMutation(unit.get(ref.resourceId), next)); if (!shouldApply) { return yield* Effect.void; } unit.set(ref.resourceId, next); units.set(unitKey, unit); return yield* Effect.void; - }); - const queryCandidates: CoreSearchProjectionStoreService['queryCandidates'] = - (input) => - Effect.sync(() => - [ - ...(units - .get( - projectionUnitKey( - input.tenantId, - input.moduleId, - input.resourceType - ) - ) - ?.values() ?? []), - ].flatMap(({ document }) => - document === undefined ? [] : [document] - ) - ); - const replace: CoreSearchProjectionStoreService['replace'] = Effect.fn( - 'CoreSearchProjectionStore.replace' - )(function* replaceCoreSearchProjection(input: UnparsedCoreSearchInput) { + }, + ); + const queryCandidates: CoreSearchProjectionStoreService['queryCandidates'] = (input) => + Effect.sync(() => + [...(units.get(projectionUnitKey(input.tenantId, input.moduleId, input.resourceType))?.values() ?? [])].flatMap( + ({ document }) => (document === undefined ? [] : [document]), + ), + ); + const replace: CoreSearchProjectionStoreService['replace'] = Effect.fn('CoreSearchProjectionStore.replace')( + function* replaceCoreSearchProjection(input: UnparsedCoreSearchInput) { const replacement = yield* decodeReplacementEffect(input); - const unitKey = projectionUnitKey( - replacement.tenantId, - replacement.moduleId, - replacement.resourceType - ); + const unitKey = projectionUnitKey(replacement.tenantId, replacement.moduleId, replacement.resourceType); const prior = rebuilds.get(unitKey); const version = BigInt(replacement.rebuildVersion); const fingerprint = coreSearchReplacementFingerprint(replacement); - const shouldReplace = yield* Effect.fromResult( - shouldReplaceProjection(prior, version, fingerprint) - ); + const shouldReplace = yield* Effect.fromResult(shouldReplaceProjection(prior, version, fingerprint)); if (!shouldReplace) { return yield* Effect.void; } const current = new Map(units.get(unitKey)); - yield* Effect.fromResult( - mergeReplacementDocuments(current, replacement.documents) - ); + yield* Effect.fromResult(mergeReplacementDocuments(current, replacement.documents)); retireMissingDocuments(current, replacement); units.set(unitKey, current); rebuilds.set(unitKey, { fingerprint, version }); return yield* Effect.void; - }); - return Object.freeze({ apply, queryCandidates, replace }); - }; + }, + ); + return Object.freeze({ apply, queryCandidates, replace }); +}; -const isEffectiveTemporalFacet = ( - temporal: CoreSearchTemporalFacet, - key: string, - effectiveAt: number -): boolean => { +const isEffectiveTemporalFacet = (temporal: CoreSearchTemporalFacet, key: string, effectiveAt: number): boolean => { const from = toEpochMillis(temporal.validFrom); - const to = - temporal.validTo === undefined - ? undefined - : toEpochMillis(temporal.validTo); - return ( - temporal.key === key && - from !== undefined && - from <= effectiveAt && - (to === undefined || effectiveAt < to) - ); + const to = temporal.validTo === undefined ? undefined : toEpochMillis(temporal.validTo); + return temporal.key === key && from !== undefined && from <= effectiveAt && (to === undefined || effectiveAt < to); }; const matchesFacets = ( document: CoreSearchProjectionDocument, requested: readonly CoreSearchFacet[], - effectiveAt: number | undefined + effectiveAt: number | undefined, ): boolean => requested.every(({ key, values }) => { - const available = new Set( - document.facets.find((candidate) => candidate.key === key)?.values - ); + const available = new Set(document.facets.find((candidate) => candidate.key === key)?.values); if (effectiveAt !== undefined) { for (const temporal of document.temporalFacets ?? []) { if (isEffectiveTemporalFacet(temporal, key, effectiveAt)) { @@ -682,14 +497,10 @@ const matchesFacets = ( }); type MutableProjectionHit = { - -readonly [ - Key in keyof CoreSearchProjectionHit - ]: CoreSearchProjectionHit[Key]; + -readonly [Key in keyof CoreSearchProjectionHit]: CoreSearchProjectionHit[Key]; }; -const toHit = ( - document: CoreSearchProjectionDocument -): CoreSearchProjectionHit => { +const toHit = (document: CoreSearchProjectionDocument): CoreSearchProjectionHit => { const hit: MutableProjectionHit = { archived: document.archived, facets: document.facets, @@ -700,10 +511,7 @@ const toHit = ( if (document.aliases === undefined && document.matchedRef !== undefined) { hit.matchedRef = document.matchedRef; } - if ( - document.aliases === undefined && - document.matchedSubjectRef !== undefined - ) { + if (document.aliases === undefined && document.matchedSubjectRef !== undefined) { hit.matchedSubjectRef = document.matchedSubjectRef; } if (document.selectedLegalEntityId !== undefined) { @@ -721,49 +529,33 @@ const toHit = ( const matchDocument = ( document: CoreSearchProjectionDocument, needle: string, - effectiveAt: number + effectiveAt: number, ): CoreSearchProjectionHit | undefined => { const hit = toHit(document); - const matches = (values: readonly string[]) => - values.some((value) => normalize(value).includes(needle)); + const matches = (values: readonly string[]) => values.some((value) => normalize(value).includes(needle)); const activeValues = ( - values: readonly (typeof CoreSearchTemporalSearchableTextSchema.Type)[] = [] + values: readonly (typeof CoreSearchTemporalSearchableTextSchema.Type)[] = [], ): readonly string[] => { const active: string[] = []; for (const { validFrom, validTo, value } of values) { const from = toEpochMillis(validFrom); const to = validTo === undefined ? undefined : toEpochMillis(validTo); - if ( - from !== undefined && - from <= effectiveAt && - (to === undefined || effectiveAt < to) - ) { + if (from !== undefined && from <= effectiveAt && (to === undefined || effectiveAt < to)) { active.push(value); } } return active; }; - if ( - matches([ - document.title, - ...document.searchableText, - ...activeValues(document.temporalSearchableText), - ]) - ) { + if (matches([document.title, ...document.searchableText, ...activeValues(document.temporalSearchableText)])) { return hit; } const alias = document.aliases?.find((candidate) => - matches([ - ...candidate.searchableText, - ...activeValues(candidate.temporalSearchableText), - ]) + matches([...candidate.searchableText, ...activeValues(candidate.temporalSearchableText)]), ); if (alias === undefined) { return undefined; } - return alias.kind === 'resource' - ? { ...hit, matchedRef: alias.ref } - : { ...hit, matchedSubjectRef: alias.ref }; + return alias.kind === 'resource' ? { ...hit, matchedRef: alias.ref } : { ...hit, matchedSubjectRef: alias.ref }; }; export const createCoreSearchQueryRuntime: Effect.Effect< @@ -772,41 +564,34 @@ export const createCoreSearchQueryRuntime: Effect.Effect< CoreSearchProjectionStore > = Effect.gen(function* createCoreSearchQueryRuntimeService() { const store = yield* CoreSearchProjectionStore; - const search: CoreSearchQueryRuntimeService['search'] = Effect.fn( - 'CoreSearchQueryRuntime.search' - )(function* searchCoreSearchProjection(input: UnparsedCoreSearchInput) { - const query = yield* Schema.decodeUnknownEffect(CoreSearchQuerySchema)( - input - ).pipe( - Effect.mapError((cause) => - invalid('Core Search query does not match its declared contract', cause) - ) - ); - const documents = yield* store.queryCandidates(query); - const needle = normalize(query.query); - const requestedFacets = query.facets ?? []; - const effectiveAt = - query.effectiveAt === undefined - ? yield* Clock.currentTimeMillis - : DateTime.toEpochMillis(query.effectiveAt); - const hits: CoreSearchProjectionHit[] = []; - for (const document of documents) { - if ( - (query.includeArchived || !document.archived) && - document.selectedLegalEntityId === query.selectedLegalEntityId && - matchesFacets(document, requestedFacets, effectiveAt) - ) { - const hit = matchDocument(document, needle, effectiveAt); - if (hit !== undefined) { - hits.push(hit); + const search: CoreSearchQueryRuntimeService['search'] = Effect.fn('CoreSearchQueryRuntime.search')( + function* searchCoreSearchProjection(input: UnparsedCoreSearchInput) { + const query = yield* Schema.decodeUnknownEffect(CoreSearchQuerySchema)(input).pipe( + Effect.mapError((cause) => invalid('Core Search query does not match its declared contract', cause)), + ); + const documents = yield* store.queryCandidates(query); + const needle = normalize(query.query); + const requestedFacets = query.facets ?? []; + const effectiveAt = + query.effectiveAt === undefined ? yield* Clock.currentTimeMillis : DateTime.toEpochMillis(query.effectiveAt); + const hits: CoreSearchProjectionHit[] = []; + for (const document of documents) { + if ( + (query.includeArchived || !document.archived) && + document.selectedLegalEntityId === query.selectedLegalEntityId && + matchesFacets(document, requestedFacets, effectiveAt) + ) { + const hit = matchDocument(document, needle, effectiveAt); + if (hit !== undefined) { + hits.push(hit); + } } } - } - return hits.toSorted( - (left, right) => - left.title.localeCompare(right.title) || - left.ref.resourceId.localeCompare(right.ref.resourceId) - ); - }); + return hits.toSorted( + (left, right) => + left.title.localeCompare(right.title) || left.ref.resourceId.localeCompare(right.ref.resourceId), + ); + }, + ); return Object.freeze({ search }); }); diff --git a/app/packages/core-runtime/src/search/query-runtime.ts b/app/packages/core-runtime/src/search/query-runtime.ts index d604e193b..5330f4447 100644 --- a/app/packages/core-runtime/src/search/query-runtime.ts +++ b/app/packages/core-runtime/src/search/query-runtime.ts @@ -11,20 +11,16 @@ type UnparsedCoreSearchInput = typeof Schema.Unknown.Type; export interface CoreSearchQueryRuntimeService { readonly search: ( - input: UnparsedCoreSearchInput + input: UnparsedCoreSearchInput, ) => Effect.Effect< readonly CoreSearchProjectionHit[], - | CoreSearchProjectionInvalid - | InstanceType + CoreSearchProjectionInvalid | InstanceType >; } /** Core-owned query port returns hits without private searchable evidence. */ -export class CoreSearchQueryRuntime extends Context.Service< - CoreSearchQueryRuntime, - CoreSearchQueryRuntimeService ->()( +export class CoreSearchQueryRuntime extends Context.Service()( // Preserve the public Context identity after splitting the service into its owning module. // @effect-diagnostics-next-line deterministicKeys:off - '@app/core-runtime/search/projection/CoreSearchQueryRuntime' + '@app/core-runtime/search/projection/CoreSearchQueryRuntime', ) {} diff --git a/app/packages/core-runtime/src/search/worker-snapshot.ts b/app/packages/core-runtime/src/search/worker-snapshot.ts index 861ce9c47..94f8ebedb 100644 --- a/app/packages/core-runtime/src/search/worker-snapshot.ts +++ b/app/packages/core-runtime/src/search/worker-snapshot.ts @@ -4,49 +4,33 @@ import { Context, Effect, Exit, Layer, Option, Schema } from 'effect'; import { SqlError, isSqlError } from 'effect/unstable/sql/SqlError'; import type { DatabaseDriverFailure } from '../database/driver-failure.ts'; -import { - DatabaseTransactionFailure, - decodeDatabaseDriverFailure, -} from '../database/driver-failure.ts'; +import { DatabaseTransactionFailure, decodeDatabaseDriverFailure } from '../database/driver-failure.ts'; import { CoreDatabase } from '../db/client.ts'; -import { - domainEvents, - legalEntities, - searchProjectionGenerations, -} from '../db/schema.ts'; +import { domainEvents, legalEntities, searchProjectionGenerations } from '../db/schema.ts'; import type { CoreDatabaseExecutor, CoreTransaction } from '../db/types.ts'; import type { OutboxWorkerHandlerContext } from '../outbox/definition.ts'; import { isVerifiedOutboxWorkerHandlerContext } from '../outbox/definition.ts'; import { CORE_SEARCH_INGESTION_REGISTRATIONS } from './ingestion.ts'; import type { CoreSearchProjectionUnavailableError } from './projection.ts'; -import { - CoreSearchProjectionInvalid, - CoreSearchProjectionUnavailable, -} from './projection.ts'; +import { CoreSearchProjectionInvalid, CoreSearchProjectionUnavailable } from './projection.ts'; export interface CoreSearchSnapshotReadExecutor { readonly select: CoreTransaction['select']; } -type SnapshotError = - | CoreSearchProjectionInvalid - | CoreSearchProjectionUnavailableError; +type SnapshotError = CoreSearchProjectionInvalid | CoreSearchProjectionUnavailableError; export interface CoreSearchWorkerSnapshotView { /** Diagnostic committed-event watermark, not a document version. */ readonly eventWatermark: string; readonly forLegalEntity: ( legalEntityId: string, - read: ( - executor: CoreSearchSnapshotReadExecutor - ) => Effect.Effect + read: (executor: CoreSearchSnapshotReadExecutor) => Effect.Effect, ) => Effect.Effect; readonly legalEntityIds: readonly string[]; readonly projectionVersion: string; readonly tenant: ( - read: ( - executor: CoreSearchSnapshotReadExecutor - ) => Effect.Effect + read: (executor: CoreSearchSnapshotReadExecutor) => Effect.Effect, ) => Effect.Effect; readonly tenantId: string; } @@ -54,9 +38,7 @@ export interface CoreSearchWorkerSnapshotView { export interface CoreSearchWorkerSnapshotService { readonly read: ( context: OutboxWorkerHandlerContext, - read: ( - snapshot: CoreSearchWorkerSnapshotView - ) => Effect.Effect + read: (snapshot: CoreSearchWorkerSnapshotView) => Effect.Effect, ) => Effect.Effect; } @@ -100,67 +82,47 @@ export interface CoreSearchSnapshotBackend { use: ( scope: SnapshotScope, executor: CoreSearchSnapshotReadExecutor, - install: ( - legalEntityId?: string - ) => Effect.Effect - ) => Effect.Effect + install: (legalEntityId?: string) => Effect.Effect, + ) => Effect.Effect, ) => Effect.Effect; } const viewForSnapshot = ( scope: SnapshotScope, executor: CoreSearchSnapshotReadExecutor, - install: ( - legalEntityId?: string - ) => Effect.Effect + install: (legalEntityId?: string) => Effect.Effect, ): OwnedSnapshotView => { let active = true; let inUse = false; const scoped = ( legalEntityId: string | undefined, - read: ( - executor: CoreSearchSnapshotReadExecutor - ) => Effect.Effect + read: (executor: CoreSearchSnapshotReadExecutor) => Effect.Effect, ): Effect.Effect => Effect.suspend((): Effect.Effect => { - if ( - !active || - inUse || - (legalEntityId !== undefined && - !scope.legalEntityIds.includes(legalEntityId)) - ) { + if (!active || inUse || (legalEntityId !== undefined && !scope.legalEntityIds.includes(legalEntityId))) { return Effect.fail(invalid()); } inUse = true; return Effect.gen(function* readOwnedScope() { - const exit = yield* Effect.exit( - install(legalEntityId).pipe(Effect.andThen(read(executor))) - ); + const exit = yield* Effect.exit(install(legalEntityId).pipe(Effect.andThen(read(executor)))); yield* install(); - return yield* Exit.isSuccess(exit) - ? Effect.succeed(exit.value) - : Effect.failCause(exit.cause); + return yield* Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause); }).pipe( Effect.ensuring( Effect.sync(() => { inUse = false; - }) - ) + }), + ), ); }); const view = Object.freeze({ ...scope, forLegalEntity: ( legalEntityId: string, - read: ( - executor: CoreSearchSnapshotReadExecutor - ) => Effect.Effect + read: (executor: CoreSearchSnapshotReadExecutor) => Effect.Effect, ) => scoped(legalEntityId, read), - tenant: ( - read: ( - executor: CoreSearchSnapshotReadExecutor - ) => Effect.Effect - ) => scoped(undefined, read), + tenant: (read: (executor: CoreSearchSnapshotReadExecutor) => Effect.Effect) => + scoped(undefined, read), }); return { close: () => { @@ -170,14 +132,10 @@ const viewForSnapshot = ( }; }; -export const makeCoreSearchWorkerSnapshot = ( - backend: CoreSearchSnapshotBackend -): CoreSearchWorkerSnapshotService => ({ +export const makeCoreSearchWorkerSnapshot = (backend: CoreSearchSnapshotBackend): CoreSearchWorkerSnapshotService => ({ read: ( context: OutboxWorkerHandlerContext, - read: ( - snapshot: CoreSearchWorkerSnapshotView - ) => Effect.Effect + read: (snapshot: CoreSearchWorkerSnapshotView) => Effect.Effect, ) => { if ( !isVerifiedOutboxWorkerHandlerContext(context) || @@ -185,7 +143,7 @@ export const makeCoreSearchWorkerSnapshot = ( (registration) => registration.producerModuleKey === context.producerModuleKey && registration.topic === context.topic && - registration.workerKey === context.workerKey + registration.workerKey === context.workerKey, ) ) { return Effect.fail(invalid()); @@ -193,193 +151,163 @@ export const makeCoreSearchWorkerSnapshot = ( return backend .run(context, (scope, executor, install) => { const snapshot = viewForSnapshot(scope, executor, install); - return read(snapshot.view).pipe( - Effect.ensuring(Effect.sync(snapshot.close)) - ); + return read(snapshot.view).pipe(Effect.ensuring(Effect.sync(snapshot.close))); }) .pipe(Effect.withSpan('CoreSearch.workerSnapshot')); }, }); -type CoreSearchSnapshotDriverError = - | DatabaseDriverFailure - | CoreSearchProjectionUnavailableError; +type CoreSearchSnapshotDriverError = DatabaseDriverFailure | CoreSearchProjectionUnavailableError; const snapshotDriverError = (cause: unknown): CoreSearchSnapshotDriverError => - Option.getOrElse(decodeDatabaseDriverFailure(cause), () => - unavailable(cause) - ); + Option.getOrElse(decodeDatabaseDriverFailure(cause), () => unavailable(cause)); const serializationFailure = (cause: unknown): boolean => Option.exists( decodeDatabaseDriverFailure(cause), (failure) => - Schema.is(DatabaseTransactionFailure)(failure) && - failure.kind === 'sqlstate' && - failure.code.slice(2) === '001' + Schema.is(DatabaseTransactionFailure)(failure) && failure.kind === 'sqlstate' && failure.code.slice(2) === '001', ); /** Bounded retry is restricted to PostgreSQL snapshot serialization failures. */ export const retryCoreSearchSnapshot = ( - run: Effect.Effect + run: Effect.Effect, ): Effect.Effect => run.pipe( Effect.retry({ times: 3, while: serializationFailure, - }) + }), ); type CoreSearchSnapshotDatabase = Readonly<{ executor: CoreDatabaseExecutor }>; export const makePostgresCoreSearchSnapshotBackend = ( - database: CoreSearchSnapshotDatabase + database: CoreSearchSnapshotDatabase, ): CoreSearchSnapshotBackend => { - const run: CoreSearchSnapshotBackend['run'] = Effect.fn( - 'CoreSearchSnapshotBackend.runPostgres' - )(function* runPostgresCoreSearchSnapshot( - context: OutboxWorkerHandlerContext, - readSnapshot: ( - scope: SnapshotScope, - executor: CoreSearchSnapshotReadExecutor, - install: ( - legalEntityId?: string - ) => Effect.Effect - ) => Effect.Effect - ) { - const transactionProgram = Effect.fn( - 'CoreSearchSnapshotBackend.transaction' - )(function* runCoreSearchSnapshotTransaction(transaction: CoreTransaction) { - const installScope = Effect.fn('CoreSearchSnapshotBackend.installScope')( - function* installCoreSearchSnapshotScope(legalEntityId?: string) { - const result = yield* transaction - .execute<{ - legal_entity_id: string; - tenant_id: string; - }>( - sql` + const run: CoreSearchSnapshotBackend['run'] = Effect.fn('CoreSearchSnapshotBackend.runPostgres')( + function* runPostgresCoreSearchSnapshot( + context: OutboxWorkerHandlerContext, + readSnapshot: ( + scope: SnapshotScope, + executor: CoreSearchSnapshotReadExecutor, + install: (legalEntityId?: string) => Effect.Effect, + ) => Effect.Effect, + ) { + const transactionProgram = Effect.fn('CoreSearchSnapshotBackend.transaction')( + function* runCoreSearchSnapshotTransaction(transaction: CoreTransaction) { + const installScope = Effect.fn('CoreSearchSnapshotBackend.installScope')( + function* installCoreSearchSnapshotScope(legalEntityId?: string) { + const result = yield* transaction + .execute<{ + legal_entity_id: string; + tenant_id: string; + }>( + sql` select set_config('ontos.tenant_id', ${context.tenantId}, true) as tenant_id, set_config('ontos.legal_entity_id', ${legalEntityId ?? ''}, true) as legal_entity_id `, - 'objects' - ) + 'objects', + ) + .pipe(Effect.mapError(snapshotDriverError)); + const [setting] = result; + if (setting?.tenant_id !== context.tenantId || setting.legal_entity_id !== (legalEntityId ?? '')) { + return yield* unavailable(); + } + return yield* Effect.void; + }, + ); + const install = (legalEntityId?: string) => installScope(legalEntityId).pipe(Effect.mapError(unavailable)); + + yield* installScope(); + // RR rejects a waiter whose snapshot predates the preceding generation commit. + // Retrying the whole transaction makes increasing generations imply fresh snapshots, + // even when business event sequences commit out of their allocation order. + const [generation] = yield* transaction + .insert(searchProjectionGenerations) + .values({ + generation: 1n, + sourceModuleKey: context.producerModuleKey, + tenantId: context.tenantId, + }) + .onConflictDoUpdate({ + set: { + generation: sql`${searchProjectionGenerations.generation} + 1`, + updatedAt: sql`now()`, + }, + target: [searchProjectionGenerations.tenantId, searchProjectionGenerations.sourceModuleKey], + }) + .returning({ version: searchProjectionGenerations.generation }) + .pipe(Effect.mapError(snapshotDriverError)); + if (generation === undefined) { + return yield* unavailable(); + } + const [watermark] = yield* transaction + .select({ + version: sql`max(${domainEvents.tenantSequenceNo})::text`, + }) + .from(domainEvents) + .where(eq(domainEvents.tenantId, context.tenantId)) .pipe(Effect.mapError(snapshotDriverError)); - const [setting] = result; if ( - setting?.tenant_id !== context.tenantId || - setting.legal_entity_id !== (legalEntityId ?? '') + watermark?.version === null || + watermark?.version === undefined || + BigInt(watermark.version) < context.tenantSequenceNo ) { return yield* unavailable(); } - return yield* Effect.void; - } - ); - const install = (legalEntityId?: string) => - installScope(legalEntityId).pipe(Effect.mapError(unavailable)); - - yield* installScope(); - // RR rejects a waiter whose snapshot predates the preceding generation commit. - // Retrying the whole transaction makes increasing generations imply fresh snapshots, - // even when business event sequences commit out of their allocation order. - const [generation] = yield* transaction - .insert(searchProjectionGenerations) - .values({ - generation: 1n, - sourceModuleKey: context.producerModuleKey, - tenantId: context.tenantId, - }) - .onConflictDoUpdate({ - set: { - generation: sql`${searchProjectionGenerations.generation} + 1`, - updatedAt: sql`now()`, - }, - target: [ - searchProjectionGenerations.tenantId, - searchProjectionGenerations.sourceModuleKey, - ], - }) - .returning({ version: searchProjectionGenerations.generation }) - .pipe(Effect.mapError(snapshotDriverError)); - if (generation === undefined) { - return yield* unavailable(); - } - const [watermark] = yield* transaction - .select({ - version: sql`max(${domainEvents.tenantSequenceNo})::text`, - }) - .from(domainEvents) - .where(eq(domainEvents.tenantId, context.tenantId)) - .pipe(Effect.mapError(snapshotDriverError)); - if ( - watermark?.version === null || - watermark?.version === undefined || - BigInt(watermark.version) < context.tenantSequenceNo - ) { - return yield* unavailable(); - } - yield* transaction - .update(searchProjectionGenerations) - .set({ - eventWatermark: BigInt(watermark.version), - }) - .where( - and( - eq(searchProjectionGenerations.tenantId, context.tenantId), - eq( - searchProjectionGenerations.sourceModuleKey, - context.producerModuleKey + yield* transaction + .update(searchProjectionGenerations) + .set({ + eventWatermark: BigInt(watermark.version), + }) + .where( + and( + eq(searchProjectionGenerations.tenantId, context.tenantId), + eq(searchProjectionGenerations.sourceModuleKey, context.producerModuleKey), + ), ) - ) - ) - .pipe(Effect.mapError(snapshotDriverError)); - const entities = yield* transaction - .select({ legalEntityId: legalEntities.legalEntityId }) - .from(legalEntities) - .where(eq(legalEntities.tenantId, context.tenantId)) - .pipe(Effect.mapError(snapshotDriverError)); - return yield* Effect.exit( - readSnapshot( - { - eventWatermark: watermark.version, - legalEntityIds: Object.freeze( - entities.map(({ legalEntityId }) => legalEntityId) + .pipe(Effect.mapError(snapshotDriverError)); + const entities = yield* transaction + .select({ legalEntityId: legalEntities.legalEntityId }) + .from(legalEntities) + .where(eq(legalEntities.tenantId, context.tenantId)) + .pipe(Effect.mapError(snapshotDriverError)); + return yield* Effect.exit( + readSnapshot( + { + eventWatermark: watermark.version, + legalEntityIds: Object.freeze(entities.map(({ legalEntityId }) => legalEntityId)), + projectionVersion: generation.version.toString(), + tenantId: context.tenantId, + }, + Object.freeze({ select: transaction.select.bind(transaction) }), + install, ), - projectionVersion: generation.version.toString(), - tenantId: context.tenantId, - }, - Object.freeze({ select: transaction.select.bind(transaction) }), - install - ) + ); + }, ); - }); - const snapshotExit = yield* retryCoreSearchSnapshot( - database.executor - .transaction( - Effect.fn('snapshotTransactionEffect')( - function* snapshotTransactionEffect(transaction: CoreTransaction) { + const snapshotExit = yield* retryCoreSearchSnapshot( + database.executor + .transaction( + Effect.fn('snapshotTransactionEffect')(function* snapshotTransactionEffect(transaction: CoreTransaction) { yield* transaction.setTransaction({ isolationLevel: 'repeatable read', }); return yield* transactionProgram(transaction); - } + }), ) - ) - .pipe( - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) + .pipe( + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), + Effect.mapError((failure) => (Schema.is(SqlError)(failure) ? snapshotDriverError(failure) : failure)), ), - Effect.mapError((failure) => - Schema.is(SqlError)(failure) - ? snapshotDriverError(failure) - : failure - ) - ) - ).pipe(Effect.mapError(unavailable)); - return yield* Exit.isSuccess(snapshotExit) - ? Effect.succeed(snapshotExit.value) - : Effect.failCause(snapshotExit.cause); - }); + ).pipe(Effect.mapError(unavailable)); + return yield* Exit.isSuccess(snapshotExit) + ? Effect.succeed(snapshotExit.value) + : Effect.failCause(snapshotExit.cause); + }, + ); return Object.freeze({ run }); }; @@ -388,8 +316,6 @@ export const CoreSearchWorkerSnapshotLive = Layer.effect( CoreSearchWorkerSnapshot, Effect.gen(function* makeCoreSearchWorkerSnapshotLive() { const database = yield* CoreDatabase; - return makeCoreSearchWorkerSnapshot( - makePostgresCoreSearchSnapshotBackend(database) - ); - }) + return makeCoreSearchWorkerSnapshot(makePostgresCoreSearchSnapshotBackend(database)); + }), ); diff --git a/app/packages/core-runtime/src/testing/actions.ts b/app/packages/core-runtime/src/testing/actions.ts index da93fd171..41fa9e4da 100644 --- a/app/packages/core-runtime/src/testing/actions.ts +++ b/app/packages/core-runtime/src/testing/actions.ts @@ -7,11 +7,7 @@ import { Reactivity } from 'effect/unstable/reactivity'; import type { Connection } from 'effect/unstable/sql/SqlConnection'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import type { - ActionRegistration, - ActionServiceFactory, - AnyActionRegistration, -} from '../actions/definition.ts'; +import type { ActionRegistration, ActionServiceFactory, AnyActionRegistration } from '../actions/definition.ts'; import { getActionServiceFactory } from '../actions/definition.ts'; import { ActionInvocationNotFound, @@ -33,35 +29,17 @@ import { DatabaseCommitAcknowledgementAmbiguous } from '../database/driver-failu import { coreRelations } from '../db/schema.ts'; import { makeModuleEntrypointGateway } from '../modules/module-entrypoint-gateway.ts'; import type { ModuleStateGateService } from '../modules/module-state-gate.ts'; -import { - checkModuleEntrypoint, - makeModuleStateSnapshot, -} from '../modules/module-state-gate.ts'; +import { checkModuleEntrypoint, makeModuleStateSnapshot } from '../modules/module-state-gate.ts'; import type { TenantModuleState } from '../modules/tenant-module-state-service.ts'; import { makeOperationalScopeResolver } from '../operations/context.ts'; import { OperationContextUnavailable } from '../operations/errors.ts'; -import type { - ContextAccessDecision, - ContextAccessService, -} from '../permissions/context-access.ts'; +import type { ContextAccessDecision, ContextAccessService } from '../permissions/context-access.ts'; -const actionTestServiceBinding: unique symbol = Symbol( - 'test-action-service-binding' -); -const querySchema = Schema.Union([ - Schema.String, - Schema.Struct({ text: Schema.String }), -]); +const actionTestServiceBinding: unique symbol = Symbol('test-action-service-binding'); +const querySchema = Schema.Union([Schema.String, Schema.Struct({ text: Schema.String })]); const scopeValuesSchema = Schema.Tuple([Schema.String, Schema.String]); -const idempotencyScopeSchema = Schema.Tuple([ - Schema.String, - Schema.String, - Schema.String, - Schema.String, -]); -const encodeIdempotencyScope = Schema.encodeEffect( - Schema.fromJsonString(idempotencyScopeSchema) -); +const idempotencyScopeSchema = Schema.Tuple([Schema.String, Schema.String, Schema.String, Schema.String]); +const encodeIdempotencyScope = Schema.encodeEffect(Schema.fromJsonString(idempotencyScopeSchema)); const testCommitAcknowledgementSqlState = ['0', '8007'].join(''); const completionTime = () => DateTime.toDateUtc(DateTime.makeUnsafe(0)); @@ -93,9 +71,7 @@ class ActionTestServiceBindingValue implements ActionTestServiceBinding { } } -const isActionTestServiceBindingValue = Schema.is( - Schema.instanceOf(ActionTestServiceBindingValue) -); +const isActionTestServiceBindingValue = Schema.is(Schema.instanceOf(ActionTestServiceBindingValue)); export const bindActionTestServices = < Payload extends Schema.ConstraintDecoder, @@ -106,18 +82,9 @@ export const bindActionTestServices = < Services, Requirements, >( - registration: ActionRegistration< - Payload, - Result, - DomainError, - Events, - Owner, - Services, - Requirements - >, - services: NoInfer -): ActionTestServiceBinding => - Object.freeze(new ActionTestServiceBindingValue({ registration, services })); + registration: ActionRegistration, + services: NoInfer, +): ActionTestServiceBinding => Object.freeze(new ActionTestServiceBindingValue({ registration, services })); export interface ActionTestHarnessOptions { readonly actionPermission?: ContextAccessDecision; @@ -160,386 +127,341 @@ const persistenceFailure = () => * Authorization defaults to denied; only the test scope's ordinary LE access defaults allowed. */ const queryRows = (result: { readonly rows: readonly object[] }) => result.rows; -const sqlFailure = (cause: unknown) => - new SqlError({ reason: new ConnectionError({ cause }) }); +const sqlFailure = (cause: unknown) => new SqlError({ reason: new ConnectionError({ cause }) }); -const actionTestHarness = Effect.fn('ActionTestHarness.make')( - function* actionTestHarness(options: ActionTestHarnessOptions = {}) { - const invocations = new Map(); - const idempotency = new Map(); - const committed: FlushActionSuccessInput[] = []; - const permissionDenials: RejectPermissionDeniedInput[] = []; - const policyDenials: FinalizeActionPolicyDenialInput[] = []; - const stages: ActionRuntimeStage[] = []; - let transactionCount = 0; - let loseCommitAcknowledgement = - options.commitAcknowledgement === 'indeterminate-once'; - let pendingCommit: Effect.Effect[] = - []; - let connectionQueue = Effect.void; +const actionTestHarness = Effect.fn('ActionTestHarness.make')(function* actionTestHarness( + options: ActionTestHarnessOptions = {}, +) { + const invocations = new Map(); + const idempotency = new Map(); + const committed: FlushActionSuccessInput[] = []; + const permissionDenials: RejectPermissionDeniedInput[] = []; + const policyDenials: FinalizeActionPolicyDenialInput[] = []; + const stages: ActionRuntimeStage[] = []; + let transactionCount = 0; + let loseCommitAcknowledgement = options.commitAcknowledgement === 'indeterminate-once'; + let pendingCommit: Effect.Effect[] = []; + let connectionQueue = Effect.void; - const find = ( - id: string - ): Effect.Effect< - ActionTestInvocation, - ActionInvocationPersistenceError - > => { - const invocation = invocations.get(id); - return invocation === undefined - ? Effect.fail(persistenceFailure()) - : Effect.succeed(invocation); + const find = (id: string): Effect.Effect => { + const invocation = invocations.get(id); + return invocation === undefined ? Effect.fail(persistenceFailure()) : Effect.succeed(invocation); + }; + const prepare = Effect.fn('ActionTestHarness.prepare')(function* prepareTestInvocation( + input: PrepareActionInvocationInput, + ) { + const key = + input.idempotencyKey === undefined + ? undefined + : yield* encodeIdempotencyScope([ + input.principal.tenantId, + input.principal.principalId, + input.actionKey, + input.idempotencyKey, + ]).pipe(Effect.orDie); + const existing = key === undefined ? undefined : idempotency.get(key); + if (existing !== undefined) { + return yield* find(existing); + } + const invocation: ActionTestInvocation = { + actionInvocationId: randomUUID(), + actionKey: input.actionKey, + completedAt: null, + idempotencyKey: input.idempotencyKey, + principalId: input.principal.principalId, + requestHash: input.requestHash, + status: 'received', + tenantId: input.principal.tenantId, }; - const prepare = Effect.fn('ActionTestHarness.prepare')( - function* prepareTestInvocation(input: PrepareActionInvocationInput) { - const key = - input.idempotencyKey === undefined - ? undefined - : yield* encodeIdempotencyScope([ - input.principal.tenantId, - input.principal.principalId, - input.actionKey, - input.idempotencyKey, - ]).pipe(Effect.orDie); - const existing = key === undefined ? undefined : idempotency.get(key); - if (existing !== undefined) { - return yield* find(existing); - } - const invocation: ActionTestInvocation = { - actionInvocationId: randomUUID(), - actionKey: input.actionKey, - completedAt: null, - idempotencyKey: input.idempotencyKey, - principalId: input.principal.principalId, - requestHash: input.requestHash, - status: 'received', - tenantId: input.principal.tenantId, - }; - invocations.set(invocation.actionInvocationId, invocation); - if (key !== undefined) { - idempotency.set(key, invocation.actionInvocationId); - } - return invocation; - } - ); - const commitSuccess = Effect.fn('ActionTestHarness.commitSuccess')( - function* commitTestSuccess(input: FlushActionSuccessInput) { - const invocation = yield* find(input.actionInvocationId); - committed.push(input); - invocations.set(input.actionInvocationId, { - ...invocation, - completedAt: completionTime(), - status: 'succeeded', - }); - } - ); - const recordRejection = < - Input extends { readonly actionInvocationId: string }, - >( - input: Input, - denials: Input[] - ) => - find(input.actionInvocationId).pipe( - Effect.flatMap((invocation) => - Effect.sync(() => { - denials.push(input); - invocations.set(input.actionInvocationId, { - ...invocation, - completedAt: completionTime(), - status: 'rejected', - }); - }) - ) - ); - const repository: ActionRepositoryService = { - createOrResolveInvocation: (_executor, input) => - Effect.suspend(() => prepare(input)), - finalizePolicyDenial: (_executor, input) => - recordRejection(input, policyDenials), - flushSuccess: (_transaction, input) => + invocations.set(invocation.actionInvocationId, invocation); + if (key !== undefined) { + idempotency.set(key, invocation.actionInvocationId); + } + return invocation; + }); + const commitSuccess = Effect.fn('ActionTestHarness.commitSuccess')(function* commitTestSuccess( + input: FlushActionSuccessInput, + ) { + const invocation = yield* find(input.actionInvocationId); + committed.push(input); + invocations.set(input.actionInvocationId, { + ...invocation, + completedAt: completionTime(), + status: 'succeeded', + }); + }); + const recordRejection = (input: Input, denials: Input[]) => + find(input.actionInvocationId).pipe( + Effect.flatMap((invocation) => Effect.sync(() => { - pendingCommit.push(commitSuccess(input)); + denials.push(input); + invocations.set(input.actionInvocationId, { + ...invocation, + completedAt: completionTime(), + status: 'rejected', + }); }), - lockInvocation: (_transaction, id) => Effect.suspend(() => find(id)), - rejectPermissionDenied: (_executor, input) => - recordRejection(input, permissionDenials), - resolveInvocation: (_executor, input) => - Effect.suspend(() => { - const invocation = invocations.get(input.invocationId); - return invocation?.tenantId === input.principal.tenantId && - invocation.principalId === input.principal.principalId - ? Effect.succeed(invocation) - : Effect.fail( - new ActionInvocationNotFound({ - code: 'action_invocation_not_found', - reason: 'The test invocation is outside this scope', - }) - ); + ), + ); + const repository: ActionRepositoryService = { + createOrResolveInvocation: (_executor, input) => Effect.suspend(() => prepare(input)), + finalizePolicyDenial: (_executor, input) => recordRejection(input, policyDenials), + flushSuccess: (_transaction, input) => + Effect.sync(() => { + pendingCommit.push(commitSuccess(input)); + }), + lockInvocation: (_transaction, id) => Effect.suspend(() => find(id)), + rejectPermissionDenied: (_executor, input) => recordRejection(input, permissionDenials), + resolveInvocation: (_executor, input) => + Effect.suspend(() => { + const invocation = invocations.get(input.invocationId); + return invocation?.tenantId === input.principal.tenantId && + invocation.principalId === input.principal.principalId + ? Effect.succeed(invocation) + : Effect.fail( + new ActionInvocationNotFound({ + code: 'action_invocation_not_found', + reason: 'The test invocation is outside this scope', + }), + ); + }), + transitionInvocationToRunning: (_executor, id) => + find(id).pipe( + Effect.map((invocation) => { + if (invocation.completedAt !== null) { + return invocation; + } + const running = { ...invocation, status: 'running' as const }; + invocations.set(id, running); + return running; }), - transitionInvocationToRunning: (_executor, id) => - find(id).pipe( - Effect.map((invocation) => { - if (invocation.completedAt !== null) { - return invocation; - } - const running = { ...invocation, status: 'running' as const }; - invocations.set(id, running); - return running; - }) - ), - }; + ), + }; - const executeTestQuery = Effect.fn('ActionTestHarness.executeQuery')( - function* executeTestQueryEffect( - scope: ActionTestConnectionScope, - query: Query, - values?: Values - ) { - const decoded = yield* Schema.decodeUnknownEffect(querySchema)(query); - const sql = Schema.is(Schema.String)(decoded) ? decoded : decoded.text; - if (sql === 'begin') { - transactionCount += 1; - } else if (sql === 'commit') { - yield* Effect.all(pendingCommit, { concurrency: 1, discard: true }); - pendingCommit = []; - if (loseCommitAcknowledgement) { - loseCommitAcknowledgement = false; - return yield* new DatabaseCommitAcknowledgementAmbiguous({ - code: testCommitAcknowledgementSqlState, - kind: 'sqlstate', - }); - } - } else if (sql === 'rollback') { - pendingCommit = []; - } else if (sql.includes('set_config')) { - [scope.tenantId, scope.legalEntityId] = - yield* Schema.decodeUnknownEffect(scopeValuesSchema)(values); - } else if (sql.includes('current_setting')) { - return { - rows: [ - { - legal_entity_id: scope.legalEntityId, - tenant_id: scope.tenantId, - }, - ], - }; - } else { - return yield* Effect.die( - 'Owner SQL is unavailable in the Action test harness; bind typed services' - ); - } - return { rows: [] }; + const executeTestQuery = Effect.fn('ActionTestHarness.executeQuery')(function* executeTestQueryEffect( + scope: ActionTestConnectionScope, + query: Query, + values?: Values, + ) { + const decoded = yield* Schema.decodeUnknownEffect(querySchema)(query); + const sql = Schema.is(Schema.String)(decoded) ? decoded : decoded.text; + if (sql === 'begin') { + transactionCount += 1; + } else if (sql === 'commit') { + yield* Effect.all(pendingCommit, { concurrency: 1, discard: true }); + pendingCommit = []; + if (loseCommitAcknowledgement) { + loseCommitAcknowledgement = false; + return yield* new DatabaseCommitAcknowledgementAmbiguous({ + code: testCommitAcknowledgementSqlState, + kind: 'sqlstate', + }); } - ); + } else if (sql === 'rollback') { + pendingCommit = []; + } else if (sql.includes('set_config')) { + [scope.tenantId, scope.legalEntityId] = yield* Schema.decodeUnknownEffect(scopeValuesSchema)(values); + } else if (sql.includes('current_setting')) { + return { + rows: [ + { + legal_entity_id: scope.legalEntityId, + tenant_id: scope.tenantId, + }, + ], + }; + } else { + return yield* Effect.die('Owner SQL is unavailable in the Action test harness; bind typed services'); + } + return { rows: [] }; + }); - const acquireConnection = Effect.suspend(() => { - const previous = connectionQueue; - const released = Deferred.makeUnsafe(); - connectionQueue = Deferred.await(released).pipe(Effect.asVoid); - return Effect.gen(function* acquireTestConnection() { - yield* previous; - yield* Effect.addFinalizer(() => Deferred.succeed(released, null)); - const scope: ActionTestConnectionScope = { - legalEntityId: '', - tenantId: '', - }; - pendingCommit = []; - const execute = (query: string, values: readonly unknown[]) => - executeTestQuery(scope, query.toLowerCase(), values).pipe( - Effect.map(queryRows), - Effect.mapError(sqlFailure) - ); - const unsupported = Effect.die( - 'Owner SQL is unavailable in the Action test harness' - ); - return { - execute, - executeRaw: execute, - executeStream: () => Stream.fromEffect(unsupported), - executeUnprepared: execute, - executeValues: () => unsupported, - executeValuesUnprepared: () => unsupported, - } satisfies Connection; - }); + const acquireConnection = Effect.suspend(() => { + const previous = connectionQueue; + const released = Deferred.makeUnsafe(); + connectionQueue = Deferred.await(released).pipe(Effect.asVoid); + return Effect.gen(function* acquireTestConnection() { + yield* previous; + yield* Effect.addFinalizer(() => Deferred.succeed(released, null)); + const scope: ActionTestConnectionScope = { + legalEntityId: '', + tenantId: '', + }; + pendingCommit = []; + const execute = (query: string, values: readonly unknown[]) => + executeTestQuery(scope, query.toLowerCase(), values).pipe(Effect.map(queryRows), Effect.mapError(sqlFailure)); + const unsupported = Effect.die('Owner SQL is unavailable in the Action test harness'); + return { + execute, + executeRaw: execute, + executeStream: () => Stream.fromEffect(unsupported), + executeUnprepared: execute, + executeValues: () => unsupported, + executeValuesUnprepared: () => unsupported, + } satisfies Connection; }); - const database = yield* Effect.scoped( - Effect.gen(function* makeTestDatabase() { - const reactivity = yield* Reactivity.make; - const client = yield* PgClient.makeWith({ - acquirer: acquireConnection, - config: {}, - listenAcquirer: Effect.die( - 'Notifications are unavailable in the Action test harness' - ), - transactionAcquirer: acquireConnection, - }).pipe( - Effect.provideService(Reactivity.Reactivity, reactivity), - Effect.orDie - ); - return { - executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client) - ), - }; - }) - ); - const contextAccess: ContextAccessService = { - legalEntities: ({ legalEntityIds, permission }) => - Effect.succeed( - legalEntityIds.map((key) => ({ - decision: - permission === undefined || permission === 'access' - ? (options.legalEntityAccess ?? 'allowed') - : (options.legalEntityPermission ?? 'denied'), - key, - })) - ), - modules: ({ moduleIds }) => - Effect.succeed( - moduleIds.map((key) => ({ decision: 'allowed' as const, key })) - ), - resources: ({ resources }) => - Effect.succeed( - resources.map((resource) => ({ - decision: options.resourcePermission ?? 'denied', - key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, - })) - ), - tenants: ({ tenantIds }) => - Effect.succeed( - tenantIds.map((key) => ({ - decision: options.tenantPermission ?? 'denied', - key, - })) + }); + const database = yield* Effect.scoped( + Effect.gen(function* makeTestDatabase() { + const reactivity = yield* Reactivity.make; + const client = yield* PgClient.makeWith({ + acquirer: acquireConnection, + config: {}, + listenAcquirer: Effect.die('Notifications are unavailable in the Action test harness'), + transactionAcquirer: acquireConnection, + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); + return { + executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( + Effect.provideService(PgClient.PgClient, client), ), - }; - const scopeResolver = makeOperationalScopeResolver( - { - load: (principal) => - options.scope === 'unavailable' - ? Effect.fail( - new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'Test scope unavailable', - }) - ) - : Effect.succeed({ - bindingPrincipalId: principal.principalId, - bindingRevokedAt: null, - bindingStatus: 'active', - bindingTenantId: principal.tenantId, - legalEntityStatus: 'active', - legalEntityTenantId: principal.tenantId, - principalStatus: - options.scope === 'denied' ? 'inactive' : 'active', - principalTenantId: principal.tenantId, - tenantStatus: 'active', + }; + }), + ); + const contextAccess: ContextAccessService = { + legalEntities: ({ legalEntityIds, permission }) => + Effect.succeed( + legalEntityIds.map((key) => ({ + decision: + permission === undefined || permission === 'access' + ? (options.legalEntityAccess ?? 'allowed') + : (options.legalEntityPermission ?? 'denied'), + key, + })), + ), + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision: 'allowed' as const, key }))), + resources: ({ resources }) => + Effect.succeed( + resources.map((resource) => ({ + decision: options.resourcePermission ?? 'denied', + key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, + })), + ), + tenants: ({ tenantIds }) => + Effect.succeed( + tenantIds.map((key) => ({ + decision: options.tenantPermission ?? 'denied', + key, + })), + ), + }; + const scopeResolver = makeOperationalScopeResolver( + { + load: (principal) => + options.scope === 'unavailable' + ? Effect.fail( + new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'Test scope unavailable', }), - }, - contextAccess - ); - const moduleStateGate: ModuleStateGateService = { - check: checkModuleEntrypoint, - prepareSnapshot: (tenantId, entrypoints) => - Effect.succeed( - makeModuleStateSnapshot( - tenantId, - entrypoints, - [ - ...new Set( - entrypoints - .filter((entrypoint) => entrypoint.scope === 'tenant') - .map((entrypoint) => entrypoint.moduleKey) - ), - ].map((moduleKey) => ({ - moduleKey, - state: options.moduleState ?? 'active', - })) - ) + ) + : Effect.succeed({ + bindingPrincipalId: principal.principalId, + bindingRevokedAt: null, + bindingStatus: 'active', + bindingTenantId: principal.tenantId, + legalEntityStatus: 'active', + legalEntityTenantId: principal.tenantId, + principalStatus: options.scope === 'denied' ? 'inactive' : 'active', + principalTenantId: principal.tenantId, + tenantStatus: 'active', + }), + }, + contextAccess, + ); + const moduleStateGate: ModuleStateGateService = { + check: checkModuleEntrypoint, + prepareSnapshot: (tenantId, entrypoints) => + Effect.succeed( + makeModuleStateSnapshot( + tenantId, + entrypoints, + [ + ...new Set( + entrypoints + .filter((entrypoint) => entrypoint.scope === 'tenant') + .map((entrypoint) => entrypoint.moduleKey), + ), + ].map((moduleKey) => ({ + moduleKey, + state: options.moduleState ?? 'active', + })), ), - recheckWrite: () => Effect.void, - }; - const bindings = new Map(); - for (const binding of options.services ?? []) { - if (isActionTestServiceBindingValue(binding)) { - const stored = binding.resolve(); - bindings.set(stored.registration, stored.services); - } + ), + recheckWrite: () => Effect.void, + }; + const bindings = new Map(); + for (const binding of options.services ?? []) { + if (isActionTestServiceBindingValue(binding)) { + const stored = binding.resolve(); + bindings.set(stored.registration, stored.services); } - const resolveServiceFactory: typeof getActionServiceFactory = < - PayloadSchema extends Schema.ConstraintDecoder, - ResultSchema extends Schema.ConstraintDecoder, - DomainErrorSchema extends Schema.ConstraintDecoder<{ - readonly _tag: string; - }>, - DomainEvents extends DomainEventContractMap, - Owner extends string, + } + const resolveServiceFactory: typeof getActionServiceFactory = < + PayloadSchema extends Schema.ConstraintDecoder, + ResultSchema extends Schema.ConstraintDecoder, + DomainErrorSchema extends Schema.ConstraintDecoder<{ + readonly _tag: string; + }>, + DomainEvents extends DomainEventContractMap, + Owner extends string, + Services, + HandlerRequirements, + >( + registration: ActionRegistration< + PayloadSchema, + ResultSchema, + DomainErrorSchema, + DomainEvents, + Owner, Services, - HandlerRequirements, - >( - registration: ActionRegistration< - PayloadSchema, - ResultSchema, - DomainErrorSchema, - DomainEvents, - Owner, - Services, - HandlerRequirements - > - ): ActionServiceFactory => { - if (!bindings.has(registration)) { - return getActionServiceFactory(registration); - } - return () => - Schema.decodeUnknownEffect(Schema.Any)(bindings.get(registration)).pipe( - Effect.orDie - ); - }; - const runtime = makeActionRuntime( - database, - repository, - { - checkActionPermission: () => - options.actionPermission === 'unavailable' - ? Effect.fail( - new ActionPermissionCheckError({ - code: 'action_permission_check_failed', - reason: 'Test authorization unavailable', - }) - ) - : Effect.succeed(options.actionPermission ?? 'denied'), - }, - scopeResolver, - { - contextAccess, - moduleEntrypointGateway: makeModuleEntrypointGateway(moduleStateGate), - moduleStateGate, - onStage: (stage) => { - stages.push(stage); - }, - resolveServiceFactory, - } - ); - return Object.freeze({ - layer: Layer.succeed(ActionRuntime, runtime), - runtime, - snapshot: (): ActionTestSnapshot => - Object.freeze({ - committed: Object.freeze([...committed]), - invocations: Object.freeze( - [...invocations.values()].map((value) => - Object.freeze({ ...value }) + HandlerRequirements + >, + ): ActionServiceFactory => { + if (!bindings.has(registration)) { + return getActionServiceFactory(registration); + } + return () => Schema.decodeUnknownEffect(Schema.Any)(bindings.get(registration)).pipe(Effect.orDie); + }; + const runtime = makeActionRuntime( + database, + repository, + { + checkActionPermission: () => + options.actionPermission === 'unavailable' + ? Effect.fail( + new ActionPermissionCheckError({ + code: 'action_permission_check_failed', + reason: 'Test authorization unavailable', + }), ) - ), - permissionDenials: Object.freeze([...permissionDenials]), - policyDenials: Object.freeze([...policyDenials]), - stages: Object.freeze([...stages]), - transactionCount, - }), - }); - } -); + : Effect.succeed(options.actionPermission ?? 'denied'), + }, + scopeResolver, + { + contextAccess, + moduleEntrypointGateway: makeModuleEntrypointGateway(moduleStateGate), + moduleStateGate, + onStage: (stage) => { + stages.push(stage); + }, + resolveServiceFactory, + }, + ); + return Object.freeze({ + layer: Layer.succeed(ActionRuntime, runtime), + runtime, + snapshot: (): ActionTestSnapshot => + Object.freeze({ + committed: Object.freeze([...committed]), + invocations: Object.freeze([...invocations.values()].map((value) => Object.freeze({ ...value }))), + permissionDenials: Object.freeze([...permissionDenials]), + policyDenials: Object.freeze([...policyDenials]), + stages: Object.freeze([...stages]), + transactionCount, + }), + }); +}); -export const makeActionTestHarness: typeof actionTestHarness = - actionTestHarness; +export const makeActionTestHarness: typeof actionTestHarness = actionTestHarness; export { makeLiveOperationFixture } from './live-operations.ts'; diff --git a/app/packages/core-runtime/src/testing/live-operations.ts b/app/packages/core-runtime/src/testing/live-operations.ts index 41b1ee4a6..675482d62 100644 --- a/app/packages/core-runtime/src/testing/live-operations.ts +++ b/app/packages/core-runtime/src/testing/live-operations.ts @@ -1,22 +1,9 @@ import { v1 } from '@authzed/authzed-node'; import { eq } from 'drizzle-orm'; -import { - Context, - Duration, - Effect, - Exit, - Layer, - Random, - Redacted, - Schema, - Scope, -} from 'effect'; +import { Context, Duration, Effect, Exit, Layer, Random, Redacted, Schema, Scope } from 'effect'; import { Pool } from 'pg'; -import { - ActionCommitIndeterminate, - ActionTransactionError, -} from '../actions/errors.ts'; +import { ActionCommitIndeterminate, ActionTransactionError } from '../actions/errors.ts'; import type { ActionRepositoryService } from '../actions/repository.ts'; import { makeActionRepository } from '../actions/repository.ts'; import { ActionRuntime, makeActionRuntime } from '../actions/runtime.ts'; @@ -38,10 +25,7 @@ import { buildActionAuthorizationRelationships } from '../install/action-authori import { makeModuleEntrypointGateway } from '../modules/module-entrypoint-gateway.ts'; import { makeModuleStateGate } from '../modules/module-state-gate.ts'; import { makeTenantModuleStateService } from '../modules/tenant-module-state-service.ts'; -import { - makeOperationalScopeRepository, - makeOperationalScopeResolver, -} from '../operations/context.ts'; +import { makeOperationalScopeRepository, makeOperationalScopeResolver } from '../operations/context.ts'; import { loadSpiceDbConfig } from '../permissions/config.ts'; import { makeContextAccessLive, @@ -60,7 +44,7 @@ const relationship = ( resourceId: string, relation: string, subjectType: string, - subjectId: string + subjectId: string, ) => v1.Relationship.create({ relation, @@ -74,29 +58,20 @@ const LiveOperationFixtureConfigurationSchema = Schema.Struct({ runtimeConnectionString: Schema.Redacted(Schema.String), }); -export type LiveOperationFixtureConfiguration = - typeof LiveOperationFixtureConfigurationSchema.Encoded; +export type LiveOperationFixtureConfiguration = typeof LiveOperationFixtureConfigurationSchema.Encoded; -class LiveOperationFixtureError extends Schema.TaggedError()( - 'LiveOperationFixtureError', - { - reason: Schema.String, - } -) {} +class LiveOperationFixtureError extends Schema.TaggedError()('LiveOperationFixtureError', { + reason: Schema.String, +}) {} -const fixtureFailure = ( - reason: string, - cause?: unknown -): LiveOperationFixtureError => { +const fixtureFailure = (reason: string, cause?: unknown): LiveOperationFixtureError => { const failure = new LiveOperationFixtureError({ reason }); - return cause === undefined - ? failure - : Object.defineProperty(failure, 'cause', { value: cause }); + return cause === undefined ? failure : Object.defineProperty(failure, 'cause', { value: cause }); }; const attemptFixturePromise = ( reason: string, - operation: () => PromiseLike + operation: () => PromiseLike, ): Effect.Effect => Effect.tryPromise({ catch: (cause) => fixtureFailure(reason, cause), @@ -105,7 +80,7 @@ const attemptFixturePromise = ( Effect.timeoutOrElse({ duration: LIVE_FIXTURE_EXTERNAL_TIMEOUT, orElse: () => Effect.fail(fixtureFailure(`${reason}: timed out`)), - }) + }), ); type FixtureExecutor = (typeof CoreDatabase)['Service']['executor']; @@ -119,48 +94,38 @@ interface FixtureFaultState { next: FixtureFault | null; } -const makeFixtureId = Effect.fn('LiveOperations.makeFixtureId')( - function* makeFixtureIdEffect() { - const chunks = yield* Effect.all( - [ - Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), - Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), - Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), - Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), - ], - { concurrency: 4 } - ); - const value = chunks - .map((chunk) => chunk.toString(16).padStart(8, '0')) - .join(''); - return `${value.slice(0, 8)}-${value.slice(8, 12)}-4${value.slice(13, 16)}-a${value.slice(17, 20)}-${value.slice(20)}`; - } -); +const makeFixtureId = Effect.fn('LiveOperations.makeFixtureId')(function* makeFixtureIdEffect() { + const chunks = yield* Effect.all( + [ + Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), + Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), + Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), + Random.nextIntBetween(0, 4_294_967_296, { halfOpen: true }), + ], + { concurrency: 4 }, + ); + const value = chunks.map((chunk) => chunk.toString(16).padStart(8, '0')).join(''); + return `${value.slice(0, 8)}-${value.slice(8, 12)}-4${value.slice(13, 16)}-a${value.slice(17, 20)}-${value.slice(20)}`; +}); -const makeFixtureActor = Effect.fn('LiveOperations.makeFixtureActor')( - function* makeFixtureActorEffect(tenantId: string) { - const [authBindingId, principalId] = yield* Effect.all( - [makeFixtureId(), makeFixtureId()], - { - concurrency: 2, - } - ); - return { - authBindingId, - authContextRef: `better-auth-session:${authBindingId}`, - authMethod: 'session' as const, - principalId, - tenantId, - }; - } -); +const makeFixtureActor = Effect.fn('LiveOperations.makeFixtureActor')(function* makeFixtureActorEffect( + tenantId: string, +) { + const [authBindingId, principalId] = yield* Effect.all([makeFixtureId(), makeFixtureId()], { + concurrency: 2, + }); + return { + authBindingId, + authContextRef: `better-auth-session:${authBindingId}`, + authMethod: 'session' as const, + principalId, + tenantId, + }; +}); type FixtureActor = Effect.Success>; -const fixturePrincipalValues = ( - actors: readonly FixtureActor[], - tenantId: string -) => +const fixturePrincipalValues = (actors: readonly FixtureActor[], tenantId: string) => actors.map((principal) => ({ displayName: 'Live actor', kind: 'human' as const, @@ -169,10 +134,7 @@ const fixturePrincipalValues = ( tenantId, })); -const fixtureAuthBindingValues = ( - actors: readonly FixtureActor[], - tenantId: string -) => +const fixtureAuthBindingValues = (actors: readonly FixtureActor[], tenantId: string) => actors.map((principal) => ({ principalAuthBindingId: principal.authBindingId, principalId: principal.principalId, @@ -192,169 +154,112 @@ const fixtureAuthorizationRelationships = (input: { readonly tenantId: string; }) => [ ...input.actors.map((principal) => - relationship( - 'tenant', - input.tenantId, - 'member', - 'principal', - principal.principalId - ) - ), - ...[ - 'party_identity_manager', - 'party_identity_reader', - 'party_identity_reviewer', - 'party_relationship_manager', - ].map((relation) => - relationship( - 'tenant', - input.tenantId, - relation, - 'principal', - input.manager.principalId - ) + relationship('tenant', input.tenantId, 'member', 'principal', principal.principalId), ), - relationship( - 'legal_entity', - input.entityObject, - 'tenant', - 'tenant', - input.tenantId + ...['party_identity_manager', 'party_identity_reader', 'party_identity_reviewer', 'party_relationship_manager'].map( + (relation) => relationship('tenant', input.tenantId, relation, 'principal', input.manager.principalId), ), + relationship('legal_entity', input.entityObject, 'tenant', 'tenant', input.tenantId), ...[input.manager, input.legalEntityOnly].flatMap((principal) => ['member', 'counterparty_manager', 'counterparty_reader'].map((relation) => - relationship( - 'legal_entity', - input.entityObject, - relation, - 'principal', - principal.principalId - ) - ) + relationship('legal_entity', input.entityObject, relation, 'principal', principal.principalId), + ), ), ...buildActionAuthorizationRelationships(input.actionKeys, [ { principalId: input.manager.principalId, tenantId: input.tenantId }, ]), ]; -const setupLiveOperationFixture = Effect.fn( - 'LiveOperations.setupLiveOperationFixture' -)(function* setupLiveOperationFixtureEffect(input: { - readonly actionKeys: readonly string[]; - readonly actors: readonly FixtureActor[]; - readonly executor: FixtureExecutor; - readonly legalEntityId: string; - readonly legalEntityOnly: FixtureActor; - readonly manager: FixtureActor; - readonly spice: FixtureSpiceClient; - readonly tenantId: string; -}) { - yield* input.executor - .insert(tenants) - .values({ - defaultLocale: 'en', - name: 'Disposable live acceptance', - slug: `live-${input.tenantId}`, - status: 'active', - tenantId: input.tenantId, - }) - .pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to create the live fixture tenant', cause) - ) - ); - yield* input.executor - .insert(legalEntities) - .values({ - legalEntityId: input.legalEntityId, - legalName: 'Disposable live acceptance', - registrationCountry: 'CZ', - registrationNumber: input.legalEntityId, - status: 'active', - tenantId: input.tenantId, - }) - .pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to create the live fixture Legal Entity', cause) - ) - ); - yield* input.executor - .insert(tenantModuleStates) - .values({ - moduleKey: 'party.registry', - state: 'active', +const setupLiveOperationFixture = Effect.fn('LiveOperations.setupLiveOperationFixture')( + function* setupLiveOperationFixtureEffect(input: { + readonly actionKeys: readonly string[]; + readonly actors: readonly FixtureActor[]; + readonly executor: FixtureExecutor; + readonly legalEntityId: string; + readonly legalEntityOnly: FixtureActor; + readonly manager: FixtureActor; + readonly spice: FixtureSpiceClient; + readonly tenantId: string; + }) { + yield* input.executor + .insert(tenants) + .values({ + defaultLocale: 'en', + name: 'Disposable live acceptance', + slug: `live-${input.tenantId}`, + status: 'active', + tenantId: input.tenantId, + }) + .pipe(Effect.mapError((cause) => fixtureFailure('Unable to create the live fixture tenant', cause))); + yield* input.executor + .insert(legalEntities) + .values({ + legalEntityId: input.legalEntityId, + legalName: 'Disposable live acceptance', + registrationCountry: 'CZ', + registrationNumber: input.legalEntityId, + status: 'active', + tenantId: input.tenantId, + }) + .pipe(Effect.mapError((cause) => fixtureFailure('Unable to create the live fixture Legal Entity', cause))); + yield* input.executor + .insert(tenantModuleStates) + .values({ + moduleKey: 'party.registry', + state: 'active', + tenantId: input.tenantId, + }) + .pipe(Effect.mapError((cause) => fixtureFailure('Unable to activate the live fixture module', cause))); + yield* input.executor + .insert(principals) + .values(fixturePrincipalValues(input.actors, input.tenantId)) + .pipe(Effect.mapError((cause) => fixtureFailure('Unable to create the live fixture principals', cause))); + yield* input.executor + .insert(principalAuthBindings) + .values(fixtureAuthBindingValues(input.actors, input.tenantId)) + .pipe(Effect.mapError((cause) => fixtureFailure('Unable to bind the live fixture principals', cause))); + const entityObject = toLegalEntityAccessObjectId(input.tenantId, input.legalEntityId); + if (entityObject === undefined) { + return yield* fixtureFailure('Invalid fixture Legal Entity'); + } + const relations = fixtureAuthorizationRelationships({ + actionKeys: input.actionKeys, + actors: input.actors, + entityObject, + legalEntityOnly: input.legalEntityOnly, + manager: input.manager, tenantId: input.tenantId, - }) - .pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to activate the live fixture module', cause) - ) - ); - yield* input.executor - .insert(principals) - .values(fixturePrincipalValues(input.actors, input.tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to create the live fixture principals', cause) - ) - ); - yield* input.executor - .insert(principalAuthBindings) - .values(fixtureAuthBindingValues(input.actors, input.tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to bind the live fixture principals', cause) - ) + }); + const writeRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: relations.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }); + yield* attemptFixturePromise( + 'Unable to write live fixture authorization relationships', + input.spice.promises.writeRelationships.bind(input.spice.promises, writeRelationshipsRequest), ); - const entityObject = toLegalEntityAccessObjectId( - input.tenantId, - input.legalEntityId - ); - if (entityObject === undefined) { - return yield* fixtureFailure('Invalid fixture Legal Entity'); - } - const relations = fixtureAuthorizationRelationships({ - actionKeys: input.actionKeys, - actors: input.actors, - entityObject, - legalEntityOnly: input.legalEntityOnly, - manager: input.manager, - tenantId: input.tenantId, - }); - const writeRelationshipsRequest = v1.WriteRelationshipsRequest.create({ - updates: relations.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }) - ), - }); - yield* attemptFixturePromise( - 'Unable to write live fixture authorization relationships', - input.spice.promises.writeRelationships.bind( - input.spice.promises, - writeRelationshipsRequest - ) - ); - return yield* Effect.void; -}); + return yield* Effect.void; + }, +); -const makeFaultActionRepository = ( - state: FixtureFaultState -): ActionRepositoryService => { +const makeFaultActionRepository = (state: FixtureFaultState): ActionRepositoryService => { const repository = makeActionRepository(); - const flushSuccess: ActionRepositoryService['flushSuccess'] = Effect.fn( - 'LiveOperations.flushSuccess' - )(function* flushSuccessEffect(transaction, input) { - state.invocationId = input.actionInvocationId; - if (state.active === 'rollback') { - return yield* new ActionTransactionError({ - code: 'action_transaction_failed', - reason: 'Controlled precommit rollback', - }); - } - return yield* repository.flushSuccess(transaction, input); - }); + const flushSuccess: ActionRepositoryService['flushSuccess'] = Effect.fn('LiveOperations.flushSuccess')( + function* flushSuccessEffect(transaction, input) { + state.invocationId = input.actionInvocationId; + if (state.active === 'rollback') { + return yield* new ActionTransactionError({ + code: 'action_transaction_failed', + reason: 'Controlled precommit rollback', + }); + } + return yield* repository.flushSuccess(transaction, input); + }, + ); return { ...repository, flushSuccess }; }; @@ -366,289 +271,176 @@ const loadFixtureEvidence = Effect.fn('LiveOperations.loadFixtureEvidence')( .select() .from(dataAccessEvents) .where(eq(dataAccessEvents.tenantId, tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure(LOAD_EVIDENCE_FAILURE, cause) - ) - ), + .pipe(Effect.mapError((cause) => fixtureFailure(LOAD_EVIDENCE_FAILURE, cause))), audits: executor .select() .from(auditEvents) .where(eq(auditEvents.tenantId, tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure(LOAD_EVIDENCE_FAILURE, cause) - ) - ), + .pipe(Effect.mapError((cause) => fixtureFailure(LOAD_EVIDENCE_FAILURE, cause))), events: executor .select() .from(domainEvents) .where(eq(domainEvents.tenantId, tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure(LOAD_EVIDENCE_FAILURE, cause) - ) - ), + .pipe(Effect.mapError((cause) => fixtureFailure(LOAD_EVIDENCE_FAILURE, cause))), invocations: executor .select() .from(actionInvocations) .where(eq(actionInvocations.tenantId, tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure(LOAD_EVIDENCE_FAILURE, cause) - ) - ), + .pipe(Effect.mapError((cause) => fixtureFailure(LOAD_EVIDENCE_FAILURE, cause))), outbox: executor .select() .from(outboxMessages) .where(eq(outboxMessages.tenantId, tenantId)) - .pipe( - Effect.mapError((cause) => - fixtureFailure(LOAD_EVIDENCE_FAILURE, cause) - ) - ), + .pipe(Effect.mapError((cause) => fixtureFailure(LOAD_EVIDENCE_FAILURE, cause))), }, - { concurrency: 5 } - ) + { concurrency: 5 }, + ), ); -const grantFixtureResourceAccess = Effect.fn( - 'LiveOperations.grantResourceAccess' -)(function* grantFixtureResourceAccessEffect( - spice: FixtureSpiceClient, - tenantId: string, - legalEntityId: string, - resource: { - readonly moduleId: string; - readonly resourceId: string; - readonly resourceType: string; - }, - principalId: string, - permission: 'reader' | 'writer' -) { - const entityObject = toLegalEntityAccessObjectId(tenantId, legalEntityId); - const moduleObject = toModuleAccessObjectId( - tenantId, - legalEntityId, - resource.moduleId - ); - const resourceObject = toResourceAccessObjectId( - tenantId, - legalEntityId, - resource - ); - if ( - entityObject === undefined || - moduleObject === undefined || - resourceObject === undefined +const grantFixtureResourceAccess = Effect.fn('LiveOperations.grantResourceAccess')( + function* grantFixtureResourceAccessEffect( + spice: FixtureSpiceClient, + tenantId: string, + legalEntityId: string, + resource: { + readonly moduleId: string; + readonly resourceId: string; + readonly resourceType: string; + }, + principalId: string, + permission: 'reader' | 'writer', ) { - return yield* fixtureFailure('Invalid resource fixture'); - } - const relations = [ - relationship( - 'module_access', - moduleObject, - 'legal_entity', - 'legal_entity', - entityObject - ), - relationship( - 'module_access', - moduleObject, - 'accessor', - 'principal', - principalId - ), - relationship( - 'resource', - resourceObject, - 'module', - 'module_access', - moduleObject - ), - relationship( - 'resource', - resourceObject, - permission, - 'principal', - principalId - ), - ]; - const writeRelationshipsRequest = v1.WriteRelationshipsRequest.create({ - updates: relations.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }) - ), - }); - return yield* attemptFixturePromise( - 'Unable to grant live fixture resource access', - spice.promises.writeRelationships.bind( - spice.promises, - writeRelationshipsRequest - ) - ); -}); + const entityObject = toLegalEntityAccessObjectId(tenantId, legalEntityId); + const moduleObject = toModuleAccessObjectId(tenantId, legalEntityId, resource.moduleId); + const resourceObject = toResourceAccessObjectId(tenantId, legalEntityId, resource); + if (entityObject === undefined || moduleObject === undefined || resourceObject === undefined) { + return yield* fixtureFailure('Invalid resource fixture'); + } + const relations = [ + relationship('module_access', moduleObject, 'legal_entity', 'legal_entity', entityObject), + relationship('module_access', moduleObject, 'accessor', 'principal', principalId), + relationship('resource', resourceObject, 'module', 'module_access', moduleObject), + relationship('resource', resourceObject, permission, 'principal', principalId), + ]; + const writeRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: relations.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }); + return yield* attemptFixturePromise( + 'Unable to grant live fixture resource access', + spice.promises.writeRelationships.bind(spice.promises, writeRelationshipsRequest), + ); + }, +); /** Real Core persistence and SpiceDB. Call only against a disposable local database. */ -const makeLiveOperationFixtureEffect = Effect.fn( - 'LiveOperations.makeLiveOperationFixture' -)(function* makeLiveOperationFixtureEffect( - input: LiveOperationFixtureConfiguration -) { - const configuration = yield* Schema.decodeEffect( - LiveOperationFixtureConfigurationSchema - )(input).pipe( - Effect.mapError((cause) => - fixtureFailure('Invalid live operation fixture configuration', cause) - ) - ); - const spiceDb = yield* loadSpiceDbConfig().pipe( - Effect.mapError((cause) => - fixtureFailure('Unable to load the SpiceDB configuration', cause) - ) - ); - const runtimeConnectionString = Redacted.value( - configuration.runtimeConnectionString - ); - const address = yield* Schema.decodeEffect(Schema.URLFromString)( - runtimeConnectionString - ).pipe( - Effect.mapError((cause) => - fixtureFailure('Invalid live operation database URL', cause) - ) - ); - if ( - !['localhost', '127.0.0.1'].includes(address.hostname) || - !spiceDb.endpoint.startsWith('localhost:') - ) { - return yield* fixtureFailure( - 'Live test fixtures require disposable localhost services' +const makeLiveOperationFixtureEffect = Effect.fn('LiveOperations.makeLiveOperationFixture')( + function* makeLiveOperationFixtureEffect(input: LiveOperationFixtureConfiguration) { + const configuration = yield* Schema.decodeEffect(LiveOperationFixtureConfigurationSchema)(input).pipe( + Effect.mapError((cause) => fixtureFailure('Invalid live operation fixture configuration', cause)), + ); + const spiceDb = yield* loadSpiceDbConfig().pipe( + Effect.mapError((cause) => fixtureFailure('Unable to load the SpiceDB configuration', cause)), ); - } + const runtimeConnectionString = Redacted.value(configuration.runtimeConnectionString); + const address = yield* Schema.decodeEffect(Schema.URLFromString)(runtimeConnectionString).pipe( + Effect.mapError((cause) => fixtureFailure('Invalid live operation database URL', cause)), + ); + if (!['localhost', '127.0.0.1'].includes(address.hostname) || !spiceDb.endpoint.startsWith('localhost:')) { + return yield* fixtureFailure('Live test fixtures require disposable localhost services'); + } - const pool = new Pool({ connectionString: runtimeConnectionString, max: 8 }); - const databaseScope = yield* Scope.make(); - const databaseConfiguration = yield* parseDatabaseConfig({ - DATABASE_URL: runtimeConnectionString, - }).pipe( - Effect.mapError((cause) => - fixtureFailure('Invalid database configuration', cause) - ) - ); - const { executor } = yield* makeCoreDatabase( - databaseConfiguration, - () => pool - ).pipe( - Scope.provide(databaseScope), - Effect.mapError((cause) => - fixtureFailure('Unable to initialize fixture database', cause) - ) - ); - const spice = v1.NewClient( - spiceDb.preSharedKey, - spiceDb.endpoint, - v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - ); - const [tenantId, legalEntityId] = yield* Effect.all( - [makeFixtureId(), makeFixtureId()], - { + const pool = new Pool({ connectionString: runtimeConnectionString, max: 8 }); + const databaseScope = yield* Scope.make(); + const databaseConfiguration = yield* parseDatabaseConfig({ + DATABASE_URL: runtimeConnectionString, + }).pipe(Effect.mapError((cause) => fixtureFailure('Invalid database configuration', cause))); + const { executor } = yield* makeCoreDatabase(databaseConfiguration, () => pool).pipe( + Scope.provide(databaseScope), + Effect.mapError((cause) => fixtureFailure('Unable to initialize fixture database', cause)), + ); + const spice = v1.NewClient(spiceDb.preSharedKey, spiceDb.endpoint, v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED); + const [tenantId, legalEntityId] = yield* Effect.all([makeFixtureId(), makeFixtureId()], { concurrency: 2, - } - ); - const [manager, legalEntityActor, denied] = yield* Effect.all( - [ - makeFixtureActor(tenantId), - makeFixtureActor(tenantId), - makeFixtureActor(tenantId), - ], - { concurrency: 3 } - ); - const legalEntityOnly = { ...legalEntityActor, legalEntityId }; - const actors = [manager, legalEntityOnly, denied]; - const closeResources = Effect.sync(spice.close.bind(spice)).pipe( - Effect.andThen(Scope.close(databaseScope, Exit.void)) - ); + }); + const [manager, legalEntityActor, denied] = yield* Effect.all( + [makeFixtureActor(tenantId), makeFixtureActor(tenantId), makeFixtureActor(tenantId)], + { concurrency: 3 }, + ); + const legalEntityOnly = { ...legalEntityActor, legalEntityId }; + const actors = [manager, legalEntityOnly, denied]; + const closeResources = Effect.sync(spice.close.bind(spice)).pipe( + Effect.andThen(Scope.close(databaseScope, Exit.void)), + ); - yield* setupLiveOperationFixture({ - actionKeys: configuration.actionKeys ?? [], - actors, - executor, - legalEntityId, - legalEntityOnly, - manager, - spice, - tenantId, - }).pipe( - Effect.onExit((exit) => - Exit.isFailure(exit) ? closeResources : Effect.void - ) - ); + yield* setupLiveOperationFixture({ + actionKeys: configuration.actionKeys ?? [], + actors, + executor, + legalEntityId, + legalEntityOnly, + manager, + spice, + tenantId, + }).pipe(Effect.onExit((exit) => (Exit.isFailure(exit) ? closeResources : Effect.void))); - const faultState: FixtureFaultState = { - active: null, - invocationId: null, - next: null, - }; - const actionDatabase = { - executor, - } satisfies (typeof CoreDatabase)['Service']; - const readDatabase = { executor } satisfies (typeof CoreDatabase)['Service']; - const layer = Layer.effectContext( - Effect.gen(function* makeLiveOperationRuntimeContext() { - const [contextAccess, actionPermission] = yield* Effect.all( - [ - makeContextAccessLive(undefined, () => Effect.succeed(spiceDb)), - makeActionPermissionLive(undefined, () => Effect.succeed(spiceDb)), - ], - { concurrency: 2 } - ); - const actionModuleStateGate = makeModuleStateGate( - makeTenantModuleStateService(actionDatabase) - ); - const actionModuleEntrypointGateway = makeModuleEntrypointGateway( - actionModuleStateGate - ); - const actionScopeResolver = makeOperationalScopeResolver( - makeOperationalScopeRepository(actionDatabase), - contextAccess - ); - const readModuleStateGate = makeModuleStateGate( - makeTenantModuleStateService(readDatabase) - ); - const readModuleEntrypointGateway = - makeModuleEntrypointGateway(readModuleStateGate); - const readScopeResolver = makeOperationalScopeResolver( - makeOperationalScopeRepository(readDatabase), - contextAccess - ); - const baseActionRuntime = makeActionRuntime( - actionDatabase, - makeFaultActionRepository(faultState), - actionPermission, - actionScopeResolver, - { + const faultState: FixtureFaultState = { + active: null, + invocationId: null, + next: null, + }; + const actionDatabase = { + executor, + } satisfies (typeof CoreDatabase)['Service']; + const readDatabase = { executor } satisfies (typeof CoreDatabase)['Service']; + const layer = Layer.effectContext( + Effect.gen(function* makeLiveOperationRuntimeContext() { + const [contextAccess, actionPermission] = yield* Effect.all( + [ + makeContextAccessLive(undefined, () => Effect.succeed(spiceDb)), + makeActionPermissionLive(undefined, () => Effect.succeed(spiceDb)), + ], + { concurrency: 2 }, + ); + const actionModuleStateGate = makeModuleStateGate(makeTenantModuleStateService(actionDatabase)); + const actionModuleEntrypointGateway = makeModuleEntrypointGateway(actionModuleStateGate); + const actionScopeResolver = makeOperationalScopeResolver( + makeOperationalScopeRepository(actionDatabase), + contextAccess, + ); + const readModuleStateGate = makeModuleStateGate(makeTenantModuleStateService(readDatabase)); + const readModuleEntrypointGateway = makeModuleEntrypointGateway(readModuleStateGate); + const readScopeResolver = makeOperationalScopeResolver( + makeOperationalScopeRepository(readDatabase), contextAccess, - moduleEntrypointGateway: actionModuleEntrypointGateway, - moduleStateGate: actionModuleStateGate, - } - ); - const runAction: (typeof ActionRuntime)['Service']['runAction'] = - Effect.fn('LiveOperations.runAction')( + ); + const baseActionRuntime = makeActionRuntime( + actionDatabase, + makeFaultActionRepository(faultState), + actionPermission, + actionScopeResolver, + { + contextAccess, + moduleEntrypointGateway: actionModuleEntrypointGateway, + moduleStateGate: actionModuleStateGate, + }, + ); + const runAction: (typeof ActionRuntime)['Service']['runAction'] = Effect.fn('LiveOperations.runAction')( function* runActionEffect(actionInput) { const fault = faultState.next; faultState.active = fault; faultState.invocationId = null; faultState.next = null; - const actionExit = yield* Effect.exit( - baseActionRuntime.runAction(actionInput) - ).pipe( + const actionExit = yield* Effect.exit(baseActionRuntime.runAction(actionInput)).pipe( Effect.ensuring( Effect.sync(() => { faultState.active = null; - }) - ) + }), + ), ); if (Exit.isFailure(actionExit)) { return yield* Effect.failCause(actionExit.cause); @@ -661,67 +453,58 @@ const makeLiveOperationFixtureEffect = Effect.fn( }); } return actionExit.value; - } + }, ); - const actionRuntime = { ...baseActionRuntime, runAction }; - const readRuntime = makeReadRuntime( - readDatabase, - readModuleEntrypointGateway, - readScopeResolver, - contextAccess - ); - return Context.empty().pipe( - Context.add(ActionRuntime, actionRuntime), - Context.add(CoreDatabase, readDatabase), - Context.add(ReadRuntime, readRuntime) - ); - }) - ); + const actionRuntime = { ...baseActionRuntime, runAction }; + const readRuntime = makeReadRuntime( + readDatabase, + readModuleEntrypointGateway, + readScopeResolver, + contextAccess, + ); + return Context.empty().pipe( + Context.add(ActionRuntime, actionRuntime), + Context.add(CoreDatabase, readDatabase), + Context.add(ReadRuntime, readRuntime), + ); + }), + ); - return { - denied, - evidence: () => loadFixtureEvidence(executor, tenantId), - faultNextTransaction: (fault: FixtureFault) => { - faultState.next = fault; - }, - grantResourceAccess: ( - resource: { - readonly moduleId: string; - readonly resourceId: string; - readonly resourceType: string; + return { + denied, + evidence: () => loadFixtureEvidence(executor, tenantId), + faultNextTransaction: (fault: FixtureFault) => { + faultState.next = fault; }, - principalId: string, - permission: 'reader' | 'writer' = 'reader' - ) => - grantFixtureResourceAccess( - spice, - tenantId, - legalEntityId, - resource, - principalId, - permission - ), - layer, - legalEntityId, - legalEntityOnly, - manager, - tenantId, - // Retain append-only proof rows until the disposable database is removed. - close: () => closeResources, - }; -}); + grantResourceAccess: ( + resource: { + readonly moduleId: string; + readonly resourceId: string; + readonly resourceType: string; + }, + principalId: string, + permission: 'reader' | 'writer' = 'reader', + ) => grantFixtureResourceAccess(spice, tenantId, legalEntityId, resource, principalId, permission), + layer, + legalEntityId, + legalEntityOnly, + manager, + tenantId, + // Retain append-only proof rows until the disposable database is removed. + close: () => closeResources, + }; + }, +); /** Real Core persistence and SpiceDB. Call only against a disposable local database. */ -export const makeLiveOperationFixture = Effect.fn( - 'LiveOperations.makeLiveOperationFixture' -)(function* makeLiveOperationFixturePublicEffect( - input: LiveOperationFixtureConfiguration -) { - return yield* makeLiveOperationFixtureEffect(input).pipe( - Effect.mapError((cause) => - Schema.is(LiveOperationFixtureError)(cause) - ? cause - : fixtureFailure('Unable to create live operation fixture', cause) - ) - ); -}); +export const makeLiveOperationFixture = Effect.fn('LiveOperations.makeLiveOperationFixture')( + function* makeLiveOperationFixturePublicEffect(input: LiveOperationFixtureConfiguration) { + return yield* makeLiveOperationFixtureEffect(input).pipe( + Effect.mapError((cause) => + Schema.is(LiveOperationFixtureError)(cause) + ? cause + : fixtureFailure('Unable to create live operation fixture', cause), + ), + ); + }, +); diff --git a/app/packages/core-runtime/tests/fixtures/operational-scope.ts b/app/packages/core-runtime/tests/fixtures/operational-scope.ts index 5b2798372..a520aa919 100644 --- a/app/packages/core-runtime/tests/fixtures/operational-scope.ts +++ b/app/packages/core-runtime/tests/fixtures/operational-scope.ts @@ -3,19 +3,13 @@ import { Effect } from 'effect'; import { preserveSystemPrincipalContextTrust } from '../../src/auth/system-principal-context-provenance.ts'; import type { OperationalScopeResolverService } from '../../src/operations/context.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); /** Explicit test seam for suites whose subject is downstream of persisted scope revalidation. */ export const testOperationalScopeResolver: OperationalScopeResolverService = { @@ -32,9 +26,9 @@ export const testOperationalScopeResolver: OperationalScopeResolverService = { traceId !== undefined, 'traceId', traceId, - {} - ) - ) - ) + {}, + ), + ), + ), ), }; diff --git a/app/packages/core-runtime/tests/fixtures/outbox-worker-process.fixture.ts b/app/packages/core-runtime/tests/fixtures/outbox-worker-process.fixture.ts index fb26e22f0..bd7dc6a8c 100644 --- a/app/packages/core-runtime/tests/fixtures/outbox-worker-process.fixture.ts +++ b/app/packages/core-runtime/tests/fixtures/outbox-worker-process.fixture.ts @@ -29,15 +29,14 @@ const registration = defineOutboxWorker( topic: 'producer.message-created', workerKey: 'process-fixture.lifecycle', }, - () => Effect.void + () => Effect.void, ); const runtimeLayer = Layer.effect( OutboxRuntime, Effect.acquireRelease( Effect.succeed({ - matchMessages: () => - Effect.succeed({ deliveriesCreated: 0, messagesMatched: 0 }), + matchMessages: () => Effect.succeed({ deliveriesCreated: 0, messagesMatched: 0 }), runCycle: () => Effect.sync(() => { process.stdout.write(`cycle:${process.listenerCount('SIGTERM')}\n`); @@ -55,8 +54,8 @@ const runtimeLayer = Layer.effect( () => Effect.sync(() => { process.stdout.write('disposed\n'); - }) - ) + }), + ), ); startOutboxWorkerProcess({ diff --git a/app/packages/core-runtime/tests/integration/action-permission.test.ts b/app/packages/core-runtime/tests/integration/action-permission.test.ts index 8ad9435a7..a3a215975 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -36,17 +36,11 @@ import { } from '../../src/permissions/service.ts'; import { testOperationalScopeResolver } from '../fixtures/operational-scope.ts'; import { openActionRuntimeOptions } from '../support/action-runtime-options.ts'; -import { - makeFaultInjectableCoreDatabase, - TestQueryHook, -} from '../support/database-faults.ts'; +import { makeFaultInjectableCoreDatabase, TestQueryHook } from '../support/database-faults.ts'; import { TestWriteError } from '../support/permission-write-error.ts'; -class PermissionAdmin extends Context.Service< - PermissionAdmin, - ReturnType ->()( - '@app/core-runtime/tests/integration/action-permission.test/PermissionAdmin' +class PermissionAdmin extends Context.Service>()( + '@app/core-runtime/tests/integration/action-permission.test/PermissionAdmin', ) {} const suiteId = randomUUID(); @@ -107,16 +101,14 @@ const transport = (idempotencyKey: string, targetResourceId: string) => ({ type ContextServiceContract = Parameters[0]; const withDatabase = ( - operation: ( - database: ContextServiceContract - ) => Effect.Effect + operation: (database: ContextServiceContract) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* databaseScope() { const configuration = yield* loadDatabaseConfig(); const database = yield* makeFaultInjectableCoreDatabase(configuration); return yield* operation(database); - }) + }), ); const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); @@ -139,7 +131,7 @@ const defaultExecutorSubject: ExecutorSubject = { const relationship = ( actionKey: string, relation: 'executor' | 'restriction', - executorSubject: ExecutorSubject = defaultExecutorSubject + executorSubject: ExecutorSubject = defaultExecutorSubject, ) => { relationshipActionKeys.add(actionKey); return v1.Relationship.create({ @@ -160,17 +152,12 @@ const relationship = ( objectType: executorSubject.objectType, }), optionalRelation: - relation === 'executor' && executorSubject.objectType === 'tenant' - ? executorSubject.optionalRelation - : '', + relation === 'executor' && executorSubject.objectType === 'tenant' ? executorSubject.optionalRelation : '', }), }); }; -const tenantMembership = ( - membershipTenantId: string, - membershipPrincipalId: string -) => +const tenantMembership = (membershipTenantId: string, membershipPrincipalId: string) => v1.Relationship.create({ relation: 'member', resource: v1.ObjectReference.create({ @@ -189,16 +176,9 @@ const NoDomainEvents = {}; interface PermissionActionServices { readonly transaction: ScopedTransactionExecutor; } -type PermissionActionContext = ActionHandlerContext< - typeof NoDomainEvents, - PermissionActionServices ->; +type PermissionActionContext = ActionHandlerContext; -const registration = ( - actionKey: string, - moduleStateKey: string, - onExecute: () => void -) => +const registration = (actionKey: string, moduleStateKey: string, onExecute: () => void) => defineAction( { accessEvidencePolicy: { @@ -227,10 +207,7 @@ const registration = ( resultSchema: Schema.Void, schemaVersion: '1', }, - Effect.fn(function* permissionIntegrationHandler( - _payload, - context: PermissionActionContext - ) { + Effect.fn(function* permissionIntegrationHandler(_payload, context: PermissionActionContext) { onExecute(); yield* context.services.transaction .insert(tenantModuleStates) @@ -239,13 +216,9 @@ const registration = ( state: 'active', tenantId: context.scope.tenantId, }) - .pipe( - Effect.mapError( - () => new TestWriteError({ reason: 'test business write failed' }) - ) - ); + .pipe(Effect.mapError(() => new TestWriteError({ reason: 'test business write failed' }))); }), - (transaction) => Effect.succeed({ transaction }) + (transaction) => Effect.succeed({ transaction }), ); const PermissionFixture = Layer.effect( @@ -255,16 +228,12 @@ const PermissionFixture = Layer.effect( const adminClient = v1.NewClient( spiceDbConfig.preSharedKey, spiceDbConfig.endpoint, - spiceDbConfig.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE + spiceDbConfig.insecureLocal ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED : v1.ClientSecurity.SECURE, ); const prepare = Effect.gen(function* preparePermissionFixture() { yield* Effect.promise(() => - adminClient.promises.writeSchema( - v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }) - ) + adminClient.promises.writeSchema(v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA })), ); yield* withDatabase( Effect.fn(function* seedPermissionFixture(database) { @@ -342,7 +311,7 @@ const PermissionFixture = Layer.effect( tenantId: otherTenantId, }, ]); - }) + }), ); yield* Effect.promise(() => @@ -374,10 +343,10 @@ const PermissionFixture = Layer.effect( v1.RelationshipUpdate.create({ operation: v1.RelationshipUpdate_Operation.TOUCH, relationship: item, - }) + }), ), - }) - ) + }), + ), ); }); @@ -393,10 +362,10 @@ const PermissionFixture = Layer.effect( optionalResourceId: toSpiceDbActionObjectId(actionKey), resourceType: 'action', }), - }) - ) + }), + ), ), - { discard: true } + { discard: true }, ).pipe( Effect.andThen( Effect.forEach( @@ -409,99 +378,43 @@ const PermissionFixture = Layer.effect( optionalResourceId: membershipTenantId, resourceType: 'tenant', }), - }) - ) + }), + ), ), - { discard: true } - ) + { discard: true }, + ), ), - Effect.ensuring(Effect.sync(() => adminClient.close())) - ) + Effect.ensuring(Effect.sync(() => adminClient.close())), + ), ); yield* withDatabase((database) => Effect.forEach( [ + () => database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, otherTenantId)), + () => database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, otherTenantId)), + () => database.executor.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, otherTenantId)), + () => database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, otherTenantId)), + () => database.executor.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, otherTenantId)), + () => database.executor.delete(actionInvocations).where(eq(actionInvocations.tenantId, otherTenantId)), + () => database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), + () => database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), + () => database.executor.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), + () => database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), + () => database.executor.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), + () => database.executor.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), () => - database.executor - .delete(outboxMessages) - .where(eq(outboxMessages.tenantId, otherTenantId)), - () => - database.executor - .delete(domainEvents) - .where(eq(domainEvents.tenantId, otherTenantId)), - () => - database.executor - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, otherTenantId)), - () => - database.executor - .delete(auditEvents) - .where(eq(auditEvents.tenantId, otherTenantId)), - () => - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, otherTenantId)), - () => - database.executor - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, otherTenantId)), - () => - database.executor - .delete(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - () => - database.executor - .delete(domainEvents) - .where(eq(domainEvents.tenantId, tenantId)), - () => - database.executor - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, tenantId)), - () => - database.executor - .delete(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - () => - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantId)), - () => - database.executor - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)), - () => - database.executor - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, otherTenantId)), - () => - database.executor - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, tenantId)), - () => - database.executor - .delete(principals) - .where(eq(principals.tenantId, otherTenantId)), - () => - database.executor - .delete(principals) - .where(eq(principals.tenantId, tenantId)), - () => - database.executor - .delete(legalEntities) - .where(eq(legalEntities.tenantId, tenantId)), - () => - database.executor - .delete(tenants) - .where(eq(tenants.tenantId, tenantId)), - () => - database.executor - .delete(tenants) - .where(eq(tenants.tenantId, otherTenantId)), + database.executor.delete(principalAuthBindings).where(eq(principalAuthBindings.tenantId, otherTenantId)), + () => database.executor.delete(principalAuthBindings).where(eq(principalAuthBindings.tenantId, tenantId)), + () => database.executor.delete(principals).where(eq(principals.tenantId, otherTenantId)), + () => database.executor.delete(principals).where(eq(principals.tenantId, tenantId)), + () => database.executor.delete(legalEntities).where(eq(legalEntities.tenantId, tenantId)), + () => database.executor.delete(tenants).where(eq(tenants.tenantId, tenantId)), + () => database.executor.delete(tenants).where(eq(tenants.tenantId, otherTenantId)), ], (query) => query(), - { concurrency: 1, discard: true } - ) + { concurrency: 1, discard: true }, + ), ); if (Exit.isFailure(relationshipCleanupExit)) { @@ -512,7 +425,7 @@ const PermissionFixture = Layer.effect( yield* Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie)); return adminClient; - }) + }), ); interface ExecutionCounter { @@ -525,20 +438,13 @@ const incrementExecution = (counter: ExecutionCounter): void => { const runWithLivePermission = ( database: ContextServiceContract, - operation: ( - runtime: ReturnType - ) => Effect.Effect, - configuration?: SpiceDbConfigValue + operation: (runtime: ReturnType) => Effect.Effect, + configuration?: SpiceDbConfigValue, ) => - (configuration === undefined - ? loadSpiceDbConfig() - : Effect.succeed(configuration) - ).pipe( + (configuration === undefined ? loadSpiceDbConfig() : Effect.succeed(configuration)).pipe( Effect.flatMap((config) => Effect.acquireUseRelease( - Effect.sync(() => - createPermissionCheckClient(config, SPICEDB_CHECK_TIMEOUT_MS) - ), + Effect.sync(() => createPermissionCheckClient(config, SPICEDB_CHECK_TIMEOUT_MS)), (client) => operation( makeActionRuntime( @@ -546,27 +452,22 @@ const runWithLivePermission = ( makeActionRepository(), makeActionPermissionService(client), testOperationalScopeResolver, - openActionRuntimeOptions - ) + openActionRuntimeOptions, + ), ), - (client) => Effect.sync(() => client.close()) - ) - ) + (client) => Effect.sync(() => client.close()), + ), + ), ); type DenialFailureStage = typeof DenialFailureStageSchema.Type; const withDenialPersistenceFailure = ( database: ContextServiceContract, - stage: DenialFailureStage + stage: DenialFailureStage, ): ContextServiceContract => { - const transaction: ContextServiceContract['executor']['transaction'] = ( - operation - ) => + const transaction: ContextServiceContract['executor']['transaction'] = (operation) => database.executor.transaction((current) => { - const prefix = - stage === 'audit' - ? 'insert into "core"."audit_events"' - : 'update "core"."action_invocations"'; + const prefix = stage === 'audit' ? 'insert into "core"."audit_events"' : 'update "core"."action_invocations"'; return operation(current).pipe( Effect.provideService(TestQueryHook, (statement) => statement.startsWith(prefix) @@ -576,42 +477,32 @@ const withDenialPersistenceFailure = ( cause: new Error('Injected SQL failure'), message: `Injected denial ${stage} failure`, }), - }) + }), ) - : Effect.void - ) + : Effect.void, + ), ); }); - const executor: ContextServiceContract['executor'] = Object.assign( - Object.create(database.executor), - { transaction } - ); + const executor: ContextServiceContract['executor'] = Object.assign(Object.create(database.executor), { transaction }); return { executor }; }; -const DenialFailureStageSchema = Schema.Literals([ - 'audit', - 'invocation-update', -]); +const DenialFailureStageSchema = Schema.Literals(['audit', 'invocation-update']); const runFailedAction = ( runtime: ReturnType, actionKey: string, key: string, moduleStateKey: string, - executions: ExecutionCounter + executions: ExecutionCounter, ) => Effect.flip( runtime.runAction({ payload: undefined, principal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions) - ), + registration: registration(actionKey, moduleStateKey, incrementExecution.bind(undefined, executions)), transport: transport(key, moduleStateKey), - }) + }), ); const testProgram1 = () => @@ -628,13 +519,9 @@ const testProgram1 = () => runtime.runAction({ payload: undefined, principal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions) - ), + registration: registration(actionKey, moduleStateKey, incrementExecution.bind(undefined, executions)), transport: transport(kind, moduleStateKey), - }) + }), ); const rows = yield* database.executor .select() @@ -644,8 +531,8 @@ const testProgram1 = () => expect(executions.value, kind).toBe(1); expect(rows.length, kind).toBe(1); }), - { concurrency: 1, discard: true } - ) + { concurrency: 1, discard: true }, + ), ); const testProgram2 = () => @@ -655,13 +542,7 @@ const testProgram2 = () => const key = 'missing'; const moduleStateKey = `${actionPrefix}.state.missing`; const failure = yield* runWithLivePermission(database, (runtime) => - runFailedAction( - runtime, - actionKeys.missing, - key, - moduleStateKey, - executions - ) + runFailedAction(runtime, actionKeys.missing, key, moduleStateKey, executions), ); const [invocation] = yield* database.executor .select() @@ -671,43 +552,25 @@ const testProgram2 = () => if (invocation === undefined) { throw new Error('Expected invocation'); } - const [audits, businessRows, accesses, events, messages] = - yield* Effect.all([ - database.executor - .select() - .from(auditEvents) - .where( - eq(auditEvents.actionInvocationId, invocation.actionInvocationId) - ), - database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)), - database.executor - .select() - .from(dataAccessEvents) - .where( - eq( - dataAccessEvents.actionInvocationId, - invocation.actionInvocationId - ) - ), - database.executor - .select() - .from(domainEvents) - .where( - eq(domainEvents.actionInvocationId, invocation.actionInvocationId) - ), - database.executor - .select() - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ]); + const [audits, businessRows, accesses, events, messages] = yield* Effect.all([ + database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor.select().from(tenantModuleStates).where(eq(tenantModuleStates.moduleKey, moduleStateKey)), + database.executor + .select() + .from(dataAccessEvents) + .where(eq(dataAccessEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor + .select() + .from(domainEvents) + .where(eq(domainEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor.select().from(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), + ]); expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); - expect(failure.reason).toBe( - 'The principal is not permitted to execute this Action' - ); + expect(failure.reason).toBe('The principal is not permitted to execute this Action'); expect(executions.value).toBe(0); expect(invocation.status).toBe('rejected'); expect(invocation.completedAt).toBeTruthy(); @@ -729,12 +592,8 @@ const testProgram2 = () => outcomeCode: 'spicedb_permission_denied', outcomeStage: 'authz', }); - expect( - encodeJson(audits[0]).includes( - (yield* loadSpiceDbConfig()).preSharedKey - ) - ).toBe(false); - }) + expect(encodeJson(audits[0]).includes((yield* loadSpiceDbConfig()).preSharedKey)).toBe(false); + }), ); const testProgram3 = () => @@ -745,11 +604,7 @@ const testProgram3 = () => ['other-tenant', actionKeys.crossTenantDenied, otherTenantPrincipal], ['non-member', actionKeys.nonMemberDenied, nonMemberPrincipal], ] as const, - Effect.fn(function* verifyDeniedAction([ - kind, - actionKey, - deniedPrincipal, - ]) { + Effect.fn(function* verifyDeniedAction([kind, actionKey, deniedPrincipal]) { const executions: ExecutionCounter = { value: 0 }; const moduleStateKey = `${actionPrefix}.state.${kind}`; const failure = yield* runWithLivePermission(database, (runtime) => @@ -757,24 +612,17 @@ const testProgram3 = () => runtime.runAction({ payload: undefined, principal: deniedPrincipal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions) - ), + registration: registration(actionKey, moduleStateKey, incrementExecution.bind(undefined, executions)), transport: transport(kind, moduleStateKey), - }) - ) + }), + ), ); - expect( - Predicate.isTagged(failure, 'ActionPermissionDenied'), - kind - ).toBe(true); + expect(Predicate.isTagged(failure, 'ActionPermissionDenied'), kind).toBe(true); expect(executions.value, kind).toBe(0); }), - { concurrency: 1, discard: true } - ) + { concurrency: 1, discard: true }, + ), ); const testProgram4 = () => @@ -789,17 +637,14 @@ const testProgram4 = () => registration: registration( actionKeys.concurrentDenied, moduleStateKey, - incrementExecution.bind(undefined, executions) + incrementExecution.bind(undefined, executions), ), transport: transport(key, moduleStateKey), }; const results = yield* Effect.forEach( [1, 2], - () => - runWithLivePermission(database, (runtime) => - Effect.flip(runtime.runAction(input)) - ), - { concurrency: 'unbounded' } + () => runWithLivePermission(database, (runtime) => Effect.flip(runtime.runAction(input))), + { concurrency: 'unbounded' }, ); const [invocation] = yield* database.executor .select() @@ -812,9 +657,7 @@ const testProgram4 = () => const audits = yield* database.executor .select() .from(auditEvents) - .where( - eq(auditEvents.actionInvocationId, invocation.actionInvocationId) - ); + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); expect(results.length).toBe(2); for (const result of results) { @@ -823,7 +666,7 @@ const testProgram4 = () => expect(executions.value).toBe(0); expect(invocation.status).toBe('rejected'); expect(audits.length).toBe(1); - }) + }), ); const testProgram5 = () => @@ -845,24 +688,18 @@ const testProgram5 = () => relationship: relationship(actionKey, 'restriction'), }), ], - }) - ) + }), + ), ); - const failure = yield* runWithLivePermission( - withDenialPersistenceFailure(database, stage), - (runtime) => - Effect.flip( - runtime.runAction({ - payload: undefined, - principal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions) - ), - transport: transport(key, moduleStateKey), - }) - ) + const failure = yield* runWithLivePermission(withDenialPersistenceFailure(database, stage), (runtime) => + Effect.flip( + runtime.runAction({ + payload: undefined, + principal, + registration: registration(actionKey, moduleStateKey, incrementExecution.bind(undefined, executions)), + transport: transport(key, moduleStateKey), + }), + ), ); const [invocation] = yield* database.executor .select() @@ -875,21 +712,16 @@ const testProgram5 = () => const audits = yield* database.executor .select() .from(auditEvents) - .where( - eq(auditEvents.actionInvocationId, invocation.actionInvocationId) - ); - - expect( - Predicate.isTagged(failure, 'ActionTransactionError'), - stage - ).toBe(true); + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); + + expect(Predicate.isTagged(failure, 'ActionTransactionError'), stage).toBe(true); expect(executions.value, stage).toBe(0); expect(invocation.status, stage).toBe('received'); expect(invocation.completedAt, stage).toBe(null); expect(audits.length, stage).toBe(0); }), - { concurrency: 1, discard: true } - ) + { concurrency: 1, discard: true }, + ), ); const testProgram6 = () => @@ -900,18 +732,11 @@ const testProgram6 = () => const moduleStateKey = `${actionPrefix}.state.invalid-credentials`; const failure = yield* runWithLivePermission( database, - (runtime) => - runFailedAction( - runtime, - actionKeys.unavailable, - key, - moduleStateKey, - executions - ), + (runtime) => runFailedAction(runtime, actionKeys.unavailable, key, moduleStateKey, executions), { ...(yield* loadSpiceDbConfig()), preSharedKey: 'invalid-integration-key', - } + }, ); const [invocation] = yield* database.executor .select() @@ -925,54 +750,28 @@ const testProgram6 = () => .select() .from(auditEvents) .where( - and( - eq(auditEvents.actionInvocationId, invocation.actionInvocationId), - eq(auditEvents.outcomeStage, 'authz') - ) + and(eq(auditEvents.actionInvocationId, invocation.actionInvocationId), eq(auditEvents.outcomeStage, 'authz')), ); - expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe( - true - ); + expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe(true); expect(failure.reason.includes('invalid-integration-key')).toBe(false); expect(executions.value).toBe(0); expect(invocation.status).toBe('received'); expect(invocation.completedAt).toBe(null); expect(audits.length).toBe(0); - }) + }), ); -it.layer(PermissionFixture, { excludeTestServices: true })( - 'Action permissions', - (suite) => { - suite.effect( - 'allows direct Principal and Tenant-membership executor grants', - testProgram1 - ); +it.layer(PermissionFixture, { excludeTestServices: true })('Action permissions', (suite) => { + suite.effect('allows direct Principal and Tenant-membership executor grants', testProgram1); - suite.effect( - 'persists one normalized terminal denial and no business or collected evidence', - testProgram2 - ); + suite.effect('persists one normalized terminal denial and no business or collected evidence', testProgram2); - suite.effect( - 'denies a legacy marker without an executor and membership-set outsiders', - testProgram3 - ); + suite.effect('denies a legacy marker without an executor and membership-set outsiders', testProgram3); - suite.effect( - 'serializes concurrent denials into one Audit Event without executing the handler', - testProgram4 - ); + suite.effect('serializes concurrent denials into one Audit Event without executing the handler', testProgram4); - suite.effect( - 'rolls back both denial evidence writes when either persistence step fails', - testProgram5 - ); + suite.effect('rolls back both denial evidence writes when either persistence step fails', testProgram5); - suite.effect( - 'fails closed for invalid SpiceDB credentials and leaves retryable received evidence', - testProgram6 - ); - } -); + suite.effect('fails closed for invalid SpiceDB credentials and leaves retryable received evidence', testProgram6); +}); diff --git a/app/packages/core-runtime/tests/integration/action-runtime.test.ts b/app/packages/core-runtime/tests/integration/action-runtime.test.ts index 65a94738c..c3201a93e 100644 --- a/app/packages/core-runtime/tests/integration/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/action-runtime.test.ts @@ -1,34 +1,16 @@ import { randomUUID } from 'node:crypto'; import { and, eq } from 'drizzle-orm'; -import { - Cause, - Deferred, - Effect, - Layer, - Exit, - Fiber, - Option, - Schema, - Predicate, -} from 'effect'; +import { Cause, Deferred, Effect, Layer, Exit, Fiber, Option, Schema, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - ConnectionError, - SqlError, - UnknownError, -} from 'effect/unstable/sql/SqlError'; +import { ConnectionError, SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; import type { ActionHandlerContext } from '../../src/actions/context.ts'; import { defineAction } from '../../src/actions/definition.ts'; import { ActionInvocationPersistenceError } from '../../src/actions/errors.ts'; import { createDomainEventReference } from '../../src/actions/events.ts'; import type { ActionPolicy } from '../../src/actions/policy.ts'; -import { - defineGlobalPolicy, - defineMicroverticalPolicy, - denyPolicy, -} from '../../src/actions/policy.ts'; +import { defineGlobalPolicy, defineMicroverticalPolicy, denyPolicy } from '../../src/actions/policy.ts'; import { makeActionRepository } from '../../src/actions/repository.ts'; import { makeActionRuntime } from '../../src/actions/runtime.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; @@ -51,10 +33,7 @@ import type { InstalledModuleCatalog } from '../../src/modules/catalog.ts'; import { InstalledModuleCatalogService } from '../../src/modules/catalog.ts'; import type { OntosModuleDeploymentContract } from '../../src/modules/manifest.ts'; import { makeModuleEntrypointGateway } from '../../src/modules/module-entrypoint-gateway.ts'; -import { - defineSystemModuleEntrypoint, - defineTenantModuleEntrypoint, -} from '../../src/modules/module-entrypoint.ts'; +import { defineSystemModuleEntrypoint, defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { makeModuleStateGate } from '../../src/modules/module-state-gate.ts'; import { TenantModuleStateService, @@ -63,47 +42,30 @@ import { import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; import { testOperationalScopeResolver } from '../fixtures/operational-scope.ts'; import { openActionRuntimeOptions } from '../support/action-runtime-options.ts'; -import { - makeFaultInjectableCoreDatabase, - TestQueryHook, -} from '../support/database-faults.ts'; - -const TestPersistenceErrorContract = Schema.TaggedStruct( - 'TestPersistenceError', - { - reason: Schema.String, - } -); +import { makeFaultInjectableCoreDatabase, TestQueryHook } from '../support/database-faults.ts'; + +const TestPersistenceErrorContract = Schema.TaggedStruct('TestPersistenceError', { + reason: Schema.String, +}); type TestPersistenceErrorSelf = typeof TestPersistenceErrorContract.Type; -const TestPersistenceError = Schema.TaggedError()( - 'TestPersistenceError', - { - reason: Schema.String, - } -); +const TestPersistenceError = Schema.TaggedError()('TestPersistenceError', { + reason: Schema.String, +}); const TestDomainRejectedContract = Schema.TaggedStruct('TestDomainRejected', { reason: Schema.String, }); type TestDomainRejectedSelf = typeof TestDomainRejectedContract.Type; -const TestDomainRejected = Schema.TaggedError()( - 'TestDomainRejected', - { - reason: Schema.String, - } -); +const TestDomainRejected = Schema.TaggedError()('TestDomainRejected', { + reason: Schema.String, +}); const TestStateIdSchema = Schema.String.pipe(Schema.brand('TestStateId')); -const ActionPolicyDeniedFailureSchema = Schema.TaggedStruct( - 'ActionPolicyDenied', - { - policyReasonCode: Schema.String, - reason: Schema.String, - } -); -const decodeActionPolicyDeniedFailure = Schema.decodeUnknownOption( - ActionPolicyDeniedFailureSchema -); +const ActionPolicyDeniedFailureSchema = Schema.TaggedStruct('ActionPolicyDenied', { + policyReasonCode: Schema.String, + reason: Schema.String, +}); +const decodeActionPolicyDeniedFailure = Schema.decodeUnknownOption(ActionPolicyDeniedFailureSchema); const tenantId = randomUUID(); const legalEntityId = randomUUID(); @@ -127,23 +89,14 @@ const transport = (idempotencyKey: string, targetResourceId = 'primary') => ({ targetResourceType: 'test-state', }); -const inventoryStockContract: OntosModuleDeploymentContract = - makeModuleContractFixture({ - appId: 'inventory-stock', - buildMarker: 'integration-test', - description: 'Inventory integration fixture', - displayName: 'Inventory', - moduleId: 'inventory.stock', - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], - }); +const inventoryStockContract: OntosModuleDeploymentContract = makeModuleContractFixture({ + appId: 'inventory-stock', + buildMarker: 'integration-test', + description: 'Inventory integration fixture', + displayName: 'Inventory', + moduleId: 'inventory.stock', + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], +}); const inventoryInstalledCatalog: InstalledModuleCatalog = Object.freeze({ contracts: Object.freeze([inventoryStockContract]), @@ -155,10 +108,8 @@ const inventoryInstalledCatalog: InstalledModuleCatalog = Object.freeze({ status: 'available' as const, }, ]), - getByDeploymentAppId: (appId: string) => - appId === 'inventory-stock' ? inventoryStockContract : undefined, - getByModuleId: (moduleId: string) => - moduleId === 'inventory.stock' ? inventoryStockContract : undefined, + getByDeploymentAppId: (appId: string) => (appId === 'inventory-stock' ? inventoryStockContract : undefined), + getByModuleId: (moduleId: string) => (moduleId === 'inventory.stock' ? inventoryStockContract : undefined), moduleIds: Object.freeze(['inventory.stock']), outboxSubscriptions: Object.freeze([]), }); @@ -168,23 +119,21 @@ const allowedPermission = { }; const withDatabase = ( - execute: ( - database: ContextServiceContract - ) => Effect.Effect + execute: (database: ContextServiceContract) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* databaseScope() { const configuration = yield* loadDatabaseConfig(); const database = yield* makeFaultInjectableCoreDatabase(configuration); return yield* execute(database); - }) + }), ); type ContextServiceContract = Parameters[0]; const withTransactionOverride = ( database: ContextServiceContract, - override: Pick + override: Pick, ): ContextServiceContract => ({ executor: Object.assign(Object.create(database.executor), override), }); @@ -217,7 +166,7 @@ type EvidencePersistenceStage = typeof EvidencePersistenceStageSchema.Type; const withEvidencePersistenceFailure = ( database: ContextServiceContract, - stage: EvidencePersistenceStage + stage: EvidencePersistenceStage, ): ContextServiceContract => { const transactionOverride = { transaction: (transactionBody) => @@ -229,8 +178,7 @@ const withEvidencePersistenceFailure = ( 'invocation-success': 'action_invocations', outbox: 'outbox_messages', }[stage]; - const operation = - stage === 'invocation-success' ? 'update' : 'insert into'; + const operation = stage === 'invocation-success' ? 'update' : 'insert into'; return transactionBody(transaction).pipe( Effect.provideService(TestQueryHook, (statement) => statement.startsWith(`${operation} "core"."${table}"`) @@ -240,10 +188,10 @@ const withEvidencePersistenceFailure = ( cause: new Error('Injected SQL failure'), message: `Injected ${stage} persistence failure`, }), - }) + }), ) - : Effect.void - ) + : Effect.void, + ), ); }), } satisfies Pick; @@ -251,9 +199,7 @@ const withEvidencePersistenceFailure = ( }; const liveModuleStateOptions = (database: ContextServiceContract) => { - const moduleStateGate = makeModuleStateGate( - makeTenantModuleStateService(database) - ); + const moduleStateGate = makeModuleStateGate(makeTenantModuleStateService(database)); return { ...openActionRuntimeOptions, moduleEntrypointGateway: makeModuleEntrypointGateway(moduleStateGate), @@ -300,7 +246,7 @@ const prepare = (() => state: 'active', tenantId, }); - }) + }), ))(); const cleanup = (() => @@ -320,11 +266,10 @@ const cleanup = (() => legalEntities, tenants, ], - (table) => - database.executor.delete(table).where(eq(table.tenantId, tenantId)), - { discard: true } + (table) => database.executor.delete(table).where(eq(table.tenantId, tenantId)), + { discard: true }, ); - }) + }), ))(); const TestDomainEvents = { @@ -334,10 +279,7 @@ const TestDomainEvents = { interface TestActionServices { readonly transaction: ScopedTransactionExecutor; } -type TestActionContext = ActionHandlerContext< - typeof TestDomainEvents, - TestActionServices ->; +type TestActionContext = ActionHandlerContext; interface RegistrationOptions { readonly actionKey: string; @@ -346,10 +288,7 @@ interface RegistrationOptions { readonly moduleStateKey: string; readonly onExecute?: () => void; readonly onExecuteEffect?: Effect.Effect; - readonly policies?: readonly ActionPolicy< - { readonly value: string }, - 'core.shell' - >[]; + readonly policies?: readonly ActionPolicy<{ readonly value: string }, 'core.shell'>[]; } const makeRegistration = ({ @@ -369,10 +308,7 @@ const makeRegistration = ({ }, actionKey, auditProfile: 'standard', - domainErrorSchema: Schema.Union([ - TestDomainRejected, - TestPersistenceError, - ]), + domainErrorSchema: Schema.Union([TestDomainRejected, TestPersistenceError]), domainEvents: TestDomainEvents, entrypoint: defineSystemModuleEntrypoint({ access: 'write', @@ -395,10 +331,7 @@ const makeRegistration = ({ }), schemaVersion: '1', }, - Effect.fn(function* integrationHandler( - payload, - context: TestActionContext - ) { + Effect.fn(function* integrationHandler(payload, context: TestActionContext) { onExecute?.(); if (onExecuteEffect !== undefined) { yield* onExecuteEffect; @@ -413,12 +346,7 @@ const makeRegistration = ({ .returning({ tenantModuleStateId: tenantModuleStates.tenantModuleStateId, }) - .pipe( - Effect.mapError( - () => - new TestPersistenceError({ reason: 'test business write failed' }) - ) - ); + .pipe(Effect.mapError(() => new TestPersistenceError({ reason: 'test business write failed' }))); yield* context.recordDataAccess({ accessKind: 'read', @@ -472,17 +400,13 @@ const makeRegistration = ({ value: payload.value, }; }), - (transaction) => Effect.succeed({ transaction }) + (transaction) => Effect.succeed({ transaction }), ); const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); -const hasFailure = ( - exit: Exit.Exit, - tag: string -): boolean => - Exit.isFailure(exit) && - Option.exists(Cause.findErrorOption(exit.cause), Predicate.isTagged(tag)); +const hasFailure = (exit: Exit.Exit, tag: string): boolean => + Exit.isFailure(exit) && Option.exists(Cause.findErrorOption(exit.cause), Predicate.isTagged(tag)); const testProgram1 = () => withDatabase( @@ -534,14 +458,14 @@ const testProgram1 = () => () => Effect.sync(() => { handlerExecutions += 1; - }) + }), ); const runtime = makeActionRuntime( database, makeActionRepository(), allowedPermission, testOperationalScopeResolver, - liveModuleStateOptions(database) + liveModuleStateOptions(database), ); const firstAttempt = yield* Effect.forkScoped( Effect.exit( @@ -550,8 +474,8 @@ const testProgram1 = () => principal, registration: action, transport: transport('business-module-concurrent-gate'), - }) - ) + }), + ), ); yield* Deferred.await(policyReached); yield* runtime @@ -570,10 +494,7 @@ const testProgram1 = () => Effect.provideService(InstalledModuleCatalogService, { load: Effect.succeed(inventoryInstalledCatalog), }), - Effect.provideService( - TenantModuleStateService, - makeTenantModuleStateService(database) - ) + Effect.provideService(TenantModuleStateService, makeTenantModuleStateService(database)), ); yield* Deferred.succeed(continuePolicy, null); const denied = yield* Fiber.join(firstAttempt); @@ -583,12 +504,7 @@ const testProgram1 = () => const [openInvocation] = yield* database.executor .select() .from(actionInvocations) - .where( - eq( - actionInvocations.idempotencyKey, - 'business-module-concurrent-gate' - ) - ); + .where(eq(actionInvocations.idempotencyKey, 'business-module-concurrent-gate')); expect(openInvocation).toBeDefined(); if (openInvocation === undefined) { throw new Error('Expected openInvocation'); @@ -597,9 +513,7 @@ const testProgram1 = () => const deniedEvidence = yield* database.executor .select() .from(auditEvents) - .where( - eq(auditEvents.actionInvocationId, openInvocation.actionInvocationId) - ); + .where(eq(auditEvents.actionInvocationId, openInvocation.actionInvocationId)); expect(deniedEvidence.length).toBe(0); yield* runtime @@ -618,10 +532,7 @@ const testProgram1 = () => Effect.provideService(InstalledModuleCatalogService, { load: Effect.succeed(inventoryInstalledCatalog), }), - Effect.provideService( - TenantModuleStateService, - makeTenantModuleStateService(database) - ) + Effect.provideService(TenantModuleStateService, makeTenantModuleStateService(database)), ); yield* runtime.runAction({ payload: undefined, @@ -630,7 +541,7 @@ const testProgram1 = () => transport: transport('business-module-concurrent-gate'), }); expect(handlerExecutions).toBe(1); - }) + }), ); const testProgram2 = Effect.fn(function* integrationProgram4() { @@ -644,7 +555,7 @@ const testProgram2 = Effect.fn(function* integrationProgram4() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const result = yield* runtime.runAction({ payload: { value: 'committed' }, @@ -656,55 +567,25 @@ const testProgram2 = Effect.fn(function* integrationProgram4() { transport: transport(key, moduleStateKey), }); - const [states, invocations, audits, accesses, events, messages] = - yield* Effect.all([ - database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)), - database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - database.executor - .select() - .from(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, tenantId)), - database.executor - .select() - .from(domainEvents) - .where(eq(domainEvents.subjectResourceId, moduleStateKey)), - database.executor - .select() - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ]); + const [states, invocations, audits, accesses, events, messages] = yield* Effect.all([ + database.executor.select().from(tenantModuleStates).where(eq(tenantModuleStates.moduleKey, moduleStateKey)), + database.executor.select().from(actionInvocations).where(eq(actionInvocations.idempotencyKey, key)), + database.executor.select().from(auditEvents).where(eq(auditEvents.tenantId, tenantId)), + database.executor.select().from(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), + database.executor.select().from(domainEvents).where(eq(domainEvents.subjectResourceId, moduleStateKey)), + database.executor.select().from(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), + ]); expect(result.value).toBe('committed'); expect(states.length).toBe(1); expect(invocations[0]?.status).toBe('succeeded'); expect(invocations[0]?.completedAt).toBeTruthy(); - expect( - audits.filter( - (row) => row.actionInvocationId === invocations[0]?.actionInvocationId - ).length - ).toBe(1); - expect( - accesses.filter( - (row) => row.actionInvocationId === invocations[0]?.actionInvocationId - ).length - ).toBe(1); + expect(audits.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId).length).toBe(1); + expect(accesses.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId).length).toBe(1); expect(events.length).toBe(1); - expect( - messages.filter((row) => row.domainEventId === events[0]?.domainEventId) - .length - ).toBe(1); + expect(messages.filter((row) => row.domainEventId === events[0]?.domainEventId).length).toBe(1); expect((events[0]?.tenantSequenceNo ?? 0) > 0).toBe(true); - }) + }), ); }); @@ -727,7 +608,7 @@ const testProgram3 = Effect.fn(function* integrationProgram6() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); yield* runtime.runAction({ payload: { value: 'committed' }, @@ -746,23 +627,15 @@ const testProgram3 = Effect.fn(function* integrationProgram6() { expect(observed).toEqual(['policy', 'handler']); expect(invocation.status).toBe('succeeded'); expect(audits.length).toBe(2); - const policyAudit = audits.find( - (row) => row.eventType === 'action.policy_checked' - ); - const executionAudit = audits.find( - (row) => row.eventType === 'action.executed' - ); + const policyAudit = audits.find((row) => row.eventType === 'action.policy_checked'); + const executionAudit = audits.find((row) => row.eventType === 'action.executed'); expect(policyAudit?.outcome).toBe('allowed'); expect(policyAudit?.outcomeStage).toBe('policy'); expect(executionAudit?.outcome).toBe('succeeded'); expect(executionAudit?.outcomeStage).toBe('execution'); - expect(encodeJson(policyAudit?.evidenceJson).includes('committed')).toBe( - false - ); - expect( - encodeJson(policyAudit?.evidenceJson).includes(policy.policyKey) - ).toBe(true); - }) + expect(encodeJson(policyAudit?.evidenceJson).includes('committed')).toBe(false); + expect(encodeJson(policyAudit?.evidenceJson).includes(policy.policyKey)).toBe(true); + }), ); }); @@ -773,13 +646,7 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { key: 'policy-denied-global', makeRegistration(handler: () => void) { const policy = defineGlobalPolicy<{ readonly value: string }>({ - evaluate: () => - Effect.fail( - denyPolicy( - 'tenant_suspended', - 'This tenant is suspended — contact support' - ) - ), + evaluate: () => Effect.fail(denyPolicy('tenant_suspended', 'This tenant is suspended — contact support')), policyKey: 'global.tenant-active.v1', }); return makeRegistration({ @@ -796,14 +663,8 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { actionKey: 'inventory.stock.policy-denied-local', key: 'policy-denied-local', makeRegistration(handler: () => void) { - const policy = defineMicroverticalPolicy< - { readonly value: string }, - 'inventory.stock' - >({ - evaluate: () => - Effect.fail( - denyPolicy('stock_locked', 'Stock is locked for reconciliation') - ), + const policy = defineMicroverticalPolicy<{ readonly value: string }, 'inventory.stock'>({ + evaluate: () => Effect.fail(denyPolicy('stock_locked', 'Stock is locked for reconciliation')), owningModuleKey: 'inventory.stock', policyKey: 'inventory.stock.unlocked.v1', }); @@ -815,10 +676,7 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { }, actionKey: this.actionKey, auditProfile: 'standard', - domainErrorSchema: Schema.Union([ - TestDomainRejected, - TestPersistenceError, - ]), + domainErrorSchema: Schema.Union([TestDomainRejected, TestPersistenceError]), domainEvents: { 'test-state.changed': Schema.Struct({ value: Schema.String }), }, @@ -850,7 +708,7 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { value: payload.value, }); }, - (transaction) => Effect.succeed({ transaction }) + (transaction) => Effect.succeed({ transaction }), ); }, reason: 'Stock is locked for reconciliation', @@ -865,7 +723,7 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); yield* Effect.forEach( scenarios, @@ -881,17 +739,11 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { registration: scenario.makeRegistration(() => { handlerExecutions += 1; }), - transport: transport( - scenario.key, - `test.${scenario.key}.${tenantId}` - ), + transport: transport(scenario.key, `test.${scenario.key}.${tenantId}`), }); const exit = yield* Effect.exit(actionEffect); const failure = Exit.isFailure(exit) - ? Option.flatMap( - Cause.findErrorOption(exit.cause), - decodeActionPolicyDeniedFailure - ) + ? Option.flatMap(Cause.findErrorOption(exit.cause), decodeActionPolicyDeniedFailure) : Option.none(); expect(hasFailure(exit, 'ActionPolicyDenied')).toBe(true); if (Option.isSome(failure)) { @@ -912,39 +764,19 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { database.executor .select() .from(auditEvents) - .where( - eq( - auditEvents.actionInvocationId, - invocation.actionInvocationId - ) - ), + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), database.executor .select() .from(dataAccessEvents) - .where( - eq( - dataAccessEvents.actionInvocationId, - invocation.actionInvocationId - ) - ), + .where(eq(dataAccessEvents.actionInvocationId, invocation.actionInvocationId)), database.executor .select() .from(domainEvents) - .where( - eq( - domainEvents.actionInvocationId, - invocation.actionInvocationId - ) - ), + .where(eq(domainEvents.actionInvocationId, invocation.actionInvocationId)), database.executor .select() .from(tenantModuleStates) - .where( - eq( - tenantModuleStates.moduleKey, - `test.${scenario.key}.${tenantId}` - ) - ), + .where(eq(tenantModuleStates.moduleKey, `test.${scenario.key}.${tenantId}`)), ]); const messages = yield* database.executor .select() @@ -954,10 +786,7 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { expect(invocation.status).toBe('rejected'); expect(invocation.completedAt).toBeTruthy(); expect(audits.length).toBe(2); - for (const eventType of [ - 'action.policy_checked', - 'action.rejected', - ]) { + for (const eventType of ['action.policy_checked', 'action.rejected']) { const audit = audits.find((row) => row.eventType === eventType); expect(audit?.outcome).toBe('denied'); expect(audit?.outcomeStage).toBe('policy'); @@ -969,16 +798,15 @@ const testProgram4 = Effect.fn(function* integrationProgram8() { expect(states.length).toBe(0); expect(messages.length).toBe(beforeMessages.length); }), - { discard: true } + { discard: true }, ); - }) + }), ); }); const testProgram5 = Effect.fn(function* integrationProgram11() { const policy = defineGlobalPolicy<{ readonly value: string }>({ - evaluate: () => - Effect.fail(denyPolicy('blocked', 'This operation is blocked')), + evaluate: () => Effect.fail(denyPolicy('blocked', 'This operation is blocked')), policyKey: 'global.blocked.v1', }); @@ -995,7 +823,7 @@ const testProgram5 = Effect.fn(function* integrationProgram11() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const exit = yield* Effect.exit( runtime.runAction({ @@ -1010,25 +838,22 @@ const testProgram5 = Effect.fn(function* integrationProgram11() { policies: [policy], }), transport: transport(key), - }) - ); - const { audits, invocation } = yield* invocationEvidence( - database, - key + }), ); + const { audits, invocation } = yield* invocationEvidence(database, key); expect( hasFailure(exit, 'ActionInvocationPersistenceError'), - Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success' + Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success', ).toBeTruthy(); expect(handlerExecutions).toBe(0); expect(invocation.status).toBe('received'); expect(invocation.completedAt).toBe(null); expect(audits.length).toBe(0); }), - { discard: true } + { discard: true }, ); - }) + }), ); }); @@ -1060,13 +885,11 @@ const testProgram6 = Effect.fn(function* integrationProgram14() { Effect.fn(function* integrationProgram15(scenario) { const moduleStateKey = `test.${scenario.key}.${tenantId}`; const runtime = makeActionRuntime( - scenario.key === 'evidence-failure' - ? withEvidencePersistenceFailure(database, 'audit') - : database, + scenario.key === 'evidence-failure' ? withEvidencePersistenceFailure(database, 'audit') : database, makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const exit = yield* Effect.exit( runtime.runAction({ @@ -1078,7 +901,7 @@ const testProgram6 = Effect.fn(function* integrationProgram14() { moduleStateKey, }), transport: transport(scenario.key, moduleStateKey), - }) + }), ); const states = yield* database.executor @@ -1120,8 +943,8 @@ const testProgram6 = Effect.fn(function* integrationProgram14() { expect(committedAccesses.length).toBe(0); expect(committedEvents.length).toBe(0); }), - { discard: true } - ) + { discard: true }, + ), ); }); @@ -1153,7 +976,7 @@ const testProgram7 = Effect.fn(function* integrationProgram16() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const exit = yield* Effect.exit( runtime.runAction({ @@ -1165,7 +988,7 @@ const testProgram7 = Effect.fn(function* integrationProgram16() { policies: stage === 'audit' ? [allowedPolicy] : [], }), transport: transport(key, moduleStateKey), - }) + }), ); const states = yield* database.executor @@ -1179,22 +1002,13 @@ const testProgram7 = Effect.fn(function* integrationProgram16() { expect(invocation).not.toBe(undefined); const invocationId = invocation?.actionInvocationId ?? ''; const [audits, accesses, events, afterOutbox] = yield* Effect.all([ - database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocationId)), + database.executor.select().from(auditEvents).where(eq(auditEvents.actionInvocationId, invocationId)), database.executor .select() .from(dataAccessEvents) .where(eq(dataAccessEvents.actionInvocationId, invocationId)), - database.executor - .select() - .from(domainEvents) - .where(eq(domainEvents.actionInvocationId, invocationId)), - database.executor - .select() - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), + database.executor.select().from(domainEvents).where(eq(domainEvents.actionInvocationId, invocationId)), + database.executor.select().from(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), ]); expect(hasFailure(exit, 'ActionTransactionError'), stage).toBe(true); @@ -1206,8 +1020,8 @@ const testProgram7 = Effect.fn(function* integrationProgram16() { expect(events.length, stage).toBe(0); expect(afterOutbox.length, stage).toBe(beforeOutbox.length); }), - { discard: true } - ) + { discard: true }, + ), ); }); @@ -1219,16 +1033,14 @@ const testProgram8 = () => makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); let evaluations = 0; let handlerExecutions = 0; const policy = defineGlobalPolicy<{ readonly value: string }>({ evaluate: () => { evaluations += 1; - return Effect.fail( - denyPolicy('terminal_rejection', 'This rejection is terminal') - ); + return Effect.fail(denyPolicy('terminal_rejection', 'This rejection is terminal')); }, policyKey: 'global.terminal-rejection.v1', }); @@ -1268,10 +1080,7 @@ const testProgram8 = () => yield* Deferred.succeed(concurrentPoliciesReached, null); } yield* Deferred.await(concurrentPoliciesReached); - return yield* denyPolicy( - 'concurrent_rejection', - 'Concurrent request rejected' - ); + return yield* denyPolicy('concurrent_rejection', 'Concurrent request rejected'); }), policyKey: 'global.concurrent-rejection.v1', }); @@ -1289,11 +1098,8 @@ const testProgram8 = () => transport: transport(concurrentKey), }; const concurrent = yield* Effect.all( - [ - Effect.exit(runtime.runAction(concurrentInput)), - Effect.exit(runtime.runAction(concurrentInput)), - ], - { concurrency: 'unbounded' } + [Effect.exit(runtime.runAction(concurrentInput)), Effect.exit(runtime.runAction(concurrentInput))], + { concurrency: 'unbounded' }, ); const [concurrentInvocation] = yield* database.executor .select() @@ -1306,12 +1112,7 @@ const testProgram8 = () => const concurrentAudits = yield* database.executor .select() .from(auditEvents) - .where( - eq( - auditEvents.actionInvocationId, - concurrentInvocation.actionInvocationId - ) - ); + .where(eq(auditEvents.actionInvocationId, concurrentInvocation.actionInvocationId)); expect(concurrent.length).toBe(2); for (const outcome of concurrent) { @@ -1321,7 +1122,7 @@ const testProgram8 = () => expect(handlerExecutions).toBe(0); expect(concurrentInvocation.status).toBe('rejected'); expect(concurrentAudits.length).toBe(2); - }) + }), ); const testProgram9 = () => @@ -1333,14 +1134,14 @@ const testProgram9 = () => repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const deniedRuntime = makeActionRuntime( database, repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const handlerStarted = yield* Deferred.make(); const denialEvaluated = yield* Deferred.make(); @@ -1356,11 +1157,7 @@ const testProgram9 = () => const denial = defineGlobalPolicy<{ readonly value: string }>({ evaluate: () => Deferred.succeed(denialEvaluated, null).pipe( - Effect.andThen( - Effect.fail( - denyPolicy('late_denial', 'This denial arrived too late') - ) - ) + Effect.andThen(Effect.fail(denyPolicy('late_denial', 'This denial arrived too late'))), ), policyKey: 'global.late-denial.v1', }); @@ -1375,30 +1172,21 @@ const testProgram9 = () => transport: transport(key, moduleStateKey), }; - const success = yield* Effect.forkScoped( - allowedRuntime.runAction({ ...sharedInput, registration: allowed }) - ); + const success = yield* Effect.forkScoped(allowedRuntime.runAction({ ...sharedInput, registration: allowed })); yield* Deferred.await(handlerStarted); const rejected = yield* Effect.forkScoped( - Effect.exit( - deniedRuntime.runAction({ ...sharedInput, registration: denied }) - ) - ); - const [successResult, rejectedExit] = yield* Effect.all( - [Fiber.join(success), Fiber.join(rejected)], - { concurrency: 'unbounded' } + Effect.exit(deniedRuntime.runAction({ ...sharedInput, registration: denied })), ); + const [successResult, rejectedExit] = yield* Effect.all([Fiber.join(success), Fiber.join(rejected)], { + concurrency: 'unbounded', + }); const { audits, invocation } = yield* invocationEvidence(database, key); expect(successResult.value).toBe('same'); - expect(hasFailure(rejectedExit, 'ActionInvocationPersistenceError')).toBe( - true - ); + expect(hasFailure(rejectedExit, 'ActionInvocationPersistenceError')).toBe(true); expect(invocation.status).toBe('succeeded'); - expect( - audits.filter((row) => row.eventType === 'action.rejected').length - ).toBe(0); - }) + expect(audits.filter((row) => row.eventType === 'action.rejected').length).toBe(0); + }), ); const testProgram10 = () => @@ -1422,7 +1210,7 @@ const testProgram10 = () => makeActionRepository(), concurrentAllowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); let executions = 0; const concurrentKey = 'concurrent-once'; @@ -1441,19 +1229,12 @@ const testProgram10 = () => }), transport: transport(concurrentKey, concurrentModule), }; - const firstAttempt = yield* Effect.exit( - runtime.runAction(concurrentInput) - ).pipe(Effect.forkChild); + const firstAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe(Effect.forkChild); yield* Deferred.await(handlerStarted); - const secondAttempt = yield* Effect.exit( - runtime.runAction(concurrentInput) - ).pipe(Effect.forkChild); + const secondAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe(Effect.forkChild); yield* Deferred.await(secondPermissionChecked); yield* Deferred.succeed(handlerRelease, null); - const concurrentResults = yield* Effect.all([ - Fiber.join(firstAttempt), - Fiber.join(secondAttempt), - ]); + const concurrentResults = yield* Effect.all([Fiber.join(firstAttempt), Fiber.join(secondAttempt)]); expect(executions).toBe(1); expect(concurrentResults.filter(Exit.isSuccess).length).toBe(1); @@ -1471,7 +1252,7 @@ const testProgram10 = () => correlationId: 'integration-concurrent-retry', traceId: 'retry-trace', }, - }) + }), ); expect(hasFailure(committedRetry, 'ActionAlreadyCommitted')).toBe(true); expect(executions).toBe(1); @@ -1480,7 +1261,7 @@ const testProgram10 = () => runtime.runAction({ ...concurrentInput, payload: { value: 'different' }, - }) + }), ); expect(hasFailure(conflict, 'ActionRequestHashConflict')).toBe(true); @@ -1496,7 +1277,7 @@ const testProgram10 = () => moduleStateKey: openModule, }), transport: transport(openKey, openModule), - }) + }), ); expect(hasFailure(rejected, 'TestDomainRejected')).toBe(true); @@ -1510,7 +1291,7 @@ const testProgram10 = () => transport: transport(openKey, openModule), }); expect(retried.value).toBe('retryable'); - }) + }), ); const testProgram11 = () => @@ -1527,12 +1308,12 @@ const testProgram11 = () => yield* Deferred.succeed(firstFlushed, null); yield* Deferred.await(firstCommitRelease); return result; - }) + }), ), } satisfies Pick; const delayedExecutor: ContextServiceContract['executor'] = Object.assign( Object.create(database.executor), - delayedTransaction + delayedTransaction, ); const repository = makeActionRepository(); const firstRuntime = makeActionRuntime( @@ -1540,14 +1321,14 @@ const testProgram11 = () => repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const secondRuntime = makeActionRuntime( database, repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const firstModule = `test.sequence.first.${tenantId}`; const secondModule = `test.sequence.second.${tenantId}`; @@ -1561,7 +1342,7 @@ const testProgram11 = () => moduleStateKey: firstModule, }), transport: transport('sequence-first', firstModule), - }) + }), ); yield* Deferred.await(firstFlushed); @@ -1578,15 +1359,13 @@ const testProgram11 = () => transport: transport('sequence-second', secondModule), }) .pipe( - Effect.provideService(TestQueryHook, () => - Deferred.succeed(secondInsertStarted, null).pipe(Effect.asVoid) - ), + Effect.provideService(TestQueryHook, () => Deferred.succeed(secondInsertStarted, null).pipe(Effect.asVoid)), Effect.ensuring( Effect.sync(() => { secondCompleted = true; - }) - ) - ) + }), + ), + ), ); yield* Deferred.await(secondInsertStarted); @@ -1597,16 +1376,9 @@ const testProgram11 = () => concurrency: 'unbounded', }); - const events = yield* database.executor - .select() - .from(domainEvents) - .where(eq(domainEvents.tenantId, tenantId)); - const firstEvent = events.find( - (event) => event.subjectResourceId === firstModule - ); - const secondEvent = events.find( - (event) => event.subjectResourceId === secondModule - ); + const events = yield* database.executor.select().from(domainEvents).where(eq(domainEvents.tenantId, tenantId)); + const firstEvent = events.find((event) => event.subjectResourceId === firstModule); + const secondEvent = events.find((event) => event.subjectResourceId === secondModule); expect(firstEvent).toBeDefined(); if (firstEvent === undefined) { @@ -1616,10 +1388,8 @@ const testProgram11 = () => if (secondEvent === undefined) { throw new Error('Expected secondEvent'); } - expect( - firstEvent.tenantSequenceNo < secondEvent.tenantSequenceNo - ).toBeTruthy(); - }) + expect(firstEvent.tenantSequenceNo < secondEvent.tenantSequenceNo).toBeTruthy(); + }), ); const testProgram12 = () => @@ -1638,9 +1408,7 @@ const testProgram12 = () => }); const uncertainTransaction = { transaction: (transactionBody) => - database.executor - .transaction(transactionBody) - .pipe(Effect.andThen(Effect.die(acknowledgementLost))), + database.executor.transaction(transactionBody).pipe(Effect.andThen(Effect.die(acknowledgementLost))), } satisfies Pick; const uncertainRuntime = makeActionRuntime( @@ -1648,7 +1416,7 @@ const testProgram12 = () => repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const first = yield* Effect.exit( uncertainRuntime.runAction({ @@ -1656,7 +1424,7 @@ const testProgram12 = () => principal, registration: actionRegistration, transport: transport(key, moduleStateKey), - }) + }), ); expect(hasFailure(first, 'ActionCommitIndeterminate')).toBe(true); @@ -1665,7 +1433,7 @@ const testProgram12 = () => repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const invocations = yield* database.executor .select() @@ -1680,7 +1448,7 @@ const testProgram12 = () => ...principal, principalId: randomUUID(), }, - }) + }), ); const unavailableRuntime = makeActionRuntime( database, @@ -1691,24 +1459,24 @@ const testProgram12 = () => new ActionInvocationPersistenceError({ code: 'action_invocation_persistence_failed', reason: 'test database unavailable', - }) + }), ), }, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const unavailableResolution = yield* Effect.exit( unavailableRuntime.resolveActionCommit({ invocationId, principal, - }) + }), ); const committedResolution = yield* Effect.exit( resolvingRuntime.resolveActionCommit({ invocationId, principal, - }) + }), ); const resolved = yield* Effect.exit( resolvingRuntime.runAction({ @@ -1716,22 +1484,16 @@ const testProgram12 = () => principal, registration: actionRegistration, transport: transport(key, moduleStateKey), - }) + }), ); const states = yield* database.executor .select() .from(tenantModuleStates) .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); - expect(hasFailure(committedResolution, 'ActionAlreadyCommitted')).toBe( - true - ); - expect( - hasFailure(unauthorizedResolution, 'ActionInvocationNotFound') - ).toBe(true); - expect( - hasFailure(unavailableResolution, 'ActionCommitIndeterminate') - ).toBe(true); + expect(hasFailure(committedResolution, 'ActionAlreadyCommitted')).toBe(true); + expect(hasFailure(unauthorizedResolution, 'ActionInvocationNotFound')).toBe(true); + expect(hasFailure(unavailableResolution, 'ActionCommitIndeterminate')).toBe(true); expect(hasFailure(resolved, 'ActionAlreadyCommitted')).toBe(true); expect(invocations[0]?.status).toBe('succeeded'); expect(states.length).toBe(1); @@ -1747,12 +1509,8 @@ const testProgram12 = () => database.executor .transaction((transaction) => transactionBody(transaction).pipe( - Effect.andThen( - Effect.die( - new Error('force rollback after the transaction body') - ) - ) - ) + Effect.andThen(Effect.die(new Error('force rollback after the transaction body'))), + ), ) .pipe(Effect.catchCause(() => Effect.die(acknowledgementLost))), } satisfies Pick; @@ -1762,7 +1520,7 @@ const testProgram12 = () => repository, allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions + openActionRuntimeOptions, ); const openFirst = yield* Effect.exit( uncertainOpenRuntime.runAction({ @@ -1770,7 +1528,7 @@ const testProgram12 = () => principal, registration: openRegistration, transport: transport(openKey, openModuleStateKey), - }) + }), ); expect(hasFailure(openFirst, 'ActionCommitIndeterminate')).toBe(true); @@ -1798,7 +1556,7 @@ const testProgram12 = () => expect(Predicate.isTagged(openResolution, 'ActionCommitOpen')).toBe(true); expect(openResolved.value).toBe('rolled-back-with-lost-ack'); expect(openStates.length).toBe(1); - }) + }), ); const testProgram13 = () => @@ -1841,14 +1599,14 @@ const testProgram13 = () => () => Effect.sync(() => { handlerExecutions += 1; - }) + }), ); const runtime = makeActionRuntime( database, makeActionRepository(), allowedPermission, testOperationalScopeResolver, - liveModuleStateOptions(database) + liveModuleStateOptions(database), ); yield* runtime.runAction({ payload: undefined, @@ -1858,26 +1616,14 @@ const testProgram13 = () => }); expect(handlerExecutions).toBe(1); - const deniedStates = [ - 'inactive', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ] as const; + const deniedStates = ['inactive', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'] as const; yield* Effect.forEach( deniedStates, Effect.fn(function* integrationProgram23(state, index) { yield* database.executor .update(tenantModuleStates) .set({ state }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, moduleKey) - ) - ); + .where(and(eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, moduleKey))); const idempotencyKey = `module-state-denied-${index}`; const exit = yield* Effect.exit( runtime.runAction({ @@ -1885,7 +1631,7 @@ const testProgram13 = () => principal, registration: action, transport: transport(idempotencyKey), - }) + }), ); expect(hasFailure(exit, 'ModuleStateDeniedError'), state).toBe(true); const invocations = yield* database.executor @@ -1894,100 +1640,57 @@ const testProgram13 = () => .where(eq(actionInvocations.idempotencyKey, idempotencyKey)); expect(invocations.length, state).toBe(0); }), - { discard: true } + { discard: true }, ); yield* database.executor .delete(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, moduleKey) - ) - ); + .where(and(eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, moduleKey))); const missingExit = yield* Effect.exit( runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('module-state-missing'), - }) + }), ); expect(hasFailure(missingExit, 'ModuleStateDeniedError')).toBe(true); expect(handlerExecutions).toBe(1); - }) + }), ); -it.layer( - Layer.effectDiscard( - Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie)) - ), - { - excludeTestServices: true, - } -)('Action runtime', (suite) => { - suite.effect( - 'rechecks business module state under the tenant lock and retries after Core recovery', - testProgram1 - ); +it.layer(Layer.effectDiscard(Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie))), { + excludeTestServices: true, +})('Action runtime', (suite) => { + suite.effect('rechecks business module state under the tenant lock and retries after Core recovery', testProgram1); - suite.effect( - 'atomically commits business state, all success evidence, and the succeeded marker', - testProgram2 - ); + suite.effect('atomically commits business state, all success evidence, and the succeeded marker', testProgram2); - suite.effect( - 'commits allowed Policy checkpoints atomically before handler success evidence', - testProgram3 - ); + suite.effect('commits allowed Policy checkpoints atomically before handler success evidence', testProgram3); suite.effect( 'atomically rejects denied global and same-owner MicroVertical Policies without handler evidence', - testProgram4 + testProgram4, ); - suite.effect( - 'rolls back every denied-Policy finalization persistence failure', - testProgram5 - ); + suite.effect('rolls back every denied-Policy finalization persistence failure', testProgram5); - suite.effect( - 'rolls back domain rejection, evidence persistence failure, and orphan outbox attempts', - testProgram6 - ); + suite.effect('rolls back domain rejection, evidence persistence failure, and orphan outbox attempts', testProgram6); - suite.effect( - 'rolls back every individual success-evidence persistence failure', - testProgram7 - ); + suite.effect('rolls back every individual success-evidence persistence failure', testProgram7); - suite.effect( - 'keeps Policy rejection terminal and deduplicates repeated and concurrent evidence', - testProgram8 - ); + suite.effect('keeps Policy rejection terminal and deduplicates repeated and concurrent evidence', testProgram8); - suite.effect( - 'never lets a losing Policy denial replace a running or successful invocation', - testProgram9 - ); + suite.effect('never lets a losing Policy denial replace a running or successful invocation', testProgram9); suite.effect( 'serializes concurrent requests and enforces committed, open-retry, and hash-conflict behavior', - testProgram10 + testProgram10, ); - suite.effect( - 'serializes Domain Event allocation by tenant commit order', - testProgram11 - ); + suite.effect('serializes Domain Event allocation by tenant commit order', testProgram11); - suite.effect( - 'resolves a lost commit acknowledgement from the durable succeeded marker', - testProgram12 - ); + suite.effect('resolves a lost commit acknowledgement from the durable succeeded marker', testProgram12); - suite.effect( - 'persists no invocation or evidence for every non-writable business module state', - testProgram13 - ); + suite.effect('persists no invocation or evidence for every non-writable business module state', testProgram13); }); diff --git a/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts b/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts index 755d2f7e4..2a1063c56 100644 --- a/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts +++ b/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts @@ -29,29 +29,11 @@ interface MigrationFixtureRow { } const tableColumns = { - action_invocations: [ - 'action_key', - 'target_module_key', - 'target_resource_type', - ], + action_invocations: ['action_key', 'target_module_key', 'target_resource_type'], audit_events: ['target_module_key', 'target_resource_type'], - data_access_events: [ - 'serving_module_key', - 'target_module_key', - 'target_resource_type', - 'evidence_policy_key', - ], - domain_events: [ - 'producer_module_key', - 'subject_module_key', - 'subject_resource_type', - ], - evidence_references: [ - 'subject_module_key', - 'subject_resource_type', - 'evidence_policy_key', - 'retention_policy_key', - ], + data_access_events: ['serving_module_key', 'target_module_key', 'target_resource_type', 'evidence_policy_key'], + domain_events: ['producer_module_key', 'subject_module_key', 'subject_resource_type'], + evidence_references: ['subject_module_key', 'subject_resource_type', 'evidence_policy_key', 'retention_policy_key'], media_links: ['target_module_key', 'target_resource_type'], outbox_deliveries: ['consumer_module_key'], outbox_messages: ['producer_module_key'], @@ -67,229 +49,179 @@ const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const columnDefinitions = (columns: readonly MigrationColumn[]): string => columns.map((column) => `"${column}" text`).join(', '); -const loadTableResult = ( - pool: Pool, - quotedSchema: string, - table: string, - columns: readonly MigrationColumn[] -) => +const loadTableResult = (pool: Pool, quotedSchema: string, table: string, columns: readonly MigrationColumn[]) => Effect.tryPromise(() => - pool.query( - `select * from ${quotedSchema}."${table}" order by record_id` - ) + pool.query(`select * from ${quotedSchema}."${table}" order by record_id`), ).pipe(Effect.map((result) => ({ columns, result, table }))); const runSequentially = ( values: readonly Value[], - operation: (value: Value) => Effect.Effect -): Effect.Effect => - Effect.forEach(values, operation, { concurrency: 1, discard: true }); + operation: (value: Value) => Effect.Effect, +): Effect.Effect => Effect.forEach(values, operation, { concurrency: 1, discard: true }); -const contactsIdentityMigrationProgram = Effect.gen( - function* contactsIdentityMigration() { - const configuration = yield* loadDatabaseConnectionPair(); - const crypto = yield* Crypto.Crypto; - const fileSystem = yield* FileSystem.FileSystem; - const pool = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ - connectionString: configuration.admin.connectionString, - max: 1, - }) - ), - (resource) => Effect.tryPromise(() => resource.end()).pipe(Effect.orDie) - ); - const schema = `core_contacts_identity_${(yield* crypto.randomUUIDv4).replaceAll('-', '')}`; - const quotedSchema = `"${schema}"`; - yield* Effect.gen(function* exerciseContactsIdentityMigration() { - yield* Effect.tryPromise(() => - pool.query(`create schema ${quotedSchema}`) - ); - yield* Effect.tryPromise(() => - pool.query( - `create table ${quotedSchema}.tenant_module_states ( +const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMigration() { + const configuration = yield* loadDatabaseConnectionPair(); + const crypto = yield* Crypto.Crypto; + const fileSystem = yield* FileSystem.FileSystem; + const pool = yield* Effect.acquireRelease( + Effect.sync( + () => + new Pool({ + connectionString: configuration.admin.connectionString, + max: 1, + }), + ), + (resource) => Effect.tryPromise(() => resource.end()).pipe(Effect.orDie), + ); + const schema = `core_contacts_identity_${(yield* crypto.randomUUIDv4).replaceAll('-', '')}`; + const quotedSchema = `"${schema}"`; + yield* Effect.gen(function* exerciseContactsIdentityMigration() { + yield* Effect.tryPromise(() => pool.query(`create schema ${quotedSchema}`)); + yield* Effect.tryPromise(() => + pool.query( + `create table ${quotedSchema}.tenant_module_states ( record_id text primary key, tenant_id text not null, module_key text not null, payload jsonb not null, recorded_at timestamptz not null, unique (tenant_id, module_key) - )` - ) - ); - yield* runSequentially(Object.entries(tableColumns), ([table, columns]) => - Effect.tryPromise(() => - pool.query( - `create table ${quotedSchema}."${table}" ( + )`, + ), + ); + yield* runSequentially(Object.entries(tableColumns), ([table, columns]) => + Effect.tryPromise(() => + pool.query( + `create table ${quotedSchema}."${table}" ( record_id text primary key, ${columnDefinitions(columns)}, payload jsonb not null default '{}'::jsonb - )` - ) - ) - ); - const recordedAt = '2026-01-02T03:04:05.678Z'; - const payload = { - freeText: 'crm.core must remain untouched inside arbitrary JSON', - }; - const encodedPayload = encodeJson(payload); - yield* Effect.tryPromise(() => - pool.query( - `insert into ${quotedSchema}.tenant_module_states + )`, + ), + ), + ); + const recordedAt = '2026-01-02T03:04:05.678Z'; + const payload = { + freeText: 'crm.core must remain untouched inside arbitrary JSON', + }; + const encodedPayload = encodeJson(payload); + yield* Effect.tryPromise(() => + pool.query( + `insert into ${quotedSchema}.tenant_module_states (record_id, tenant_id, module_key, payload, recorded_at) values ('legacy-state', 'tenant-a', $1, $2::jsonb, $3), ('unrelated-state', 'tenant-b', 'commerce.core', $2::jsonb, $3)`, - [legacyModule, encodedPayload, recordedAt] - ) - ); - yield* runSequentially( - Object.entries(tableColumns), - ([table, columns]) => { - const names = ['record_id', ...columns, 'payload']; - const oldValues = [ - `${table}-legacy`, - ...columns.map((_, index) => - index % 2 === 0 ? legacyModule : `${legacyModule}.record` - ), - encodedPayload, - ]; - const unrelatedValues = [ - `${table}-unrelated`, - ...columns.map(() => 'commerce.core.record'), - encodedPayload, - ]; - const placeholders = names - .map((_, index) => `$${index + 1}`) - .join(', '); - const quotedNames = names.map((name) => `"${name}"`).join(', '); - const unrelatedPlaceholders = names - .map((_, index) => `$${index + names.length + 1}`) - .join(', '); - return Effect.tryPromise(() => - pool.query( - `insert into ${quotedSchema}."${table}" (${quotedNames}) + [legacyModule, encodedPayload, recordedAt], + ), + ); + yield* runSequentially(Object.entries(tableColumns), ([table, columns]) => { + const names = ['record_id', ...columns, 'payload']; + const oldValues = [ + `${table}-legacy`, + ...columns.map((_, index) => (index % 2 === 0 ? legacyModule : `${legacyModule}.record`)), + encodedPayload, + ]; + const unrelatedValues = [`${table}-unrelated`, ...columns.map(() => 'commerce.core.record'), encodedPayload]; + const placeholders = names.map((_, index) => `$${index + 1}`).join(', '); + const quotedNames = names.map((name) => `"${name}"`).join(', '); + const unrelatedPlaceholders = names.map((_, index) => `$${index + names.length + 1}`).join(', '); + return Effect.tryPromise(() => + pool.query( + `insert into ${quotedSchema}."${table}" (${quotedNames}) values (${placeholders}), (${unrelatedPlaceholders})`, - [...oldValues, ...unrelatedValues] - ) - ); - } + [...oldValues, ...unrelatedValues], + ), ); + }); - const migrationSource = yield* fileSystem.readFileString( - new URL( - '../../drizzle/20260901102632_rename-crm-module-identity/migration.sql', - import.meta.url - ).pathname - ); - const migrationTables = [ - 'tenant_module_states', - ...Object.keys(tableColumns), - ]; - let isolatedMigrationSource = migrationSource; - for (const table of migrationTables) { - isolatedMigrationSource = isolatedMigrationSource.replaceAll( - `core.${table}`, - `${quotedSchema}."${table}"` - ); - } - const statements = isolatedMigrationSource - .split('--> statement-breakpoint') - .map((statement) => statement.trim()) - .filter((statement) => statement.length > 0); - yield* runSequentially([...statements, ...statements], (statement) => - Effect.tryPromise(() => pool.query(statement)) - ); + const migrationSource = yield* fileSystem.readFileString( + new URL('../../drizzle/20260901102632_rename-crm-module-identity/migration.sql', import.meta.url).pathname, + ); + const migrationTables = ['tenant_module_states', ...Object.keys(tableColumns)]; + let isolatedMigrationSource = migrationSource; + for (const table of migrationTables) { + isolatedMigrationSource = isolatedMigrationSource.replaceAll(`core.${table}`, `${quotedSchema}."${table}"`); + } + const statements = isolatedMigrationSource + .split('--> statement-breakpoint') + .map((statement) => statement.trim()) + .filter((statement) => statement.length > 0); + yield* runSequentially([...statements, ...statements], (statement) => + Effect.tryPromise(() => pool.query(statement)), + ); - const stateResult = yield* Effect.tryPromise(() => - pool.query<{ - module_key: string; - payload: typeof payload; - record_id: string; - recorded_at: Date; - }>( - `select record_id, module_key, payload, recorded_at - from ${quotedSchema}.tenant_module_states order by record_id` - ) - ); - expect( - stateResult.rows.map(({ module_key, record_id }) => ({ - module_key, - record_id, - })) - ).toEqual([ - { module_key: contactsModule, record_id: 'legacy-state' }, - { module_key: 'commerce.core', record_id: 'unrelated-state' }, - ]); - expect(stateResult.rows[0]?.payload).toEqual(payload); - expect(stateResult.rows[0]?.recorded_at.toISOString()).toBe(recordedAt); - const tableResults = yield* Effect.forEach( - Object.entries(tableColumns), - ([table, columns]) => - loadTableResult(pool, quotedSchema, table, columns), - { concurrency: 'unbounded' } - ); - for (const { columns, result, table } of tableResults) { - const [migrated, unrelated] = result.rows; - expect(migrated).toBeDefined(); - if (migrated === undefined) { - throw new Error('Expected migrated'); - } - expect(unrelated).toBeDefined(); - if (unrelated === undefined) { - throw new Error('Expected unrelated'); - } - for (const column of columns) { - expect( - String(migrated[column]), - `${table}.${column} was not migrated` - ).toMatch(/^contacts\.core(?:\.|$)/u); - expect(unrelated[column]).toBe('commerce.core.record'); - } - expect(migrated.payload).toEqual(payload); + const stateResult = yield* Effect.tryPromise(() => + pool.query<{ + module_key: string; + payload: typeof payload; + record_id: string; + recorded_at: Date; + }>( + `select record_id, module_key, payload, recorded_at + from ${quotedSchema}.tenant_module_states order by record_id`, + ), + ); + expect( + stateResult.rows.map(({ module_key, record_id }) => ({ + module_key, + record_id, + })), + ).toEqual([ + { module_key: contactsModule, record_id: 'legacy-state' }, + { module_key: 'commerce.core', record_id: 'unrelated-state' }, + ]); + expect(stateResult.rows[0]?.payload).toEqual(payload); + expect(stateResult.rows[0]?.recorded_at.toISOString()).toBe(recordedAt); + const tableResults = yield* Effect.forEach( + Object.entries(tableColumns), + ([table, columns]) => loadTableResult(pool, quotedSchema, table, columns), + { concurrency: 'unbounded' }, + ); + for (const { columns, result, table } of tableResults) { + const [migrated, unrelated] = result.rows; + expect(migrated).toBeDefined(); + if (migrated === undefined) { + throw new Error('Expected migrated'); + } + expect(unrelated).toBeDefined(); + if (unrelated === undefined) { + throw new Error('Expected unrelated'); + } + for (const column of columns) { + expect(String(migrated[column]), `${table}.${column} was not migrated`).toMatch(/^contacts\.core(?:\.|$)/u); + expect(unrelated[column]).toBe('commerce.core.record'); } + expect(migrated.payload).toEqual(payload); + } - yield* Effect.tryPromise(() => - pool.query(`truncate ${quotedSchema}.tenant_module_states`) - ); - yield* Effect.tryPromise(() => - pool.query( - `insert into ${quotedSchema}.tenant_module_states + yield* Effect.tryPromise(() => pool.query(`truncate ${quotedSchema}.tenant_module_states`)); + yield* Effect.tryPromise(() => + pool.query( + `insert into ${quotedSchema}.tenant_module_states (record_id, tenant_id, module_key, payload, recorded_at) values ('legacy-collision', 'tenant-c', $1, '{}'::jsonb, now()), ('contacts-collision', 'tenant-c', $2, '{}'::jsonb, now())`, - [legacyModule, contactsModule] - ) - ); - const collisionError = yield* Effect.flip( - Effect.tryPromise(() => pool.query(statements[0] ?? '')) - ); - expect(String(collisionError.cause)).toMatch(/would collide/u); - const collisionRows = yield* Effect.tryPromise(() => - pool.query<{ module_key: string }>( - `select module_key from ${quotedSchema}.tenant_module_states order by module_key` - ) - ); - expect(collisionRows.rows.map((row) => row.module_key)).toEqual([ - contactsModule, - legacyModule, - ]); - }).pipe( - Effect.ensuring( - Effect.tryPromise(() => - pool.query(`drop schema if exists ${quotedSchema} cascade`) - ).pipe(Effect.orDie) - ) + [legacyModule, contactsModule], + ), ); - } -).pipe(Effect.scoped); - -it.layer(NodeServices.layer, { excludeTestServices: true })( - 'contacts-identity-migration', - (suite) => { - suite.effect( - 'Contacts Core identity migration is preserving, scoped, rerunnable, and collision-safe', - () => contactsIdentityMigrationProgram + const collisionError = yield* Effect.flip(Effect.tryPromise(() => pool.query(statements[0] ?? ''))); + expect(String(collisionError.cause)).toMatch(/would collide/u); + const collisionRows = yield* Effect.tryPromise(() => + pool.query<{ module_key: string }>( + `select module_key from ${quotedSchema}.tenant_module_states order by module_key`, + ), ); - } -); + expect(collisionRows.rows.map((row) => row.module_key)).toEqual([contactsModule, legacyModule]); + }).pipe( + Effect.ensuring( + Effect.tryPromise(() => pool.query(`drop schema if exists ${quotedSchema} cascade`)).pipe(Effect.orDie), + ), + ); +}).pipe(Effect.scoped); + +it.layer(NodeServices.layer, { excludeTestServices: true })('contacts-identity-migration', (suite) => { + suite.effect( + 'Contacts Core identity migration is preserving, scoped, rerunnable, and collision-safe', + () => contactsIdentityMigrationProgram, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/context-access.test.ts b/app/packages/core-runtime/tests/integration/context-access.test.ts index 16fb11d87..a0c495f1a 100644 --- a/app/packages/core-runtime/tests/integration/context-access.test.ts +++ b/app/packages/core-runtime/tests/integration/context-access.test.ts @@ -3,10 +3,7 @@ import { NodeServices } from '@effect/platform-node'; import { Crypto, Effect, FileSystem } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - SPICEDB_CHECK_TIMEOUT_MS, - createSpiceDbPermissionClient, -} from '../../src/permissions/client.ts'; +import { SPICEDB_CHECK_TIMEOUT_MS, createSpiceDbPermissionClient } from '../../src/permissions/client.ts'; import { loadSpiceDbConfig } from '../../src/permissions/config.ts'; import { makeContextAccess, @@ -15,15 +12,14 @@ import { toResourceAccessObjectId, } from '../../src/permissions/context-access.ts'; -const spiceDbEffect = (operation: PromiseLike) => - Effect.tryPromise(() => operation); +const spiceDbEffect = (operation: PromiseLike) => Effect.tryPromise(() => operation); const relationship = ( resourceType: string, resourceId: string, relation: string, subjectType: string, - subjectId: string + subjectId: string, ) => v1.Relationship.create({ relation, @@ -43,14 +39,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { const configuration = yield* loadSpiceDbConfig(); const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; - const [ - tenantId, - otherTenantId, - legalEntityId, - otherLegalEntityId, - principalId, - resourceId, - ] = yield* Effect.all( + const [tenantId, otherTenantId, legalEntityId, otherLegalEntityId, principalId, resourceId] = yield* Effect.all( [ crypto.randomUUIDv4, crypto.randomUUIDv4, @@ -59,38 +48,22 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { crypto.randomUUIDv4, crypto.randomUUIDv4, ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const moduleId = 'property.registry'; const resource = { moduleId, resourceId, resourceType: 'property.unit' }; const legalObjectId = toLegalEntityAccessObjectId(tenantId, legalEntityId); - const moduleObjectId = toModuleAccessObjectId( - tenantId, - legalEntityId, - moduleId - ); - const resourceObjectId = toResourceAccessObjectId( - tenantId, - legalEntityId, - resource - ); - if ( - legalObjectId === undefined || - moduleObjectId === undefined || - resourceObjectId === undefined - ) { + const moduleObjectId = toModuleAccessObjectId(tenantId, legalEntityId, moduleId); + const resourceObjectId = toResourceAccessObjectId(tenantId, legalEntityId, resource); + if (legalObjectId === undefined || moduleObjectId === undefined || resourceObjectId === undefined) { throw new Error('Expected valid SpiceDB object identifiers'); } const client = v1.NewClient( configuration.preSharedKey, configuration.endpoint, - configuration.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE - ); - const bootstrap = yield* fileSystem.readFileString( - new URL('../../spicedb/bootstrap.yaml', import.meta.url).pathname + configuration.insecureLocal ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED : v1.ClientSecurity.SECURE, ); + const bootstrap = yield* fileSystem.readFileString(new URL('../../spicedb/bootstrap.yaml', import.meta.url).pathname); const bootstrapLines = bootstrap.split('\n'); const schemaStart = bootstrapLines.indexOf('schema: |-') + 1; const schemaEnd = bootstrapLines.indexOf('relationships: |-'); @@ -103,104 +76,26 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { client.promises.writeSchema( v1.WriteSchemaRequest.create({ schema: schemaBlock, - }) - ) + }), + ), ); const relationships = [ relationship('tenant', tenantId, 'member', 'principal', principalId), - relationship( - 'tenant', - tenantId, - 'identity_admin', - 'principal', - principalId - ), - relationship( - 'tenant', - tenantId, - 'party_identity_manager', - 'principal', - principalId - ), - relationship( - 'tenant', - tenantId, - 'party_identity_merger', - 'principal', - principalId - ), - relationship( - 'tenant', - tenantId, - 'party_identity_reader', - 'principal', - principalId - ), - relationship( - 'tenant', - tenantId, - 'party_identity_reviewer', - 'principal', - principalId - ), - relationship( - 'tenant', - tenantId, - 'party_relationship_manager', - 'principal', - principalId - ), + relationship('tenant', tenantId, 'identity_admin', 'principal', principalId), + relationship('tenant', tenantId, 'party_identity_manager', 'principal', principalId), + relationship('tenant', tenantId, 'party_identity_merger', 'principal', principalId), + relationship('tenant', tenantId, 'party_identity_reader', 'principal', principalId), + relationship('tenant', tenantId, 'party_identity_reviewer', 'principal', principalId), + relationship('tenant', tenantId, 'party_relationship_manager', 'principal', principalId), relationship('tenant', tenantId, 'support', 'principal', principalId), relationship('legal_entity', legalObjectId, 'tenant', 'tenant', tenantId), - relationship( - 'legal_entity', - legalObjectId, - 'member', - 'principal', - principalId - ), - relationship( - 'legal_entity', - legalObjectId, - 'counterparty_manager', - 'principal', - principalId - ), - relationship( - 'legal_entity', - legalObjectId, - 'counterparty_reader', - 'principal', - principalId - ), - relationship( - 'module_access', - moduleObjectId, - 'legal_entity', - 'legal_entity', - legalObjectId - ), - relationship( - 'module_access', - moduleObjectId, - 'accessor', - 'principal', - principalId - ), - relationship( - 'resource', - resourceObjectId, - 'module', - 'module_access', - moduleObjectId - ), - relationship( - 'resource', - resourceObjectId, - 'reader', - 'principal', - principalId - ), + relationship('legal_entity', legalObjectId, 'member', 'principal', principalId), + relationship('legal_entity', legalObjectId, 'counterparty_manager', 'principal', principalId), + relationship('legal_entity', legalObjectId, 'counterparty_reader', 'principal', principalId), + relationship('module_access', moduleObjectId, 'legal_entity', 'legal_entity', legalObjectId), + relationship('module_access', moduleObjectId, 'accessor', 'principal', principalId), + relationship('resource', resourceObjectId, 'module', 'module_access', moduleObjectId), + relationship('resource', resourceObjectId, 'reader', 'principal', principalId), ]; yield* Effect.gen(function* exerciseContextAccess() { @@ -211,15 +106,12 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { v1.RelationshipUpdate.create({ operation: v1.RelationshipUpdate_Operation.TOUCH, relationship: item, - }) + }), ), - }) - ) - ); - const permissionClient = createSpiceDbPermissionClient( - configuration, - SPICEDB_CHECK_TIMEOUT_MS + }), + ), ); + const permissionClient = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); yield* Effect.gen(function* checkContextAccess() { const access = makeContextAccess(permissionClient); const tenantDecisions = yield* Effect.forEach( @@ -238,7 +130,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { principalId, tenantIds: [tenantId, otherTenantId], }), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); for (const decisions of tenantDecisions) { expect(decisions).toEqual([ @@ -255,7 +147,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { principalId, tenantId, }), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); for (const decisions of legalEntityDecisions) { expect(decisions).toEqual([ @@ -269,7 +161,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { moduleIds: [moduleId], principalId, tenantId, - }) + }), ).toEqual([{ decision: 'allowed', key: moduleId }]); expect( yield* access.modules({ @@ -277,7 +169,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { moduleIds: [moduleId], principalId, tenantId: otherTenantId, - }) + }), ).toEqual([{ decision: 'denied', key: moduleId }]); expect( yield* access.resources({ @@ -285,7 +177,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { principalId, resources: [resource], tenantId, - }) + }), ).toEqual([ { decision: 'allowed', @@ -310,21 +202,18 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { optionalResourceId: cleanupResourceId, resourceType, }), - }) - ) + }), + ), ), - { concurrency: 'unbounded', discard: true } - ).pipe(Effect.ensuring(Effect.sync(() => client.close())), Effect.orDie) - ) + { concurrency: 'unbounded', discard: true }, + ).pipe(Effect.ensuring(Effect.sync(() => client.close())), Effect.orDie), + ), ); }); -it.layer(NodeServices.layer, { excludeTestServices: true })( - 'context-access', - (suite) => { - suite.effect( - 'isolates live legal-entity, module, and resource batches by tenant and entity', - () => contextAccessProgram - ); - } -); +it.layer(NodeServices.layer, { excludeTestServices: true })('context-access', (suite) => { + suite.effect( + 'isolates live legal-entity, module, and resource batches by tenant and entity', + () => contextAccessProgram, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts index 558107985..2c8afa1fc 100644 --- a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts @@ -14,10 +14,7 @@ import { principalManagementRepositoryFromTransaction, } from '../../src/auth/principal-management.ts'; import { makeSupportRecoveryPrincipalContextResolver } from '../../src/auth/support-recovery-principal-context.ts'; -import { - makeSystemPrincipalContextResolver, - registerSystemWorkload, -} from '../../src/auth/system-principal-context.ts'; +import { makeSystemPrincipalContextResolver, registerSystemWorkload } from '../../src/auth/system-principal-context.ts'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; import { actionInvocations, @@ -35,45 +32,30 @@ import { createNonHumanPrincipalAction } from '../../src/modules/actions/create- import { recordSupportImpersonationAction } from '../../src/modules/actions/record-support-impersonation.action.ts'; import { setManagedApiKeyBindingStatusAction } from '../../src/modules/actions/set-managed-api-key-binding-status.action.ts'; import { setSelfApiKeyBindingStatusAction } from '../../src/modules/actions/set-self-api-key-binding-status.action.ts'; -import { - makeOperationalScopeRepository, - makeOperationalScopeResolver, -} from '../../src/operations/context.ts'; -import { - SPICEDB_CHECK_TIMEOUT_MS, - createSpiceDbPermissionClient, -} from '../../src/permissions/client.ts'; +import { makeOperationalScopeRepository, makeOperationalScopeResolver } from '../../src/operations/context.ts'; +import { SPICEDB_CHECK_TIMEOUT_MS, createSpiceDbPermissionClient } from '../../src/permissions/client.ts'; import { loadSpiceDbConfig } from '../../src/permissions/config.ts'; import { makeContextAccess } from '../../src/permissions/context-access.ts'; -import { - makeActionPermissionService, - toSpiceDbActionObjectId, -} from '../../src/permissions/service.ts'; +import { makeActionPermissionService, toSpiceDbActionObjectId } from '../../src/permissions/service.ts'; import { makeReadRuntime } from '../../src/reads/runtime.ts'; import { openActionRuntimeOptions } from '../support/action-runtime-options.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; -const withOptionalProperty = < - Base extends object, - Key extends PropertyKey, - Value, - Trailing extends object, ->( +const withOptionalProperty = ( base: Base, condition: boolean, key: Key, value: Value, - trailing: Trailing -) => - condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }; + trailing: Trailing, +) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); const relationship = ( resourceType: string, resourceId: string, relation: string, subjectType: string, - subjectId: string + subjectId: string, ) => v1.Relationship.create({ relation, @@ -89,520 +71,382 @@ const relationship = ( }), }); -it.live( - 'runs identity mutations and tenant-isolated administration through live Action and Read runtimes', - () => - Effect.gen(function* identityRuntimeIntegration() { - const connections = yield* loadDatabaseConnectionPair(); - const spiceDbConfiguration = yield* loadSpiceDbConfig(); - const adminPool = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.admin.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie) - ); - const runtimePool = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.runtime.connectionString }) +it.live('runs identity mutations and tenant-isolated administration through live Action and Read runtimes', () => + Effect.gen(function* identityRuntimeIntegration() { + const connections = yield* loadDatabaseConnectionPair(); + const spiceDbConfiguration = yield* loadSpiceDbConfig(); + const adminPool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), + ); + const admin = yield* makeTestDatabaseFromPool(adminPool, coreRelations); + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); + const principalManagementRepository = principalManagementRepositoryFromTransaction(runtimeDatabase); + const runIdentityAction = (action: Effect.Effect) => + action.pipe(Effect.provideService(PrincipalManagementRepository, principalManagementRepository)); + const tenantId = randomUUID(); + const foreignTenantId = randomUUID(); + const administratorPrincipalId = randomUUID(); + const administratorAuthBindingId = randomUUID(); + const foreignPrincipalId = randomUUID(); + const supportTargetPrincipalId = randomUUID(); + const supportTargetAuthBindingId = randomUUID(); + const systemPrincipalId = randomUUID(); + const providerUserId = `identity-runtime-user-${randomUUID()}`; + const providerKeyId = `identity-runtime-key-${randomUUID()}`; + const selfProviderKeyId = `identity-runtime-self-key-${randomUUID()}`; + const supportTargetUserId = `identity-runtime-target-${randomUUID()}`; + const spiceDbClient = v1.NewClient( + spiceDbConfiguration.preSharedKey, + spiceDbConfiguration.endpoint, + spiceDbConfiguration.insecureLocal ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED : v1.ClientSecurity.SECURE, + ); + const permissionClient = createSpiceDbPermissionClient(spiceDbConfiguration, SPICEDB_CHECK_TIMEOUT_MS); + const contextAccess = makeContextAccess(permissionClient); + const actionPermission = makeActionPermissionService(permissionClient); + const operationalScope = makeOperationalScopeResolver( + makeOperationalScopeRepository({ executor: runtimeDatabase }), + contextAccess, + ); + const actionRuntime = makeActionRuntime( + { executor: runtimeDatabase }, + makeActionRepository(), + actionPermission, + operationalScope, + { ...openActionRuntimeOptions, contextAccess }, + ); + const readRuntime = makeReadRuntime( + { executor: runtimeDatabase }, + openModuleEntrypointGateway, + operationalScope, + contextAccess, + ); + const principal = { + authBindingId: administratorAuthBindingId, + authContextRef: `better-auth-session:${randomUUID()}`, + authMethod: 'session' as const, + principalId: administratorPrincipalId, + tenantId, + }; + const identityActionKeys = [ + 'core.identity.bind-managed-api-key', + 'core.identity.bind-self-api-key', + 'core.identity.change-principal-status', + 'core.identity.create-non-human-principal', + 'core.identity.record-support-impersonation', + 'core.identity.set-managed-api-key-binding-status', + 'core.identity.set-self-api-key-binding-status', + ] as const; + const spiceDbRelationships = [ + relationship('tenant', tenantId, 'member', 'principal', administratorPrincipalId), + relationship('tenant', tenantId, 'identity_admin', 'principal', administratorPrincipalId), + relationship('tenant', tenantId, 'support', 'principal', administratorPrincipalId), + ...identityActionKeys.flatMap((actionKey) => { + const objectId = toSpiceDbActionObjectId(actionKey); + return [ + relationship('action', objectId, 'executor', 'principal', administratorPrincipalId), + relationship('action', objectId, 'executor', 'principal', systemPrincipalId), + ]; + }), + ]; + const cleanup = Effect.gen(function* cleanIdentityRuntimeFixtures() { + yield* admin.delete(dataAccessEvents).where(inArray(dataAccessEvents.tenantId, [tenantId])); + yield* admin.delete(auditEvents).where(inArray(auditEvents.tenantId, [tenantId])); + yield* admin.delete(actionInvocations).where(inArray(actionInvocations.tenantId, [tenantId])); + yield* admin + .delete(principalAuthBindings) + .where(inArray(principalAuthBindings.tenantId, [tenantId, foreignTenantId])); + yield* admin.delete(principals).where(inArray(principals.tenantId, [tenantId, foreignTenantId])); + yield* admin.delete(tenants).where(inArray(tenants.tenantId, [tenantId, foreignTenantId])); + }); + + const exercise = Effect.gen(function* exerciseIdentityRuntime() { + const initialRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: spiceDbRelationships.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), ), - (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie) - ); - const admin = yield* makeTestDatabaseFromPool(adminPool, coreRelations); - const runtimeDatabase = yield* makeTestDatabaseFromPool( - runtimePool, - coreRelations - ); - const principalManagementRepository = - principalManagementRepositoryFromTransaction(runtimeDatabase); - const runIdentityAction = ( - action: Effect.Effect - ) => - action.pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepository - ) - ); - const tenantId = randomUUID(); - const foreignTenantId = randomUUID(); - const administratorPrincipalId = randomUUID(); - const administratorAuthBindingId = randomUUID(); - const foreignPrincipalId = randomUUID(); - const supportTargetPrincipalId = randomUUID(); - const supportTargetAuthBindingId = randomUUID(); - const systemPrincipalId = randomUUID(); - const providerUserId = `identity-runtime-user-${randomUUID()}`; - const providerKeyId = `identity-runtime-key-${randomUUID()}`; - const selfProviderKeyId = `identity-runtime-self-key-${randomUUID()}`; - const supportTargetUserId = `identity-runtime-target-${randomUUID()}`; - const spiceDbClient = v1.NewClient( - spiceDbConfiguration.preSharedKey, - spiceDbConfiguration.endpoint, - spiceDbConfiguration.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE - ); - const permissionClient = createSpiceDbPermissionClient( - spiceDbConfiguration, - SPICEDB_CHECK_TIMEOUT_MS - ); - const contextAccess = makeContextAccess(permissionClient); - const actionPermission = makeActionPermissionService(permissionClient); - const operationalScope = makeOperationalScopeResolver( - makeOperationalScopeRepository({ executor: runtimeDatabase }), - contextAccess - ); - const actionRuntime = makeActionRuntime( - { executor: runtimeDatabase }, - makeActionRepository(), - actionPermission, - operationalScope, - { ...openActionRuntimeOptions, contextAccess } - ); - const readRuntime = makeReadRuntime( - { executor: runtimeDatabase }, - openModuleEntrypointGateway, - operationalScope, - contextAccess - ); - const principal = { - authBindingId: administratorAuthBindingId, - authContextRef: `better-auth-session:${randomUUID()}`, - authMethod: 'session' as const, - principalId: administratorPrincipalId, - tenantId, - }; - const identityActionKeys = [ - 'core.identity.bind-managed-api-key', - 'core.identity.bind-self-api-key', - 'core.identity.change-principal-status', - 'core.identity.create-non-human-principal', - 'core.identity.record-support-impersonation', - 'core.identity.set-managed-api-key-binding-status', - 'core.identity.set-self-api-key-binding-status', - ] as const; - const spiceDbRelationships = [ - relationship( - 'tenant', + }); + yield* Effect.promise(() => spiceDbClient.promises.writeRelationships(initialRelationshipsRequest)); + yield* admin.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Identity runtime tenant', + slug: `identity-runtime-${tenantId}`, + status: 'active', tenantId, - 'member', - 'principal', - administratorPrincipalId - ), - relationship( - 'tenant', + }, + { + defaultLocale: 'en', + name: 'Foreign identity runtime tenant', + slug: `identity-runtime-${foreignTenantId}`, + status: 'active', + tenantId: foreignTenantId, + }, + ]); + yield* admin.insert(principals).values([ + { + displayName: 'Identity administrator', + kind: 'human', + principalId: administratorPrincipalId, + status: 'active', tenantId, - 'identity_admin', - 'principal', - administratorPrincipalId - ), - relationship( - 'tenant', + }, + { + displayName: 'Foreign managed service', + kind: 'service', + principalId: foreignPrincipalId, + status: 'active', + tenantId: foreignTenantId, + }, + { + displayName: 'Support target', + kind: 'human', + principalId: supportTargetPrincipalId, + status: 'active', tenantId, - 'support', - 'principal', - administratorPrincipalId - ), - ...identityActionKeys.flatMap((actionKey) => { - const objectId = toSpiceDbActionObjectId(actionKey); - return [ - relationship( - 'action', - objectId, - 'executor', - 'principal', - administratorPrincipalId - ), - relationship( - 'action', - objectId, - 'executor', - 'principal', - systemPrincipalId - ), - ]; - }), - ]; - const cleanup = Effect.gen(function* cleanIdentityRuntimeFixtures() { - yield* admin - .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.tenantId, [tenantId])); - yield* admin - .delete(auditEvents) - .where(inArray(auditEvents.tenantId, [tenantId])); - yield* admin - .delete(actionInvocations) - .where(inArray(actionInvocations.tenantId, [tenantId])); - yield* admin - .delete(principalAuthBindings) - .where( - inArray(principalAuthBindings.tenantId, [tenantId, foreignTenantId]) - ); - yield* admin - .delete(principals) - .where(inArray(principals.tenantId, [tenantId, foreignTenantId])); - yield* admin - .delete(tenants) - .where(inArray(tenants.tenantId, [tenantId, foreignTenantId])); - }); + }, + { + displayName: 'Identity runtime system', + kind: 'system', + principalId: systemPrincipalId, + status: 'active', + tenantId, + }, + ]); + yield* admin.insert(principalAuthBindings).values([ + { + principalAuthBindingId: administratorAuthBindingId, + principalId: administratorPrincipalId, + provider: 'better_auth', + providerSubjectId: providerUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: supportTargetAuthBindingId, + principalId: supportTargetPrincipalId, + provider: 'better_auth', + providerSubjectId: supportTargetUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + ]); - const exercise = Effect.gen(function* exerciseIdentityRuntime() { - const initialRelationshipsRequest = v1.WriteRelationshipsRequest.create( - { - updates: spiceDbRelationships.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }) - ), - } - ); - yield* Effect.promise(() => - spiceDbClient.promises.writeRelationships(initialRelationshipsRequest) - ); - yield* admin.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Identity runtime tenant', - slug: `identity-runtime-${tenantId}`, - status: 'active', - tenantId, + const created = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + displayName: 'Managed runtime service', + kind: 'service', }, - { - defaultLocale: 'en', - name: 'Foreign identity runtime tenant', - slug: `identity-runtime-${foreignTenantId}`, - status: 'active', - tenantId: foreignTenantId, + principal, + registration: createNonHumanPrincipalAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), }, - ]); - yield* admin.insert(principals).values([ - { - displayName: 'Identity administrator', - kind: 'human', - principalId: administratorPrincipalId, - status: 'active', - tenantId, + }), + ); + const binding = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + principalId: created.principalId, + providerSubjectId: providerKeyId, }, - { - displayName: 'Foreign managed service', - kind: 'service', - principalId: foreignPrincipalId, - status: 'active', - tenantId: foreignTenantId, + principal, + registration: bindManagedApiKeyAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), }, - { - displayName: 'Support target', - kind: 'human', - principalId: supportTargetPrincipalId, - status: 'active', - tenantId, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: binding.authBindingId, + expectedStatus: 'active', + newStatus: 'disabled', + principalId: created.principalId, }, - { - displayName: 'Identity runtime system', - kind: 'system', - principalId: systemPrincipalId, - status: 'active', - tenantId, + principal, + registration: setManagedApiKeyBindingStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), }, - ]); - yield* admin.insert(principalAuthBindings).values([ - { - principalAuthBindingId: administratorAuthBindingId, - principalId: administratorPrincipalId, - provider: 'better_auth', - providerSubjectId: providerUserId, - status: 'active', - subjectType: 'user', - tenantId, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: binding.authBindingId, + expectedStatus: 'disabled', + newStatus: 'active', + principalId: created.principalId, }, - { - principalAuthBindingId: supportTargetAuthBindingId, - principalId: supportTargetPrincipalId, - provider: 'better_auth', - providerSubjectId: supportTargetUserId, - status: 'active', - subjectType: 'user', - tenantId, + principal, + registration: setManagedApiKeyBindingStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + expectedStatus: 'active', + newStatus: 'disabled', + principalId: created.principalId, + reason: 'Exercise disabled managed-principal state', }, - ]); - - const created = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - displayName: 'Managed runtime service', - kind: 'service', - }, - principal, - registration: createNonHumanPrincipalAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - const binding = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - principalId: created.principalId, - providerSubjectId: providerKeyId, - }, - principal, - registration: bindManagedApiKeyAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: binding.authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', - principalId: created.principalId, - }, - principal, - registration: setManagedApiKeyBindingStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: binding.authBindingId, - expectedStatus: 'disabled', - newStatus: 'active', - principalId: created.principalId, - }, - principal, - registration: setManagedApiKeyBindingStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - expectedStatus: 'active', - newStatus: 'disabled', - principalId: created.principalId, - reason: 'Exercise disabled managed-principal state', - }, - principal, - registration: changePrincipalStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - expectedStatus: 'disabled', - newStatus: 'active', - principalId: created.principalId, - }, - principal, - registration: changePrincipalStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - const selfBinding = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { providerSubjectId: selfProviderKeyId }, - principal, - registration: bindSelfApiKeyAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: selfBinding.authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', - }, - principal, - registration: setSelfApiKeyBindingStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: selfBinding.authBindingId, - expectedStatus: 'disabled', - newStatus: 'active', - }, - principal, - registration: setSelfApiKeyBindingStatusAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ); - const listed = yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, principal, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); - - expect(binding.status).toBe('active'); - expect( - listed.items.map( - ({ authBindingId, principalId: listedPrincipalId }) => ({ - authBindingId: Option.getOrThrow(authBindingId), - principalId: listedPrincipalId, - }) - ) - ).toEqual([ - { - authBindingId: binding.authBindingId, + registration: changePrincipalStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + expectedStatus: 'disabled', + newStatus: 'active', principalId: created.principalId, }, - ]); - yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, - principal: { + principal, + registration: changePrincipalStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + const selfBinding = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { providerSubjectId: selfProviderKeyId }, + principal, + registration: bindSelfApiKeyAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { authBindingId: selfBinding.authBindingId, - authContextRef: `better-auth-api-key:${selfProviderKeyId}`, - authMethod: 'api_key', - principalId: administratorPrincipalId, - tenantId, + expectedStatus: 'active', + newStatus: 'disabled', }, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); - const committed = yield* admin - .select({ - actionKey: actionInvocations.actionKey, - status: actionInvocations.status, - }) - .from(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)); - expect( - [ - ...new Set( - committed - .filter(({ status }) => status === 'succeeded') - .map(({ actionKey }) => actionKey) - ), - ].toSorted() - ).toEqual( - identityActionKeys - .filter((actionKey) => !actionKey.includes('support')) - .toSorted() - ); - const [readEvidence] = yield* admin - .select({ resultCount: dataAccessEvents.resultCount }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq( - dataAccessEvents.evidencePolicyKey, - 'core.identity.managed-principals.access.v1' - ) - ) - ); - expect(readEvidence?.resultCount).toBe(1); - const [apiKeyReadEvidence] = yield* admin - .select({ authBindingId: dataAccessEvents.authBindingId }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq(dataAccessEvents.authMethod, 'api_key') - ) - ); - expect(apiKeyReadEvidence?.authBindingId).toBe( - selfBinding.authBindingId - ); + principal, + registration: setSelfApiKeyBindingStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: selfBinding.authBindingId, + expectedStatus: 'disabled', + newStatus: 'active', + }, + principal, + registration: setSelfApiKeyBindingStatusAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + const listed = yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, + principal, + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); - const systemPrincipal = yield* makeSystemPrincipalContextResolver({ - executor: runtimeDatabase, - }).resolve({ - principalId: systemPrincipalId, - registration: registerSystemWorkload({ - jobKey: 'identity-runtime-integration', - }), - runReference: randomUUID(), + expect(binding.status).toBe('active'); + expect( + listed.items.map(({ authBindingId, principalId: listedPrincipalId }) => ({ + authBindingId: Option.getOrThrow(authBindingId), + principalId: listedPrincipalId, + })), + ).toEqual([ + { + authBindingId: binding.authBindingId, + principalId: created.principalId, + }, + ]); + yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, + principal: { + authBindingId: selfBinding.authBindingId, + authContextRef: `better-auth-api-key:${selfProviderKeyId}`, + authMethod: 'api_key', + principalId: administratorPrincipalId, tenantId, - }); - const systemDenied = yield* runIdentityAction( - Effect.flip( - actionRuntime.runAction({ - payload: { - displayName: 'Executor-only system integration', - kind: 'integration', - }, - principal: systemPrincipal, - registration: createNonHumanPrincipalAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }, - }) - ) - ); - expect(Predicate.isTagged(systemDenied, 'ActionPermissionDenied')).toBe( - true - ); - const systemTenantMember = relationship( - 'tenant', - tenantId, - 'member', - 'principal', - systemPrincipalId - ); - const systemIdentityAdministrator = relationship( - 'tenant', - tenantId, - 'identity_admin', - 'principal', - systemPrincipalId - ); - spiceDbRelationships.push( - systemTenantMember, - systemIdentityAdministrator - ); - const systemRelationshipsRequest = v1.WriteRelationshipsRequest.create({ - updates: [systemTenantMember, systemIdentityAdministrator].map( - (item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }) + }, + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); + const committed = yield* admin + .select({ + actionKey: actionInvocations.actionKey, + status: actionInvocations.status, + }) + .from(actionInvocations) + .where(eq(actionInvocations.tenantId, tenantId)); + expect( + [ + ...new Set(committed.filter(({ status }) => status === 'succeeded').map(({ actionKey }) => actionKey)), + ].toSorted(), + ).toEqual(identityActionKeys.filter((actionKey) => !actionKey.includes('support')).toSorted()); + const [readEvidence] = yield* admin + .select({ resultCount: dataAccessEvents.resultCount }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq(dataAccessEvents.evidencePolicyKey, 'core.identity.managed-principals.access.v1'), ), - }); - yield* Effect.promise(() => - spiceDbClient.promises.writeRelationships(systemRelationshipsRequest) ); - const systemCreated = yield* runIdentityAction( + expect(readEvidence?.resultCount).toBe(1); + const [apiKeyReadEvidence] = yield* admin + .select({ authBindingId: dataAccessEvents.authBindingId }) + .from(dataAccessEvents) + .where(and(eq(dataAccessEvents.tenantId, tenantId), eq(dataAccessEvents.authMethod, 'api_key'))); + expect(apiKeyReadEvidence?.authBindingId).toBe(selfBinding.authBindingId); + + const systemPrincipal = yield* makeSystemPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolve({ + principalId: systemPrincipalId, + registration: registerSystemWorkload({ + jobKey: 'identity-runtime-integration', + }), + runReference: randomUUID(), + tenantId, + }); + const systemDenied = yield* runIdentityAction( + Effect.flip( actionRuntime.runAction({ payload: { - displayName: 'System-created integration', + displayName: 'Executor-only system integration', kind: 'integration', }, principal: systemPrincipal, @@ -611,194 +455,181 @@ it.live( correlationId: randomUUID(), idempotencyKey: randomUUID(), }, - }) - ); - expect(systemCreated.status).toBe('active'); - const systemRead = yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, + }), + ), + ); + expect(Predicate.isTagged(systemDenied, 'ActionPermissionDenied')).toBe(true); + const systemTenantMember = relationship('tenant', tenantId, 'member', 'principal', systemPrincipalId); + const systemIdentityAdministrator = relationship( + 'tenant', + tenantId, + 'identity_admin', + 'principal', + systemPrincipalId, + ); + spiceDbRelationships.push(systemTenantMember, systemIdentityAdministrator); + const systemRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: [systemTenantMember, systemIdentityAdministrator].map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }); + yield* Effect.promise(() => spiceDbClient.promises.writeRelationships(systemRelationshipsRequest)); + const systemCreated = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + displayName: 'System-created integration', + kind: 'integration', + }, principal: systemPrincipal, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); - expect(systemRead.items.length >= 2).toBe(true); + registration: createNonHumanPrincipalAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }, + }), + ); + expect(systemCreated.status).toBe('active'); + const systemRead = yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, + principal: systemPrincipal, + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); + expect(systemRead.items.length >= 2).toBe(true); - const supportReason = 'Investigate a live support incident'; - const supportSessionRef = `better-auth-session:${randomUUID()}`; - yield* runIdentityAction( - Effect.forEach( - ['requested', 'started'] as const, - (checkpoint) => - actionRuntime.runAction({ - payload: withOptionalProperty( - { - checkpoint, - originalPrincipalId: administratorPrincipalId, - reason: supportReason, - }, - checkpoint === 'started', - 'sessionRef', - supportSessionRef, - { - targetPrincipalId: supportTargetPrincipalId, - } - ), - principal, - registration: recordSupportImpersonationAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: `support-live-${checkpoint}-${randomUUID()}`, + const supportReason = 'Investigate a live support incident'; + const supportSessionRef = `better-auth-session:${randomUUID()}`; + yield* runIdentityAction( + Effect.forEach( + ['requested', 'started'] as const, + (checkpoint) => + actionRuntime.runAction({ + payload: withOptionalProperty( + { + checkpoint, + originalPrincipalId: administratorPrincipalId, + reason: supportReason, }, - }), - { concurrency: 1, discard: true } - ) - ); - const supportRelationship = relationship( - 'tenant', - tenantId, - 'support', - 'principal', - administratorPrincipalId - ); - const removeSupportRelationshipRequest = - v1.WriteRelationshipsRequest.create({ - updates: [ - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.DELETE, - relationship: supportRelationship, - }), - ], - }); - yield* Effect.promise(() => - spiceDbClient.promises.writeRelationships( - removeSupportRelationshipRequest - ) - ); - yield* admin - .update(principalAuthBindings) - .set({ - revokedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-08-09T00:00:00.000Z') - ), - status: 'revoked', - }) - .where( - eq( - principalAuthBindings.principalAuthBindingId, - administratorAuthBindingId - ) - ); - yield* admin - .update(principals) - .set({ status: 'disabled' }) - .where( - inArray(principals.principalId, [ - administratorPrincipalId, - supportTargetPrincipalId, - ]) - ); - const recoveryPrincipal = - yield* makeSupportRecoveryPrincipalContextResolver({ - executor: runtimeDatabase, - }).resolveStoppedImpersonation({ - originalAuthBindingId: administratorAuthBindingId, - originalPrincipalId: administratorPrincipalId, - originalSessionId: randomUUID(), - tenantId, - }); - const stopped = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - checkpoint: 'stopped', - originalPrincipalId: administratorPrincipalId, - reason: supportReason, - sessionRef: supportSessionRef, - targetPrincipalId: supportTargetPrincipalId, - }, - principal: recoveryPrincipal, - registration: recordSupportImpersonationAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: `support-live-stopped-${randomUUID()}`, - }, - }) - ); - expect(stopped).toEqual({ checkpoint: 'stopped', recorded: true }); - const supportAudits = yield* admin - .select({ evidence: auditEvents.evidenceJson }) - .from(auditEvents) - .where( - and( - eq(auditEvents.tenantId, tenantId), - eq(auditEvents.eventType, 'action.executed') - ) - ); - expect( - supportAudits - .map(({ evidence }) => - Predicate.isObjectKeyword(evidence) && - evidence !== null && - 'checkpoint' in evidence - ? evidence.checkpoint - : undefined - ) - .filter((checkpoint): checkpoint is string => - Predicate.isString(checkpoint) - ) - .toSorted() - ).toEqual(['requested', 'started', 'stopped']); - const supportAccess = yield* admin - .select({ count: dataAccessEvents.resultCount }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq( - dataAccessEvents.evidencePolicyKey, - 'core.identity.record-support-impersonation.access.v1' - ) - ) - ); - expect(supportAccess.length).toBe(6); - const succeededIdentityActions = yield* admin - .select({ actionKey: actionInvocations.actionKey }) - .from(actionInvocations) - .where( - and( - eq(actionInvocations.tenantId, tenantId), - eq(actionInvocations.status, 'succeeded') - ) - ); - expect( - [ - ...new Set( - succeededIdentityActions.map(({ actionKey }) => actionKey) - ), - ].toSorted() - ).toEqual([...identityActionKeys].toSorted()); + checkpoint === 'started', + 'sessionRef', + supportSessionRef, + { + targetPrincipalId: supportTargetPrincipalId, + }, + ), + principal, + registration: recordSupportImpersonationAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: `support-live-${checkpoint}-${randomUUID()}`, + }, + }), + { concurrency: 1, discard: true }, + ), + ); + const supportRelationship = relationship('tenant', tenantId, 'support', 'principal', administratorPrincipalId); + const removeSupportRelationshipRequest = v1.WriteRelationshipsRequest.create({ + updates: [ + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.DELETE, + relationship: supportRelationship, + }), + ], }); - const cleanupRelationships = Effect.suspend(() => { - const request = v1.WriteRelationshipsRequest.create({ - updates: spiceDbRelationships.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.DELETE, - relationship: item, - }) + yield* Effect.promise(() => spiceDbClient.promises.writeRelationships(removeSupportRelationshipRequest)); + yield* admin + .update(principalAuthBindings) + .set({ + revokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), + status: 'revoked', + }) + .where(eq(principalAuthBindings.principalAuthBindingId, administratorAuthBindingId)); + yield* admin + .update(principals) + .set({ status: 'disabled' }) + .where(inArray(principals.principalId, [administratorPrincipalId, supportTargetPrincipalId])); + const recoveryPrincipal = yield* makeSupportRecoveryPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolveStoppedImpersonation({ + originalAuthBindingId: administratorAuthBindingId, + originalPrincipalId: administratorPrincipalId, + originalSessionId: randomUUID(), + tenantId, + }); + const stopped = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + checkpoint: 'stopped', + originalPrincipalId: administratorPrincipalId, + reason: supportReason, + sessionRef: supportSessionRef, + targetPrincipalId: supportTargetPrincipalId, + }, + principal: recoveryPrincipal, + registration: recordSupportImpersonationAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: `support-live-stopped-${randomUUID()}`, + }, + }), + ); + expect(stopped).toEqual({ checkpoint: 'stopped', recorded: true }); + const supportAudits = yield* admin + .select({ evidence: auditEvents.evidenceJson }) + .from(auditEvents) + .where(and(eq(auditEvents.tenantId, tenantId), eq(auditEvents.eventType, 'action.executed'))); + expect( + supportAudits + .map(({ evidence }) => + Predicate.isObjectKeyword(evidence) && evidence !== null && 'checkpoint' in evidence + ? evidence.checkpoint + : undefined, + ) + .filter((checkpoint): checkpoint is string => Predicate.isString(checkpoint)) + .toSorted(), + ).toEqual(['requested', 'started', 'stopped']); + const supportAccess = yield* admin + .select({ count: dataAccessEvents.resultCount }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq(dataAccessEvents.evidencePolicyKey, 'core.identity.record-support-impersonation.access.v1'), ), - }); - return Effect.promise(() => - spiceDbClient.promises.writeRelationships(request) ); - }); - const release = cleanup.pipe( - Effect.ensuring(cleanupRelationships.pipe(Effect.orDie)), - Effect.ensuring( - Effect.sync(() => { - permissionClient.close(); - spiceDbClient.close(); - }) - ) + expect(supportAccess.length).toBe(6); + const succeededIdentityActions = yield* admin + .select({ actionKey: actionInvocations.actionKey }) + .from(actionInvocations) + .where(and(eq(actionInvocations.tenantId, tenantId), eq(actionInvocations.status, 'succeeded'))); + expect([...new Set(succeededIdentityActions.map(({ actionKey }) => actionKey))].toSorted()).toEqual( + [...identityActionKeys].toSorted(), ); - yield* Effect.addFinalizer(() => release.pipe(Effect.orDie)); - yield* exercise; - }) + }); + const cleanupRelationships = Effect.suspend(() => { + const request = v1.WriteRelationshipsRequest.create({ + updates: spiceDbRelationships.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.DELETE, + relationship: item, + }), + ), + }); + return Effect.promise(() => spiceDbClient.promises.writeRelationships(request)); + }); + const release = cleanup.pipe( + Effect.ensuring(cleanupRelationships.pipe(Effect.orDie)), + Effect.ensuring( + Effect.sync(() => { + permissionClient.close(); + spiceDbClient.close(); + }), + ), + ); + yield* Effect.addFinalizer(() => release.pipe(Effect.orDie)); + yield* exercise; + }), ); diff --git a/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts b/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts index 16a234156..5d372445c 100644 --- a/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts +++ b/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts @@ -14,96 +14,82 @@ const activeTwo = '21000000-0000-4000-8000-000000000002'; const suspended = '21000000-0000-4000-8000-000000000003'; const foreign = '21000000-0000-4000-8000-000000000004'; -it.live( - 'lists and validates only active legal entities inside the exact tenant', - () => - Effect.gen(function* legalEntityContextIntegration() { - const configuration = yield* loadDatabaseConfig(); - const { executor: database } = yield* makeCoreDatabase(configuration); - const context = makeLegalEntityContext({ executor: database }); - const cleanup = Effect.gen(function* cleanLegalEntityContextFixtures() { - yield* database - .delete(legalEntities) - .where(eq(legalEntities.tenantId, tenantOne)); - yield* database - .delete(legalEntities) - .where(eq(legalEntities.tenantId, tenantTwo)); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); - }); +it.live('lists and validates only active legal entities inside the exact tenant', () => + Effect.gen(function* legalEntityContextIntegration() { + const configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const context = makeLegalEntityContext({ executor: database }); + const cleanup = Effect.gen(function* cleanLegalEntityContextFixtures() { + yield* database.delete(legalEntities).where(eq(legalEntities.tenantId, tenantOne)); + yield* database.delete(legalEntities).where(eq(legalEntities.tenantId, tenantTwo)); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); + }); - yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); - yield* database.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Legal context tenant one', - slug: 'legal-context-tenant-one', - status: 'active', - tenantId: tenantOne, - }, - { - defaultLocale: 'en', - name: 'Legal context tenant two', - slug: 'legal-context-tenant-two', - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.insert(legalEntities).values([ - { - legalEntityId: activeOne, - legalName: 'Zeta entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-1', - status: 'active', - tenantId: tenantOne, - }, - { - legalEntityId: activeTwo, - legalName: 'Alpha entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-2', - status: 'active', - tenantId: tenantOne, - }, - { - legalEntityId: suspended, - legalName: 'Suspended entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-3', - status: 'suspended', - tenantId: tenantOne, - }, - { - legalEntityId: foreign, - legalName: 'Foreign entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-4', - status: 'active', - tenantId: tenantTwo, - }, - ]); - - expect(yield* context.listActiveForTenant(tenantOne)).toEqual([ - { legalEntityId: activeTwo, legalName: 'Alpha entity' }, - { legalEntityId: activeOne, legalName: 'Zeta entity' }, - ]); - expect(yield* context.validateSelection(tenantOne, activeOne)).toEqual({ + yield* database.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Legal context tenant one', + slug: 'legal-context-tenant-one', + status: 'active', + tenantId: tenantOne, + }, + { + defaultLocale: 'en', + name: 'Legal context tenant two', + slug: 'legal-context-tenant-two', + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.insert(legalEntities).values([ + { legalEntityId: activeOne, legalName: 'Zeta entity', - }); - const inactiveError = yield* Effect.flip( - context.validateSelection(tenantOne, suspended) - ); - expect( - Predicate.isTagged(inactiveError, 'LegalEntityContextInactiveError') - ).toBe(true); - const missingError = yield* Effect.flip( - context.validateSelection(tenantOne, foreign) - ); - expect( - Predicate.isTagged(missingError, 'LegalEntityContextMissingError') - ).toBe(true); - }) + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-1', + status: 'active', + tenantId: tenantOne, + }, + { + legalEntityId: activeTwo, + legalName: 'Alpha entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-2', + status: 'active', + tenantId: tenantOne, + }, + { + legalEntityId: suspended, + legalName: 'Suspended entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-3', + status: 'suspended', + tenantId: tenantOne, + }, + { + legalEntityId: foreign, + legalName: 'Foreign entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-4', + status: 'active', + tenantId: tenantTwo, + }, + ]); + + expect(yield* context.listActiveForTenant(tenantOne)).toEqual([ + { legalEntityId: activeTwo, legalName: 'Alpha entity' }, + { legalEntityId: activeOne, legalName: 'Zeta entity' }, + ]); + expect(yield* context.validateSelection(tenantOne, activeOne)).toEqual({ + legalEntityId: activeOne, + legalName: 'Zeta entity', + }); + const inactiveError = yield* Effect.flip(context.validateSelection(tenantOne, suspended)); + expect(Predicate.isTagged(inactiveError, 'LegalEntityContextInactiveError')).toBe(true); + const missingError = yield* Effect.flip(context.validateSelection(tenantOne, foreign)); + expect(Predicate.isTagged(missingError, 'LegalEntityContextMissingError')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/integration/module-state-gate.test.ts b/app/packages/core-runtime/tests/integration/module-state-gate.test.ts index 58580127e..8d944b7c6 100644 --- a/app/packages/core-runtime/tests/integration/module-state-gate.test.ts +++ b/app/packages/core-runtime/tests/integration/module-state-gate.test.ts @@ -9,22 +9,14 @@ import { makeCoreDatabase } from '../../src/db/client.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { tenantModuleStates, tenants } from '../../src/db/schema.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { - decideModuleStateAccess, - makeModuleStateGate, -} from '../../src/modules/module-state-gate.ts'; +import { decideModuleStateAccess, makeModuleStateGate } from '../../src/modules/module-state-gate.ts'; import { TenantModuleStateReadUnavailableError } from '../../src/modules/tenant-module-state-errors.ts'; import type { TenantModuleStateServiceContract } from '../../src/modules/tenant-module-state-service.ts'; -import { - TENANT_MODULE_STATES, - makeTenantModuleStateService, -} from '../../src/modules/tenant-module-state-service.ts'; +import { TENANT_MODULE_STATES, makeTenantModuleStateService } from '../../src/modules/tenant-module-state-service.ts'; type DatabaseService = (typeof CoreDatabase)['Service']; -const unavailableStateService = ( - reason: string -): TenantModuleStateServiceContract => { +const unavailableStateService = (reason: string): TenantModuleStateServiceContract => { const failure = new TenantModuleStateReadUnavailableError({ code: 'tenant_module_state_read_unavailable', reason, @@ -36,212 +28,163 @@ const unavailableStateService = ( }; }; -it.live( - 'batches tenant-isolated states once, rejects malformed/unavailable reads, and rechecks transactionally', - () => - Effect.gen(function* moduleStateGate1() { - const tenantOne = randomUUID(); - const tenantTwo = randomUUID(); - const moduleKey = `gate.integration-${tenantOne}`; - const stateModuleKey = ( - state: (typeof TENANT_MODULE_STATES)[number] - ): string => `${moduleKey}.${state.replaceAll('_', '-')}`; - const configuration = yield* loadDatabaseConfig(); - const database = yield* makeCoreDatabase(configuration); - yield* Effect.addFinalizer(() => - Effect.forEach( - [tenantModuleStates, tenants], - (table) => - Effect.forEach( - [tenantOne, tenantTwo], - (tenantId) => - database.executor - .delete(table) - .where(eq(table.tenantId, tenantId)), - { discard: true } - ), - { discard: true } - ).pipe(Effect.orDie) - ); - yield* database.executor.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Gate Integration One', - slug: `gate-one-${tenantOne}`, - status: 'active', - tenantId: tenantOne, - }, - { - defaultLocale: 'en', - name: 'Gate Integration Two', - slug: `gate-two-${tenantTwo}`, - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.executor.insert(tenantModuleStates).values([ - { moduleKey, state: 'active', tenantId: tenantOne }, - { moduleKey, state: 'quarantined', tenantId: tenantTwo }, - ...TENANT_MODULE_STATES.map((state) => ({ - moduleKey: stateModuleKey(state), - state, - tenantId: tenantOne, - })), - ]); +it.live('batches tenant-isolated states once, rejects malformed/unavailable reads, and rechecks transactionally', () => + Effect.gen(function* moduleStateGate1() { + const tenantOne = randomUUID(); + const tenantTwo = randomUUID(); + const moduleKey = `gate.integration-${tenantOne}`; + const stateModuleKey = (state: (typeof TENANT_MODULE_STATES)[number]): string => + `${moduleKey}.${state.replaceAll('_', '-')}`; + const configuration = yield* loadDatabaseConfig(); + const database = yield* makeCoreDatabase(configuration); + yield* Effect.addFinalizer(() => + Effect.forEach( + [tenantModuleStates, tenants], + (table) => + Effect.forEach( + [tenantOne, tenantTwo], + (tenantId) => database.executor.delete(table).where(eq(table.tenantId, tenantId)), + { discard: true }, + ), + { discard: true }, + ).pipe(Effect.orDie), + ); + yield* database.executor.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Gate Integration One', + slug: `gate-one-${tenantOne}`, + status: 'active', + tenantId: tenantOne, + }, + { + defaultLocale: 'en', + name: 'Gate Integration Two', + slug: `gate-two-${tenantTwo}`, + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.executor.insert(tenantModuleStates).values([ + { moduleKey, state: 'active', tenantId: tenantOne }, + { moduleKey, state: 'quarantined', tenantId: tenantTwo }, + ...TENANT_MODULE_STATES.map((state) => ({ + moduleKey: stateModuleKey(state), + state, + tenantId: tenantOne, + })), + ]); - let selects = 0; - const countingExecutor: DatabaseService['executor'] = Object.create( - database.executor - ); - Object.defineProperty(countingExecutor, 'select', { - configurable: true, - get: () => { - selects += 1; - return database.executor.select; - }, - }); - const gate = makeModuleStateGate( - makeTenantModuleStateService({ executor: countingExecutor }) - ); - const read = defineTenantModuleEntrypoint({ + let selects = 0; + const countingExecutor: DatabaseService['executor'] = Object.create(database.executor); + Object.defineProperty(countingExecutor, 'select', { + configurable: true, + get: () => { + selects += 1; + return database.executor.select; + }, + }); + const gate = makeModuleStateGate(makeTenantModuleStateService({ executor: countingExecutor })); + const read = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: `${moduleKey}.page`, + moduleKey, + role: 'page', + }); + const write = defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: `${moduleKey}.write`, + moduleKey, + role: 'action', + }); + const snapshot = yield* gate.prepareSnapshot(tenantOne, [read, read, write]); + yield* gate.check(snapshot, read); + yield* gate.check(snapshot, read); + expect(selects).toBe(1); + + const persistedStateDescriptors = TENANT_MODULE_STATES.map((state) => + defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access', }, - entrypointKey: `${moduleKey}.page`, - moduleKey, + entrypointKey: `${stateModuleKey(state)}.page`, + moduleKey: stateModuleKey(state), role: 'page', - }); - const write = defineTenantModuleEntrypoint({ - access: 'write', - authorization: { - kind: 'action_execution', - provisioning: 'tenant_membership_default', - }, - entrypointKey: `${moduleKey}.write`, - moduleKey, - role: 'action', - }); - const snapshot = yield* gate.prepareSnapshot(tenantOne, [ - read, - read, - write, - ]); - yield* gate.check(snapshot, read); - yield* gate.check(snapshot, read); - expect(selects).toBe(1); - - const persistedStateDescriptors = TENANT_MODULE_STATES.map((state) => - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: `${stateModuleKey(state)}.page`, - moduleKey: stateModuleKey(state), - role: 'page', - }) - ); - selects = 0; - const firstPersistedDescriptor = Option.getOrThrow( - Option.fromNullishOr(persistedStateDescriptors[0]) - ); - expect(firstPersistedDescriptor).toBeDefined(); - const persistedStateSnapshot = yield* gate.prepareSnapshot(tenantOne, [ - ...persistedStateDescriptors, - firstPersistedDescriptor, - ]); - expect(selects).toBe(1); - const persistedStateExits = yield* Effect.forEach( - TENANT_MODULE_STATES, - (_, index) => { - const descriptor = Option.getOrThrow( - Option.fromNullishOr(persistedStateDescriptors[index]) - ); - expect(descriptor).toBeDefined(); - return Effect.exit(gate.check(persistedStateSnapshot, descriptor)); - }, - { concurrency: 'unbounded' } - ); - for (const [index, state] of TENANT_MODULE_STATES.entries()) { - const descriptor = Option.getOrThrow( - Option.fromNullishOr(persistedStateDescriptors[index]) - ); + }), + ); + selects = 0; + const firstPersistedDescriptor = Option.getOrThrow(Option.fromNullishOr(persistedStateDescriptors[0])); + expect(firstPersistedDescriptor).toBeDefined(); + const persistedStateSnapshot = yield* gate.prepareSnapshot(tenantOne, [ + ...persistedStateDescriptors, + firstPersistedDescriptor, + ]); + expect(selects).toBe(1); + const persistedStateExits = yield* Effect.forEach( + TENANT_MODULE_STATES, + (_, index) => { + const descriptor = Option.getOrThrow(Option.fromNullishOr(persistedStateDescriptors[index])); expect(descriptor).toBeDefined(); - const exit = Option.getOrThrow( - Option.fromNullishOr(persistedStateExits[index]) - ); - expect(exit).toBeDefined(); - expect(Exit.isSuccess(exit), state).toBe( - decideModuleStateAccess(state, 'read') === 'allow' - ); - } + return Effect.exit(gate.check(persistedStateSnapshot, descriptor)); + }, + { concurrency: 'unbounded' }, + ); + for (const [index, state] of TENANT_MODULE_STATES.entries()) { + const descriptor = Option.getOrThrow(Option.fromNullishOr(persistedStateDescriptors[index])); + expect(descriptor).toBeDefined(); + const exit = Option.getOrThrow(Option.fromNullishOr(persistedStateExits[index])); + expect(exit).toBeDefined(); + expect(Exit.isSuccess(exit), state).toBe(decideModuleStateAccess(state, 'read') === 'allow'); + } - const tenantTwoSnapshot = yield* gate.prepareSnapshot(tenantTwo, [read]); - const quarantined = yield* Effect.flip( - gate.check(tenantTwoSnapshot, read) - ); - expect(Predicate.isTagged(quarantined, 'ModuleStateDeniedError')).toBe( - true - ); + const tenantTwoSnapshot = yield* gate.prepareSnapshot(tenantTwo, [read]); + const quarantined = yield* Effect.flip(gate.check(tenantTwoSnapshot, read)); + expect(Predicate.isTagged(quarantined, 'ModuleStateDeniedError')).toBe(true); - const missingDescriptor = defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: `${moduleKey}.missing`, - moduleKey: `${moduleKey}.missing-module`, - role: 'page', - }); - const missingSnapshot = yield* gate.prepareSnapshot(tenantOne, [ - missingDescriptor, - ]); - const missing = yield* Effect.flip( - gate.check(missingSnapshot, missingDescriptor) - ); - expect(Predicate.isTagged(missing, 'ModuleStateDeniedError')).toBe(true); + const missingDescriptor = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: `${moduleKey}.missing`, + moduleKey: `${moduleKey}.missing-module`, + role: 'page', + }); + const missingSnapshot = yield* gate.prepareSnapshot(tenantOne, [missingDescriptor]); + const missing = yield* Effect.flip(gate.check(missingSnapshot, missingDescriptor)); + expect(Predicate.isTagged(missing, 'ModuleStateDeniedError')).toBe(true); - yield* database.executor.transaction((transaction) => - gate.recheckWrite(transaction, tenantOne, write) - ); - yield* database.executor - .update(tenantModuleStates) - .set({ state: 'read_only' }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantOne), - eq(tenantModuleStates.moduleKey, moduleKey) - ) - ); - const lockedDenial = yield* database.executor.transaction((transaction) => - Effect.flip(gate.recheckWrite(transaction, tenantOne, write)) - ); - expect(Predicate.isTagged(lockedDenial, 'ModuleStateDeniedError')).toBe( - true - ); + yield* database.executor.transaction((transaction) => gate.recheckWrite(transaction, tenantOne, write)); + yield* database.executor + .update(tenantModuleStates) + .set({ state: 'read_only' }) + .where(and(eq(tenantModuleStates.tenantId, tenantOne), eq(tenantModuleStates.moduleKey, moduleKey))); + const lockedDenial = yield* database.executor.transaction((transaction) => + Effect.flip(gate.recheckWrite(transaction, tenantOne, write)), + ); + expect(Predicate.isTagged(lockedDenial, 'ModuleStateDeniedError')).toBe(true); - const unavailable = yield* Effect.flip( - makeModuleStateGate( - unavailableStateService('secret db failure') - ).prepareSnapshot(tenantOne, [read]) - ); - expect( - Predicate.isTagged(unavailable, 'ModuleStateCheckUnavailableError') - ).toBe(true); - expect(unavailable.reason).not.toMatch(/secret|db failure/u); + const unavailable = yield* Effect.flip( + makeModuleStateGate(unavailableStateService('secret db failure')).prepareSnapshot(tenantOne, [read]), + ); + expect(Predicate.isTagged(unavailable, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(unavailable.reason).not.toMatch(/secret|db failure/u); - const malformed = yield* Effect.flip( - makeModuleStateGate( - unavailableStateService('corrupt-storage-value') - ).prepareSnapshot(tenantOne, [read]) - ); - expect( - Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError') - ).toBe(true); - expect(malformed.reason).not.toMatch(/corrupt|storage/u); - }) + const malformed = yield* Effect.flip( + makeModuleStateGate(unavailableStateService('corrupt-storage-value')).prepareSnapshot(tenantOne, [read]), + ); + expect(Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(malformed.reason).not.toMatch(/corrupt|storage/u); + }), ); diff --git a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts index 5a1c6d53a..8e62f34ae 100644 --- a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts @@ -25,20 +25,16 @@ import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; const MessageKeySchema = Schema.String.pipe(Schema.brand('MessageKey')); const payloadSchema = Schema.Struct({ messageKey: MessageKeySchema }); -const dateAt = (instant: string): Date => - DateTime.toDateUtc(DateTime.makeUnsafe(instant)); +const dateAt = (instant: string): Date => DateTime.toDateUtc(DateTime.makeUnsafe(instant)); const advanceDate = (date: Date, milliseconds: number): Date => - DateTime.makeUnsafe(date).pipe( - DateTime.add({ milliseconds }), - DateTime.toDateUtc - ); + DateTime.makeUnsafe(date).pipe(DateTime.add({ milliseconds }), DateTime.toDateUtc); const makeWorker = ( workerKey: string, options: { readonly consumerModuleKey?: string; readonly maxAttempts?: number; readonly topic?: string; - } = {} + } = {}, ) => defineOutboxWorker( { @@ -62,10 +58,9 @@ const makeWorker = ( topic: options.topic ?? 'producer.message-created', workerKey, }, - () => Effect.void + () => Effect.void, ); -const subscriptionOf = (registration: AnyOutboxWorkerRegistration) => - registration.descriptor; +const subscriptionOf = (registration: AnyOutboxWorkerRegistration) => registration.descriptor; const insertTenant = (database: CoreDatabaseExecutor) => Effect.gen(function* insertTenantEffect() { const tenantId = randomUUID(); @@ -78,11 +73,7 @@ const insertTenant = (database: CoreDatabaseExecutor) => }); return tenantId; }); -const activateConsumer = ( - database: CoreDatabaseExecutor, - tenantId: string, - state = 'active' -) => +const activateConsumer = (database: CoreDatabaseExecutor, tenantId: string, state = 'active') => database.insert(tenantModuleStates).values({ moduleKey: 'consumer', state, @@ -92,7 +83,7 @@ const insertMessage = ( database: CoreDatabaseExecutor, tenantId: string, topic = 'producer.message-created', - messageKey = randomUUID() + messageKey = randomUUID(), ) => Effect.gen(function* insertMessageEffect() { const event = Option.getOrThrow( @@ -111,8 +102,8 @@ const insertMessage = ( .returning({ domainEventId: domainEvents.domainEventId, tenantSequenceNo: domainEvents.tenantSequenceNo, - }))[0] - ) + }))[0], + ), ); expect(event).toBeDefined(); const message = Option.getOrThrow( @@ -126,17 +117,15 @@ const insertMessage = ( tenantId, topic, }) - .returning({ messageId: outboxMessages.outboxMessageId }))[0] - ) + .returning({ messageId: outboxMessages.outboxMessageId }))[0], + ), ); expect(message).toBeDefined(); return { ...event, ...message }; }); const cleanupTenant = (database: CoreDatabaseExecutor, tenantId: string) => Effect.gen(function* cleanupTenantEffect() { - yield* database - .delete(workerCheckpoints) - .where(eq(workerCheckpoints.tenantId, tenantId)); + yield* database.delete(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)); const messageRows = yield* database .select({ messageId: outboxMessages.outboxMessageId }) .from(outboxMessages) @@ -149,20 +138,14 @@ const cleanupTenant = (database: CoreDatabaseExecutor, tenantId: string) => yield* database.delete(outboxAttempts).where( inArray( outboxAttempts.outboxDeliveryId, - deliveries.map(({ deliveryId }) => deliveryId) - ) + deliveries.map(({ deliveryId }) => deliveryId), + ), ); - yield* database - .delete(outboxDeliveries) - .where(inArray(outboxDeliveries.outboxMessageId, messageIds)); + yield* database.delete(outboxDeliveries).where(inArray(outboxDeliveries.outboxMessageId, messageIds)); yield* purgeFixtureRows([ - database - .delete(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), + database.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), database.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), - database - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantId)), + database.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), database.delete(tenants).where(eq(tenants.tenantId, tenantId)), ]); }); @@ -171,7 +154,7 @@ const tenantFixture = Effect.gen(function* tenantFixture() { const configuration = yield* loadDatabaseConfig(); const { executor: database } = yield* makeCoreDatabase(configuration); const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => - cleanupTenant(database, id).pipe(Effect.orDie) + cleanupTenant(database, id).pipe(Effect.orDie), ); return { database, tenantId }; }); @@ -183,12 +166,11 @@ const matchedWorker = Effect.fn(function* matchedWorker( workerKey: string, options: Parameters[1] & { readonly messages?: number; - } = {} + } = {}, ) { yield* activateConsumer(database, tenantId); - const messages = yield* Effect.forEach( - Array.from({ length: options.messages ?? 1 }), - () => insertMessage(database, tenantId) + const messages = yield* Effect.forEach(Array.from({ length: options.messages ?? 1 }), () => + insertMessage(database, tenantId), ); const registration = makeWorker(workerKey, options); const repository = makeOutboxRepository(database); @@ -197,430 +179,257 @@ const matchedWorker = Effect.fn(function* matchedWorker( return { messages, now, registration, repository }; }); -it.live( - 'matches zero, one, or multiple exact workers once without historical backfill', - () => - Effect.gen(function* matchesZeroOneOrMultiple() { - const { database, tenantId } = yield* tenantFixture; - yield* insertMessage(database, tenantId); - yield* insertMessage(database, tenantId, 'producer.unmatched'); - const repository = makeOutboxRepository(database); - const workers = [ - makeWorker('consumer.alpha'), - makeWorker('consumer.beta'), - ]; - const firstMatch = yield* repository.matchUnmatched( - workers.map(subscriptionOf), - dateAt('2026-08-03T10:00:00Z') - ); - expect( - firstMatch.deliveriesCreated, - `Initial matcher batch processed ${firstMatch.messagesMatched} unmatched messages` - ).toBe(2); - expect(firstMatch.messagesMatched >= 2).toBe(true); - const repeatMatch = yield* repository.matchUnmatched( - workers.map(subscriptionOf), - dateAt('2026-08-03T10:01:00Z') - ); - expect(repeatMatch.deliveriesCreated).toBe(0); - const lateWorkerMatch = yield* repository.matchUnmatched( - [...workers, makeWorker('consumer.late')].map(subscriptionOf), - dateAt('2026-08-03T10:02:00Z') - ); - expect(lateWorkerMatch.deliveriesCreated).toBe(0); - const deliveries = yield* database - .select() - .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId) - ) - .where(eq(outboxMessages.tenantId, tenantId)); - expect(deliveries.length).toBe(2); - expect( - deliveries.map((row) => row.outbox_deliveries.workerKey).toSorted() - ).toEqual(['consumer.alpha', 'consumer.beta']); - const messages = yield* database - .select({ matchedAt: outboxMessages.matchedAt }) - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)); - expect(messages.every(({ matchedAt }) => matchedAt !== null)).toBe(true); - }) +it.live('matches zero, one, or multiple exact workers once without historical backfill', () => + Effect.gen(function* matchesZeroOneOrMultiple() { + const { database, tenantId } = yield* tenantFixture; + yield* insertMessage(database, tenantId); + yield* insertMessage(database, tenantId, 'producer.unmatched'); + const repository = makeOutboxRepository(database); + const workers = [makeWorker('consumer.alpha'), makeWorker('consumer.beta')]; + const firstMatch = yield* repository.matchUnmatched(workers.map(subscriptionOf), dateAt('2026-08-03T10:00:00Z')); + expect( + firstMatch.deliveriesCreated, + `Initial matcher batch processed ${firstMatch.messagesMatched} unmatched messages`, + ).toBe(2); + expect(firstMatch.messagesMatched >= 2).toBe(true); + const repeatMatch = yield* repository.matchUnmatched(workers.map(subscriptionOf), dateAt('2026-08-03T10:01:00Z')); + expect(repeatMatch.deliveriesCreated).toBe(0); + const lateWorkerMatch = yield* repository.matchUnmatched( + [...workers, makeWorker('consumer.late')].map(subscriptionOf), + dateAt('2026-08-03T10:02:00Z'), + ); + expect(lateWorkerMatch.deliveriesCreated).toBe(0); + const deliveries = yield* database + .select() + .from(outboxDeliveries) + .innerJoin(outboxMessages, eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId)) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(deliveries.length).toBe(2); + expect(deliveries.map((row) => row.outbox_deliveries.workerKey).toSorted()).toEqual([ + 'consumer.alpha', + 'consumer.beta', + ]); + const messages = yield* database + .select({ matchedAt: outboxMessages.matchedAt }) + .from(outboxMessages) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(messages.every(({ matchedAt }) => matchedAt !== null)).toBe(true); + }), ); -it.live( - 'matches the complete subscription catalog before owner-local processes claim work', - () => - Effect.gen(function* matchesTheCompleteSubscriptionCatalog() { - const { database, tenantId } = yield* tenantFixture; - yield* activateConsumer(database, tenantId); - yield* database.insert(tenantModuleStates).values({ - moduleKey: 'reporting', - state: 'active', - tenantId, - }); - yield* insertMessage(database, tenantId); - const consumerWorker = makeWorker('consumer.local'); - const reportingWorker = makeWorker('reporting.local', { - consumerModuleKey: 'reporting', - }); - const repository = makeOutboxRepository(database); - const subscriptions = [consumerWorker, reportingWorker].map( - subscriptionOf - ); - const matched = yield* repository.matchUnmatched( - subscriptions, - dateAt('2026-08-03T10:00:00Z') - ); - expect( - matched.deliveriesCreated, - `Catalog matcher batch processed ${matched.messagesMatched} unmatched messages` - ).toBe(2); - const claimAt = yield* DateTime.nowAsDate; - const consumerClaim = Option.getOrNull( - yield* repository.claimNext( - [consumerWorker], - 'consumer-process', - claimAt - ) - ); - const reportingClaim = Option.getOrNull( - yield* repository.claimNext( - [reportingWorker], - 'reporting-process', - claimAt - ) - ); - expect(consumerClaim?.workerKey).toBe('consumer.local'); - expect(reportingClaim?.workerKey).toBe('reporting.local'); - }) +it.live('matches the complete subscription catalog before owner-local processes claim work', () => + Effect.gen(function* matchesTheCompleteSubscriptionCatalog() { + const { database, tenantId } = yield* tenantFixture; + yield* activateConsumer(database, tenantId); + yield* database.insert(tenantModuleStates).values({ + moduleKey: 'reporting', + state: 'active', + tenantId, + }); + yield* insertMessage(database, tenantId); + const consumerWorker = makeWorker('consumer.local'); + const reportingWorker = makeWorker('reporting.local', { + consumerModuleKey: 'reporting', + }); + const repository = makeOutboxRepository(database); + const subscriptions = [consumerWorker, reportingWorker].map(subscriptionOf); + const matched = yield* repository.matchUnmatched(subscriptions, dateAt('2026-08-03T10:00:00Z')); + expect( + matched.deliveriesCreated, + `Catalog matcher batch processed ${matched.messagesMatched} unmatched messages`, + ).toBe(2); + const claimAt = yield* DateTime.nowAsDate; + const consumerClaim = Option.getOrNull(yield* repository.claimNext([consumerWorker], 'consumer-process', claimAt)); + const reportingClaim = Option.getOrNull( + yield* repository.claimNext([reportingWorker], 'reporting-process', claimAt), + ); + expect(consumerClaim?.workerKey).toBe('consumer.local'); + expect(reportingClaim?.workerKey).toBe('reporting.local'); + }), ); -it.live( - 'gates claims on every non-active consumer state and permits one concurrent live claim', - () => - Effect.gen(function* gatesClaimsOnEveryNonactive() { - const { database, tenantId } = yield* tenantFixture; - yield* insertMessage(database, tenantId); - const registration = makeWorker('consumer.module-gated'); - const repository = makeOutboxRepository(database); - const matched = yield* repository.matchUnmatched( - [subscriptionOf(registration)], - dateAt('2026-08-03T11:00:00Z') - ); - expect( - matched.deliveriesCreated, - `Module-gated matcher batch processed ${matched.messagesMatched} unmatched messages` - ).toBe(1); - const claimAt = advanceDate(yield* DateTime.nowAsDate, 1000); - expect( - Option.getOrNull( - yield* repository.claimNext([registration], 'runtime-a', claimAt) - ) - ).toBe(null); - yield* activateConsumer(database, tenantId, 'inactive'); - yield* pipe( - [ - 'inactive', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ] as const, - Effect.forEach((state) => - Effect.gen(function* checksInactiveState() { - yield* database - .update(tenantModuleStates) - .set({ state }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, 'consumer') - ) - ); - expect( - Option.getOrNull( - yield* repository.claimNext( - [registration], - `runtime-${state}`, - claimAt - ) - ) - ).toBe(null); - }) - ) - ); - yield* database - .update(tenantModuleStates) - .set({ state: 'active' }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, 'consumer') - ) - ); - const claimOptions = yield* Effect.all( - [ - repository.claimNext([registration], 'runtime-a', claimAt), - repository.claimNext([registration], 'runtime-b', claimAt), - ], - { concurrency: 'unbounded' } - ); - const claims = claimOptions.map(Option.getOrNull); - expect(claims.filter((candidate) => candidate !== null).length).toBe(1); - const claimed = Option.getOrThrow( - Option.fromNullishOr(claims.find((candidate) => candidate !== null)) - ); - expect(claimed).toBeDefined(); - const attempt = Option.getOrThrow( - Option.fromNullishOr( - (yield* database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, claimed.deliveryId)))[0] - ) - ); - expect(attempt).toBeDefined(); - expect(attempt.finishedAt).toBe(null); - }) +it.live('gates claims on every non-active consumer state and permits one concurrent live claim', () => + Effect.gen(function* gatesClaimsOnEveryNonactive() { + const { database, tenantId } = yield* tenantFixture; + yield* insertMessage(database, tenantId); + const registration = makeWorker('consumer.module-gated'); + const repository = makeOutboxRepository(database); + const matched = yield* repository.matchUnmatched([subscriptionOf(registration)], dateAt('2026-08-03T11:00:00Z')); + expect( + matched.deliveriesCreated, + `Module-gated matcher batch processed ${matched.messagesMatched} unmatched messages`, + ).toBe(1); + const claimAt = advanceDate(yield* DateTime.nowAsDate, 1000); + expect(Option.getOrNull(yield* repository.claimNext([registration], 'runtime-a', claimAt))).toBe(null); + yield* activateConsumer(database, tenantId, 'inactive'); + yield* pipe( + ['inactive', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'] as const, + Effect.forEach((state) => + Effect.gen(function* checksInactiveState() { + yield* database + .update(tenantModuleStates) + .set({ state }) + .where(and(eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, 'consumer'))); + expect(Option.getOrNull(yield* repository.claimNext([registration], `runtime-${state}`, claimAt))).toBe(null); + }), + ), + ); + yield* database + .update(tenantModuleStates) + .set({ state: 'active' }) + .where(and(eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, 'consumer'))); + const claimOptions = yield* Effect.all( + [ + repository.claimNext([registration], 'runtime-a', claimAt), + repository.claimNext([registration], 'runtime-b', claimAt), + ], + { concurrency: 'unbounded' }, + ); + const claims = claimOptions.map(Option.getOrNull); + expect(claims.filter((candidate) => candidate !== null).length).toBe(1); + const claimed = Option.getOrThrow(Option.fromNullishOr(claims.find((candidate) => candidate !== null))); + expect(claimed).toBeDefined(); + const attempt = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, claimed.deliveryId)))[0], + ), + ); + expect(attempt).toBeDefined(); + expect(attempt.finishedAt).toBe(null); + }), ); -it.live( - 'reclaims only expired leases, abandons the old attempt, and rejects stale finalization', - () => - Effect.gen(function* reclaimsOnlyExpiredLeasesAbandons() { - const { database, tenantId } = yield* tenantFixture; - const { - now: started, - registration, - repository, - } = yield* matchedWorker(database, tenantId, 'consumer.lease-proof'); - const first = Option.getOrThrow( - yield* repository.claimNext([registration], 'runtime-a', started) - ); - expect(first).toBeDefined(); - expect( - Option.getOrNull( - yield* repository.claimNext( - [registration], - 'runtime-b', - advanceDate(started, 999) - ) - ) - ).toBe(null); - const second = Option.getOrThrow( - yield* repository.claimNext( - [registration], - 'runtime-b', - advanceDate(started, 1001) - ) - ); - expect(second).toBeDefined(); - expect(second.claimId).not.toBe(first.claimId); - expect( - Schema.is(OutboxClaimLostError)( - yield* Effect.flip( - repository.complete(first, advanceDate(started, 1002)) - ) - ) - ).toBe(true); - const attempts = yield* database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, first.deliveryId)) - .orderBy(asc(outboxAttempts.startedAt)); - expect(attempts.length).toBe(2); - expect(attempts[0]?.errorMessage).toBe( - 'Outbox Worker lease expired before completion' - ); - expect(Option.isSome(Option.fromNullishOr(attempts[0]?.finishedAt))).toBe( - true - ); - expect(attempts[1]?.finishedAt).toBe(null); - }) +it.live('reclaims only expired leases, abandons the old attempt, and rejects stale finalization', () => + Effect.gen(function* reclaimsOnlyExpiredLeasesAbandons() { + const { database, tenantId } = yield* tenantFixture; + const { now: started, registration, repository } = yield* matchedWorker(database, tenantId, 'consumer.lease-proof'); + const first = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-a', started)); + expect(first).toBeDefined(); + expect(Option.getOrNull(yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 999)))).toBe( + null, + ); + const second = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 1001)), + ); + expect(second).toBeDefined(); + expect(second.claimId).not.toBe(first.claimId); + expect( + Schema.is(OutboxClaimLostError)(yield* Effect.flip(repository.complete(first, advanceDate(started, 1002)))), + ).toBe(true); + const attempts = yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, first.deliveryId)) + .orderBy(asc(outboxAttempts.startedAt)); + expect(attempts.length).toBe(2); + expect(attempts[0]?.errorMessage).toBe('Outbox Worker lease expired before completion'); + expect(Option.isSome(Option.fromNullishOr(attempts[0]?.finishedAt))).toBe(true); + expect(attempts[1]?.finishedAt).toBe(null); + }), ); -it.live( - 'finishes an abandoned final attempt before dead-lettering its expired delivery', - () => - Effect.gen(function* finishesAnAbandonedFinalAttempt() { - const { database, tenantId } = yield* tenantFixture; - const { - now: started, - registration, - repository, - } = yield* matchedWorker(database, tenantId, 'consumer.final-lease', { - maxAttempts: 1, - }); - const claim = Option.getOrThrow( - yield* repository.claimNext([registration], 'runtime-a', started) - ); - expect(claim).toBeDefined(); - expect( - Option.getOrNull( - yield* repository.claimNext( - [registration], - 'runtime-b', - advanceDate(started, 1001) - ) - ) - ).toBe(null); - const [delivery] = yield* database - .select() - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId)); - const attempt = Option.getOrThrow( - Option.fromNullishOr( - (yield* database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, claim.deliveryId)))[0] - ) - ); - expect(delivery?.status).toBe('dead'); - expect(attempt?.errorMessage).toBe( - 'Outbox Worker lease expired before completion' - ); - expect(Option.isSome(Option.fromNullishOr(attempt?.finishedAt))).toBe( - true - ); - }) +it.live('finishes an abandoned final attempt before dead-lettering its expired delivery', () => + Effect.gen(function* finishesAnAbandonedFinalAttempt() { + const { database, tenantId } = yield* tenantFixture; + const { + now: started, + registration, + repository, + } = yield* matchedWorker(database, tenantId, 'consumer.final-lease', { + maxAttempts: 1, + }); + const claim = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-a', started)); + expect(claim).toBeDefined(); + expect(Option.getOrNull(yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 1001)))).toBe( + null, + ); + const [delivery] = yield* database + .select() + .from(outboxDeliveries) + .where(eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId)); + const attempt = Option.getOrThrow( + Option.fromNullishOr( + (yield* database.select().from(outboxAttempts).where(eq(outboxAttempts.outboxDeliveryId, claim.deliveryId)))[0], + ), + ); + expect(delivery?.status).toBe('dead'); + expect(attempt?.errorMessage).toBe('Outbox Worker lease expired before completion'); + expect(Option.isSome(Option.fromNullishOr(attempt?.finishedAt))).toBe(true); + }), ); -it.live( - 'finalizes success atomically and advances only through contiguous done deliveries', - () => - Effect.gen(function* finalizesSuccessAtomicallyAndAdvances() { - const { database, tenantId } = yield* tenantFixture; - const { messages, now, registration, repository } = yield* matchedWorker( - database, - tenantId, - 'consumer.checkpoint-proof', - { messages: 2 } - ); - const firstMessage = Option.getOrThrow(Option.fromNullishOr(messages[0])); - const secondMessage = Option.getOrThrow( - Option.fromNullishOr(messages[1]) - ); - const first = Option.getOrThrow( - yield* repository.claimNext([registration], 'runtime-a', now) - ); - const second = Option.getOrThrow( - yield* repository.claimNext([registration], 'runtime-b', now) - ); - expect(first).toBeDefined(); - expect(second).toBeDefined(); - yield* repository.complete(second, advanceDate(now, 1)); - expect( - yield* database - .select() - .from(workerCheckpoints) - .where(eq(workerCheckpoints.tenantId, tenantId)) - ).toEqual([]); - yield* repository.complete(first, advanceDate(now, 2)); - const checkpoint = Option.getOrThrow( - Option.fromNullishOr( - (yield* database - .select() - .from(workerCheckpoints) - .where(eq(workerCheckpoints.tenantId, tenantId)))[0] - ) - ); - expect(checkpoint).toBeDefined(); - expect(checkpoint.consumerName).toBe(registration.descriptor.workerKey); - expect(checkpoint.streamKey).toBe('producer:producer.message-created'); - expect(checkpoint.lastTenantSequenceNo).toBe( - secondMessage.tenantSequenceNo - ); - expect( - Option.getOrThrow( - Option.fromNullishOr(checkpoint.lastTenantSequenceNo) - ) > firstMessage.tenantSequenceNo - ).toBe(true); - const deliveries = yield* database - .select() - .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId) - ) - .where(eq(outboxMessages.tenantId, tenantId)); - expect( - deliveries.every((row) => row.outbox_deliveries.status === 'done') - ).toBe(true); - expect( - deliveries.every((row) => row.outbox_deliveries.claimedBy === null) - ).toBe(true); - }) +it.live('finalizes success atomically and advances only through contiguous done deliveries', () => + Effect.gen(function* finalizesSuccessAtomicallyAndAdvances() { + const { database, tenantId } = yield* tenantFixture; + const { messages, now, registration, repository } = yield* matchedWorker( + database, + tenantId, + 'consumer.checkpoint-proof', + { messages: 2 }, + ); + const firstMessage = Option.getOrThrow(Option.fromNullishOr(messages[0])); + const secondMessage = Option.getOrThrow(Option.fromNullishOr(messages[1])); + const first = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-a', now)); + const second = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-b', now)); + expect(first).toBeDefined(); + expect(second).toBeDefined(); + yield* repository.complete(second, advanceDate(now, 1)); + expect(yield* database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId))).toEqual( + [], + ); + yield* repository.complete(first, advanceDate(now, 2)); + const checkpoint = Option.getOrThrow( + Option.fromNullishOr( + (yield* database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)))[0], + ), + ); + expect(checkpoint).toBeDefined(); + expect(checkpoint.consumerName).toBe(registration.descriptor.workerKey); + expect(checkpoint.streamKey).toBe('producer:producer.message-created'); + expect(checkpoint.lastTenantSequenceNo).toBe(secondMessage.tenantSequenceNo); + expect( + Option.getOrThrow(Option.fromNullishOr(checkpoint.lastTenantSequenceNo)) > firstMessage.tenantSequenceNo, + ).toBe(true); + const deliveries = yield* database + .select() + .from(outboxDeliveries) + .innerJoin(outboxMessages, eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId)) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(deliveries.every((row) => row.outbox_deliveries.status === 'done')).toBe(true); + expect(deliveries.every((row) => row.outbox_deliveries.claimedBy === null)).toBe(true); + }), ); -it.live( - 'schedules bounded retry, dead-letters exhaustion, stores safe errors, and never checkpoints failure', - () => - Effect.gen(function* schedulesBoundedRetryDeadlettersExhaustion() { - const { database, tenantId } = yield* tenantFixture; - const { now, registration, repository } = yield* matchedWorker( - database, - tenantId, - 'consumer.retry-proof', - { maxAttempts: 2 } - ); - const first = Option.getOrThrow( - yield* repository.claimNext([registration], 'runtime-a', now) - ); - expect(first).toBeDefined(); - expect( - yield* repository.fail( - first, - ' safe\nretry\tmessage ', - advanceDate(now, 1) - ) - ).toBe('pending'); - expect( - Option.getOrNull( - yield* repository.claimNext( - [registration], - 'runtime-b', - advanceDate(now, 999) - ) - ) - ).toBe(null); - const second = Option.getOrThrow( - yield* repository.claimNext( - [registration], - 'runtime-b', - advanceDate(now, 1001) - ) - ); - expect(second).toBeDefined(); - expect( - yield* repository.fail( - second, - 'terminal safe failure', - advanceDate(now, 1002) - ) - ).toBe('dead'); - const [delivery] = yield* database - .select() - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxDeliveryId, second.deliveryId)); - expect(delivery?.status).toBe('dead'); - expect(delivery?.attemptsCount).toBe(2); - const attempts = yield* database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, second.deliveryId)) - .orderBy(asc(outboxAttempts.startedAt)); - expect(attempts.map(({ errorMessage }) => errorMessage)).toEqual([ - 'safe retry message', - 'terminal safe failure', - ]); - expect( - yield* database - .select() - .from(workerCheckpoints) - .where(eq(workerCheckpoints.tenantId, tenantId)) - ).toEqual([]); - }) +it.live('schedules bounded retry, dead-letters exhaustion, stores safe errors, and never checkpoints failure', () => + Effect.gen(function* schedulesBoundedRetryDeadlettersExhaustion() { + const { database, tenantId } = yield* tenantFixture; + const { now, registration, repository } = yield* matchedWorker(database, tenantId, 'consumer.retry-proof', { + maxAttempts: 2, + }); + const first = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-a', now)); + expect(first).toBeDefined(); + expect(yield* repository.fail(first, ' safe\nretry\tmessage ', advanceDate(now, 1))).toBe('pending'); + expect(Option.getOrNull(yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 999)))).toBe( + null, + ); + const second = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 1001))); + expect(second).toBeDefined(); + expect(yield* repository.fail(second, 'terminal safe failure', advanceDate(now, 1002))).toBe('dead'); + const [delivery] = yield* database + .select() + .from(outboxDeliveries) + .where(eq(outboxDeliveries.outboxDeliveryId, second.deliveryId)); + expect(delivery?.status).toBe('dead'); + expect(delivery?.attemptsCount).toBe(2); + const attempts = yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, second.deliveryId)) + .orderBy(asc(outboxAttempts.startedAt)); + expect(attempts.map(({ errorMessage }) => errorMessage)).toEqual(['safe retry message', 'terminal safe failure']); + expect(yield* database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId))).toEqual( + [], + ); + }), ); it('keeps test descriptor arrays compatible with the erased startup registry surface', () => { - const registry: readonly AnyOutboxWorkerRegistration[] = [ - makeWorker('consumer.registry-proof'), - ]; + const registry: readonly AnyOutboxWorkerRegistration[] = [makeWorker('consumer.registry-proof')]; expect(registry[0]?.descriptor.workerKey).toBe('consumer.registry-proof'); }); diff --git a/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts b/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts index 83dd059d3..13a7477e3 100644 --- a/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts +++ b/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts @@ -9,19 +9,19 @@ import { configureDatabasePool } from '../../src/db/pool-configuration.ts'; const rollbackAndRelease = (client: PoolClient) => Effect.tryPromise(() => client.query('rollback')).pipe( Effect.ignore, - Effect.ensuring(Effect.sync(() => client.release())) + Effect.ensuring(Effect.sync(() => client.release())), ); it.live('applies PostgreSQL pool connection and statement deadlines', () => Effect.gen(function* poolDeadlines1() { const databaseConfiguration = yield* loadDatabaseConfig(); - const poolConfiguration = yield* configureDatabasePool( - Redacted.make(databaseConfiguration.connectionString), - { connectionTimeoutMillis: 200, statement_timeout: 120 } - ); + const poolConfiguration = yield* configureDatabasePool(Redacted.make(databaseConfiguration.connectionString), { + connectionTimeoutMillis: 200, + statement_timeout: 120, + }); const acquirePool = Effect.acquireRelease( Effect.sync(() => new Pool({ ...poolConfiguration, max: 1 })), - (resource) => Effect.promise(() => resource.end()).pipe(Effect.orDie) + (resource) => Effect.promise(() => resource.end()).pipe(Effect.orDie), ); const pool = yield* acquirePool; const blocker = yield* acquirePool; @@ -30,73 +30,57 @@ it.live('applies PostgreSQL pool connection and statement deadlines', () => Effect.gen(function* poolDeadlines2() { const client = yield* Effect.acquireRelease( Effect.promise(() => pool.connect()), - (connection) => Effect.sync(() => connection.release()) + (connection) => Effect.sync(() => connection.release()), ); const settings = yield* Effect.promise(() => - client.query<{ statement_timeout: string }>('show statement_timeout') + client.query<{ statement_timeout: string }>('show statement_timeout'), ); expect(settings.rows[0]?.statement_timeout).toBe('120ms'); - const identity = yield* Effect.promise(() => - client.query<{ current_user: string }>('select current_user') - ); + const identity = yield* Effect.promise(() => client.query<{ current_user: string }>('select current_user')); expect(identity.rows[0]?.current_user).toBe(databaseConfiguration.user); - const pidResult = yield* Effect.promise(() => - client.query<{ pid: number }>('select pg_backend_pid() as pid') - ); + const pidResult = yield* Effect.promise(() => client.query<{ pid: number }>('select pg_backend_pid() as pid')); const pid = pidResult.rows[0]?.pid; expect(pid !== undefined).toBe(true); - const cancellation = yield* Effect.flip( - Effect.tryPromise(() => client.query('select pg_sleep(1)')) - ); + const cancellation = yield* Effect.flip(Effect.tryPromise(() => client.query('select pg_sleep(1)'))); expect(cancellation.cause).toMatchObject({ code: '57014' }); const afterCancellation = yield* Effect.promise(() => - client.query<{ ok: number; pid: number }>( - 'select pg_backend_pid() as pid, 1 as ok' - ) + client.query<{ ok: number; pid: number }>('select pg_backend_pid() as pid, 1 as ok'), ); expect(afterCancellation.rows[0]?.pid).toBe(pid); expect(afterCancellation.rows[0]?.ok).toBe(1); - const timeout = yield* Effect.flip( - Effect.tryPromise(() => pool.connect()) - ); + const timeout = yield* Effect.flip(Effect.tryPromise(() => pool.connect())); expect(timeout.cause).toMatchObject({ message: expect.stringMatching(/timeout/iu), }); - }) + }), ); const holder = yield* Effect.acquireRelease( Effect.promise(() => blocker.connect()), - rollbackAndRelease + rollbackAndRelease, ); yield* Effect.promise(() => holder.query('begin')); - yield* Effect.promise(() => - holder.query('select pg_advisory_xact_lock(424242)') - ); + yield* Effect.promise(() => holder.query('select pg_advisory_xact_lock(424242)')); const waiter = yield* Effect.acquireRelease( Effect.promise(() => pool.connect()), - rollbackAndRelease + rollbackAndRelease, ); yield* Effect.promise(() => waiter.query('begin')); const lockTimeout = yield* Effect.flip( - Effect.tryPromise(() => - waiter.query('select pg_advisory_xact_lock(424242)') - ) + Effect.tryPromise(() => waiter.query('select pg_advisory_xact_lock(424242)')), ); expect(lockTimeout.cause).toMatchObject({ code: '57014' }); yield* Effect.promise(() => waiter.query('rollback')); yield* Effect.promise(() => holder.query('rollback')); yield* Effect.promise(() => waiter.query('begin')); - yield* Effect.promise(() => - waiter.query('select pg_advisory_xact_lock(424242)') - ); + yield* Effect.promise(() => waiter.query('select pg_advisory_xact_lock(424242)')); yield* Effect.promise(() => waiter.query('rollback')); - }) + }), ); diff --git a/app/packages/core-runtime/tests/integration/principal-management.test.ts b/app/packages/core-runtime/tests/integration/principal-management.test.ts index 276301ac3..70bee9ea8 100644 --- a/app/packages/core-runtime/tests/integration/principal-management.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-management.test.ts @@ -13,152 +13,116 @@ import { setApiKeyBindingStatus, } from '../../src/auth/principal-management.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; -import { - coreRelations, - principalAuthBindings, - principals, - tenants, -} from '../../src/db/schema.ts'; +import { coreRelations, principalAuthBindings, principals, tenants } from '../../src/db/schema.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; -it.live( - 'persists managed key lifecycle without credential material and enforces global key cardinality', - () => - Effect.gen(function* principalManagement1() { - const tenantId = randomUUID(); - const providerKeyId = `better-auth-principal-management-${randomUUID()}`; - const configuration = yield* loadDatabaseConfig(); - const pool = yield* Effect.acquireRelease( - Effect.sync( - () => new Pool({ connectionString: configuration.connectionString }) - ), - (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie) - ); - const database = yield* makeTestDatabaseFromPool(pool, coreRelations); - const cleanup = purgeFixtureRows([ - database - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, providerKeyId)), - database.delete(principals).where(eq(principals.tenantId, tenantId)), - database.delete(tenants).where(eq(tenants.tenantId, tenantId)), - ]); +it.live('persists managed key lifecycle without credential material and enforces global key cardinality', () => + Effect.gen(function* principalManagement1() { + const tenantId = randomUUID(); + const providerKeyId = `better-auth-principal-management-${randomUUID()}`; + const configuration = yield* loadDatabaseConfig(); + const pool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + const database = yield* makeTestDatabaseFromPool(pool, coreRelations); + const cleanup = purgeFixtureRows([ + database.delete(principalAuthBindings).where(eq(principalAuthBindings.providerSubjectId, providerKeyId)), + database.delete(principals).where(eq(principals.tenantId, tenantId)), + database.delete(tenants).where(eq(tenants.tenantId, tenantId)), + ]); - yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); - yield* database.insert(tenants).values({ - defaultLocale: 'en', - name: 'Principal management integration', - slug: `principal-management-${tenantId}`, - status: 'active', + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); + yield* database.insert(tenants).values({ + defaultLocale: 'en', + name: 'Principal management integration', + slug: `principal-management-${tenantId}`, + status: 'active', + tenantId, + }); + const first = yield* database.transaction((transaction) => + createNonHumanPrincipal({ + displayName: 'Managed integration', + kind: 'integration', tenantId, - }); - const first = yield* database.transaction((transaction) => - createNonHumanPrincipal({ - displayName: 'Managed integration', - kind: 'integration', - tenantId, - }).pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ); - const second = yield* database.transaction((transaction) => - createNonHumanPrincipal({ - displayName: 'Managed service', - kind: 'service', - tenantId, - }).pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ); - const binding = yield* database.transaction((transaction) => + }).pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepositoryFromTransaction(transaction)), + ), + ); + const second = yield* database.transaction((transaction) => + createNonHumanPrincipal({ + displayName: 'Managed service', + kind: 'service', + tenantId, + }).pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepositoryFromTransaction(transaction)), + ), + ); + const binding = yield* database.transaction((transaction) => + bindApiKey({ + managed: true, + principalId: first.principalId, + providerSubjectId: providerKeyId, + tenantId, + }).pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepositoryFromTransaction(transaction)), + ), + ); + const duplicate = yield* database.transaction((transaction) => + Effect.flip( bindApiKey({ managed: true, - principalId: first.principalId, + principalId: second.principalId, providerSubjectId: providerKeyId, tenantId, }).pipe( Effect.provideService( PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ); - const duplicate = yield* database.transaction((transaction) => - Effect.flip( - bindApiKey({ - managed: true, - principalId: second.principalId, - providerSubjectId: providerKeyId, - tenantId, - }).pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ) - ); - expect( - Predicate.isTagged(duplicate, 'IdentityLifecycleConflictError') - ).toBe(true); - - const missingReason = yield* database.transaction((transaction) => - Effect.flip( - setApiKeyBindingStatus({ - authBindingId: binding.authBindingId, - expectedStatus: 'active', - managed: true, - newStatus: 'revoked', - principalId: first.principalId, - tenantId, - }).pipe( - Effect.provideService( - PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ) - ); - expect( - Predicate.isTagged(missingReason, 'IdentityTargetInvalidError') - ).toBe(true); + principalManagementRepositoryFromTransaction(transaction), + ), + ), + ), + ); + expect(Predicate.isTagged(duplicate, 'IdentityLifecycleConflictError')).toBe(true); - yield* database.transaction((transaction) => + const missingReason = yield* database.transaction((transaction) => + Effect.flip( setApiKeyBindingStatus({ authBindingId: binding.authBindingId, expectedStatus: 'active', managed: true, newStatus: 'revoked', principalId: first.principalId, - reason: 'Integration lifecycle proof', tenantId, }).pipe( Effect.provideService( PrincipalManagementRepository, - principalManagementRepositoryFromTransaction(transaction) - ) - ) - ); - const [stored] = yield* database - .select() - .from(principalAuthBindings) - .where( - eq( - principalAuthBindings.principalAuthBindingId, - binding.authBindingId - ) - ); - expect(stored?.status).toBe('revoked'); - expect( - (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - stored - )).includes('secret') - ).toBe(false); - }) + principalManagementRepositoryFromTransaction(transaction), + ), + ), + ), + ); + expect(Predicate.isTagged(missingReason, 'IdentityTargetInvalidError')).toBe(true); + + yield* database.transaction((transaction) => + setApiKeyBindingStatus({ + authBindingId: binding.authBindingId, + expectedStatus: 'active', + managed: true, + newStatus: 'revoked', + principalId: first.principalId, + reason: 'Integration lifecycle proof', + tenantId, + }).pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepositoryFromTransaction(transaction)), + ), + ); + const [stored] = yield* database + .select() + .from(principalAuthBindings) + .where(eq(principalAuthBindings.principalAuthBindingId, binding.authBindingId)); + expect(stored?.status).toBe('revoked'); + expect((yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(stored)).includes('secret')).toBe(false); + }), ); diff --git a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts index c47ed51ba..315e9d9e1 100644 --- a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts @@ -5,11 +5,7 @@ import { expect, it } from 'effect-rstest'; import { makePrincipalResolver } from '../../src/auth/principal-resolver.ts'; import { makeCoreDatabase } from '../../src/db/client.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; -import { - principalAuthBindings, - principals, - tenants, -} from '../../src/db/schema.ts'; +import { principalAuthBindings, principals, tenants } from '../../src/db/schema.ts'; const tenantOne = '10000000-0000-4000-8000-000000000001'; const tenantTwo = '10000000-0000-4000-8000-000000000002'; @@ -17,160 +13,112 @@ const principalOne = '20000000-0000-4000-8000-000000000001'; const principalTwo = '20000000-0000-4000-8000-000000000002'; const subject = 'better-auth-integration-subject'; -it.live( - 'lists and selects multiple tenant-scoped principals and fails closed after access changes', - () => - Effect.gen(function* principalResolverIntegration() { - const configuration = yield* loadDatabaseConfig(); - const { executor: database } = yield* makeCoreDatabase(configuration); - const resolver = makePrincipalResolver({ executor: database }); - const cleanup = Effect.gen(function* cleanPrincipalResolverFixtures() { - yield* database - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, subject)); - yield* database - .delete(principals) - .where( - and( - eq(principals.principalId, principalOne), - eq(principals.tenantId, tenantOne) - ) - ); - yield* database - .delete(principals) - .where( - and( - eq(principals.principalId, principalTwo), - eq(principals.tenantId, tenantTwo) - ) - ); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); - }); +it.live('lists and selects multiple tenant-scoped principals and fails closed after access changes', () => + Effect.gen(function* principalResolverIntegration() { + const configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const resolver = makePrincipalResolver({ executor: database }); + const cleanup = Effect.gen(function* cleanPrincipalResolverFixtures() { + yield* database.delete(principalAuthBindings).where(eq(principalAuthBindings.providerSubjectId, subject)); + yield* database + .delete(principals) + .where(and(eq(principals.principalId, principalOne), eq(principals.tenantId, tenantOne))); + yield* database + .delete(principals) + .where(and(eq(principals.principalId, principalTwo), eq(principals.tenantId, tenantTwo))); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); + }); - yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); - yield* database.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Resolver tenant one', - slug: 'resolver-tenant-one', - status: 'active', - tenantId: tenantOne, - }, - { - defaultLocale: 'en', - name: 'Resolver tenant two', - slug: 'resolver-tenant-two', - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.insert(principals).values([ - { - displayName: 'Resolver principal one', - kind: 'human', - principalId: principalOne, - status: 'active', - tenantId: tenantOne, - }, - { - displayName: 'Resolver principal two', - kind: 'human', - principalId: principalTwo, - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.insert(principalAuthBindings).values([ - { - principalId: principalOne, - provider: 'better_auth', - providerSubjectId: subject, - status: 'active', - subjectType: 'user', - tenantId: tenantOne, - }, - { - principalId: principalTwo, - provider: 'better_auth', - providerSubjectId: subject, - status: 'active', - subjectType: 'user', - tenantId: tenantTwo, - }, - ]); + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); + yield* database.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Resolver tenant one', + slug: 'resolver-tenant-one', + status: 'active', + tenantId: tenantOne, + }, + { + defaultLocale: 'en', + name: 'Resolver tenant two', + slug: 'resolver-tenant-two', + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.insert(principals).values([ + { + displayName: 'Resolver principal one', + kind: 'human', + principalId: principalOne, + status: 'active', + tenantId: tenantOne, + }, + { + displayName: 'Resolver principal two', + kind: 'human', + principalId: principalTwo, + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.insert(principalAuthBindings).values([ + { + principalId: principalOne, + provider: 'better_auth', + providerSubjectId: subject, + status: 'active', + subjectType: 'user', + tenantId: tenantOne, + }, + { + principalId: principalTwo, + provider: 'better_auth', + providerSubjectId: subject, + status: 'active', + subjectType: 'user', + tenantId: tenantTwo, + }, + ]); - expect(yield* resolver.listAvailableTenants(subject)).toEqual([ - { name: 'Resolver tenant one', tenantId: tenantOne }, - { name: 'Resolver tenant two', tenantId: tenantTwo }, - ]); - const resolvedOne = yield* resolver.resolveBetterAuthUserForTenant( - subject, - tenantOne - ); - const resolvedTwo = yield* resolver.resolveBetterAuthUserForTenant( - subject, - tenantTwo - ); - expect(resolvedOne.principalId).toBe(principalOne); - expect(resolvedTwo.principalId).toBe(principalTwo); - const foreignResolution = yield* Effect.flip( - resolver.resolveBetterAuthUserForTenant( - 'foreign-better-auth-subject', - tenantOne - ) - ); - expect( - Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError') - ).toBe(true); + expect(yield* resolver.listAvailableTenants(subject)).toEqual([ + { name: 'Resolver tenant one', tenantId: tenantOne }, + { name: 'Resolver tenant two', tenantId: tenantTwo }, + ]); + const resolvedOne = yield* resolver.resolveBetterAuthUserForTenant(subject, tenantOne); + const resolvedTwo = yield* resolver.resolveBetterAuthUserForTenant(subject, tenantTwo); + expect(resolvedOne.principalId).toBe(principalOne); + expect(resolvedTwo.principalId).toBe(principalTwo); + const foreignResolution = yield* Effect.flip( + resolver.resolveBetterAuthUserForTenant('foreign-better-auth-subject', tenantOne), + ); + expect(Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError')).toBe(true); - yield* database - .update(principalAuthBindings) - .set({ - revokedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-09-07T00:00:00.000Z') - ), - status: 'revoked', - }) - .where(eq(principalAuthBindings.tenantId, tenantOne)); - expect(yield* resolver.listAvailableTenants(subject)).toEqual([ - { name: 'Resolver tenant two', tenantId: tenantTwo }, - ]); - const revokedResolution = yield* Effect.flip( - resolver.resolveBetterAuthUserForTenant(subject, tenantOne) - ); - expect( - Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError') - ).toBe(true); + yield* database + .update(principalAuthBindings) + .set({ + revokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-07T00:00:00.000Z')), + status: 'revoked', + }) + .where(eq(principalAuthBindings.tenantId, tenantOne)); + expect(yield* resolver.listAvailableTenants(subject)).toEqual([ + { name: 'Resolver tenant two', tenantId: tenantTwo }, + ]); + const revokedResolution = yield* Effect.flip(resolver.resolveBetterAuthUserForTenant(subject, tenantOne)); + expect(Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError')).toBe(true); - yield* database - .update(principalAuthBindings) - .set({ revokedAt: null, status: 'active' }) - .where(eq(principalAuthBindings.tenantId, tenantOne)); - yield* database - .update(principals) - .set({ status: 'disabled' }) - .where(eq(principals.principalId, principalOne)); - const inactivePrincipal = yield* Effect.flip( - resolver.resolveBetterAuthUserForTenant(subject, tenantOne) - ); - expect( - Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError') - ).toBe(true); + yield* database + .update(principalAuthBindings) + .set({ revokedAt: null, status: 'active' }) + .where(eq(principalAuthBindings.tenantId, tenantOne)); + yield* database.update(principals).set({ status: 'disabled' }).where(eq(principals.principalId, principalOne)); + const inactivePrincipal = yield* Effect.flip(resolver.resolveBetterAuthUserForTenant(subject, tenantOne)); + expect(Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError')).toBe(true); - yield* database - .update(principals) - .set({ status: 'active' }) - .where(eq(principals.principalId, principalOne)); - yield* database - .update(tenants) - .set({ status: 'suspended' }) - .where(eq(tenants.tenantId, tenantOne)); - const inactiveTenant = yield* Effect.flip( - resolver.resolveBetterAuthUserForTenant(subject, tenantOne) - ); - expect(Predicate.isTagged(inactiveTenant, 'TenantInactiveError')).toBe( - true - ); - }) + yield* database.update(principals).set({ status: 'active' }).where(eq(principals.principalId, principalOne)); + yield* database.update(tenants).set({ status: 'suspended' }).where(eq(tenants.tenantId, tenantOne)); + const inactiveTenant = yield* Effect.flip(resolver.resolveBetterAuthUserForTenant(subject, tenantOne)); + expect(Predicate.isTagged(inactiveTenant, 'TenantInactiveError')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/integration/read-runtime.test.ts b/app/packages/core-runtime/tests/integration/read-runtime.test.ts index aa0562333..d647cde5c 100644 --- a/app/packages/core-runtime/tests/integration/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/read-runtime.test.ts @@ -4,165 +4,135 @@ import { getTableConfig } from 'drizzle-orm/pg-core'; import { Effect, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - makeSystemPrincipalContextResolver, - registerSystemWorkload, -} from '../../src/auth/system-principal-context.ts'; +import { makeSystemPrincipalContextResolver, registerSystemWorkload } from '../../src/auth/system-principal-context.ts'; import { coreRelations, dataAccessEvents } from '../../src/db/schema.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { - makeOperationalScopeRepository, - makeOperationalScopeResolver, -} from '../../src/operations/context.ts'; +import { makeOperationalScopeRepository, makeOperationalScopeResolver } from '../../src/operations/context.ts'; import { defineRead } from '../../src/reads/definition.ts'; import { makeReadRuntime } from '../../src/reads/runtime.ts'; -import { - makeTestDatabaseFromPool, - testDatabasePools, -} from '../support/database.ts'; +import { makeTestDatabaseFromPool, testDatabasePools } from '../support/database.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; it('standalone governed-read evidence permits no Action invocation and requires outcome fields', () => { const config = getTableConfig(dataAccessEvents); - const column = (name: string) => - config.columns.find((candidate) => candidate.name === name); + const column = (name: string) => config.columns.find((candidate) => candidate.name === name); expect(column('action_invocation_id')?.notNull).toBe(false); expect(column('outcome')?.notNull).toBe(true); expect(column('outcome_stage')?.notNull).toBe(true); expect(column('outcome_code')?.notNull).toBe(true); }); -it.live( - 'commits live allowed evidence before releasing a governed read result', - () => - Effect.gen(function* readRuntime1() { - const { admin, runtimePool } = yield* testDatabasePools; - const runtimeDatabase = yield* makeTestDatabaseFromPool( - runtimePool, - coreRelations - ); - const tenantId = randomUUID(); - const principalId = randomUUID(); - const readKey = `core.shell.integration.${randomUUID()}`; - const correlationId = randomUUID(); - const registration = defineRead( - { - accessKind: 'list', - entrypoint: defineSystemModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: readKey, - moduleKey: 'core.shell', - role: 'api', - }), - evidencePolicy: { - captureMode: 'metadata_only', - policyKey: `${readKey}.v1`, +it.live('commits live allowed evidence before releasing a governed read result', () => + Effect.gen(function* readRuntime1() { + const { admin, runtimePool } = yield* testDatabasePools; + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); + const tenantId = randomUUID(); + const principalId = randomUUID(); + const readKey = `core.shell.integration.${randomUUID()}`; + const correlationId = randomUUID(); + const registration = defineRead( + { + accessKind: 'list', + entrypoint: defineSystemModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', }, - inputSchema: Schema.Struct({}), - legalEntityScope: 'forbidden', - owningModuleKey: 'core.shell', - permissionTarget: 'module', - policies: [], - readKey, - resultSchema: Schema.Array(Schema.String), - schemaVersion: '1', + entrypointKey: readKey, + moduleKey: 'core.shell', + role: 'api', + }), + evidencePolicy: { + captureMode: 'metadata_only', + policyKey: `${readKey}.v1`, }, - () => - Effect.succeed({ evidence: { resultCount: 1 }, result: ['visible'] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); + inputSchema: Schema.Struct({}), + legalEntityScope: 'forbidden', + owningModuleKey: 'core.shell', + permissionTarget: 'module', + policies: [], + readKey, + resultSchema: Schema.Array(Schema.String), + schemaVersion: '1', + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: ['visible'] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); - yield* Effect.addFinalizer(() => - Effect.gen(function* readRuntime2() { - yield* Effect.promise(() => - admin.query( - 'delete from core.data_access_events where tenant_id = $1', - [tenantId] - ) - ); - yield* Effect.promise(() => - admin.query('delete from core.principals where tenant_id = $1', [ - tenantId, - ]) - ); - yield* Effect.promise(() => - admin.query('delete from core.tenants where tenant_id = $1', [ - tenantId, - ]) - ); - }).pipe(Effect.orDie) - ); + yield* Effect.addFinalizer(() => + Effect.gen(function* readRuntime2() { + yield* Effect.promise(() => + admin.query('delete from core.data_access_events where tenant_id = $1', [tenantId]), + ); + yield* Effect.promise(() => admin.query('delete from core.principals where tenant_id = $1', [tenantId])); + yield* Effect.promise(() => admin.query('delete from core.tenants where tenant_id = $1', [tenantId])); + }).pipe(Effect.orDie), + ); - yield* Effect.promise(() => - admin.query( - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Read runtime tenant', 'active', 'en')`, - [tenantId, `read-runtime-${tenantId}`] - ) - ); - yield* Effect.promise(() => - admin.query( - `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $2, 'system', 'Read runtime principal', 'active')`, - [principalId, tenantId] - ) - ); - const contextAccess = { - legalEntities: () => Effect.succeed([]), - modules: () => Effect.succeed([]), - resources: () => Effect.succeed([]), - tenants: () => Effect.succeed([]), - }; - const principal = yield* makeSystemPrincipalContextResolver({ - executor: runtimeDatabase, - }).resolve({ - principalId, - registration: registerSystemWorkload({ - jobKey: 'read-runtime-integration', - }), - runReference: readKey, - tenantId, - }); - const runtime = makeReadRuntime( - { executor: runtimeDatabase }, - openModuleEntrypointGateway, - makeOperationalScopeResolver( - makeOperationalScopeRepository({ executor: runtimeDatabase }), - contextAccess - ), - contextAccess - ); - expect( - yield* runtime.runRead({ - input: {}, - principal, - registration, - transport: { correlationId }, - }) - ).toEqual(['visible']); - const evidence = yield* Effect.promise(() => - admin.query<{ - action_invocation_id: null; - outcome: string; - outcome_code: string; - query_hash: null; - result_count: number; - }>( - `select action_invocation_id, outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and evidence_policy_key = $2`, - [tenantId, `${readKey}.v1`] - ) - ); - expect(evidence.rows).toEqual([ - { - action_invocation_id: null, - outcome: 'allowed', - outcome_code: 'read_allowed', - query_hash: null, - result_count: 1, - }, - ]); - }) + yield* Effect.promise(() => + admin.query( + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Read runtime tenant', 'active', 'en')`, + [tenantId, `read-runtime-${tenantId}`], + ), + ); + yield* Effect.promise(() => + admin.query( + `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $2, 'system', 'Read runtime principal', 'active')`, + [principalId, tenantId], + ), + ); + const contextAccess = { + legalEntities: () => Effect.succeed([]), + modules: () => Effect.succeed([]), + resources: () => Effect.succeed([]), + tenants: () => Effect.succeed([]), + }; + const principal = yield* makeSystemPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolve({ + principalId, + registration: registerSystemWorkload({ + jobKey: 'read-runtime-integration', + }), + runReference: readKey, + tenantId, + }); + const runtime = makeReadRuntime( + { executor: runtimeDatabase }, + openModuleEntrypointGateway, + makeOperationalScopeResolver(makeOperationalScopeRepository({ executor: runtimeDatabase }), contextAccess), + contextAccess, + ); + expect( + yield* runtime.runRead({ + input: {}, + principal, + registration, + transport: { correlationId }, + }), + ).toEqual(['visible']); + const evidence = yield* Effect.promise(() => + admin.query<{ + action_invocation_id: null; + outcome: string; + outcome_code: string; + query_hash: null; + result_count: number; + }>( + `select action_invocation_id, outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and evidence_policy_key = $2`, + [tenantId, `${readKey}.v1`], + ), + ); + expect(evidence.rows).toEqual([ + { + action_invocation_id: null, + outcome: 'allowed', + outcome_code: 'read_allowed', + query_hash: null, + result_count: 1, + }, + ]); + }), ); diff --git a/app/packages/core-runtime/tests/integration/search-persistence.test.ts b/app/packages/core-runtime/tests/integration/search-persistence.test.ts index 28f58fdc6..c38321d49 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -6,315 +6,252 @@ import type { Pool, QueryResult, QueryResultRow } from 'pg'; import { coreRelations } from '../../src/db/schema.ts'; import { makePostgresCoreSearchProjectionStore } from '../../src/search/persistence.ts'; -import { - CoreSearchProjectionStore, - createCoreSearchQueryRuntime, -} from '../../src/search/projection.ts'; -import { - makeTestDatabaseFromPool, - testDatabasePools, -} from '../support/database.ts'; +import { CoreSearchProjectionStore, createCoreSearchQueryRuntime } from '../../src/search/projection.ts'; +import { makeTestDatabaseFromPool, testDatabasePools } from '../support/database.ts'; const queryEffect = ( client: Pool, statement: string, - parameters?: readonly unknown[] + parameters?: readonly unknown[], ): Effect.Effect> => - Effect.suspend(() => - Effect.promise( - Fn.constant(client.query(statement, [...(parameters ?? [])])) - ) - ); + Effect.suspend(() => Effect.promise(Fn.constant(client.query(statement, [...(parameters ?? [])])))); const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -it.live( - 'durably rebuilds tenant projections with tombstones and selected-Legal-Entity filtering', - () => - Effect.gen(function* searchPersistenceIntegration() { - const { admin, runtimePool } = yield* testDatabasePools; - const tenantId = randomUUID(); - const otherTenantId = randomUUID(); - const legalEntityId = randomUUID(); - const otherLegalEntityId = randomUUID(); - const partyId = randomUUID(); - const removedPartyId = randomUUID(); - const counterpartyId = randomUUID(); - const otherCounterpartyId = randomUUID(); - const aliasRef = { +it.live('durably rebuilds tenant projections with tombstones and selected-Legal-Entity filtering', () => + Effect.gen(function* searchPersistenceIntegration() { + const { admin, runtimePool } = yield* testDatabasePools; + const tenantId = randomUUID(); + const otherTenantId = randomUUID(); + const legalEntityId = randomUUID(); + const otherLegalEntityId = randomUUID(); + const partyId = randomUUID(); + const removedPartyId = randomUUID(); + const counterpartyId = randomUUID(); + const otherCounterpartyId = randomUUID(); + const aliasRef = { + moduleId: 'party.registry', + resourceId: randomUUID(), + resourceType: 'party.registry.party', + tenantId, + }; + const store = makePostgresCoreSearchProjectionStore({ + executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), + }); + const search = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + const partyDocument = (resourceId: string, projectionVersion: string, title: string) => ({ + aliases: [ + { + kind: 'resource', + ref: aliasRef, + searchableText: ['Former Acme'], + temporalSearchableText: [ + { + validFrom: '2026-01-01T00:00:00Z', + validTo: '2026-02-01T00:00:00Z', + value: 'alias-private@example.test', + }, + ], + }, + ], + archived: false, + facets: [], + metadata: [], + projectionVersion, + ref: { moduleId: 'party.registry', - resourceId: randomUUID(), + resourceId, resourceType: 'party.registry.party', tenantId, - }; - const store = makePostgresCoreSearchProjectionStore({ - executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), - }); - const search = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - const partyDocument = ( - resourceId: string, - projectionVersion: string, - title: string - ) => ({ - aliases: [ - { - kind: 'resource', - ref: aliasRef, - searchableText: ['Former Acme'], - temporalSearchableText: [ - { - validFrom: '2026-01-01T00:00:00Z', - validTo: '2026-02-01T00:00:00Z', - value: 'alias-private@example.test', - }, - ], - }, - ], - archived: false, - facets: [], - metadata: [], - projectionVersion, - ref: { - moduleId: 'party.registry', - resourceId, - resourceType: 'party.registry.party', - tenantId, - }, - searchableText: [title, 'private@example.test'], - temporalSearchableText: [ - { - validFrom: '2026-02-01T00:00:00Z', - value: 'canonical-private@example.test', - }, - ], - title, - }); - const counterpartyDocument = ( - resourceId: string, - selectedLegalEntityId: string - ) => ({ - archived: false, - facets: [], - metadata: [], - projectionVersion: '1', - ref: { - moduleId: 'party.registry', - resourceId, - resourceType: 'party.registry.counterparty', - tenantId, + }, + searchableText: [title, 'private@example.test'], + temporalSearchableText: [ + { + validFrom: '2026-02-01T00:00:00Z', + value: 'canonical-private@example.test', }, - searchableText: ['Acme counterparty'], - selectedLegalEntityId, - title: 'Acme counterparty', - }); + ], + title, + }); + const counterpartyDocument = (resourceId: string, selectedLegalEntityId: string) => ({ + archived: false, + facets: [], + metadata: [], + projectionVersion: '1', + ref: { + moduleId: 'party.registry', + resourceId, + resourceType: 'party.registry.counterparty', + tenantId, + }, + searchableText: ['Acme counterparty'], + selectedLegalEntityId, + title: 'Acme counterparty', + }); - const cleanup = Effect.gen(function* cleanSearchPersistenceFixtures() { - yield* queryEffect( - admin, - `delete from core.search_index_entries where tenant_id = $1`, - [tenantId] - ); - yield* queryEffect( - admin, - `delete from core.search_projection_rebuilds where tenant_id = $1`, - [tenantId] - ); - yield* queryEffect( - admin, - `delete from core.legal_entities where tenant_id = $1`, - [tenantId] - ); - yield* queryEffect( - admin, - `delete from core.tenants where tenant_id in ($1, $2)`, - [tenantId, otherTenantId] - ); - }).pipe(Effect.orDie); + const cleanup = Effect.gen(function* cleanSearchPersistenceFixtures() { + yield* queryEffect(admin, `delete from core.search_index_entries where tenant_id = $1`, [tenantId]); + yield* queryEffect(admin, `delete from core.search_projection_rebuilds where tenant_id = $1`, [tenantId]); + yield* queryEffect(admin, `delete from core.legal_entities where tenant_id = $1`, [tenantId]); + yield* queryEffect(admin, `delete from core.tenants where tenant_id in ($1, $2)`, [tenantId, otherTenantId]); + }).pipe(Effect.orDie); - yield* Effect.addFinalizer(() => cleanup); - yield* queryEffect( - admin, - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Search tenant', 'active', 'en'), ($3, $4, 'Other tenant', 'active', 'en')`, - [ - tenantId, - `search-${tenantId}`, - otherTenantId, - `search-${otherTenantId}`, - ] - ); - yield* queryEffect( - admin, - `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1::uuid, $2, 'Search LE', 'CZ', $1::uuid::text, 'active'), ($3::uuid, $2, 'Other LE', 'CZ', $3::uuid::text, 'active')`, - [legalEntityId, tenantId, otherLegalEntityId] - ); + yield* Effect.addFinalizer(() => cleanup); + yield* queryEffect( + admin, + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Search tenant', 'active', 'en'), ($3, $4, 'Other tenant', 'active', 'en')`, + [tenantId, `search-${tenantId}`, otherTenantId, `search-${otherTenantId}`], + ); + yield* queryEffect( + admin, + `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1::uuid, $2, 'Search LE', 'CZ', $1::uuid::text, 'active'), ($3::uuid, $2, 'Other LE', 'CZ', $3::uuid::text, 'active')`, + [legalEntityId, tenantId, otherLegalEntityId], + ); - yield* store.replace({ - documents: [ - partyDocument(partyId, '1', 'Acme'), - partyDocument(removedPartyId, '1', 'Remove me'), - ], - moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.party', - tenantId, - }); - yield* store.replace({ - documents: [partyDocument(partyId, '2', 'Acme current')], - moduleId: 'party.registry', - rebuildVersion: '2', - resourceType: 'party.registry.party', - tenantId, - }); - yield* store.apply({ - document: partyDocument(partyId, '1', 'Acme stale'), - kind: 'upsert', - }); - yield* store.apply({ - document: counterpartyDocument(counterpartyId, legalEntityId), - kind: 'upsert', - }); - yield* store.apply({ - document: counterpartyDocument(otherCounterpartyId, otherLegalEntityId), - kind: 'upsert', - }); + yield* store.replace({ + documents: [partyDocument(partyId, '1', 'Acme'), partyDocument(removedPartyId, '1', 'Remove me')], + moduleId: 'party.registry', + rebuildVersion: '1', + resourceType: 'party.registry.party', + tenantId, + }); + yield* store.replace({ + documents: [partyDocument(partyId, '2', 'Acme current')], + moduleId: 'party.registry', + rebuildVersion: '2', + resourceType: 'party.registry.party', + tenantId, + }); + yield* store.apply({ + document: partyDocument(partyId, '1', 'Acme stale'), + kind: 'upsert', + }); + yield* store.apply({ + document: counterpartyDocument(counterpartyId, legalEntityId), + kind: 'upsert', + }); + yield* store.apply({ + document: counterpartyDocument(otherCounterpartyId, otherLegalEntityId), + kind: 'upsert', + }); - const partyHits = yield* search.search({ + const partyHits = yield* search.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'private@example.test', + resourceType: 'party.registry.party', + tenantId, + }); + expect(partyHits.map(({ title }) => title)).toEqual(['Acme current']); + expect(yield* encodeJson(partyHits)).not.toMatch(/private@example\.test/u); + const evidenceSearch = (query: string, effectiveAt = '2026-02-01T00:00:00Z') => + search.search({ + effectiveAt, includeArchived: false, moduleId: 'party.registry', - query: 'private@example.test', + query, resourceType: 'party.registry.party', tenantId, }); - expect(partyHits.map(({ title }) => title)).toEqual(['Acme current']); - expect(yield* encodeJson(partyHits)).not.toMatch( - /private@example\.test/u - ); - const evidenceSearch = ( - query: string, - effectiveAt = '2026-02-01T00:00:00Z' - ) => - search.search({ - effectiveAt, - includeArchived: false, - moduleId: 'party.registry', - query, - resourceType: 'party.registry.party', - tenantId, - }); - const aliasHits = yield* evidenceSearch('former'); - expect(aliasHits.length).toBe(1); - expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); - const canonicalHits = yield* evidenceSearch('acme'); - expect(canonicalHits[0]?.matchedRef).toBe(undefined); - const historicalAliasHits = yield* evidenceSearch( - 'alias-private', - '2026-01-01T00:00:00Z' - ); - expect(historicalAliasHits[0]?.matchedRef).toEqual(aliasRef); - expect(yield* evidenceSearch('alias-private')).toEqual([]); - expect( - yield* evidenceSearch('canonical-private', '2026-01-31T00:00:00Z') - ).toEqual([]); - const temporalHits = yield* evidenceSearch('canonical-private'); - expect(temporalHits.length).toBe(1); - expect(temporalHits[0]?.matchedRef).toBe(undefined); - expect(yield* encodeJson([aliasHits, temporalHits])).not.toMatch( - /private@example|searchableText|aliases/u - ); - const floorRef = { - ...aliasRef, - resourceType: 'party.registry.floor-test', - }; - const emptyRebuild = { - documents: [], + const aliasHits = yield* evidenceSearch('former'); + expect(aliasHits.length).toBe(1); + expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); + const canonicalHits = yield* evidenceSearch('acme'); + expect(canonicalHits[0]?.matchedRef).toBe(undefined); + const historicalAliasHits = yield* evidenceSearch('alias-private', '2026-01-01T00:00:00Z'); + expect(historicalAliasHits[0]?.matchedRef).toEqual(aliasRef); + expect(yield* evidenceSearch('alias-private')).toEqual([]); + expect(yield* evidenceSearch('canonical-private', '2026-01-31T00:00:00Z')).toEqual([]); + const temporalHits = yield* evidenceSearch('canonical-private'); + expect(temporalHits.length).toBe(1); + expect(temporalHits[0]?.matchedRef).toBe(undefined); + expect(yield* encodeJson([aliasHits, temporalHits])).not.toMatch(/private@example|searchableText|aliases/u); + const floorRef = { + ...aliasRef, + resourceType: 'party.registry.floor-test', + }; + const emptyRebuild = { + documents: [], + moduleId: floorRef.moduleId, + rebuildVersion: '2', + resourceType: floorRef.resourceType, + tenantId, + }; + const staleDocument = { + ...partyDocument(floorRef.resourceId, '1', 'Unseen resource'), + ref: floorRef, + }; + yield* store.replace(emptyRebuild); + // A fresh service instance must observe the durable floor, not process-local state. + const restarted = makePostgresCoreSearchProjectionStore({ + executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), + }); + const restartedSearch = yield* createCoreSearchQueryRuntime.pipe( + Effect.provideService(CoreSearchProjectionStore, restarted), + ); + const floorSearch = () => + restartedSearch.search({ + includeArchived: false, moduleId: floorRef.moduleId, - rebuildVersion: '2', + query: 'unseen', resourceType: floorRef.resourceType, tenantId, - }; - const staleDocument = { - ...partyDocument(floorRef.resourceId, '1', 'Unseen resource'), - ref: floorRef, - }; - yield* store.replace(emptyRebuild); - // A fresh service instance must observe the durable floor, not process-local state. - const restarted = makePostgresCoreSearchProjectionStore({ - executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), - }); - const restartedSearch = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, restarted) - ); - const floorSearch = () => - restartedSearch.search({ - includeArchived: false, - moduleId: floorRef.moduleId, - query: 'unseen', - resourceType: floorRef.resourceType, - tenantId, - }); - yield* restarted.apply({ document: staleDocument, kind: 'upsert' }); - yield* restarted.replace({ - ...emptyRebuild, - documents: [staleDocument], - rebuildVersion: '1', }); - expect(yield* floorSearch()).toEqual([]); - yield* restarted.replace(emptyRebuild); - const divergence = yield* Effect.flip( - restarted.replace({ ...emptyRebuild, documents: [staleDocument] }) - ); - expect( - Predicate.isTagged(divergence, 'CoreSearchProjectionInvalid') - ).toBe(true); - yield* restarted.apply({ - document: { ...staleDocument, projectionVersion: '3' }, - kind: 'upsert', - }); - yield* restarted.replace(emptyRebuild); - const rebuiltFloorHits = yield* floorSearch(); - expect(rebuiltFloorHits.length).toBe(1); - const rebuildRows = yield* queryEffect( - runtimePool, - `select rebuild_version from core.search_projection_rebuilds where tenant_id = $1`, - [tenantId] - ); - expect(rebuildRows.rowCount).toBe(0); - const counterpartyHits = yield* search.search({ + yield* restarted.apply({ document: staleDocument, kind: 'upsert' }); + yield* restarted.replace({ + ...emptyRebuild, + documents: [staleDocument], + rebuildVersion: '1', + }); + expect(yield* floorSearch()).toEqual([]); + yield* restarted.replace(emptyRebuild); + const divergence = yield* Effect.flip(restarted.replace({ ...emptyRebuild, documents: [staleDocument] })); + expect(Predicate.isTagged(divergence, 'CoreSearchProjectionInvalid')).toBe(true); + yield* restarted.apply({ + document: { ...staleDocument, projectionVersion: '3' }, + kind: 'upsert', + }); + yield* restarted.replace(emptyRebuild); + const rebuiltFloorHits = yield* floorSearch(); + expect(rebuiltFloorHits.length).toBe(1); + const rebuildRows = yield* queryEffect( + runtimePool, + `select rebuild_version from core.search_projection_rebuilds where tenant_id = $1`, + [tenantId], + ); + expect(rebuildRows.rowCount).toBe(0); + const counterpartyHits = yield* search.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'acme', + resourceType: 'party.registry.counterparty', + selectedLegalEntityId: legalEntityId, + tenantId, + }); + expect(counterpartyHits.map(({ ref }) => ref.resourceId)).toEqual([counterpartyId]); + expect( + yield* search.search({ includeArchived: false, moduleId: 'party.registry', query: 'acme', resourceType: 'party.registry.counterparty', - selectedLegalEntityId: legalEntityId, tenantId, - }); - expect(counterpartyHits.map(({ ref }) => ref.resourceId)).toEqual([ - counterpartyId, - ]); - expect( - yield* search.search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'acme', - resourceType: 'party.registry.counterparty', - tenantId, - }) - ).toEqual([]); + }), + ).toEqual([]); - const runtimeRows = yield* queryEffect( - runtimePool, - `select source_resource_id from core.search_index_entries where tenant_id = $1`, - [tenantId] - ); - expect(runtimeRows.rowCount).toBe(0); - const stored = yield* queryEffect<{ - deleted: boolean; - projection_version: string; - }>( - admin, - `select deleted, projection_version::text from core.search_index_entries where tenant_id = $1 and source_resource_id = $2`, - [tenantId, removedPartyId] - ); - expect(stored.rows).toEqual([{ deleted: true, projection_version: '2' }]); - }) + const runtimeRows = yield* queryEffect( + runtimePool, + `select source_resource_id from core.search_index_entries where tenant_id = $1`, + [tenantId], + ); + expect(runtimeRows.rowCount).toBe(0); + const stored = yield* queryEffect<{ + deleted: boolean; + projection_version: string; + }>( + admin, + `select deleted, projection_version::text from core.search_index_entries where tenant_id = $1 and source_resource_id = $2`, + [tenantId, removedPartyId], + ); + expect(stored.rows).toEqual([{ deleted: true, projection_version: '2' }]); + }), ); diff --git a/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts index 70514f2d7..b9472b5b6 100644 --- a/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts +++ b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts @@ -19,10 +19,7 @@ import { } from '../../src/search/worker-snapshot.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; -const readLegalEntitySettings = ( - executor: CoreSearchSnapshotReadExecutor, - eventId: string -) => +const readLegalEntitySettings = (executor: CoreSearchSnapshotReadExecutor, eventId: string) => executor .select({ isolation: sql`current_setting('transaction_isolation')`, @@ -33,10 +30,7 @@ const readLegalEntitySettings = ( .from(domainEvents) .where(eq(domainEvents.domainEventId, eventId)); -const readTenantMaxVersion = ( - executor: CoreSearchSnapshotReadExecutor, - tenantId: string -) => +const readTenantMaxVersion = (executor: CoreSearchSnapshotReadExecutor, tenantId: string) => executor .select({ version: sql`max(${domainEvents.tenantSequenceNo})::text`, @@ -44,15 +38,12 @@ const readTenantMaxVersion = ( .from(domainEvents) .where(eq(domainEvents.tenantId, tenantId)); -const readSnapshotPosition = ( - source: CoreSearchWorkerSnapshotService, - context: OutboxWorkerHandlerContext -) => +const readSnapshotPosition = (source: CoreSearchWorkerSnapshotService, context: OutboxWorkerHandlerContext) => source.read(context, (snapshot) => Effect.succeed({ eventWatermark: snapshot.eventWatermark, generation: snapshot.projectionVersion, - }) + }), ); const beginTransaction = (client: PoolClient) => @@ -69,19 +60,14 @@ const commitTransaction = (client: PoolClient) => try: () => client.query('commit'), }); -const insertPendingEvent = ( - client: PoolClient, - pendingEventId: string, - tenantId: string, - pendingSubjectId: string -) => +const insertPendingEvent = (client: PoolClient, pendingEventId: string, tenantId: string, pendingSubjectId: string) => Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), try: () => client.query( `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3)`, - [pendingEventId, tenantId, pendingSubjectId] + [pendingEventId, tenantId, pendingSubjectId], ), }); @@ -90,7 +76,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const connections = yield* loadDatabaseConnectionPair(); const [tenantId, legalEntityId, eventId] = yield* Effect.all( [crypto.randomUUIDv4, crypto.randomUUIDv4, crypto.randomUUIDv4], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const admin = new Pool({ connectionString: connections.admin.connectionString, @@ -103,7 +89,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const source = makeCoreSearchWorkerSnapshot( makePostgresCoreSearchSnapshotBackend({ executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), - }) + }), ); const insertEvent = (id: string) => Effect.gen(function* insertDomainEvent() { @@ -114,7 +100,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { try: () => admin.query<{ tenant_sequence_no: string }>( `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3) returning tenant_sequence_no::text`, - [id, tenantId, subjectId] + [id, tenantId, subjectId], ), }); const row = Option.getOrThrow(Option.fromNullishOr(result.rows[0])); @@ -125,35 +111,22 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - try: () => - admin.query( - 'delete from core.search_projection_generations where tenant_id = $1', - [tenantId] - ), + try: () => admin.query('delete from core.search_projection_generations where tenant_id = $1', [tenantId]), }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - try: () => - admin.query('delete from core.domain_events where tenant_id = $1', [ - tenantId, - ]), + try: () => admin.query('delete from core.domain_events where tenant_id = $1', [tenantId]), }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - try: () => - admin.query('delete from core.legal_entities where tenant_id = $1', [ - tenantId, - ]), + try: () => admin.query('delete from core.legal_entities where tenant_id = $1', [tenantId]), }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - try: () => - admin.query('delete from core.tenants where tenant_id = $1', [ - tenantId, - ]), + try: () => admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), }); yield* Effect.all( [ @@ -168,7 +141,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { try: () => runtimePool.end(), }), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); }).pipe(Effect.orDie); @@ -180,7 +153,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { try: () => admin.query( `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Snapshot tenant', 'active', 'en')`, - [tenantId, `snapshot-${tenantId}`] + [tenantId, `snapshot-${tenantId}`], ), }); yield* Effect.tryPromise({ @@ -189,13 +162,13 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { try: () => admin.query( `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1::uuid, $2, 'Snapshot LE', 'CZ', $1::uuid::text, 'active')`, - [legalEntityId, tenantId] + [legalEntityId, tenantId], ), }); const originalVersion = yield* insertEvent(eventId); const [claimId, deliveryId, messageId] = yield* Effect.all( [crypto.randomUUIDv4, crypto.randomUUIDv4, crypto.randomUUIDv4], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const context = attestOutboxWorkerHandlerContext({ attemptNumber: 1, @@ -209,24 +182,19 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { topic: 'party.registry.party-updated.v1', workerKey: 'party.registry.project-party-updated-to-search', }); - const readEventSettings = (executor: CoreSearchSnapshotReadExecutor) => - readLegalEntitySettings(executor, eventId); - const readMaxTenantVersion = (executor: CoreSearchSnapshotReadExecutor) => - readTenantMaxVersion(executor, tenantId); + const readEventSettings = (executor: CoreSearchSnapshotReadExecutor) => readLegalEntitySettings(executor, eventId); + const readMaxTenantVersion = (executor: CoreSearchSnapshotReadExecutor) => readTenantMaxVersion(executor, tenantId); let newerVersion = ''; const result = yield* source.read(context, (snapshot) => Effect.gen(function* inspectSnapshot() { expect(snapshot.projectionVersion).toBe('1'); expect(snapshot.eventWatermark).toBe(originalVersion); - const settings = yield* snapshot.forLegalEntity( - legalEntityId, - readEventSettings - ); + const settings = yield* snapshot.forLegalEntity(legalEntityId, readEventSettings); const newerEventId = yield* crypto.randomUUIDv4; newerVersion = yield* insertEvent(newerEventId); const rows = yield* snapshot.tenant(readMaxTenantVersion); return { settings, version: rows[0]?.version }; - }) + }), ); expect(result.settings).toEqual([ { @@ -237,11 +205,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }, ]); expect(result.version).toBe(originalVersion); - expect( - yield* source.read(context, (snapshot) => - Effect.succeed(snapshot.projectionVersion) - ) - ).toBe('2'); + expect(yield* source.read(context, (snapshot) => Effect.succeed(snapshot.projectionVersion))).toBe('2'); const nextSnapshot = yield* readSnapshotPosition(source, context); expect(nextSnapshot).toEqual({ eventWatermark: newerVersion, @@ -251,12 +215,9 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { // A second snapshot starts while the first owns the generation row. It must // retry its old RR snapshot after the first commits, never publish stale data // with a greater generation. No Party business transaction shares this lock. - const [started, release] = yield* Effect.all( - [Deferred.make(), Deferred.make()], - { - concurrency: 'unbounded', - } - ); + const [started, release] = yield* Effect.all([Deferred.make(), Deferred.make()], { + concurrency: 'unbounded', + }); const first = yield* source .read(context, (snapshot) => Effect.gen(function* firstSnapshot() { @@ -266,13 +227,11 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { eventWatermark: snapshot.eventWatermark, generation: snapshot.projectionVersion, }; - }) + }), ) .pipe(Effect.forkChild); yield* Deferred.await(started); - const second = yield* readSnapshotPosition(source, context).pipe( - Effect.forkChild - ); + const second = yield* readSnapshotPosition(source, context).pipe(Effect.forkChild); const waiting = yield* Effect.reduce( Array.from({ length: 100 }), @@ -292,26 +251,20 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { try: () => admin.query<{ count: number }>( `select count(*)::int as count from pg_stat_activity where application_name = $1 and wait_event_type = 'Lock'`, - [applicationName] + [applicationName], ), - }) + }), ), - Effect.map((activity) => activity.rows[0]?.count === 1) - ) + Effect.map((activity) => activity.rows[0]?.count === 1), + ), ); - expect( - waiting, - 'second snapshot must wait on first generation before retrying' - ).toBe(true); + expect(waiting, 'second snapshot must wait on first generation before retrying').toBe(true); const latestEventId = yield* crypto.randomUUIDv4; const latestEvent = yield* insertEvent(latestEventId); yield* Deferred.succeed(release, null); - const [firstResult, secondResult] = yield* Effect.all( - [Fiber.join(first), Fiber.join(second)], - { - concurrency: 'unbounded', - } - ); + const [firstResult, secondResult] = yield* Effect.all([Fiber.join(first), Fiber.join(second)], { + concurrency: 'unbounded', + }); expect(firstResult).toEqual({ eventWatermark: newerVersion, generation: '4', @@ -325,17 +278,12 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { // Both snapshots below have the same event max but must get new generations. const [pendingEventId, pendingSubjectId, higherEventId] = yield* Effect.all( [crypto.randomUUIDv4, crypto.randomUUIDv4, crypto.randomUUIDv4], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const lateCommitSnapshot = (pending: PoolClient) => Effect.gen(function* lateCommitSnapshotEffect() { yield* beginTransaction(pending); - yield* insertPendingEvent( - pending, - pendingEventId, - tenantId, - pendingSubjectId - ); + yield* insertPendingEvent(pending, pendingEventId, tenantId, pendingSubjectId); const higherEvent = yield* insertEvent(higherEventId); const beforeLateCommit = yield* readSnapshotPosition(source, context); yield* commitTransaction(pending); @@ -361,21 +309,15 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { catch: (cause) => new Cause.UnknownError(cause), try: () => pending.query('rollback'), - }).pipe( - Effect.orDie, - Effect.ensuring(Effect.sync(() => pending.release())) - ) + }).pipe(Effect.orDie, Effect.ensuring(Effect.sync(() => pending.release()))), ); }); yield* exercise; }); -it.layer(NodeServices.layer, { excludeTestServices: true })( - 'worker snapshots', - (suite) => { - suite.effect( - 'worker projection uses independent generations and one repeatable snapshot across tenant and Legal Entity scopes', - () => workerSnapshotProgram - ); - } -); +it.layer(NodeServices.layer, { excludeTestServices: true })('worker snapshots', (suite) => { + suite.effect( + 'worker projection uses independent generations and one repeatable snapshot across tenant and Legal Entity scopes', + () => workerSnapshotProgram, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts b/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts index 9c28f45be..a32562ac8 100644 --- a/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts +++ b/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts @@ -24,10 +24,7 @@ import { tenantModuleStateChanges, } from '../../src/db/schema.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { - makeOperationalScopeRepository, - makeOperationalScopeResolver, -} from '../../src/operations/context.ts'; +import { makeOperationalScopeRepository, makeOperationalScopeResolver } from '../../src/operations/context.ts'; import type { ReadHandlerContext } from '../../src/reads/context.ts'; import { defineRead } from '../../src/reads/definition.ts'; import { makeReadRuntime } from '../../src/reads/runtime.ts'; @@ -35,27 +32,22 @@ import { makeTestDatabaseFromPool } from '../support/database.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; type DatabaseQueryFailureSelf = typeof DatabaseQueryFailureContract.Type; -const DatabaseQueryFailureContract = Schema.TaggedStruct( - 'DatabaseQueryFailure', - { - code: Schema.String, - } -); -const DatabaseQueryFailure = Schema.TaggedError()( - 'DatabaseQueryFailure', - { code: Schema.String } -); +const DatabaseQueryFailureContract = Schema.TaggedStruct('DatabaseQueryFailure', { + code: Schema.String, +}); +const DatabaseQueryFailure = Schema.TaggedError()('DatabaseQueryFailure', { + code: Schema.String, +}); const DatabaseErrorCode = Schema.Struct({ code: Schema.String }); const queryEffect = ( client: Pool | PoolClient, statement: string, - parameters?: readonly unknown[] -): Effect.Effect> => - Effect.promise(() => client.query(statement, [...(parameters ?? [])])); + parameters?: readonly unknown[], +): Effect.Effect> => Effect.promise(() => client.query(statement, [...(parameters ?? [])])); const queryTryEffect = ( client: Pool | PoolClient, statement: string, - parameters?: readonly unknown[] + parameters?: readonly unknown[], ): Effect.Effect, DatabaseQueryFailureSelf> => Effect.tryPromise({ catch: (error) => { @@ -78,16 +70,11 @@ const toReadResult = (rows: readonly { readonly value: string }[]) => ({ it('declares the composite same-tenant parent keys used by isolation foreign keys', () => { const names = new Set( - [ - legalEntities, - principals, - principalAuthBindings, - actionInvocations, - ].flatMap((table) => + [legalEntities, principals, principalAuthBindings, actionInvocations].flatMap((table) => getTableConfig(table) .indexes.filter((index) => index.config.unique) - .map((index) => index.config.name) - ) + .map((index) => index.config.name), + ), ); expect(names.has('core_legal_entities_tenant_id_uk')).toBe(true); expect(names.has('core_principals_tenant_id_uk')).toBe(true); @@ -109,55 +96,44 @@ it('declares the composite same-tenant parent keys used by isolation foreign key ]; for (const table of tenantQualifiedChildren) { const businessReferences = getTableConfig(table) - .foreignKeys.map((foreignKey) => - foreignKey.reference().columns.map((column) => column.name) - ) + .foreignKeys.map((foreignKey) => foreignKey.reference().columns.map((column) => column.name)) .filter((columns) => columns.some((column) => column !== 'tenant_id')); expect(businessReferences.length > 0).toBe(true); - expect( - businessReferences.every( - (columns) => columns.length === 2 && columns[0] === 'tenant_id' - ) - ).toBe(true); + expect(businessReferences.every((columns) => columns.length === 2 && columns[0] === 'tenant_id')).toBe(true); } }); -it.live( - 'runtime RLS isolates tenant and legal-entity rows and never leaks transaction scope', - () => - Effect.gen(function* runtimeRlsIsolation() { - const connections = yield* loadDatabaseConnectionPair(); - const admin = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.admin.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const runtime = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ - connectionString: connections.runtime.connectionString, - max: 1, - }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const schema = `isolation_${randomUUID().replaceAll('-', '')}`; - const tenantA = randomUUID(); - const tenantB = randomUUID(); - const entityA = randomUUID(); - const entityB = randomUUID(); - const entityC = randomUUID(); - const resourceId = randomUUID(); - const predicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; +it.live('runtime RLS isolates tenant and legal-entity rows and never leaks transaction scope', () => + Effect.gen(function* runtimeRlsIsolation() { + const connections = yield* loadDatabaseConnectionPair(); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const runtime = yield* Effect.acquireRelease( + Effect.sync( + () => + new Pool({ + connectionString: connections.runtime.connectionString, + max: 1, + }), + ), + (pool) => Effect.promise(() => pool.end()), + ); + const schema = `isolation_${randomUUID().replaceAll('-', '')}`; + const tenantA = randomUUID(); + const tenantB = randomUUID(); + const entityA = randomUUID(); + const entityB = randomUUID(); + const entityC = randomUUID(); + const resourceId = randomUUID(); + const predicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; - const exercise = Effect.gen(function* exerciseRuntimeRls() { - yield* queryEffect(admin, `create schema ${schema}`); - yield* queryEffect( - admin, - ` + const exercise = Effect.gen(function* exerciseRuntimeRls() { + yield* queryEffect(admin, `create schema ${schema}`); + yield* queryEffect( + admin, + ` create table ${schema}.records ( tenant_id uuid not null, legal_entity_id uuid not null, @@ -165,285 +141,228 @@ it.live( value text not null, primary key (tenant_id, legal_entity_id, resource_id) ) - ` - ); - yield* queryEffect( - admin, - `alter table ${schema}.records enable row level security` - ); - yield* queryEffect( - admin, - `alter table ${schema}.records force row level security` - ); - yield* queryEffect( - admin, - `create policy records_select on ${schema}.records for select to ontos_runtime using (${predicate})` - ); - yield* queryEffect( - admin, - `create policy records_insert on ${schema}.records for insert to ontos_runtime with check (${predicate})` - ); - yield* queryEffect( - admin, - `create policy records_update on ${schema}.records for update to ontos_runtime using (${predicate}) with check (${predicate})` - ); - yield* queryEffect( - admin, - `create policy records_delete on ${schema}.records for delete to ontos_runtime using (${predicate})` - ); - yield* queryEffect( - admin, - `grant usage on schema ${schema} to ontos_runtime` - ); - yield* queryEffect( - admin, - `grant select, insert, update, delete on ${schema}.records to ontos_runtime` - ); - yield* queryEffect( - admin, - `insert into ${schema}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $4, 'entity-a'), ($1, $3, $4, 'entity-b'), ($5, $6, $4, 'tenant-b')`, - [tenantA, entityA, entityB, resourceId, tenantB, entityC] - ); + `, + ); + yield* queryEffect(admin, `alter table ${schema}.records enable row level security`); + yield* queryEffect(admin, `alter table ${schema}.records force row level security`); + yield* queryEffect( + admin, + `create policy records_select on ${schema}.records for select to ontos_runtime using (${predicate})`, + ); + yield* queryEffect( + admin, + `create policy records_insert on ${schema}.records for insert to ontos_runtime with check (${predicate})`, + ); + yield* queryEffect( + admin, + `create policy records_update on ${schema}.records for update to ontos_runtime using (${predicate}) with check (${predicate})`, + ); + yield* queryEffect( + admin, + `create policy records_delete on ${schema}.records for delete to ontos_runtime using (${predicate})`, + ); + yield* queryEffect(admin, `grant usage on schema ${schema} to ontos_runtime`); + yield* queryEffect(admin, `grant select, insert, update, delete on ${schema}.records to ontos_runtime`); + yield* queryEffect( + admin, + `insert into ${schema}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $4, 'entity-a'), ($1, $3, $4, 'entity-b'), ($5, $6, $4, 'tenant-b')`, + [tenantA, entityA, entityB, resourceId, tenantB, entityC], + ); - const catalog = yield* queryEffect<{ - policy_count: number; - relforcerowsecurity: boolean; - relrowsecurity: boolean; - }>( - admin, - ` + const catalog = yield* queryEffect<{ + policy_count: number; + relforcerowsecurity: boolean; + relrowsecurity: boolean; + }>( + admin, + ` select relation.relrowsecurity, relation.relforcerowsecurity, (select count(*)::int from pg_catalog.pg_policy where polrelid = relation.oid) as policy_count from pg_catalog.pg_class as relation inner join pg_catalog.pg_namespace as namespace on namespace.oid = relation.relnamespace where namespace.nspname = $1 and relation.relname = 'records' `, - [schema] - ); - expect(catalog.rows[0]).toEqual({ - policy_count: 4, - relforcerowsecurity: true, - relrowsecurity: true, - }); + [schema], + ); + expect(catalog.rows[0]).toEqual({ + policy_count: 4, + relforcerowsecurity: true, + relrowsecurity: true, + }); - const unscopedRows = yield* queryEffect( - runtime, - `select * from ${schema}.records` + const unscopedRows = yield* queryEffect(runtime, `select * from ${schema}.records`); + expect(unscopedRows.rowCount).toBe(0); + const client = yield* Effect.promise(() => runtime.connect()); + yield* Effect.gen(function* scopedRuntimeQueries() { + yield* queryEffect(client, 'begin'); + yield* queryEffect( + client, + "select set_config('ontos.tenant_id', $1, true), set_config('ontos.legal_entity_id', $2, true)", + [tenantA, entityA], ); - expect(unscopedRows.rowCount).toBe(0); - const client = yield* Effect.promise(() => runtime.connect()); - yield* Effect.gen(function* scopedRuntimeQueries() { - yield* queryEffect(client, 'begin'); - yield* queryEffect( - client, - "select set_config('ontos.tenant_id', $1, true), set_config('ontos.legal_entity_id', $2, true)", - [tenantA, entityA] - ); - const entityARows = yield* queryEffect<{ value: string }>( - client, - `select value from ${schema}.records` - ); - expect(entityARows.rows).toEqual([{ value: 'entity-a' }]); - const foreignUpdate = yield* queryEffect( - client, - `update ${schema}.records set value = 'hacked' where value = 'tenant-b'` - ); - expect(foreignUpdate.rowCount).toBe(0); - const foreignDelete = yield* queryEffect( - client, - `delete from ${schema}.records where value = 'entity-b'` - ); - expect(foreignDelete.rowCount).toBe(0); - const forbiddenInsert = yield* Effect.flip( - queryTryEffect( - client, - `insert into ${schema}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $3, 'forbidden')`, - [tenantB, entityC, randomUUID()] - ) - ); - expect(forbiddenInsert.code).toBe('42501'); - yield* queryEffect(client, 'rollback'); - - yield* queryEffect(client, 'begin'); - yield* queryEffect( - client, - "select set_config('ontos.tenant_id', $1, true), set_config('ontos.legal_entity_id', $2, true)", - [tenantA, entityB] - ); - const entityBRows = yield* queryEffect<{ value: string }>( + const entityARows = yield* queryEffect<{ value: string }>(client, `select value from ${schema}.records`); + expect(entityARows.rows).toEqual([{ value: 'entity-a' }]); + const foreignUpdate = yield* queryEffect( + client, + `update ${schema}.records set value = 'hacked' where value = 'tenant-b'`, + ); + expect(foreignUpdate.rowCount).toBe(0); + const foreignDelete = yield* queryEffect(client, `delete from ${schema}.records where value = 'entity-b'`); + expect(foreignDelete.rowCount).toBe(0); + const forbiddenInsert = yield* Effect.flip( + queryTryEffect( client, - `select value from ${schema}.records` - ); - expect(entityBRows.rows).toEqual([{ value: 'entity-b' }]); - yield* queryEffect(client, 'commit'); - }).pipe(Effect.ensuring(Effect.sync(() => client.release()))); - - const resetRows = yield* queryEffect( - runtime, - `select * from ${schema}.records` + `insert into ${schema}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $3, 'forbidden')`, + [tenantB, entityC, randomUUID()], + ), ); - expect(resetRows.rowCount).toBe(0); - const protectedRows = yield* queryEffect<{ value: string }>( - admin, - `select value from ${schema}.records order by value` + expect(forbiddenInsert.code).toBe('42501'); + yield* queryEffect(client, 'rollback'); + + yield* queryEffect(client, 'begin'); + yield* queryEffect( + client, + "select set_config('ontos.tenant_id', $1, true), set_config('ontos.legal_entity_id', $2, true)", + [tenantA, entityB], ); - expect(protectedRows.rows).toEqual([ - { value: 'entity-a' }, - { value: 'entity-b' }, - { value: 'tenant-b' }, - ]); - }); - const release = queryEffect( + const entityBRows = yield* queryEffect<{ value: string }>(client, `select value from ${schema}.records`); + expect(entityBRows.rows).toEqual([{ value: 'entity-b' }]); + yield* queryEffect(client, 'commit'); + }).pipe(Effect.ensuring(Effect.sync(() => client.release()))); + + const resetRows = yield* queryEffect(runtime, `select * from ${schema}.records`); + expect(resetRows.rowCount).toBe(0); + const protectedRows = yield* queryEffect<{ value: string }>( admin, - `drop schema if exists ${schema} cascade` + `select value from ${schema}.records order by value`, ); - yield* exercise.pipe(Effect.ensuring(release)); - }) + expect(protectedRows.rows).toEqual([{ value: 'entity-a' }, { value: 'entity-b' }, { value: 'tenant-b' }]); + }); + const release = queryEffect(admin, `drop schema if exists ${schema} cascade`); + yield* exercise.pipe(Effect.ensuring(release)); + }), ); -it.live( - 'an unscoped owner repository remains isolated inside a governed read transaction', - () => - Effect.gen(function* governedReadIsolation() { - const connections = yield* loadDatabaseConnectionPair(); - const admin = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.admin.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const runtimePool = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.runtime.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const runtimeDatabase = yield* makeTestDatabaseFromPool( - runtimePool, - coreRelations - ); - const schemaName = `governed_isolation_${randomUUID().replaceAll('-', '')}`; - const ownerSchema = pgSchema(schemaName); - const records = ownerSchema.table('records', { - legalEntityId: uuid('legal_entity_id').notNull(), - resourceId: uuid('resource_id').notNull(), - tenantId: uuid('tenant_id').notNull(), - value: text('value').notNull(), - }); - const tenantA = randomUUID(); - const tenantB = randomUUID(); - const entityA = randomUUID(); - const entityB = randomUUID(); - const entityC = randomUUID(); - const principalA = randomUUID(); - const principalB = randomUUID(); - const bindingA = randomUUID(); - const bindingB = randomUUID(); - const resourceId = randomUUID(); - const predicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; - const entrypoint = defineSystemModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'core.shell.governed-isolation-fixture', - moduleKey: 'core.shell', - role: 'api', - }); +it.live('an unscoped owner repository remains isolated inside a governed read transaction', () => + Effect.gen(function* governedReadIsolation() { + const connections = yield* loadDatabaseConnectionPair(); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); + const schemaName = `governed_isolation_${randomUUID().replaceAll('-', '')}`; + const ownerSchema = pgSchema(schemaName); + const records = ownerSchema.table('records', { + legalEntityId: uuid('legal_entity_id').notNull(), + resourceId: uuid('resource_id').notNull(), + tenantId: uuid('tenant_id').notNull(), + value: text('value').notNull(), + }); + const tenantA = randomUUID(); + const tenantB = randomUUID(); + const entityA = randomUUID(); + const entityB = randomUUID(); + const entityC = randomUUID(); + const principalA = randomUUID(); + const principalB = randomUUID(); + const bindingA = randomUUID(); + const bindingB = randomUUID(); + const resourceId = randomUUID(); + const predicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; + const entrypoint = defineSystemModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'core.shell.governed-isolation-fixture', + moduleKey: 'core.shell', + role: 'api', + }); - const runForScope = (scope: { - readonly authBindingId: string; - readonly authMethod: 'session'; - readonly correlationId: string; - readonly legalEntityId: string; - readonly principalId: string; - readonly tenantId: string; - }) => { - const registration = defineRead( - { - accessKind: 'list', - entrypoint, - evidencePolicy: { - captureMode: 'metadata_only', - policyKey: 'core.shell.governed-isolation-fixture.v1', - }, - inputSchema: Schema.Struct({}), - legalEntityScope: 'required', - owningModuleKey: 'core.shell', - permissionTarget: 'legal_entity', - policies: [], - readKey: 'core.shell.governed-isolation-fixture', - resultSchema: Schema.Array(Schema.String), - schemaVersion: '1', - }, - ( - _input, - context: ReadHandlerContext<{ - readonly listWithoutPredicates: () => Effect.Effect< - readonly { readonly value: string }[] - >; - }> - ) => - context.services - .listWithoutPredicates() - .pipe(Effect.map(toReadResult)), - (transaction) => { - const rows = transaction.select().from(records); - return Effect.succeed({ - // Deliberately buggy: RLS, not a repository predicate, must enforce the scope. - listWithoutPredicates: effectAccessor(rows.pipe(Effect.orDie)), - }); + const runForScope = (scope: { + readonly authBindingId: string; + readonly authMethod: 'session'; + readonly correlationId: string; + readonly legalEntityId: string; + readonly principalId: string; + readonly tenantId: string; + }) => { + const registration = defineRead( + { + accessKind: 'list', + entrypoint, + evidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'core.shell.governed-isolation-fixture.v1', }, - () => ({ kind: 'legal_entity' }) - ); - const contextAccess = { - legalEntities: ({ - legalEntityIds, - }: { - readonly legalEntityIds: readonly string[]; - }) => - Effect.succeed( - legalEntityIds.map((key) => ({ - decision: 'allowed' as const, - key, - })) - ), - modules: () => Effect.succeed([]), - resources: () => Effect.succeed([]), - tenants: () => Effect.succeed([]), - }; - const runtime = makeReadRuntime( - { executor: runtimeDatabase }, - openModuleEntrypointGateway, - makeOperationalScopeResolver( - makeOperationalScopeRepository({ executor: runtimeDatabase }), - contextAccess + inputSchema: Schema.Struct({}), + legalEntityScope: 'required', + owningModuleKey: 'core.shell', + permissionTarget: 'legal_entity', + policies: [], + readKey: 'core.shell.governed-isolation-fixture', + resultSchema: Schema.Array(Schema.String), + schemaVersion: '1', + }, + ( + _input, + context: ReadHandlerContext<{ + readonly listWithoutPredicates: () => Effect.Effect; + }>, + ) => context.services.listWithoutPredicates().pipe(Effect.map(toReadResult)), + (transaction) => { + const rows = transaction.select().from(records); + return Effect.succeed({ + // Deliberately buggy: RLS, not a repository predicate, must enforce the scope. + listWithoutPredicates: effectAccessor(rows.pipe(Effect.orDie)), + }); + }, + () => ({ kind: 'legal_entity' }), + ); + const contextAccess = { + legalEntities: ({ legalEntityIds }: { readonly legalEntityIds: readonly string[] }) => + Effect.succeed( + legalEntityIds.map((key) => ({ + decision: 'allowed' as const, + key, + })), ), - contextAccess - ); - return runtime.runRead({ - input: {}, - principal: { - authBindingId: scope.authBindingId, - authContextRef: `better-auth-session:${scope.correlationId}`, - authMethod: scope.authMethod, - legalEntityId: scope.legalEntityId, - principalId: scope.principalId, - tenantId: scope.tenantId, - }, - registration, - transport: { correlationId: scope.correlationId }, - }); + modules: () => Effect.succeed([]), + resources: () => Effect.succeed([]), + tenants: () => Effect.succeed([]), }; + const runtime = makeReadRuntime( + { executor: runtimeDatabase }, + openModuleEntrypointGateway, + makeOperationalScopeResolver(makeOperationalScopeRepository({ executor: runtimeDatabase }), contextAccess), + contextAccess, + ); + return runtime.runRead({ + input: {}, + principal: { + authBindingId: scope.authBindingId, + authContextRef: `better-auth-session:${scope.correlationId}`, + authMethod: scope.authMethod, + legalEntityId: scope.legalEntityId, + principalId: scope.principalId, + tenantId: scope.tenantId, + }, + registration, + transport: { correlationId: scope.correlationId }, + }); + }; - const exercise = Effect.gen(function* exerciseGovernedReadIsolation() { - yield* queryEffect(admin, `create schema ${schemaName}`); - yield* queryEffect( - admin, - ` + const exercise = Effect.gen(function* exerciseGovernedReadIsolation() { + yield* queryEffect(admin, `create schema ${schemaName}`); + yield* queryEffect( + admin, + ` create table ${schemaName}.records ( tenant_id uuid not null, legal_entity_id uuid not null, @@ -451,217 +370,140 @@ it.live( value text not null, primary key (tenant_id, legal_entity_id, resource_id) ) - ` - ); - yield* queryEffect( - admin, - `alter table ${schemaName}.records enable row level security` - ); - yield* queryEffect( - admin, - `alter table ${schemaName}.records force row level security` - ); - yield* queryEffect( - admin, - `create policy records_select on ${schemaName}.records for select to ontos_runtime using (${predicate})` - ); - yield* queryEffect( - admin, - `grant usage on schema ${schemaName} to ontos_runtime` - ); - yield* queryEffect( - admin, - `grant select on ${schemaName}.records to ontos_runtime` - ); - yield* queryEffect( - admin, - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Governed A', 'active', 'en'), ($2, $4, 'Governed B', 'active', 'en')`, - [tenantA, tenantB, `governed-a-${tenantA}`, `governed-b-${tenantB}`] - ); - yield* queryEffect( - admin, - `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $4, 'Entity A', 'CZ', $6, 'active'), ($2, $4, 'Entity B', 'CZ', $7, 'active'), ($3, $5, 'Entity C', 'CZ', $8, 'active')`, - [ - entityA, - entityB, - entityC, - tenantA, - tenantB, - `A-${entityA}`, - `B-${entityB}`, - `C-${entityC}`, - ] - ); - yield* queryEffect( - admin, - `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Principal A', 'active'), ($2, $4, 'human', 'Principal B', 'active')`, - [principalA, principalB, tenantA, tenantB] - ); - yield* queryEffect( - admin, - `insert into core.principal_auth_bindings (principal_auth_binding_id, tenant_id, principal_id, provider, subject_type, provider_subject_id, status) values ($1, $3, $5, 'better_auth', 'user', $7, 'active'), ($2, $4, $6, 'better_auth', 'user', $8, 'active')`, - [ - bindingA, - bindingB, - tenantA, - tenantB, - principalA, - principalB, - `user-${principalA}`, - `user-${principalB}`, - ] - ); - yield* queryEffect( - admin, - `insert into ${schemaName}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $6, 'tenant-a-entity-a'), ($1, $3, $6, 'tenant-a-entity-b'), ($4, $5, $6, 'tenant-b-entity-c')`, - [tenantA, entityA, entityB, tenantB, entityC, resourceId] - ); + `, + ); + yield* queryEffect(admin, `alter table ${schemaName}.records enable row level security`); + yield* queryEffect(admin, `alter table ${schemaName}.records force row level security`); + yield* queryEffect( + admin, + `create policy records_select on ${schemaName}.records for select to ontos_runtime using (${predicate})`, + ); + yield* queryEffect(admin, `grant usage on schema ${schemaName} to ontos_runtime`); + yield* queryEffect(admin, `grant select on ${schemaName}.records to ontos_runtime`); + yield* queryEffect( + admin, + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Governed A', 'active', 'en'), ($2, $4, 'Governed B', 'active', 'en')`, + [tenantA, tenantB, `governed-a-${tenantA}`, `governed-b-${tenantB}`], + ); + yield* queryEffect( + admin, + `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $4, 'Entity A', 'CZ', $6, 'active'), ($2, $4, 'Entity B', 'CZ', $7, 'active'), ($3, $5, 'Entity C', 'CZ', $8, 'active')`, + [entityA, entityB, entityC, tenantA, tenantB, `A-${entityA}`, `B-${entityB}`, `C-${entityC}`], + ); + yield* queryEffect( + admin, + `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Principal A', 'active'), ($2, $4, 'human', 'Principal B', 'active')`, + [principalA, principalB, tenantA, tenantB], + ); + yield* queryEffect( + admin, + `insert into core.principal_auth_bindings (principal_auth_binding_id, tenant_id, principal_id, provider, subject_type, provider_subject_id, status) values ($1, $3, $5, 'better_auth', 'user', $7, 'active'), ($2, $4, $6, 'better_auth', 'user', $8, 'active')`, + [bindingA, bindingB, tenantA, tenantB, principalA, principalB, `user-${principalA}`, `user-${principalB}`], + ); + yield* queryEffect( + admin, + `insert into ${schemaName}.records (tenant_id, legal_entity_id, resource_id, value) values ($1, $2, $6, 'tenant-a-entity-a'), ($1, $3, $6, 'tenant-a-entity-b'), ($4, $5, $6, 'tenant-b-entity-c')`, + [tenantA, entityA, entityB, tenantB, entityC, resourceId], + ); - expect( - yield* runForScope({ - authBindingId: bindingA, - authMethod: 'session', - correlationId: randomUUID(), - legalEntityId: entityA, - principalId: principalA, - tenantId: tenantA, - }) - ).toEqual(['tenant-a-entity-a']); - expect( - yield* runForScope({ - authBindingId: bindingA, - authMethod: 'session', - correlationId: randomUUID(), - legalEntityId: entityB, - principalId: principalA, - tenantId: tenantA, - }) - ).toEqual(['tenant-a-entity-b']); - expect( - yield* runForScope({ - authBindingId: bindingB, - authMethod: 'session', - correlationId: randomUUID(), - legalEntityId: entityC, - principalId: principalB, - tenantId: tenantB, - }) - ).toEqual(['tenant-b-entity-c']); - }); - const release = Effect.gen(function* cleanGovernedReadIsolation() { - yield* queryEffect( - admin, - 'delete from core.data_access_events where tenant_id in ($1, $2)', - [tenantA, tenantB] - ); - yield* queryEffect( - admin, - 'delete from core.principal_auth_bindings where tenant_id in ($1, $2)', - [tenantA, tenantB] - ); - yield* queryEffect( - admin, - 'delete from core.principals where tenant_id in ($1, $2)', - [tenantA, tenantB] - ); - yield* queryEffect( - admin, - 'delete from core.legal_entities where tenant_id in ($1, $2)', - [tenantA, tenantB] - ); - yield* queryEffect( - admin, - 'delete from core.tenants where tenant_id in ($1, $2)', - [tenantA, tenantB] - ); - yield* queryEffect( - admin, - `drop schema if exists ${schemaName} cascade` - ); - }).pipe(Effect.orDie); - yield* exercise.pipe(Effect.ensuring(release)); - }) + expect( + yield* runForScope({ + authBindingId: bindingA, + authMethod: 'session', + correlationId: randomUUID(), + legalEntityId: entityA, + principalId: principalA, + tenantId: tenantA, + }), + ).toEqual(['tenant-a-entity-a']); + expect( + yield* runForScope({ + authBindingId: bindingA, + authMethod: 'session', + correlationId: randomUUID(), + legalEntityId: entityB, + principalId: principalA, + tenantId: tenantA, + }), + ).toEqual(['tenant-a-entity-b']); + expect( + yield* runForScope({ + authBindingId: bindingB, + authMethod: 'session', + correlationId: randomUUID(), + legalEntityId: entityC, + principalId: principalB, + tenantId: tenantB, + }), + ).toEqual(['tenant-b-entity-c']); + }); + const release = Effect.gen(function* cleanGovernedReadIsolation() { + yield* queryEffect(admin, 'delete from core.data_access_events where tenant_id in ($1, $2)', [tenantA, tenantB]); + yield* queryEffect(admin, 'delete from core.principal_auth_bindings where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]); + yield* queryEffect(admin, 'delete from core.principals where tenant_id in ($1, $2)', [tenantA, tenantB]); + yield* queryEffect(admin, 'delete from core.legal_entities where tenant_id in ($1, $2)', [tenantA, tenantB]); + yield* queryEffect(admin, 'delete from core.tenants where tenant_id in ($1, $2)', [tenantA, tenantB]); + yield* queryEffect(admin, `drop schema if exists ${schemaName} cascade`); + }).pipe(Effect.orDie); + yield* exercise.pipe(Effect.ensuring(release)); + }), ); -it.live( - 'PostgreSQL rejects cross-tenant entity, principal, and Action references', - () => - Effect.gen(function* crossTenantForeignKeys() { - const connections = yield* loadDatabaseConnectionPair(); - const admin = yield* Effect.acquireRelease( - Effect.sync( - () => - new Pool({ connectionString: connections.admin.connectionString }) - ), - (pool) => Effect.promise(() => pool.end()) - ); - const client = yield* Effect.promise(() => admin.connect()); - const tenantA = randomUUID(); - const tenantB = randomUUID(); - const entityA = randomUUID(); - const entityB = randomUUID(); - const principalA = randomUUID(); - const principalB = randomUUID(); - const invocationA = randomUUID(); +it.live('PostgreSQL rejects cross-tenant entity, principal, and Action references', () => + Effect.gen(function* crossTenantForeignKeys() { + const connections = yield* loadDatabaseConnectionPair(); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const client = yield* Effect.promise(() => admin.connect()); + const tenantA = randomUUID(); + const tenantB = randomUUID(); + const entityA = randomUUID(); + const entityB = randomUUID(); + const principalA = randomUUID(); + const principalB = randomUUID(); + const invocationA = randomUUID(); - const expectForeignKeyFailure = ( - statement: string, - parameters: readonly string[] - ) => - Effect.gen(function* rejectCrossTenantReference() { - yield* queryEffect(client, 'savepoint isolation_failure'); - const failure = yield* Effect.flip( - queryTryEffect(client, statement, parameters) - ); - expect(failure.code).toBe('23503'); - yield* queryEffect(client, 'rollback to savepoint isolation_failure'); - }); + const expectForeignKeyFailure = (statement: string, parameters: readonly string[]) => + Effect.gen(function* rejectCrossTenantReference() { + yield* queryEffect(client, 'savepoint isolation_failure'); + const failure = yield* Effect.flip(queryTryEffect(client, statement, parameters)); + expect(failure.code).toBe('23503'); + yield* queryEffect(client, 'rollback to savepoint isolation_failure'); + }); - const exercise = Effect.gen(function* exerciseCrossTenantForeignKeys() { - yield* queryEffect(client, 'begin'); - yield* queryEffect( - client, - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Tenant A', 'active', 'en'), ($2, $4, 'Tenant B', 'active', 'en')`, - [tenantA, tenantB, `isolation-a-${tenantA}`, `isolation-b-${tenantB}`] - ); - yield* queryEffect( - client, - `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $3, 'Entity A', 'CZ', $5, 'active'), ($2, $4, 'Entity B', 'CZ', $6, 'active')`, - [entityA, entityB, tenantA, tenantB, `A-${entityA}`, `B-${entityB}`] - ); - yield* queryEffect( - client, - `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Principal A', 'active'), ($2, $4, 'human', 'Principal B', 'active')`, - [principalA, principalB, tenantA, tenantB] - ); + const exercise = Effect.gen(function* exerciseCrossTenantForeignKeys() { + yield* queryEffect(client, 'begin'); + yield* queryEffect( + client, + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Tenant A', 'active', 'en'), ($2, $4, 'Tenant B', 'active', 'en')`, + [tenantA, tenantB, `isolation-a-${tenantA}`, `isolation-b-${tenantB}`], + ); + yield* queryEffect( + client, + `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $3, 'Entity A', 'CZ', $5, 'active'), ($2, $4, 'Entity B', 'CZ', $6, 'active')`, + [entityA, entityB, tenantA, tenantB, `A-${entityA}`, `B-${entityB}`], + ); + yield* queryEffect( + client, + `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Principal A', 'active'), ($2, $4, 'human', 'Principal B', 'active')`, + [principalA, principalB, tenantA, tenantB], + ); - const invocationInsert = `insert into core.action_invocations (action_invocation_id, tenant_id, legal_entity_id, principal_id, action_key, status, request_hash) values ($1, $2, $3, $4, 'isolation.test', 'received', 'bounded-hash')`; - yield* expectForeignKeyFailure(invocationInsert, [ - randomUUID(), - tenantA, - entityB, - principalA, - ]); - yield* expectForeignKeyFailure(invocationInsert, [ - randomUUID(), - tenantA, - entityA, - principalB, - ]); - yield* queryEffect(client, invocationInsert, [ - invocationA, - tenantA, - entityA, - principalA, - ]); - yield* expectForeignKeyFailure( - `insert into core.tenant_module_state_changes (tenant_id, module_key, new_state, changed_by_principal_id, action_invocation_id, change_source) values ($1, 'core.shell', 'active', $2, $3, 'user')`, - [tenantB, principalB, invocationA] - ); - }); - const release = queryEffect(client, 'rollback').pipe( - Effect.ensuring(Effect.sync(() => client.release())) + const invocationInsert = `insert into core.action_invocations (action_invocation_id, tenant_id, legal_entity_id, principal_id, action_key, status, request_hash) values ($1, $2, $3, $4, 'isolation.test', 'received', 'bounded-hash')`; + yield* expectForeignKeyFailure(invocationInsert, [randomUUID(), tenantA, entityB, principalA]); + yield* expectForeignKeyFailure(invocationInsert, [randomUUID(), tenantA, entityA, principalB]); + yield* queryEffect(client, invocationInsert, [invocationA, tenantA, entityA, principalA]); + yield* expectForeignKeyFailure( + `insert into core.tenant_module_state_changes (tenant_id, module_key, new_state, changed_by_principal_id, action_invocation_id, change_source) values ($1, 'core.shell', 'active', $2, $3, 'user')`, + [tenantB, principalB, invocationA], ); - yield* exercise.pipe(Effect.ensuring(release)); - }) + }); + const release = queryEffect(client, 'rollback').pipe(Effect.ensuring(Effect.sync(() => client.release()))); + yield* exercise.pipe(Effect.ensuring(release)); + }), ); diff --git a/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts b/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts index e7a53d74f..b77cfb2dd 100644 --- a/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts +++ b/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts @@ -16,10 +16,7 @@ import { tenantModuleStates, tenants, } from '../../src/db/schema.ts'; -import type { - InstalledModuleCatalog, - OntosModuleDeploymentContract, -} from '../../src/index.ts'; +import type { InstalledModuleCatalog, OntosModuleDeploymentContract } from '../../src/index.ts'; import { changeTenantModuleStateAction } from '../../src/modules/actions/change-tenant-module-state.action.ts'; import { InstalledModuleCatalogService } from '../../src/modules/catalog.ts'; import { @@ -29,10 +26,7 @@ import { import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; import { testOperationalScopeResolver } from '../fixtures/operational-scope.ts'; import { openActionRuntimeOptions } from '../support/action-runtime-options.ts'; -import { - makeFaultInjectableCoreDatabase, - TestQueryHook, -} from '../support/database-faults.ts'; +import { makeFaultInjectableCoreDatabase, TestQueryHook } from '../support/database-faults.ts'; import { makeInstalledCatalogFixture as catalogFrom } from '../support/installed-catalog.ts'; const tenantOne = '70000000-0000-4000-8000-000000000001'; @@ -54,15 +48,7 @@ const installedContract = (moduleId: string): OntosModuleDeploymentContract => description: 'Integration test module', displayName: 'Integration test module', moduleId, - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ], + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'], }); // State-transition tests deliberately accept arbitrary module IDs without discovery. @@ -73,12 +59,8 @@ const installedCatalog: InstalledModuleCatalog = Object.freeze({ const withDatabase = ( operation: ( - database: DatabaseService - ) => Effect.Effect< - Value, - Error, - InstalledModuleCatalogService | TenantModuleStateService - > + database: DatabaseService, + ) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* tenantModuleStateDatabaseScope() { @@ -88,12 +70,9 @@ const withDatabase = ( Effect.provideService(InstalledModuleCatalogService, { load: Effect.succeed(installedCatalog), }), - Effect.provideService( - TenantModuleStateService, - makeTenantModuleStateService(database) - ) + Effect.provideService(TenantModuleStateService, makeTenantModuleStateService(database)), ); - }) + }), ); const cleanup = withDatabase((database) => @@ -109,11 +88,9 @@ const cleanup = withDatabase((database) => principals, tenants, ]) { - yield* database.executor - .delete(table) - .where(inArray(table.tenantId, tenantIds)); + yield* database.executor.delete(table).where(inArray(table.tenantId, tenantIds)); } - }) + }), ); const setup = Effect.gen(function* initializeTenantModuleStateFixtures() { @@ -172,13 +149,11 @@ const setup = Effect.gen(function* initializeTenantModuleStateFixtures() { tenantId: tenantTwo, }, ]); - }) + }), ); }); -const Fixtures = Layer.effectDiscard( - Effect.acquireRelease(setup, () => cleanup.pipe(Effect.orDie)) -); +const Fixtures = Layer.effectDiscard(Effect.acquireRelease(setup, () => cleanup.pipe(Effect.orDie))); const allowedPermission = { checkActionPermission: () => Effect.succeed('allowed' as const), @@ -192,14 +167,13 @@ const principal = (tenantId = tenantOne, principalId = principalOne) => ({ tenantId, }); -const testModuleKey = (prefix: string, tenantId: string): string => - `${prefix}.id-${tenantId}`; +const testModuleKey = (prefix: string, tenantId: string): string => `${prefix}.id-${tenantId}`; const actionInput = ( moduleKey: string, newState: (typeof changeTenantModuleStateAction.descriptor.payloadSchema)['Type']['newState'], idempotencyKey: string, - trustedPrincipal = principal() + trustedPrincipal = principal(), ) => ({ payload: { moduleKey, @@ -217,33 +191,25 @@ const actionInput = ( }, }); -const failureTag = ( - exit: Exit.Exit -): string | undefined => { +const failureTag = (exit: Exit.Exit): string | undefined => { const failure = Match.value(exit).pipe( Match.tag('Failure', ({ cause }) => Cause.findErrorOption(cause)), Match.tag('Success', () => Option.none()), - Match.exhaustive + Match.exhaustive, ); const tag = Option.match(failure, { onNone: () => Option.none(), - onSome: (error) => - decodeFailureTag(error).pipe(Option.map(({ _tag }) => _tag)), + onSome: (error) => decodeFailureTag(error).pipe(Option.map(({ _tag }) => _tag)), }); return Option.getOrUndefined(tag); }; -const verifyHistoryEvidence = ( - database: DatabaseService, - row: typeof tenantModuleStateChanges.$inferSelect -) => +const verifyHistoryEvidence = (database: DatabaseService, row: typeof tenantModuleStateChanges.$inferSelect) => Effect.gen(function* verifyHistoryEvidenceEffect() { const [invocation] = yield* database.executor .select() .from(actionInvocations) - .where( - eq(actionInvocations.actionInvocationId, row.actionInvocationId ?? '') - ); + .where(eq(actionInvocations.actionInvocationId, row.actionInvocationId ?? '')); expect(invocation?.principalId).toBe(principalOne); expect(invocation?.status).toBe('succeeded'); const audit = yield* database.executor @@ -253,16 +219,10 @@ const verifyHistoryEvidence = ( const access = yield* database.executor .select() .from(dataAccessEvents) - .where( - eq(dataAccessEvents.actionInvocationId, row.actionInvocationId ?? '') - ); - expect(audit.some((event) => event.eventType === 'action.executed')).toBe( - true - ); + .where(eq(dataAccessEvents.actionInvocationId, row.actionInvocationId ?? '')); + expect(audit.some((event) => event.eventType === 'action.executed')).toBe(true); expect(access.length).toBe(1); - expect(access.map((event) => event.targetResourceType)).toEqual([ - 'tenant-module-state', - ]); + expect(access.map((event) => event.targetResourceType)).toEqual(['tenant-module-state']); expect(access.every((event) => event.accessKind === 'read')).toBe(true); }); @@ -280,291 +240,221 @@ const tenantModuleStateTest1 = withDatabase((database) => { moduleKey: 'list.alpha', state: 'active' }, { moduleKey: 'list.zeta', state: 'active' }, ]); - expect(yield* service.listActiveTenantModules(tenantTwo)).toEqual([ - { moduleKey: 'list.alpha', state: 'active' }, - ]); + expect(yield* service.listActiveTenantModules(tenantTwo)).toEqual([{ moduleKey: 'list.alpha', state: 'active' }]); expect(yield* service.listTenantModuleStates(tenantOne)).toEqual([ { moduleKey: 'list.alpha', state: 'active' }, { moduleKey: 'list.inactive', state: 'inactive' }, { moduleKey: 'list.zeta', state: 'active' }, ]); - expect(yield* service.listTenantModuleStates(tenantTwo)).toEqual([ - { moduleKey: 'list.alpha', state: 'active' }, - ]); - }) + expect(yield* service.listTenantModuleStates(tenantTwo)).toEqual([{ moduleKey: 'list.alpha', state: 'active' }]); + }), ); -const tenantModuleStateTest2 = Effect.gen( - function* createAndTransitionTenantModuleState() { - const moduleKey = testModuleKey('testing', tenantOne); - - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return Effect.gen(function* transitionSequence() { - const created = yield* runtime.runAction( - actionInput(moduleKey, 'active', 'create') - ); - expect(created).toEqual({ - moduleKey, - newState: 'active', - previousState: null, - }); - const suspended = yield* runtime.runAction( - actionInput(moduleKey, 'suspended', 'suspend') - ); - expect(suspended).toEqual({ - moduleKey, +const tenantModuleStateTest2 = Effect.gen(function* createAndTransitionTenantModuleState() { + const moduleKey = testModuleKey('testing', tenantOne); + + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + return Effect.gen(function* transitionSequence() { + const created = yield* runtime.runAction(actionInput(moduleKey, 'active', 'create')); + expect(created).toEqual({ + moduleKey, + newState: 'active', + previousState: null, + }); + const suspended = yield* runtime.runAction(actionInput(moduleKey, 'suspended', 'suspend')); + expect(suspended).toEqual({ + moduleKey, + newState: 'suspended', + previousState: 'active', + }); + const reactivated = yield* runtime.runAction(actionInput(moduleKey, 'active', 'reactivate')); + expect(reactivated).toEqual({ + moduleKey, + newState: 'active', + previousState: 'suspended', + }); + }); + }); + + yield* withDatabase((database) => + Effect.gen(function* verifyTenantModuleStateHistory() { + const [current] = yield* database.executor + .select() + .from(tenantModuleStates) + .where(and(eq(tenantModuleStates.tenantId, tenantOne), eq(tenantModuleStates.moduleKey, moduleKey))); + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(and(eq(tenantModuleStateChanges.tenantId, tenantOne), eq(tenantModuleStateChanges.moduleKey, moduleKey))) + .orderBy(asc(tenantModuleStateChanges.occurredAt)); + expect(current?.state).toBe('active'); + expect(history.length).toBe(3); + expect( + history.map(({ changeSource, newState, previousState }) => ({ + changeSource, + newState, + previousState, + })), + ).toEqual([ + { changeSource: 'user', newState: 'active', previousState: null }, + { + changeSource: 'user', newState: 'suspended', previousState: 'active', - }); - const reactivated = yield* runtime.runAction( - actionInput(moduleKey, 'active', 'reactivate') - ); - expect(reactivated).toEqual({ - moduleKey, + }, + { + changeSource: 'user', newState: 'active', previousState: 'suspended', - }); + }, + ]); + expect(current?.lastChangeId).toBe(history.at(-1)?.moduleStateChangeId); + expect(history.every((row) => row.changedByPrincipalId === principalOne)).toBe(true); + expect(history.every((row) => row.actionInvocationId !== null)).toBe(true); + expect(history.every((row) => row.reason?.startsWith('Integration transition to ') === true)).toBe(true); + + yield* Effect.forEach(history, (row) => verifyHistoryEvidence(database, row), { + concurrency: 1, }); - }); + }), + ); +}); +const tenantModuleStateTest3 = Effect.gen(function* allDeclaredTenantModuleStates() { + const otherModuleKey = testModuleKey('other', tenantOne); + const targetModuleKey = testModuleKey('independent', tenantOne); + const transitionCatalog = catalogFrom(installedContract(otherModuleKey), installedContract(targetModuleKey)); + yield* withDatabase((database) => + database.executor.insert(tenantModuleStates).values({ + moduleKey: otherModuleKey, + state: 'inactive', + tenantId: tenantOne, + }), + ); - yield* withDatabase((database) => - Effect.gen(function* verifyTenantModuleStateHistory() { - const [current] = yield* database.executor - .select() - .from(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, tenantOne), - eq(tenantModuleStates.moduleKey, moduleKey) - ) - ); - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where( - and( - eq(tenantModuleStateChanges.tenantId, tenantOne), - eq(tenantModuleStateChanges.moduleKey, moduleKey) - ) - ) - .orderBy(asc(tenantModuleStateChanges.occurredAt)); - expect(current?.state).toBe('active'); - expect(history.length).toBe(3); - expect( - history.map(({ changeSource, newState, previousState }) => ({ - changeSource, - newState, - previousState, - })) - ).toEqual([ - { changeSource: 'user', newState: 'active', previousState: null }, - { - changeSource: 'user', - newState: 'suspended', - previousState: 'active', - }, - { - changeSource: 'user', - newState: 'active', - previousState: 'suspended', - }, - ]); - expect(current?.lastChangeId).toBe(history.at(-1)?.moduleStateChangeId); - expect( - history.every((row) => row.changedByPrincipalId === principalOne) - ).toBe(true); - expect(history.every((row) => row.actionInvocationId !== null)).toBe( - true - ); - expect( - history.every( - (row) => - row.reason?.startsWith('Integration transition to ') === true - ) - ).toBe(true); - - yield* Effect.forEach( - history, - (row) => verifyHistoryEvidence(database, row), - { - concurrency: 1, - } - ); - }) - ); - } -); -const tenantModuleStateTest3 = Effect.gen( - function* allDeclaredTenantModuleStates() { - const otherModuleKey = testModuleKey('other', tenantOne); - const targetModuleKey = testModuleKey('independent', tenantOne); - const transitionCatalog = catalogFrom( - installedContract(otherModuleKey), - installedContract(targetModuleKey) + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - yield* withDatabase((database) => - database.executor.insert(tenantModuleStates).values({ - moduleKey: otherModuleKey, - state: 'inactive', - tenantId: tenantOne, - }) + const withCatalog = ( + effect: Effect.Effect, + ) => + effect.pipe( + Effect.provideService(InstalledModuleCatalogService, { + load: Effect.succeed(transitionCatalog), + }), + ); + const states = ['active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived', 'inactive'] as const; + return Effect.forEach( + states, + (state) => withCatalog(runtime.runAction(actionInput(targetModuleKey, state, `independent-${state}`))), + { concurrency: 1, discard: true }, ); + }); - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - const withCatalog = ( - effect: Effect.Effect< - Value, - Error, - Requirements | InstalledModuleCatalogService - > - ) => - effect.pipe( - Effect.provideService(InstalledModuleCatalogService, { - load: Effect.succeed(transitionCatalog), - }) - ); - const states = [ + yield* withDatabase((database) => + Effect.gen(function* verifyAllDeclaredStates() { + const stateRows = yield* database.executor + .select({ + moduleKey: tenantModuleStates.moduleKey, + state: tenantModuleStates.state, + }) + .from(tenantModuleStates) + .where(inArray(tenantModuleStates.moduleKey, [otherModuleKey, targetModuleKey])); + const historyRows = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, targetModuleKey)); + expect(Object.fromEntries(stateRows.map((row) => [row.moduleKey, row.state]))).toEqual({ + [otherModuleKey]: 'inactive', + [targetModuleKey]: 'inactive', + }); + expect(historyRows.map(({ newState }) => newState).toSorted()).toEqual([ 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', 'archived', + 'deprecated', 'inactive', - ] as const; - return Effect.forEach( - states, - (state) => - withCatalog( - runtime.runAction( - actionInput(targetModuleKey, state, `independent-${state}`) - ) - ), - { concurrency: 1, discard: true } - ); - }); + 'quarantined', + 'read_only', + 'suspended', + ]); + }), + ); +}); +const tenantModuleStateTest4 = Effect.gen(function* idempotentReplayAndSameStateRejection() { + const moduleKey = testModuleKey('idempotency', tenantOne); + const input = actionInput(moduleKey, 'active', 'same-intent'); - yield* withDatabase((database) => - Effect.gen(function* verifyAllDeclaredStates() { - const stateRows = yield* database.executor - .select({ - moduleKey: tenantModuleStates.moduleKey, - state: tenantModuleStates.state, - }) - .from(tenantModuleStates) - .where( - inArray(tenantModuleStates.moduleKey, [ - otherModuleKey, - targetModuleKey, - ]) - ); - const historyRows = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, targetModuleKey)); - expect( - Object.fromEntries(stateRows.map((row) => [row.moduleKey, row.state])) - ).toEqual({ - [otherModuleKey]: 'inactive', - [targetModuleKey]: 'inactive', - }); - expect(historyRows.map(({ newState }) => newState).toSorted()).toEqual([ - 'active', - 'archived', - 'deprecated', - 'inactive', - 'quarantined', - 'read_only', - 'suspended', - ]); - }) + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - } -); -const tenantModuleStateTest4 = Effect.gen( - function* idempotentReplayAndSameStateRejection() { - const moduleKey = testModuleKey('idempotency', tenantOne); - const input = actionInput(moduleKey, 'active', 'same-intent'); - - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return runtime.runAction(input); - }); - const replay = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return Effect.exit(runtime.runAction(input)); - }); - expect(failureTag(replay)).toBe('ActionAlreadyCommitted'); - - const unchanged = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return Effect.exit( - runtime.runAction(actionInput(moduleKey, 'active', 'same-state')) - ); - }); - expect(failureTag(unchanged)).toBe('TenantModuleStateUnchangedError'); - - yield* withDatabase((database) => - Effect.gen(function* verifyIdempotentEvidence() { - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - expect(history.length).toBe(1); - const unchangedInvocation = yield* database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'same-state')); - expect(unchangedInvocation.length).toBe(1); - const invocationId = unchangedInvocation[0]?.actionInvocationId ?? ''; - const unchangedAudit = yield* database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocationId)); - expect(unchangedAudit.length).toBe(0); - const unchangedAccess = yield* database.executor - .select() - .from(dataAccessEvents) - .where(eq(dataAccessEvents.actionInvocationId, invocationId)); - expect(unchangedAccess.length).toBe(0); - }) + return runtime.runAction(input); + }); + const replay = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - } -); -const withTenantStateWriteFailure = ( - database: DatabaseService -): DatabaseService => { + return Effect.exit(runtime.runAction(input)); + }); + expect(failureTag(replay)).toBe('ActionAlreadyCommitted'); + + const unchanged = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'same-state'))); + }); + expect(failureTag(unchanged)).toBe('TenantModuleStateUnchangedError'); + + yield* withDatabase((database) => + Effect.gen(function* verifyIdempotentEvidence() { + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); + expect(history.length).toBe(1); + const unchangedInvocation = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, 'same-state')); + expect(unchangedInvocation.length).toBe(1); + const invocationId = unchangedInvocation[0]?.actionInvocationId ?? ''; + const unchangedAudit = yield* database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocationId)); + expect(unchangedAudit.length).toBe(0); + const unchangedAccess = yield* database.executor + .select() + .from(dataAccessEvents) + .where(eq(dataAccessEvents.actionInvocationId, invocationId)); + expect(unchangedAccess.length).toBe(0); + }), + ); +}); +const withTenantStateWriteFailure = (database: DatabaseService): DatabaseService => { const transaction: DatabaseService['executor']['transaction'] = (operation) => database.executor.transaction((currentTransaction) => operation(currentTransaction).pipe( @@ -576,138 +466,112 @@ const withTenantStateWriteFailure = ( cause: new Error('Injected SQL failure'), message: 'Injected current-state persistence failure', }), - }) + }), ) - : Effect.void - ) - ) + : Effect.void, + ), + ), ); - const transactionOverride = { transaction } satisfies Pick< - DatabaseService['executor'], - 'transaction' - >; - const executor: DatabaseService['executor'] = Object.assign( - Object.create(database.executor), - transactionOverride - ); + const transactionOverride = { transaction } satisfies Pick; + const executor: DatabaseService['executor'] = Object.assign(Object.create(database.executor), transactionOverride); return { executor }; }; -const tenantModuleStateTest5 = Effect.gen( - function* rollbackFailedTenantModuleStateWrite() { - const moduleKey = testModuleKey('rollback', tenantOne); - const failure = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - withTenantStateWriteFailure(database), - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return Effect.exit( - runtime.runAction(actionInput(moduleKey, 'active', 'forced-failure')) - ); - }); - expect( - failureTag(failure), - Exit.isFailure(failure) ? Cause.pretty(failure.cause) : 'success' - ).toBe('TenantModuleStatePersistenceUnavailableError'); - - yield* withDatabase((database) => - Effect.gen(function* verifyFailedWriteRollback() { - const states = yield* database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleKey)); - expect(states.length).toBe(0); - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - expect(history.length).toBe(0); - const [invocation] = yield* database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'forced-failure')); - expect(invocation).toBeDefined(); - const audits = yield* database.executor - .select() - .from(auditEvents) - .where( - eq( - auditEvents.actionInvocationId, - invocation?.actionInvocationId ?? '' - ) - ); - expect(audits.length).toBe(0); - }) +const tenantModuleStateTest5 = Effect.gen(function* rollbackFailedTenantModuleStateWrite() { + const moduleKey = testModuleKey('rollback', tenantOne); + const failure = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + withTenantStateWriteFailure(database), + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - } -); -const tenantModuleStateTest6 = Effect.gen( - function* serializeConcurrentTenantModuleStateTransitions() { - const moduleKey = testModuleKey('concurrency', tenantOne); - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return runtime.runAction( - actionInput(moduleKey, 'inactive', 'concurrent-initial') - ); - }); + return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'forced-failure'))); + }); + expect(failureTag(failure), Exit.isFailure(failure) ? Cause.pretty(failure.cause) : 'success').toBe( + 'TenantModuleStatePersistenceUnavailableError', + ); - const exits = yield* Effect.forEach( - [ - ['active', 'concurrent-active'], - ['suspended', 'concurrent-suspended'], - ] as const, - ([state, key]) => - withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions - ); - return Effect.exit( - runtime.runAction(actionInput(moduleKey, state, key)) - ); - }), - { concurrency: 'unbounded' } + yield* withDatabase((database) => + Effect.gen(function* verifyFailedWriteRollback() { + const states = yield* database.executor + .select() + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, moduleKey)); + expect(states.length).toBe(0); + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); + expect(history.length).toBe(0); + const [invocation] = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, 'forced-failure')); + expect(invocation).toBeDefined(); + const audits = yield* database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocation?.actionInvocationId ?? '')); + expect(audits.length).toBe(0); + }), + ); +}); +const tenantModuleStateTest6 = Effect.gen(function* serializeConcurrentTenantModuleStateTransitions() { + const moduleKey = testModuleKey('concurrency', tenantOne); + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - expect(exits.every(Exit.isSuccess)).toBe(true); - - yield* withDatabase((database) => - Effect.gen(function* verifySerializedTransitions() { - const [current] = yield* database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleKey)); - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - expect(history.length).toBe(3); - const last = history.find( - (row) => row.moduleStateChangeId === current?.lastChangeId - ); - const concurrentFirst = history.find( - (row) => - row.previousState === 'inactive' && - row.moduleStateChangeId !== last?.moduleStateChangeId + return runtime.runAction(actionInput(moduleKey, 'inactive', 'concurrent-initial')); + }); + + const exits = yield* Effect.forEach( + [ + ['active', 'concurrent-active'], + ['suspended', 'concurrent-suspended'], + ] as const, + ([state, key]) => + withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - expect(last).toBeDefined(); - expect(concurrentFirst).toBeDefined(); - expect(last?.previousState).toBe(concurrentFirst?.newState); - expect(current?.state).toBe(last?.newState); - }) - ); - } -); + return Effect.exit(runtime.runAction(actionInput(moduleKey, state, key))); + }), + { concurrency: 'unbounded' }, + ); + expect(exits.every(Exit.isSuccess)).toBe(true); + + yield* withDatabase((database) => + Effect.gen(function* verifySerializedTransitions() { + const [current] = yield* database.executor + .select() + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, moduleKey)); + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); + expect(history.length).toBe(3); + const last = history.find((row) => row.moduleStateChangeId === current?.lastChangeId); + const concurrentFirst = history.find( + (row) => row.previousState === 'inactive' && row.moduleStateChangeId !== last?.moduleStateChangeId, + ); + expect(last).toBeDefined(); + expect(concurrentFirst).toBeDefined(); + expect(last?.previousState).toBe(concurrentFirst?.newState); + expect(current?.state).toBe(last?.newState); + }), + ); +}); const tenantModuleStateTest7 = Effect.gen(function* deriveTrustedTenantScope() { const moduleKey = testModuleKey('isolation', tenantOne); yield* withDatabase((database) => @@ -715,7 +579,7 @@ const tenantModuleStateTest7 = Effect.gen(function* deriveTrustedTenantScope() { moduleKey, state: 'active', tenantId: tenantTwo, - }) + }), ); yield* withDatabase((database) => { @@ -724,11 +588,9 @@ const tenantModuleStateTest7 = Effect.gen(function* deriveTrustedTenantScope() { makeActionRepository(), allowedPermission, testOperationalScopeResolver, - openActionRuntimeOptions - ); - return runtime.runAction( - actionInput(moduleKey, 'suspended', 'tenant-isolation') + openActionRuntimeOptions, ); + return runtime.runAction(actionInput(moduleKey, 'suspended', 'tenant-isolation')); }); yield* withDatabase((database) => @@ -741,51 +603,40 @@ const tenantModuleStateTest7 = Effect.gen(function* deriveTrustedTenantScope() { .from(tenantModuleStates) .where(eq(tenantModuleStates.moduleKey, moduleKey)) .orderBy(asc(tenantModuleStates.tenantId)); - expect( - Object.fromEntries(rows.map((row) => [row.tenantId, row.state])) - ).toEqual({ + expect(Object.fromEntries(rows.map((row) => [row.tenantId, row.state]))).toEqual({ [tenantOne]: 'suspended', [tenantTwo]: 'active', }); - }) + }), ); }); -it.layer(Fixtures, { excludeTestServices: true })( - 'tenant module state', - (suite) => { - suite.effect( - 'lists exact active rows and all states for one trusted tenant in module-key order', - () => tenantModuleStateTest1 - ); +it.layer(Fixtures, { excludeTestServices: true })('tenant module state', (suite) => { + suite.effect( + 'lists exact active rows and all states for one trusted tenant in module-key order', + () => tenantModuleStateTest1, + ); - suite.effect( - 'atomically creates and transitions state with truthful Action history and evidence', - () => tenantModuleStateTest2 - ); + suite.effect( + 'atomically creates and transitions state with truthful Action history and evidence', + () => tenantModuleStateTest2, + ); - suite.effect( - 'supports every declared state independently of other installed module states', - () => tenantModuleStateTest3 - ); + suite.effect( + 'supports every declared state independently of other installed module states', + () => tenantModuleStateTest3, + ); - suite.effect( - 'idempotent replay and same-state rejection create no duplicate history or evidence', - () => tenantModuleStateTest4 - ); + suite.effect( + 'idempotent replay and same-state rejection create no duplicate history or evidence', + () => tenantModuleStateTest4, + ); - suite.effect( - 'rolls back history and Action evidence when current-state persistence fails', - () => tenantModuleStateTest5 - ); + suite.effect( + 'rolls back history and Action evidence when current-state persistence fails', + () => tenantModuleStateTest5, + ); - suite.effect( - 'serializes concurrent transitions into one truthful history chain', - () => tenantModuleStateTest6 - ); + suite.effect('serializes concurrent transitions into one truthful history chain', () => tenantModuleStateTest6); - suite.effect( - 'derives tenant scope only from the trusted principal', - () => tenantModuleStateTest7 - ); - } -); + suite.effect('derives tenant scope only from the trusted principal', () => tenantModuleStateTest7); +}); diff --git a/app/packages/core-runtime/tests/support/action-runtime-options.ts b/app/packages/core-runtime/tests/support/action-runtime-options.ts index b2d230079..9d4350d23 100644 --- a/app/packages/core-runtime/tests/support/action-runtime-options.ts +++ b/app/packages/core-runtime/tests/support/action-runtime-options.ts @@ -5,7 +5,4 @@ import { openModuleStateGate } from './open-module-state-gate.ts'; export const openActionRuntimeOptions = { moduleEntrypointGateway: openModuleEntrypointGateway, moduleStateGate: openModuleStateGate, -} satisfies Pick< - ActionRuntimeOptions, - 'moduleEntrypointGateway' | 'moduleStateGate' ->; +} satisfies Pick; diff --git a/app/packages/core-runtime/tests/support/database-faults.ts b/app/packages/core-runtime/tests/support/database-faults.ts index 94895dfb2..71cf8fa98 100644 --- a/app/packages/core-runtime/tests/support/database-faults.ts +++ b/app/packages/core-runtime/tests/support/database-faults.ts @@ -12,64 +12,41 @@ import { coreRelations } from '../../src/db/schema.ts'; /** Per-fiber faults run where the native SQL driver executes a statement. */ export const TestQueryHook = Context.Reference('TestQueryHook', { - defaultValue: - (): ((statement: string) => Effect.Effect) => () => - Effect.void, + defaultValue: (): ((statement: string) => Effect.Effect) => () => Effect.void, }); -export const makeFaultInjectableCoreDatabase = Effect.fn( - 'makeFaultInjectableCoreDatabase' -)(function* makeFaultInjectableCoreDatabase( - configuration: DatabaseConfigValue -) { - const pool = yield* acquirePoolResource( - () => new Pool({ connectionString: configuration.connectionString }) - ); - const reactivity = yield* Reactivity.make; - const source = yield* PgClient.fromPool({ - acquire: Effect.succeed(pool), - }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)); - const before = (statement: string) => - TestQueryHook.pipe(Effect.flatMap((hook) => hook(statement))); - const acquirer = source.reserve.pipe( - Effect.map((connection): Connection => ({ - ...connection, - execute: (statement, params, transform) => - before(statement).pipe( - Effect.andThen(() => connection.execute(statement, params, transform)) - ), - executeRaw: (statement, params) => - before(statement).pipe( - Effect.andThen(() => connection.executeRaw(statement, params)) - ), - executeUnprepared: (statement, params, transform) => - before(statement).pipe( - Effect.andThen(() => - connection.executeUnprepared(statement, params, transform) - ) - ), - executeValues: (statement, params) => - before(statement).pipe( - Effect.andThen(() => connection.executeValues(statement, params)) - ), - executeValuesUnprepared: (statement, params) => - before(statement).pipe( - Effect.andThen(() => - connection.executeValuesUnprepared(statement, params) - ) - ), - })) - ); - const client = yield* PgClient.makeWith({ - acquirer, - config: {}, - listenAcquirer: Effect.die( - 'This test database does not support notifications' - ), - transactionAcquirer: acquirer, - }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)); - const executor = yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client) - ); - return { executor }; -}); +export const makeFaultInjectableCoreDatabase = Effect.fn('makeFaultInjectableCoreDatabase')( + function* makeFaultInjectableCoreDatabase(configuration: DatabaseConfigValue) { + const pool = yield* acquirePoolResource(() => new Pool({ connectionString: configuration.connectionString })); + const reactivity = yield* Reactivity.make; + const source = yield* PgClient.fromPool({ + acquire: Effect.succeed(pool), + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)); + const before = (statement: string) => TestQueryHook.pipe(Effect.flatMap((hook) => hook(statement))); + const acquirer = source.reserve.pipe( + Effect.map((connection): Connection => ({ + ...connection, + execute: (statement, params, transform) => + before(statement).pipe(Effect.andThen(() => connection.execute(statement, params, transform))), + executeRaw: (statement, params) => + before(statement).pipe(Effect.andThen(() => connection.executeRaw(statement, params))), + executeUnprepared: (statement, params, transform) => + before(statement).pipe(Effect.andThen(() => connection.executeUnprepared(statement, params, transform))), + executeValues: (statement, params) => + before(statement).pipe(Effect.andThen(() => connection.executeValues(statement, params))), + executeValuesUnprepared: (statement, params) => + before(statement).pipe(Effect.andThen(() => connection.executeValuesUnprepared(statement, params))), + })), + ); + const client = yield* PgClient.makeWith({ + acquirer, + config: {}, + listenAcquirer: Effect.die('This test database does not support notifications'), + transactionAcquirer: acquirer, + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)); + const executor = yield* makeWithDefaults({ relations: coreRelations }).pipe( + Effect.provideService(PgClient.PgClient, client), + ); + return { executor }; + }, +); diff --git a/app/packages/core-runtime/tests/support/database.ts b/app/packages/core-runtime/tests/support/database.ts index 08469c985..1a6f235b5 100644 --- a/app/packages/core-runtime/tests/support/database.ts +++ b/app/packages/core-runtime/tests/support/database.ts @@ -13,10 +13,7 @@ import { testSqlConnection } from './sql-connection.ts'; /** Native SQL connection fixture; Drizzle and Effect own query and transaction execution. */ export const makeTestDatabase = ( - execute: ( - sql: string, - params: readonly unknown[] - ) => Effect.Effect + execute: (sql: string, params: readonly unknown[]) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* makeNativeTestDatabase() { @@ -25,45 +22,31 @@ export const makeTestDatabase = ( const client = yield* PgClient.makeWith({ acquirer: Effect.succeed(connection), config: {}, - listenAcquirer: Effect.die( - 'This fixture does not support notifications' - ), + listenAcquirer: Effect.die('This fixture does not support notifications'), transactionAcquirer: Effect.succeed(connection), - }).pipe( - Effect.provideService(Reactivity.Reactivity, reactivity), - Effect.orDie - ); + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); return yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client) + Effect.provideService(PgClient.PgClient, client), ); - }) + }), ); /** The caller owns the pool and keeps this scope open until its tests finish. */ -export const makeTestDatabaseFromPool = ( - pool: Pool, - relations: Relations -) => +export const makeTestDatabaseFromPool = (pool: Pool, relations: Relations) => Effect.gen(function* makePoolTestDatabase() { const reactivity = yield* Reactivity.make; const client = yield* PgClient.fromPool({ acquire: Effect.succeed(pool), }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)); - return yield* makeWithDefaults({ relations }).pipe( - Effect.provideService(PgClient.PgClient, client) - ); + return yield* makeWithDefaults({ relations }).pipe(Effect.provideService(PgClient.PgClient, client)); }); /** Fresh pools per execution; the caller's scope releases them after test cleanup. */ -export const testDatabasePools = Effect.gen( - function* acquireTestDatabasePools() { - const connections = yield* loadDatabaseConnectionPair(); - const admin = yield* acquirePoolResource( - () => new Pool({ connectionString: connections.admin.connectionString }) - ); - const runtimePool = yield* acquirePoolResource( - () => new Pool({ connectionString: connections.runtime.connectionString }) - ); - return { admin, runtimePool }; - } -); +export const testDatabasePools = Effect.gen(function* acquireTestDatabasePools() { + const connections = yield* loadDatabaseConnectionPair(); + const admin = yield* acquirePoolResource(() => new Pool({ connectionString: connections.admin.connectionString })); + const runtimePool = yield* acquirePoolResource( + () => new Pool({ connectionString: connections.runtime.connectionString }), + ); + return { admin, runtimePool }; +}); diff --git a/app/packages/core-runtime/tests/support/fixture-cleanup.ts b/app/packages/core-runtime/tests/support/fixture-cleanup.ts index fa53edd1f..4f7e29b63 100644 --- a/app/packages/core-runtime/tests/support/fixture-cleanup.ts +++ b/app/packages/core-runtime/tests/support/fixture-cleanup.ts @@ -5,7 +5,5 @@ import { Effect } from 'effect'; * the first deletion that fails. Callers build the delete Effects inline, which keeps the * owned table order explicit at the call site instead of behind a generic cascade. */ -export const purgeFixtureRows = ( - deletions: readonly Effect.Effect[] -): Effect.Effect => +export const purgeFixtureRows = (deletions: readonly Effect.Effect[]): Effect.Effect => Effect.all(deletions, { concurrency: 1, discard: true }); diff --git a/app/packages/core-runtime/tests/support/installed-catalog.ts b/app/packages/core-runtime/tests/support/installed-catalog.ts index c8228c928..e78842c0e 100644 --- a/app/packages/core-runtime/tests/support/installed-catalog.ts +++ b/app/packages/core-runtime/tests/support/installed-catalog.ts @@ -1,32 +1,22 @@ -import type { - InstalledModuleCatalog, - OntosModuleDeploymentContract, -} from '../../src/index.ts'; +import type { InstalledModuleCatalog, OntosModuleDeploymentContract } from '../../src/index.ts'; export const makeInstalledCatalogFixture = ( ...contracts: readonly OntosModuleDeploymentContract[] ): InstalledModuleCatalog => { - const byModuleId = new Map( - contracts.map((item) => [item.manifest.module.id, item]) - ); + const byModuleId = new Map(contracts.map((item) => [item.manifest.module.id, item])); return Object.freeze({ contracts: Object.freeze([...contracts]), - deploymentAppIds: Object.freeze( - contracts.map(({ deployment }) => deployment.appId) - ), + deploymentAppIds: Object.freeze(contracts.map(({ deployment }) => deployment.appId)), deploymentStatuses: Object.freeze( contracts.map((contract) => ({ appId: contract.deployment.appId, moduleId: contract.manifest.module.id, status: 'available' as const, - })) + })), ), - getByDeploymentAppId: (appId: string) => - contracts.find(({ deployment }) => deployment.appId === appId), + getByDeploymentAppId: (appId: string) => contracts.find(({ deployment }) => deployment.appId === appId), getByModuleId: (moduleId: string) => byModuleId.get(moduleId), - moduleIds: Object.freeze( - contracts.map(({ manifest }) => manifest.module.id) - ), + moduleIds: Object.freeze(contracts.map(({ manifest }) => manifest.module.id)), outboxSubscriptions: Object.freeze([]), }); }; diff --git a/app/packages/core-runtime/tests/support/open-module-entrypoint-gateway.ts b/app/packages/core-runtime/tests/support/open-module-entrypoint-gateway.ts index 8549de61b..37e97d518 100644 --- a/app/packages/core-runtime/tests/support/open-module-entrypoint-gateway.ts +++ b/app/packages/core-runtime/tests/support/open-module-entrypoint-gateway.ts @@ -12,24 +12,17 @@ const unavailable = () => reason: 'Module state could not be checked safely', }); -const prepareSnapshotInput = ( - context: Input, - entrypoints: readonly ModuleEntrypointDescriptor[] -) => +const prepareSnapshotInput = (context: Input, entrypoints: readonly ModuleEntrypointDescriptor[]) => decodeTrustedPrincipalContext(context).pipe( Effect.mapError(unavailable), - Effect.flatMap((trustedContext) => - openModuleStateGate.prepareSnapshot(trustedContext.tenantId, entrypoints) - ) + Effect.flatMap((trustedContext) => openModuleStateGate.prepareSnapshot(trustedContext.tenantId, entrypoints)), ); -const run: ModuleEntrypointGatewayService['run'] = (input) => - input.authorize.pipe(Effect.andThen(input.load)); +const run: ModuleEntrypointGatewayService['run'] = (input) => input.authorize.pipe(Effect.andThen(input.load)); -export const openModuleEntrypointGateway: ModuleEntrypointGatewayService = - Object.freeze({ - check: () => Effect.void, - prepareSnapshot: prepareSnapshotInput, - prepareSnapshotInput, - run, - }); +export const openModuleEntrypointGateway: ModuleEntrypointGatewayService = Object.freeze({ + check: () => Effect.void, + prepareSnapshot: prepareSnapshotInput, + prepareSnapshotInput, + run, +}); diff --git a/app/packages/core-runtime/tests/support/open-module-state-gate.ts b/app/packages/core-runtime/tests/support/open-module-state-gate.ts index fcfa1d26b..604f90377 100644 --- a/app/packages/core-runtime/tests/support/open-module-state-gate.ts +++ b/app/packages/core-runtime/tests/support/open-module-state-gate.ts @@ -2,24 +2,15 @@ import { Effect } from 'effect'; import type { ModuleStateGateService } from '../../src/modules/module-state-gate.ts'; -const prepareSnapshot: ModuleStateGateService['prepareSnapshot'] = ( - tenantId, - entrypoints -) => +const prepareSnapshot: ModuleStateGateService['prepareSnapshot'] = (tenantId, entrypoints) => Effect.succeed( Object.freeze({ - entrypointKeys: Object.freeze( - entrypoints.map(({ entrypointKey }) => entrypointKey) - ), + entrypointKeys: Object.freeze(entrypoints.map(({ entrypointKey }) => entrypointKey)), moduleKeys: Object.freeze([ - ...new Set( - entrypoints - .filter((entrypoint) => entrypoint.scope === 'tenant') - .map(({ moduleKey }) => moduleKey) - ), + ...new Set(entrypoints.filter((entrypoint) => entrypoint.scope === 'tenant').map(({ moduleKey }) => moduleKey)), ]), tenantId, - }) + }), ); export const openModuleStateGate: ModuleStateGateService = Object.freeze({ diff --git a/app/packages/core-runtime/tests/support/permission-write-error.ts b/app/packages/core-runtime/tests/support/permission-write-error.ts index e46cd0cf1..24251bfb9 100644 --- a/app/packages/core-runtime/tests/support/permission-write-error.ts +++ b/app/packages/core-runtime/tests/support/permission-write-error.ts @@ -1,8 +1,5 @@ import { Schema } from 'effect'; -export class TestWriteError extends Schema.TaggedError()( - 'TestWriteError', - { - reason: Schema.String, - } -) {} +export class TestWriteError extends Schema.TaggedError()('TestWriteError', { + reason: Schema.String, +}) {} diff --git a/app/packages/core-runtime/tests/support/sql-connection.ts b/app/packages/core-runtime/tests/support/sql-connection.ts index f15eee1af..944740229 100644 --- a/app/packages/core-runtime/tests/support/sql-connection.ts +++ b/app/packages/core-runtime/tests/support/sql-connection.ts @@ -3,18 +3,14 @@ import type { Connection } from 'effect/unstable/sql/SqlConnection'; import type { SqlError } from 'effect/unstable/sql/SqlError'; export const testSqlConnection = ( - execute: ( - sql: string, - params: readonly unknown[] - ) => Effect.Effect + execute: (sql: string, params: readonly unknown[]) => Effect.Effect, ): Connection => { const values = (sql: string, params: readonly unknown[]) => execute(sql, params).pipe(Effect.map((rows) => rows.map(Object.values))); const connection: Connection = { execute, executeRaw: execute, - executeStream: (sql, params) => - Stream.fromIterableEffect(execute(sql, params)), + executeStream: (sql, params) => Stream.fromIterableEffect(execute(sql, params)), executeUnprepared: execute, executeValues: values, executeValuesUnprepared: values, diff --git a/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts b/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts index d135e3154..c3bb73a26 100644 --- a/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts +++ b/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts @@ -24,43 +24,35 @@ const input = { surface: 'action' as const, }; -it.effect( - 'active, baselined report-only compatibility preserves only missing-policy behavior', - () => - Effect.gen(function* authorizationRollout() { - const events: AuthorizationWouldDenyEvent[] = []; - expect( - decideAuthorizationRollout(input, { - contract, - emit: (event) => { - events.push(event); - }, - }) - ).toBe('allowed'); - expect(events).toEqual([ - { - denialReason: 'missing_policy', - entrypointKey: 'contacts.create-contact', - inventoryHash: 'inventory-hash', - policyClass: 'action_execution', - schemaVersion: 1, - sourceRevision: 'source-revision', - surface: 'action', - timestamp: '2026-09-10T00:00:00.000Z', - type: 'authorization.would_deny', +it.effect('active, baselined report-only compatibility preserves only missing-policy behavior', () => + Effect.gen(function* authorizationRollout() { + const events: AuthorizationWouldDenyEvent[] = []; + expect( + decideAuthorizationRollout(input, { + contract, + emit: (event) => { + events.push(event); }, - ]); - expect( - (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - events - )).includes('principal') - ).toBe(false); - expect( - (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - events - )).includes('tenant') - ).toBe(false); - }) + }), + ).toBe('allowed'); + expect(events).toEqual([ + { + denialReason: 'missing_policy', + entrypointKey: 'contacts.create-contact', + inventoryHash: 'inventory-hash', + policyClass: 'action_execution', + schemaVersion: 1, + sourceRevision: 'source-revision', + surface: 'action', + timestamp: '2026-09-10T00:00:00.000Z', + type: 'authorization.would_deny', + }, + ]); + expect((yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(events)).includes('principal')).toBe( + false, + ); + expect((yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(events)).includes('tenant')).toBe(false); + }), ); it('enforced, expired, and unbaselined entrypoints deny without evidence', () => { @@ -76,7 +68,7 @@ it('enforced, expired, and unbaselined entrypoints deny without evidence', () => emit: (event) => { events.push(event); }, - }) + }), ).toBe('denied'); expect(events).toEqual([]); } @@ -91,18 +83,13 @@ it('a candidate allow never broadens a denial from the current authorization pat emit: () => { expect.unreachable(); }, - } - ) + }, + ), ).toBe('denied'); }); it('all protected surfaces keep credential, tenancy, module, replay, and infrastructure failures non-bypassable', () => { - for (const surface of [ - 'action', - 'capability_issuance', - 'route', - 'worker', - ] as const) { + for (const surface of ['action', 'capability_issuance', 'route', 'worker'] as const) { for (const denialReason of [ 'cross_tenant', 'expired_credential', @@ -120,8 +107,8 @@ it('all protected surfaces keep credential, tenancy, module, replay, and infrast emit: () => { expect.unreachable(); }, - } - ) + }, + ), ).toBe('denied'); } } diff --git a/app/packages/core-runtime/tests/unit/action-collector.test.ts b/app/packages/core-runtime/tests/unit/action-collector.test.ts index 79b3b1374..cb8e89a44 100644 --- a/app/packages/core-runtime/tests/unit/action-collector.test.ts +++ b/app/packages/core-runtime/tests/unit/action-collector.test.ts @@ -37,44 +37,35 @@ const makeCollector = () => Schema.Struct({ checkpoint: Schema.String, nested: Schema.optionalKey(Schema.Json), - }) + }), ); -it.effect( - 'preserves event order, multiple messages, and events without messages', - () => - Effect.gen(function* preservesEventOrderMultipleMessagesAndEventsWithout() { - const collector = makeCollector(); - const first = yield* collector.addDomainEvent(event('first')); - yield* collector.addDomainEvent(event('second')); - yield* collector.addOutboxMessage(first, message('counter.project')); - yield* collector.addOutboxMessage(first, message('counter.notify')); - yield* collector.recordDataAccess({ - accessKind: 'read', - queryHash: 'query-hash', - resultCount: 1, - servingModuleKey: 'shell.core', - targetModuleKey: 'shell.core', - targetResourceId: 'first', - targetResourceType: 'counter', - }); - - const snapshot = collector.snapshot(); - - expect( - snapshot.domainEvents.map((item) => item.subjectResourceId) - ).toEqual(['first', 'second']); - expect( - snapshot.outboxMessages.map((item) => [ - item.domainEventIndex, - item.message.topic, - ]) - ).toEqual([ - [0, 'counter.project'], - [0, 'counter.notify'], - ]); - expect(snapshot.dataAccessEvents.length).toBe(1); - }) +it.effect('preserves event order, multiple messages, and events without messages', () => + Effect.gen(function* preservesEventOrderMultipleMessagesAndEventsWithout() { + const collector = makeCollector(); + const first = yield* collector.addDomainEvent(event('first')); + yield* collector.addDomainEvent(event('second')); + yield* collector.addOutboxMessage(first, message('counter.project')); + yield* collector.addOutboxMessage(first, message('counter.notify')); + yield* collector.recordDataAccess({ + accessKind: 'read', + queryHash: 'query-hash', + resultCount: 1, + servingModuleKey: 'shell.core', + targetModuleKey: 'shell.core', + targetResourceId: 'first', + targetResourceType: 'counter', + }); + + const snapshot = collector.snapshot(); + + expect(snapshot.domainEvents.map((item) => item.subjectResourceId)).toEqual(['first', 'second']); + expect(snapshot.outboxMessages.map((item) => [item.domainEventIndex, item.message.topic])).toEqual([ + [0, 'counter.project'], + [0, 'counter.notify'], + ]); + expect(snapshot.dataAccessEvents.length).toBe(1); + }), ); it.effect('rejects orphan and foreign Domain Event references', () => @@ -83,204 +74,156 @@ it.effect('rejects orphan and foreign Domain Event references', () => const second = makeCollector(); const foreign = yield* first.addDomainEvent(event('foreign')); - const foreignError = yield* Effect.flip( - second.addOutboxMessage(foreign, message('counter.project')) - ); - const orphanError = yield* Effect.flip( - second.addOutboxMessageInput({}, message('counter.project')) - ); + const foreignError = yield* Effect.flip(second.addOutboxMessage(foreign, message('counter.project'))); + const orphanError = yield* Effect.flip(second.addOutboxMessageInput({}, message('counter.project'))); expect(Predicate.isTagged(foreignError, 'ActionCollectorError')).toBe(true); expect(Predicate.isTagged(orphanError, 'ActionCollectorError')).toBe(true); - }) + }), ); -it.effect( - 'does not expose externally mutable collector arrays or captured payloads', - () => - Effect.gen(function* doesNotExposeExternallyMutableCollectorArraysOr() { - const collector = makeCollector(); - const mutablePayload = { value: 1 }; - yield* collector.addDomainEvent({ - ...event('immutable'), - payloadJson: { id: 'immutable', mutable: mutablePayload }, - }); - mutablePayload.value = 2; - - const snapshot = collector.snapshot(); - - expect(Object.isFrozen(snapshot.domainEvents)).toBe(true); - expect(Object.isFrozen(snapshot.domainEvents[0])).toBe(true); - expect(snapshot.domainEvents[0]?.payloadJson).toEqual({ - id: 'immutable', - mutable: { value: 1 }, +it.effect('does not expose externally mutable collector arrays or captured payloads', () => + Effect.gen(function* doesNotExposeExternallyMutableCollectorArraysOr() { + const collector = makeCollector(); + const mutablePayload = { value: 1 }; + yield* collector.addDomainEvent({ + ...event('immutable'), + payloadJson: { id: 'immutable', mutable: mutablePayload }, + }); + mutablePayload.value = 2; + + const snapshot = collector.snapshot(); + + expect(Object.isFrozen(snapshot.domainEvents)).toBe(true); + expect(Object.isFrozen(snapshot.domainEvents[0])).toBe(true); + expect(snapshot.domainEvents[0]?.payloadJson).toEqual({ + id: 'immutable', + mutable: { value: 1 }, + }); + expect(() => { + Object.defineProperty(snapshot.domainEvents, snapshot.domainEvents.length, { + value: event('mutated'), }); - expect(() => { - Object.defineProperty( - snapshot.domainEvents, - snapshot.domainEvents.length, - { - value: event('mutated'), - } - ); - }).toThrow(); - }) + }).toThrow(); + }), ); -it.effect( - 'captures one immutable JSON audit-evidence object and rejects invalid repeats', - () => - Effect.gen( - function* capturesOneImmutableJSONAuditevidenceObjectAndRejects() { - const collector = makeCollector(); - const nested = { reason: 'support request' }; - yield* collector.recordAuditEvidence({ checkpoint: 'started', nested }); - nested.reason = 'mutated'; - - const snapshot = collector.snapshot(); - expect(snapshot.auditEvidence).toEqual({ - checkpoint: 'started', - nested: { reason: 'support request' }, - }); - expect(Object.isFrozen(snapshot.auditEvidence)).toBe(true); - expect(Object.isFrozen(snapshot.auditEvidence['nested'])).toBe(true); - - const repeated = yield* Effect.flip( - collector.recordAuditEvidence({ checkpoint: 'stopped' }) - ); - const invalid = yield* Effect.flip( - makeCollector().recordAuditEvidenceInput({ value: undefined }) - ); - const undeclared = yield* Effect.flip( - makeCollector().recordAuditEvidence({ - checkpoint: 'started', - secret: 'must-not-persist', - }) - ); - const missingSchema = yield* Effect.flip( - createActionCollector(domainEventContracts, 'shell.core', { - captureMode: 'metadata_only', - policyKey: 'counter.read.v1', - }).recordAuditEvidence({ checkpoint: 'started' }) - ); - expect(Predicate.isTagged(repeated, 'ActionCollectorError')).toBe(true); - expect(Predicate.isTagged(invalid, 'ActionCollectorError')).toBe(true); - expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe( - true - ); - expect(Predicate.isTagged(missingSchema, 'ActionCollectorError')).toBe( - true - ); - } - ) +it.effect('captures one immutable JSON audit-evidence object and rejects invalid repeats', () => + Effect.gen(function* capturesOneImmutableJSONAuditevidenceObjectAndRejects() { + const collector = makeCollector(); + const nested = { reason: 'support request' }; + yield* collector.recordAuditEvidence({ checkpoint: 'started', nested }); + nested.reason = 'mutated'; + + const snapshot = collector.snapshot(); + expect(snapshot.auditEvidence).toEqual({ + checkpoint: 'started', + nested: { reason: 'support request' }, + }); + expect(Object.isFrozen(snapshot.auditEvidence)).toBe(true); + expect(Object.isFrozen(snapshot.auditEvidence['nested'])).toBe(true); + + const repeated = yield* Effect.flip(collector.recordAuditEvidence({ checkpoint: 'stopped' })); + const invalid = yield* Effect.flip(makeCollector().recordAuditEvidenceInput({ value: undefined })); + const undeclared = yield* Effect.flip( + makeCollector().recordAuditEvidence({ + checkpoint: 'started', + secret: 'must-not-persist', + }), + ); + const missingSchema = yield* Effect.flip( + createActionCollector(domainEventContracts, 'shell.core', { + captureMode: 'metadata_only', + policyKey: 'counter.read.v1', + }).recordAuditEvidence({ checkpoint: 'started' }), + ); + expect(Predicate.isTagged(repeated, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(invalid, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(missingSchema, 'ActionCollectorError')).toBe(true); + }), ); -it.effect( - 'applies descriptor evidence policy and rejects incompatible evidence', - () => - Effect.gen( - function* appliesDescriptorEvidencePolicyAndRejectsIncompatibleEvidence() { - const collector = createActionCollector( - domainEventContracts, - 'shell.core', - { - captureMode: 'redacted_payload', - policyKey: 'counter.read.redacted.v1', - redactionProfile: 'counter.summary.v1', - } - ); - const error = yield* Effect.flip( - collector.recordDataAccessInput({ - accessKind: 'read', - queryHash: 'query-hash', - resultCount: 1, - servingModuleKey: 'shell.core', - }) - ); - - expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); +it.effect('applies descriptor evidence policy and rejects incompatible evidence', () => + Effect.gen(function* appliesDescriptorEvidencePolicyAndRejectsIncompatibleEvidence() { + const collector = createActionCollector(domainEventContracts, 'shell.core', { + captureMode: 'redacted_payload', + policyKey: 'counter.read.redacted.v1', + redactionProfile: 'counter.summary.v1', + }); + const error = yield* Effect.flip( + collector.recordDataAccessInput({ + accessKind: 'read', + queryHash: 'query-hash', + resultCount: 1, + servingModuleKey: 'shell.core', + }), + ); - const metadataCollector = makeCollector(); - yield* metadataCollector.recordDataAccessInput({ - accessKind: 'read', - evidenceCaptureMode: 'stored_artifact', - evidencePolicyKey: 'handler-controlled', - queryHash: 'metadata-query', - resultCount: 1, - servingModuleKey: 'shell.core', - }); - expect( - metadataCollector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode - ).toBe('metadata_only'); - expect( - metadataCollector.snapshot().dataAccessEvents[0]?.evidencePolicyKey - ).toBe('counter.read.v1'); - } - ) + expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); + + const metadataCollector = makeCollector(); + yield* metadataCollector.recordDataAccessInput({ + accessKind: 'read', + evidenceCaptureMode: 'stored_artifact', + evidencePolicyKey: 'handler-controlled', + queryHash: 'metadata-query', + resultCount: 1, + servingModuleKey: 'shell.core', + }); + expect(metadataCollector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode).toBe('metadata_only'); + expect(metadataCollector.snapshot().dataAccessEvents[0]?.evidencePolicyKey).toBe('counter.read.v1'); + }), ); -it.effect( - 'rejects an Outbox producer that differs from its registered Domain Event', - () => - Effect.gen(function* rejectsAnOutboxProducerThatDiffersFromIts() { - const collector = makeCollector(); - const reference = yield* collector.addDomainEvent(event('producer')); - const error = yield* Effect.flip( - collector.addOutboxMessage(reference, { - payloadJson: {}, - producerModuleKey: 'another.module', - topic: 'counter.project', - }) - ); +it.effect('rejects an Outbox producer that differs from its registered Domain Event', () => + Effect.gen(function* rejectsAnOutboxProducerThatDiffersFromIts() { + const collector = makeCollector(); + const reference = yield* collector.addDomainEvent(event('producer')); + const error = yield* Effect.flip( + collector.addOutboxMessage(reference, { + payloadJson: {}, + producerModuleKey: 'another.module', + topic: 'counter.project', + }), + ); - expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); - }) + expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); + }), ); -it.effect( - 'enforces Action-declared event payloads and producer ownership', - () => - Effect.gen( - function* enforcesActiondeclaredEventPayloadsAndProducerOwnership() { - const collector = makeCollector(); - const invalidPayload = yield* Effect.flip( - collector.addDomainEventInput({ - ...event('payload'), - payloadJson: { id: 1 }, - }) - ); - const invalidProducer = yield* Effect.flip( - collector.addDomainEvent({ - ...event('producer'), - producerModuleKey: 'another.module', - }) - ); - const undeclared = yield* Effect.flip( - collector.addDomainEventInput({ - ...event('undeclared'), - eventType: 'counter.reset', - }) - ); - const inheritedName = yield* Effect.flip( - collector.addDomainEventInput({ - ...event('inherited'), - eventType: 'toString', - }) - ); +it.effect('enforces Action-declared event payloads and producer ownership', () => + Effect.gen(function* enforcesActiondeclaredEventPayloadsAndProducerOwnership() { + const collector = makeCollector(); + const invalidPayload = yield* Effect.flip( + collector.addDomainEventInput({ + ...event('payload'), + payloadJson: { id: 1 }, + }), + ); + const invalidProducer = yield* Effect.flip( + collector.addDomainEvent({ + ...event('producer'), + producerModuleKey: 'another.module', + }), + ); + const undeclared = yield* Effect.flip( + collector.addDomainEventInput({ + ...event('undeclared'), + eventType: 'counter.reset', + }), + ); + const inheritedName = yield* Effect.flip( + collector.addDomainEventInput({ + ...event('inherited'), + eventType: 'toString', + }), + ); - expect(Predicate.isTagged(invalidPayload, 'ActionCollectorError')).toBe( - true - ); - expect( - Predicate.isTagged(invalidProducer, 'ActionCollectorError') - ).toBe(true); - expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe( - true - ); - expect(Predicate.isTagged(inheritedName, 'ActionCollectorError')).toBe( - true - ); - expect(collector.snapshot().domainEvents.length).toBe(0); - } - ) + expect(Predicate.isTagged(invalidPayload, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(invalidProducer, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(inheritedName, 'ActionCollectorError')).toBe(true); + expect(collector.snapshot().domainEvents.length).toBe(0); + }), ); diff --git a/app/packages/core-runtime/tests/unit/action-definition.test.ts b/app/packages/core-runtime/tests/unit/action-definition.test.ts index 5f2721971..ae000170e 100644 --- a/app/packages/core-runtime/tests/unit/action-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/action-definition.test.ts @@ -8,14 +8,8 @@ import { defineActionResourcePermission, validateActionDescriptorInput, } from '../../src/actions/definition.ts'; -import { - defineGlobalPolicy, - defineMicroverticalPolicy, -} from '../../src/actions/policy.ts'; -import { - defineSystemModuleEntrypoint, - defineTenantModuleEntrypoint, -} from '../../src/modules/module-entrypoint.ts'; +import { defineGlobalPolicy, defineMicroverticalPolicy } from '../../src/actions/policy.ts'; +import { defineSystemModuleEntrypoint, defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; const counterActionDescriptor = () => ({ @@ -45,38 +39,30 @@ const counterActionDescriptor = () => schemaVersion: '1', }) as const; -it.effect( - 'defines an immutable typed descriptor and decodes typed payloads and results', - () => - Effect.gen(function* definesAnImmutableTypedDescriptorAndDecodesTyped() { - const registration = defineAction( - { - ...counterActionDescriptor(), - resultSchema: Schema.Struct({ total: Schema.Finite }), - schemaVersion: '1', - }, - (payload) => Effect.succeed({ total: payload.amount }) - ); +it.effect('defines an immutable typed descriptor and decodes typed payloads and results', () => + Effect.gen(function* definesAnImmutableTypedDescriptorAndDecodesTyped() { + const registration = defineAction( + { + ...counterActionDescriptor(), + resultSchema: Schema.Struct({ total: Schema.Finite }), + schemaVersion: '1', + }, + (payload) => Effect.succeed({ total: payload.amount }), + ); - const payload = yield* decodeActionPayload( - registration.descriptor.payloadSchema, - { - amount: 4, - } - ); - const result = yield* decodeActionResult( - registration.descriptor.resultSchema, - { - total: payload.amount, - } - ); + const payload = yield* decodeActionPayload(registration.descriptor.payloadSchema, { + amount: 4, + }); + const result = yield* decodeActionResult(registration.descriptor.resultSchema, { + total: payload.amount, + }); - expect(payload).toEqual({ amount: 4 }); - expect(result).toEqual({ total: 4 }); - expect(Object.isFrozen(registration)).toBe(true); - expect(Object.isFrozen(registration.descriptor)).toBe(true); - expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); - }) + expect(payload).toEqual({ amount: 4 }); + expect(result).toEqual({ total: 4 }); + expect(Object.isFrozen(registration)).toBe(true); + expect(Object.isFrozen(registration.descriptor)).toBe(true); + expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); + }), ); it('keeps the Resource permission resolver private behind an immutable declaration', () => { @@ -115,7 +101,7 @@ it('requires trusted Legal Entity scope for a Counterparty permission declaratio legalEntityScope: 'optional', owningModuleKey: 'party.registry', policies: [], - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ @@ -124,7 +110,7 @@ it('requires trusted Legal Entity scope for a Counterparty permission declaratio legalEntityScope: 'required', owningModuleKey: 'party.registry', policies: [], - }) + }), ).not.toThrow(); }); @@ -158,22 +144,18 @@ it.effect('uses Schema.Void for a no-payload Action', () => resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.void + () => Effect.void, ); const payload = yield* decodeActionPayload( registration.descriptor.payloadSchema, - Option.getOrUndefined(Option.none()) - ); - const invalid = yield* Effect.flip( - decodeActionPayload(registration.descriptor.payloadSchema, {}) + Option.getOrUndefined(Option.none()), ); + const invalid = yield* Effect.flip(decodeActionPayload(registration.descriptor.payloadSchema, {})); expect(payload).toBeUndefined(); - expect(Predicate.isTagged(invalid, 'ActionPayloadValidationError')).toBe( - true - ); - }) + expect(Predicate.isTagged(invalid, 'ActionPayloadValidationError')).toBe(true); + }), ); it('keeps the private handler outside the public Action registration', () => { @@ -183,7 +165,7 @@ it('keeps the private handler outside the public Action registration', () => { resultSchema: Schema.Finite, schemaVersion: '1', }, - (payload) => Effect.succeed(payload.amount) + (payload) => Effect.succeed(payload.amount), ); expect('handler' in registration).toBe(false); @@ -192,36 +174,28 @@ it('keeps the private handler outside the public Action registration', () => { it.effect('rejects invalid declared results through a typed error', () => Effect.gen(function* rejectsInvalidDeclaredResultsThroughATypedError() { - const error = yield* Effect.flip( - decodeActionResult(Schema.Struct({ id: Schema.String }), { id: 1 }) - ); + const error = yield* Effect.flip(decodeActionResult(Schema.Struct({ id: Schema.String }), { id: 1 })); expect(Predicate.isTagged(error, 'ActionResultValidationError')).toBe(true); expect(error.code).toBe('action_result_invalid'); - }) + }), ); -it.effect( - 'validates decoded DateTime and Option results through their encoded representation', - () => - Effect.gen( - function* validatesDecodedDateTimeAndOptionResultsThroughTheir() { - const resultSchema = Schema.Struct({ - archivedAt: Schema.OptionFromNullOr(Schema.DateTimeUtcFromString), - createdAt: Schema.DateTimeUtcFromString, - }); - const decoded = yield* Schema.decodeEffect(resultSchema)({ - archivedAt: null, - createdAt: '2026-09-07T10:30:00.000Z', - }); - const result = yield* decodeActionResult(resultSchema, decoded); +it.effect('validates decoded DateTime and Option results through their encoded representation', () => + Effect.gen(function* validatesDecodedDateTimeAndOptionResultsThroughTheir() { + const resultSchema = Schema.Struct({ + archivedAt: Schema.OptionFromNullOr(Schema.DateTimeUtcFromString), + createdAt: Schema.DateTimeUtcFromString, + }); + const decoded = yield* Schema.decodeEffect(resultSchema)({ + archivedAt: null, + createdAt: '2026-09-07T10:30:00.000Z', + }); + const result = yield* decodeActionResult(resultSchema, decoded); - expect(Option.isNone(result.archivedAt)).toBe(true); - expect(DateTime.formatIso(result.createdAt)).toBe( - '2026-09-07T10:30:00.000Z' - ); - } - ) + expect(Option.isNone(result.archivedAt)).toBe(true); + expect(DateTime.formatIso(result.createdAt)).toBe('2026-09-07T10:30:00.000Z'); + }), ); it('accepts global and same-owner Policy references and copies the collection', () => { @@ -229,10 +203,7 @@ it('accepts global and same-owner Policy references and copies the collection', evaluate: () => Effect.void, policyKey: 'global.tenant-active.v1', }); - const modulePolicy = defineMicroverticalPolicy< - { readonly amount: number }, - 'inventory.stock' - >({ + const modulePolicy = defineMicroverticalPolicy<{ readonly amount: number }, 'inventory.stock'>({ evaluate: () => Effect.void, owningModuleKey: 'inventory.stock', policyKey: 'inventory.stock.available.v1', @@ -266,14 +237,11 @@ it('accepts global and same-owner Policy references and copies the collection', resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.void + () => Effect.void, ); policies.pop(); - expect(registration.descriptor.policies).toEqual([ - globalPolicy, - modulePolicy, - ]); + expect(registration.descriptor.policies).toEqual([globalPolicy, modulePolicy]); expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); expect(registration.descriptor.policies[0]).toBe(globalPolicy); expect(registration.descriptor.policies[1]).toBe(modulePolicy); @@ -324,7 +292,7 @@ it('rejects cross-owner, string, copied, and missing Policy references at defini // @ts-expect-error Policy payload input must accept the decoded Action payload. policies: [incompatiblePayloadPolicy], }, - () => Effect.void + () => Effect.void, ); defineAction( { @@ -332,7 +300,7 @@ it('rejects cross-owner, string, copied, and missing Policy references at defini // @ts-expect-error Raw Policy keys are not Policy object references. policies: ['inventory.stock.available.v1'], }, - () => Effect.void + () => Effect.void, ); }; @@ -343,21 +311,21 @@ it('rejects cross-owner, string, copied, and missing Policy references at defini // @ts-expect-error A foreign MicroVertical Policy is rejected by the owner contract. policies: [foreignPolicy], }, - () => Effect.void - ) + () => Effect.void, + ), ).toThrow(); expect(Predicate.isFunction(compileOnlyInvalidReferences)).toBe(true); expect(() => validateActionDescriptorInput({ ...descriptor, policies: ['inventory.stock.available.v1'], - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ ...descriptor, policies: [{ ...foreignPolicy }], - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput(descriptor)).toThrow(); }); @@ -391,7 +359,7 @@ it('rejects Action entrypoint owner, scope, role/access, and forged immutability resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.void + () => Effect.void, ); expect(() => validateActionDescriptorInput({ @@ -406,7 +374,7 @@ it('rejects Action entrypoint owner, scope, role/access, and forged immutability moduleKey: 'billing.invoice', role: 'action', }), - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ @@ -421,7 +389,7 @@ it('rejects Action entrypoint owner, scope, role/access, and forged immutability moduleKey: 'inventory.stock', role: 'action', }), - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ @@ -437,18 +405,18 @@ it('rejects Action entrypoint owner, scope, role/access, and forged immutability role: 'action', }), owningModuleKey: 'core.modules', - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: { ...registration.descriptor.entrypoint }, - }) + }), ).toThrow(); expect(() => validateActionDescriptorInput({ ...registration.descriptor, legalEntityScope: 'implicit', - }) + }), ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/action-http-runner.test.ts b/app/packages/core-runtime/tests/unit/action-http-runner.test.ts index 173b42087..9a4a94257 100644 --- a/app/packages/core-runtime/tests/unit/action-http-runner.test.ts +++ b/app/packages/core-runtime/tests/unit/action-http-runner.test.ts @@ -43,12 +43,11 @@ const registration = defineAction( resultSchema: Schema.Struct({}), schemaVersion: '1', }, - () => Effect.succeed({}) + () => Effect.succeed({}), ); const unusedRuntime = (onRun: () => void): ActionRuntimeService => ({ - resolveActionCommit: () => - Effect.die('Action commit recovery is outside the runner fixture'), + resolveActionCommit: () => Effect.die('Action commit recovery is outside the runner fixture'), runAction: () => { onRun(); return Effect.die('The Action runtime must not be reached'); @@ -59,54 +58,20 @@ const invalidProblem = { _tag: 'InvalidProblem' as const }; const internalProblem = { _tag: 'InternalProblem' as const }; const authorization = (value?: string) => Redacted.make(value); -it.effect( - 'invalid correlation metadata is rejected before principal acquisition and runtime lookup', - () => - Effect.gen(function* rejectInvalidCorrelation() { - const requestHeaders = [ - {}, - { 'x-correlation-id': '' }, - { 'x-correlation-id': ' ' }, - ] as const; - let authenticationCalls = 0; - let runtimeCalls = 0; - const runtime = unusedRuntime(() => { - runtimeCalls += 1; - }); - const authenticate = () => { - authenticationCalls += 1; - return Effect.succeed(principal); - }; +it.effect('invalid correlation metadata is rejected before principal acquisition and runtime lookup', () => + Effect.gen(function* rejectInvalidCorrelation() { + const requestHeaders = [{}, { 'x-correlation-id': '' }, { 'x-correlation-id': ' ' }] as const; + let authenticationCalls = 0; + let runtimeCalls = 0; + const runtime = unusedRuntime(() => { + runtimeCalls += 1; + }); + const authenticate = () => { + authenticationCalls += 1; + return Effect.succeed(principal); + }; - for (const headers of requestHeaders) { - const effect = runGovernedActionHttp({ - endpointHeaders: { - idempotencyKey: 'not-reached', - traceId: 'not-reached', - }, - internalProblem: () => internalProblem, - invalidCorrelationProblem: () => invalidProblem, - mapError: () => internalProblem, - payload: {}, - principal: { authenticate }, - registration, - requestHeaders: { authorization: authorization(), ...headers }, - }).pipe(Effect.provideService(ActionRuntime, runtime)); - - expect(yield* Effect.flip(effect)).toBe(invalidProblem); - } - - expect(authenticationCalls).toBe(0); - expect(runtimeCalls).toBe(0); - }) -); - -it.effect( - 'principal authentication failure prevents Action runtime execution', - () => - Effect.gen(function* rejectAuthenticationFailure() { - const authenticationProblem = { _tag: 'AuthenticationProblem' as const }; - let runtimeCalls = 0; + for (const headers of requestHeaders) { const effect = runGovernedActionHttp({ endpointHeaders: { idempotencyKey: 'not-reached', @@ -116,76 +81,100 @@ it.effect( invalidCorrelationProblem: () => invalidProblem, mapError: () => internalProblem, payload: {}, - principal: { authenticate: () => Effect.fail(authenticationProblem) }, + principal: { authenticate }, registration, - requestHeaders: { - authorization: authorization(), - 'x-correlation-id': 'correlation-test', - }, - }).pipe( - Effect.provideService( - ActionRuntime, - unusedRuntime(() => { - runtimeCalls += 1; - }) - ) - ); + requestHeaders: { authorization: authorization(), ...headers }, + }).pipe(Effect.provideService(ActionRuntime, runtime)); - expect(yield* Effect.flip(effect)).toBe(authenticationProblem); - expect(runtimeCalls).toBe(0); - }) + expect(yield* Effect.flip(effect)).toBe(invalidProblem); + } + + expect(authenticationCalls).toBe(0); + expect(runtimeCalls).toBe(0); + }), ); -it.effect( - 'synchronous endpoint callback defects are sanitized before the Action runtime', - () => - Effect.gen(function* sanitizeCallbackDefects() { - const callbackDefects = [ - { - invalidCorrelationProblem: () => { - throw new Error('private invalid-problem constructor defect'); - }, - principal: { authenticate: () => Effect.succeed(principal) }, - requestHeaders: { - authorization: authorization('Bearer private-token'), - }, +it.effect('principal authentication failure prevents Action runtime execution', () => + Effect.gen(function* rejectAuthenticationFailure() { + const authenticationProblem = { _tag: 'AuthenticationProblem' as const }; + let runtimeCalls = 0; + const effect = runGovernedActionHttp({ + endpointHeaders: { + idempotencyKey: 'not-reached', + traceId: 'not-reached', + }, + internalProblem: () => internalProblem, + invalidCorrelationProblem: () => invalidProblem, + mapError: () => internalProblem, + payload: {}, + principal: { authenticate: () => Effect.fail(authenticationProblem) }, + registration, + requestHeaders: { + authorization: authorization(), + 'x-correlation-id': 'correlation-test', + }, + }).pipe( + Effect.provideService( + ActionRuntime, + unusedRuntime(() => { + runtimeCalls += 1; + }), + ), + ); + + expect(yield* Effect.flip(effect)).toBe(authenticationProblem); + expect(runtimeCalls).toBe(0); + }), +); + +it.effect('synchronous endpoint callback defects are sanitized before the Action runtime', () => + Effect.gen(function* sanitizeCallbackDefects() { + const callbackDefects = [ + { + invalidCorrelationProblem: () => { + throw new Error('private invalid-problem constructor defect'); }, - { - invalidCorrelationProblem: () => invalidProblem, - principal: { - authenticate: () => { - throw new Error('private principal authentication defect'); - }, - }, - requestHeaders: { - authorization: authorization('Bearer private-token'), - 'x-correlation-id': 'correlation-auth-defect', + principal: { authenticate: () => Effect.succeed(principal) }, + requestHeaders: { + authorization: authorization('Bearer private-token'), + }, + }, + { + invalidCorrelationProblem: () => invalidProblem, + principal: { + authenticate: () => { + throw new Error('private principal authentication defect'); }, }, - ] as const; - let runtimeCalls = 0; - const runtime = unusedRuntime(() => { - runtimeCalls += 1; - }); + requestHeaders: { + authorization: authorization('Bearer private-token'), + 'x-correlation-id': 'correlation-auth-defect', + }, + }, + ] as const; + let runtimeCalls = 0; + const runtime = unusedRuntime(() => { + runtimeCalls += 1; + }); - for (const fixture of callbackDefects) { - const effect = runGovernedActionHttp({ - endpointHeaders: { - idempotencyKey: 'not-reached', - traceId: 'not-reached', - }, - internalProblem: () => internalProblem, - invalidCorrelationProblem: fixture.invalidCorrelationProblem, - mapError: () => internalProblem, - payload: {}, - principal: fixture.principal, - registration, - requestHeaders: fixture.requestHeaders, - }).pipe(Effect.provideService(ActionRuntime, runtime)); + for (const fixture of callbackDefects) { + const effect = runGovernedActionHttp({ + endpointHeaders: { + idempotencyKey: 'not-reached', + traceId: 'not-reached', + }, + internalProblem: () => internalProblem, + invalidCorrelationProblem: fixture.invalidCorrelationProblem, + mapError: () => internalProblem, + payload: {}, + principal: fixture.principal, + registration, + requestHeaders: fixture.requestHeaders, + }).pipe(Effect.provideService(ActionRuntime, runtime)); - expect(yield* Effect.flip(effect)).toBe(internalProblem); - } + expect(yield* Effect.flip(effect)).toBe(internalProblem); + } - expect(runtimeCalls).toBe(0); - }) + expect(runtimeCalls).toBe(0); + }), ); diff --git a/app/packages/core-runtime/tests/unit/action-identity.test.ts b/app/packages/core-runtime/tests/unit/action-identity.test.ts index cbc40af7a..900100fc1 100644 --- a/app/packages/core-runtime/tests/unit/action-identity.test.ts +++ b/app/packages/core-runtime/tests/unit/action-identity.test.ts @@ -23,98 +23,78 @@ const registrations = [ it('identity Actions are generated, sensitive, idempotent, and owned by Core identity', () => { for (const registration of registrations) { - expect(registration.descriptor.actionKey.startsWith('core.identity.')).toBe( - true - ); + expect(registration.descriptor.actionKey.startsWith('core.identity.')).toBe(true); expect(registration.descriptor.auditProfile).toBe('sensitive'); expect(registration.descriptor.idempotency).toBe('required'); expect(registration.descriptor.owningModuleKey).toBe('core.identity'); - expect(registration.descriptor.accessEvidencePolicy.captureMode).toBe( - 'metadata_only' - ); + expect(registration.descriptor.accessEvidencePolicy.captureMode).toBe('metadata_only'); expect(Object.isFrozen(registration.descriptor)).toBe(true); } }); -it.effect( - 'identity administration and support starts declare independent tenant permissions', - () => - Effect.gen(function* identityScenario2() { - const principalId = '00000000-0000-4000-8000-000000000001'; - const authBindingId = '00000000-0000-4000-8000-000000000002'; - const originalPrincipalId = '00000000-0000-4000-8000-000000000003'; - const managedPermissions = [ - bindManagedApiKeyAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - bindManagedApiKeyAction.descriptor.payloadSchema - )({ - principalId, - providerSubjectId: 'provider-key-id', - }) - ), - changePrincipalStatusAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - changePrincipalStatusAction.descriptor.payloadSchema - )({ - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - reason: 'Offboarding', - }) - ), - createNonHumanPrincipalAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - createNonHumanPrincipalAction.descriptor.payloadSchema - )({ - displayName: 'Inventory service', - kind: 'service', - }) - ), - setManagedApiKeyBindingStatusAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - setManagedApiKeyBindingStatusAction.descriptor.payloadSchema - )({ - authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - }) - ), - ]; - for (const permission of managedPermissions) { - expect(permission).toBe('manage_identity'); - } - expect(bindSelfApiKeyAction.descriptor.tenantPermission).toBe(undefined); - expect(setSelfApiKeyBindingStatusAction.descriptor.tenantPermission).toBe( - undefined - ); - const supportPayload = { - originalPrincipalId, - reason: 'Investigating a support request', - targetPrincipalId: principalId, - }; - expect( - recordSupportImpersonationAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - recordSupportImpersonationAction.descriptor.payloadSchema - )({ - ...supportPayload, - checkpoint: 'requested', - }) - ) - ).toBe('impersonate'); - expect( - recordSupportImpersonationAction.descriptor.tenantPermission?.( - yield* Schema.decodeEffect( - recordSupportImpersonationAction.descriptor.payloadSchema - )({ - ...supportPayload, - checkpoint: 'stopped', - sessionRef: 'better-auth-session:safe-session-reference', - }) - ) - ).toBe(undefined); - }) +it.effect('identity administration and support starts declare independent tenant permissions', () => + Effect.gen(function* identityScenario2() { + const principalId = '00000000-0000-4000-8000-000000000001'; + const authBindingId = '00000000-0000-4000-8000-000000000002'; + const originalPrincipalId = '00000000-0000-4000-8000-000000000003'; + const managedPermissions = [ + bindManagedApiKeyAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(bindManagedApiKeyAction.descriptor.payloadSchema)({ + principalId, + providerSubjectId: 'provider-key-id', + }), + ), + changePrincipalStatusAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(changePrincipalStatusAction.descriptor.payloadSchema)({ + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + reason: 'Offboarding', + }), + ), + createNonHumanPrincipalAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(createNonHumanPrincipalAction.descriptor.payloadSchema)({ + displayName: 'Inventory service', + kind: 'service', + }), + ), + setManagedApiKeyBindingStatusAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ + authBindingId, + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + }), + ), + ]; + for (const permission of managedPermissions) { + expect(permission).toBe('manage_identity'); + } + expect(bindSelfApiKeyAction.descriptor.tenantPermission).toBe(undefined); + expect(setSelfApiKeyBindingStatusAction.descriptor.tenantPermission).toBe(undefined); + const supportPayload = { + originalPrincipalId, + reason: 'Investigating a support request', + targetPrincipalId: principalId, + }; + expect( + recordSupportImpersonationAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(recordSupportImpersonationAction.descriptor.payloadSchema)({ + ...supportPayload, + checkpoint: 'requested', + }), + ), + ).toBe('impersonate'); + expect( + recordSupportImpersonationAction.descriptor.tenantPermission?.( + yield* Schema.decodeEffect(recordSupportImpersonationAction.descriptor.payloadSchema)({ + ...supportPayload, + checkpoint: 'stopped', + sessionRef: 'better-auth-session:safe-session-reference', + }), + ), + ).toBe(undefined); + }), ); it('identity status schemas require reasons for disabling, archiving, and revoking', () => { @@ -122,89 +102,76 @@ it('identity status schemas require reasons for disabling, archiving, and revoki const authBindingId = '00000000-0000-4000-8000-000000000002'; expect(() => - Schema.decodeUnknownSync( - changePrincipalStatusAction.descriptor.payloadSchema - )({ + Schema.decodeUnknownSync(changePrincipalStatusAction.descriptor.payloadSchema)({ expectedStatus: 'active', newStatus: 'disabled', principalId, - }) + }), ).toThrow(); expect(() => - Schema.decodeUnknownSync( - setSelfApiKeyBindingStatusAction.descriptor.payloadSchema - )({ + Schema.decodeUnknownSync(setSelfApiKeyBindingStatusAction.descriptor.payloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'revoked', - }) + }), ).toThrow(); expect(() => - Schema.decodeUnknownSync( - setManagedApiKeyBindingStatusAction.descriptor.payloadSchema - )({ + Schema.decodeUnknownSync(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'revoked', principalId, - }) + }), ).toThrow(); }); -it.effect( - 'support checkpoints forbid unsafe or misplaced session references', - () => - Effect.gen(function* identityScenario4() { - const originalPrincipalId = '00000000-0000-4000-8000-000000000001'; - const targetPrincipalId = '00000000-0000-4000-8000-000000000002'; - const decode = Schema.decodeUnknownEffect( - recordSupportImpersonationAction.descriptor.payloadSchema - ); +it.effect('support checkpoints forbid unsafe or misplaced session references', () => + Effect.gen(function* identityScenario4() { + const originalPrincipalId = '00000000-0000-4000-8000-000000000001'; + const targetPrincipalId = '00000000-0000-4000-8000-000000000002'; + const decode = Schema.decodeUnknownEffect(recordSupportImpersonationAction.descriptor.payloadSchema); - expect( - yield* decode({ - checkpoint: 'requested', - originalPrincipalId, - reason: 'Investigating a support request', - sessionRef: 'better-auth-session:must-not-exist-yet', - targetPrincipalId, - }) - ).toEqual({ + expect( + yield* decode({ checkpoint: 'requested', originalPrincipalId, reason: 'Investigating a support request', + sessionRef: 'better-auth-session:must-not-exist-yet', targetPrincipalId, - }); - for (const sessionRef of [ - 'raw-session-token', - 'better-auth-session:contains whitespace', - ]) { - expect( - yield* Effect.flip( - decode({ - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigating a support request', - sessionRef, - targetPrincipalId, - }) - ) - ).toBeDefined(); - } + }), + ).toEqual({ + checkpoint: 'requested', + originalPrincipalId, + reason: 'Investigating a support request', + targetPrincipalId, + }); + for (const sessionRef of ['raw-session-token', 'better-auth-session:contains whitespace']) { expect( - yield* decode({ - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigating a support request', - sessionRef: 'better-auth-session:safe-session-reference', - targetPrincipalId, - }) - ).toEqual({ + yield* Effect.flip( + decode({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigating a support request', + sessionRef, + targetPrincipalId, + }), + ), + ).toBeDefined(); + } + expect( + yield* decode({ checkpoint: 'stopped', originalPrincipalId, reason: 'Investigating a support request', sessionRef: 'better-auth-session:safe-session-reference', targetPrincipalId, - }); - }) + }), + ).toEqual({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigating a support request', + sessionRef: 'better-auth-session:safe-session-reference', + targetPrincipalId, + }); + }), ); diff --git a/app/packages/core-runtime/tests/unit/action-permission.test.ts b/app/packages/core-runtime/tests/unit/action-permission.test.ts index 53e00f277..d63e5c8b3 100644 --- a/app/packages/core-runtime/tests/unit/action-permission.test.ts +++ b/app/packages/core-runtime/tests/unit/action-permission.test.ts @@ -6,11 +6,7 @@ import { ActionPermissionCheckError } from '../../src/actions/errors.ts'; import type { SpiceDbPermissionClientError } from '../../src/permissions/client.ts'; import { spiceDbPermissionClientError } from '../../src/permissions/client.ts'; import { SpiceDbConfigError } from '../../src/permissions/config-error.ts'; -import { - SPICEDB_ROOT_ENV_PATH, - loadSpiceDbConfig, - parseSpiceDbConfig, -} from '../../src/permissions/config.ts'; +import { SPICEDB_ROOT_ENV_PATH, loadSpiceDbConfig, parseSpiceDbConfig } from '../../src/permissions/config.ts'; import { SPICEDB_ACTION_OBJECT_TYPE, SPICEDB_CHECK_TIMEOUT_MS, @@ -34,11 +30,8 @@ const response = (permissionship: v1.CheckPermissionResponse_Permissionship) => Effect.succeed(v1.CheckPermissionResponse.create({ permissionship })); const makeClient = ( - responses: readonly Effect.Effect< - v1.CheckPermissionResponse, - SpiceDbPermissionClientError - >[], - requests: v1.CheckPermissionRequest[] = [] + responses: readonly Effect.Effect[], + requests: v1.CheckPermissionRequest[] = [], ): PermissionCheckClient => { let index = 0; return { @@ -53,361 +46,297 @@ const makeClient = ( }; }; -it.effect( - 'loads the root SpiceDB environment independently of the invocation directory', - () => - Effect.gen(function* loadsTheRootSpiceDBEnvironmentIndependentlyOfThe() { - const originalDirectory = process.cwd(); - const rootExamplePath = SPICEDB_ROOT_ENV_PATH.replace( - /\.env$/u, - '.env.example' - ); +it.effect('loads the root SpiceDB environment independently of the invocation directory', () => + Effect.gen(function* loadsTheRootSpiceDBEnvironmentIndependentlyOfThe() { + const originalDirectory = process.cwd(); + const rootExamplePath = SPICEDB_ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); - yield* Effect.addFinalizer(() => - Effect.sync(() => { - process.chdir(originalDirectory); - }) - ); - process.chdir('/'); - const configuration = yield* loadSpiceDbConfig({ - environment: {}, - envPath: rootExamplePath, - }); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + process.chdir(originalDirectory); + }), + ); + process.chdir('/'); + const configuration = yield* loadSpiceDbConfig({ + environment: {}, + envPath: rootExamplePath, + }); - expect(SPICEDB_ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); - expect(configuration).toEqual({ - endpoint: 'localhost:50051', - insecureLocal: true, - preSharedKey: 'ontos-local-development-key', - }); - }) + expect(SPICEDB_ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); + expect(configuration).toEqual({ + endpoint: 'localhost:50051', + insecureLocal: true, + preSharedKey: 'ontos-local-development-key', + }); + }), ); -it.effect( - 'requires complete configuration and explicit secure or localhost-insecure transport', - () => - Effect.gen( - function* requiresCompleteConfigurationAndExplicitSecureOrLocalhostinsecure() { - const validSecure = yield* parseSpiceDbConfig({ - SPICEDB_ENDPOINT: 'spicedb.internal.example:443', +it.effect('requires complete configuration and explicit secure or localhost-insecure transport', () => + Effect.gen(function* requiresCompleteConfigurationAndExplicitSecureOrLocalhostinsecure() { + const validSecure = yield* parseSpiceDbConfig({ + SPICEDB_ENDPOINT: 'spicedb.internal.example:443', + SPICEDB_INSECURE: 'false', + SPICEDB_PRESHARED_KEY: 'test-key', + }); + const failures = yield* Effect.forEach( + [ + {}, + { + SPICEDB_ENDPOINT: 'localhost:50051', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'spicedb.internal.example:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'https://spicedb.internal.example/path', SPICEDB_INSECURE: 'false', SPICEDB_PRESHARED_KEY: 'test-key', - }); - const failures = yield* Effect.forEach( - [ - {}, - { - SPICEDB_ENDPOINT: 'localhost:50051', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'spicedb.internal.example:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'https://spicedb.internal.example/path', - SPICEDB_INSECURE: 'false', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: - 'spicedb.internal.example:443?credential=test-key', - SPICEDB_INSECURE: 'false', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'localhost:50051#fragment', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'localhost:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: ' ', - }, - ], - (environment) => Effect.flip(parseSpiceDbConfig(environment)) - ); + }, + { + SPICEDB_ENDPOINT: 'spicedb.internal.example:443?credential=test-key', + SPICEDB_INSECURE: 'false', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'localhost:50051#fragment', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'localhost:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: ' ', + }, + ], + (environment) => Effect.flip(parseSpiceDbConfig(environment)), + ); - expect(validSecure).toEqual({ - endpoint: 'spicedb.internal.example:443', - insecureLocal: false, - preSharedKey: 'test-key', - }); - expect(failures.every(Schema.is(SpiceDbConfigError))).toBe(true); - expect( - failures.some((failure) => failure.reason.includes('test-key')) - ).toBe(false); - } - ) + expect(validSecure).toEqual({ + endpoint: 'spicedb.internal.example:443', + insecureLocal: false, + preSharedKey: 'test-key', + }); + expect(failures.every(Schema.is(SpiceDbConfigError))).toBe(true); + expect(failures.some((failure) => failure.reason.includes('test-key'))).toBe(false); + }), ); -it.effect( - 'allows insecure transport only for the exact Zerops stage private endpoint', - () => - Effect.gen(function* allowsInsecureTransportOnlyForTheExactZerops() { - const stage = yield* parseSpiceDbConfig({ - SPICEDB_ENDPOINT: 'spicedb:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', - }); - const rejected = yield* Effect.forEach( - [ - { - SPICEDB_ENDPOINT: 'spicedb:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'spicedb:50052', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', - }, - { - SPICEDB_ENDPOINT: 'spicedb:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', - }, - ], - (environment) => Effect.flip(parseSpiceDbConfig(environment)) - ); +it.effect('allows insecure transport only for the exact Zerops stage private endpoint', () => + Effect.gen(function* allowsInsecureTransportOnlyForTheExactZerops() { + const stage = yield* parseSpiceDbConfig({ + SPICEDB_ENDPOINT: 'spicedb:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', + }); + const rejected = yield* Effect.forEach( + [ + { + SPICEDB_ENDPOINT: 'spicedb:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'spicedb:50052', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', + }, + { + SPICEDB_ENDPOINT: 'spicedb:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', + }, + ], + (environment) => Effect.flip(parseSpiceDbConfig(environment)), + ); - expect(stage).toEqual({ - deploymentEnvironment: 'stage', - endpoint: 'spicedb:50051', - insecureLocal: true, - preSharedKey: 'test-key', - }); - expect(rejected.every(Schema.is(SpiceDbConfigError))).toBe(true); - }) + expect(stage).toEqual({ + deploymentEnvironment: 'stage', + endpoint: 'spicedb:50051', + insecureLocal: true, + preSharedKey: 'test-key', + }); + expect(rejected.every(Schema.is(SpiceDbConfigError))).toBe(true); + }), ); -it.effect( - 'losslessly maps Action keys and exact principal identities using fully consistent requests', - () => - Effect.gen(function* losslesslyMapsActionKeysAndExactPrincipalIdentities() { - const requests: v1.CheckPermissionRequest[] = []; - const service = makeActionPermissionService( - makeClient( - [response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)], - requests - ) - ); +it.effect('losslessly maps Action keys and exact principal identities using fully consistent requests', () => + Effect.gen(function* losslesslyMapsActionKeysAndExactPrincipalIdentities() { + const requests: v1.CheckPermissionRequest[] = []; + const service = makeActionPermissionService( + makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)], requests), + ); - const decision = yield* service.checkActionPermission(input); + const decision = yield* service.checkActionPermission(input); - expect(decision).toBe('allowed'); - expect(requests.length).toBe(1); - expect(requests[0]?.resource).toEqual({ - objectId: toSpiceDbActionObjectId(input.actionKey), - objectType: SPICEDB_ACTION_OBJECT_TYPE, - }); - expect(toSpiceDbActionObjectId(input.actionKey)).toBe( - 'ak_aW52ZW50b3J5LnN0b2NrLnJlc2VydmU' - ); - expect(toSpiceDbActionObjectId('inventory.stock.reserve')).not.toBe( - toSpiceDbActionObjectId('inventory-stock-reserve') - ); - expect(requests[0]?.subject?.object).toEqual({ - objectId: input.principalId, - objectType: SPICEDB_PRINCIPAL_OBJECT_TYPE, + expect(decision).toBe('allowed'); + expect(requests.length).toBe(1); + expect(requests[0]?.resource).toEqual({ + objectId: toSpiceDbActionObjectId(input.actionKey), + objectType: SPICEDB_ACTION_OBJECT_TYPE, + }); + expect(toSpiceDbActionObjectId(input.actionKey)).toBe('ak_aW52ZW50b3J5LnN0b2NrLnJlc2VydmU'); + expect(toSpiceDbActionObjectId('inventory.stock.reserve')).not.toBe( + toSpiceDbActionObjectId('inventory-stock-reserve'), + ); + expect(requests[0]?.subject?.object).toEqual({ + objectId: input.principalId, + objectType: SPICEDB_PRINCIPAL_OBJECT_TYPE, + }); + expect(requests[0]?.permission).toBe(SPICEDB_EXECUTE_PERMISSION); + for (const request of requests) { + expect(request.consistency?.requirement).toEqual({ + fullyConsistent: true, + oneofKind: 'fullyConsistent', }); - expect(requests[0]?.permission).toBe(SPICEDB_EXECUTE_PERMISSION); - for (const request of requests) { - expect(request.consistency?.requirement).toEqual({ - fullyConsistent: true, - oneofKind: 'fullyConsistent', - }); - } - }) + } + }), ); -it.effect( - 'classifies fully consistent execute permission as allowed or denied with one check', - () => - Effect.gen( - function* classifiesFullyConsistentExecutePermissionAsAllowedOr() { - const deniedRequests: v1.CheckPermissionRequest[] = []; - const allowed = makeActionPermissionService( - makeClient([ - response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), - ]) - ); - const denied = makeActionPermissionService( - makeClient( - [response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION)], - deniedRequests - ) - ); +it.effect('classifies fully consistent execute permission as allowed or denied with one check', () => + Effect.gen(function* classifiesFullyConsistentExecutePermissionAsAllowedOr() { + const deniedRequests: v1.CheckPermissionRequest[] = []; + const allowed = makeActionPermissionService( + makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)]), + ); + const denied = makeActionPermissionService( + makeClient([response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION)], deniedRequests), + ); - expect(yield* allowed.checkActionPermission(input)).toBe('allowed'); - expect(yield* denied.checkActionPermission(input)).toBe('denied'); - expect(deniedRequests.length).toBe(1); - } - ) + expect(yield* allowed.checkActionPermission(input)).toBe('allowed'); + expect(yield* denied.checkActionPermission(input)).toBe('denied'); + expect(deniedRequests.length).toBe(1); + }), ); -it.effect( - 'report-only compatibility distinguishes missing policy from an explicit restriction', - () => - Effect.gen( - function* reportonlyCompatibilityDistinguishesMissingPolicyFromAnExplicit() { - const nowEpochMs = Date.parse('2026-09-10T00:00:00.000Z'); - const events: unknown[] = []; - const rollout = { - activatedAtEpochMs: nowEpochMs - 1000, - compatibilityEntrypoints: new Set([input.actionKey]), - expiresAtEpochMs: nowEpochMs + 1000, - inventoryHash: 'inventory', - mode: 'report_only' as const, - sourceRevision: 'revision', - }; - const missingRequests: v1.CheckPermissionRequest[] = []; - const missing = makeActionPermissionService( - makeClient( - [ - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - ], - missingRequests - ), - { - emit: (event) => { - events.push(event); - }, - nowEpochMs: () => nowEpochMs, - rollout, - } - ); - expect(yield* missing.checkActionPermission(input)).toBe('allowed'); - expect(missingRequests.map(({ permission }) => permission)).toEqual([ - SPICEDB_EXECUTE_PERMISSION, - SPICEDB_RESTRICTION_PERMISSION, - ]); - expect(events.length).toBe(1); +it.effect('report-only compatibility distinguishes missing policy from an explicit restriction', () => + Effect.gen(function* reportonlyCompatibilityDistinguishesMissingPolicyFromAnExplicit() { + const nowEpochMs = Date.parse('2026-09-10T00:00:00.000Z'); + const events: unknown[] = []; + const rollout = { + activatedAtEpochMs: nowEpochMs - 1000, + compatibilityEntrypoints: new Set([input.actionKey]), + expiresAtEpochMs: nowEpochMs + 1000, + inventoryHash: 'inventory', + mode: 'report_only' as const, + sourceRevision: 'revision', + }; + const missingRequests: v1.CheckPermissionRequest[] = []; + const missing = makeActionPermissionService( + makeClient( + [ + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + ], + missingRequests, + ), + { + emit: (event) => { + events.push(event); + }, + nowEpochMs: () => nowEpochMs, + rollout, + }, + ); + expect(yield* missing.checkActionPermission(input)).toBe('allowed'); + expect(missingRequests.map(({ permission }) => permission)).toEqual([ + SPICEDB_EXECUTE_PERMISSION, + SPICEDB_RESTRICTION_PERMISSION, + ]); + expect(events.length).toBe(1); - const restricted = makeActionPermissionService( - makeClient([ - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), - ]), - { - emit: () => expect.unreachable(), - nowEpochMs: () => nowEpochMs, - rollout, - } - ); - expect(yield* restricted.checkActionPermission(input)).toBe('denied'); - } - ) + const restricted = makeActionPermissionService( + makeClient([ + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), + ]), + { + emit: () => expect.unreachable(), + nowEpochMs: () => nowEpochMs, + rollout, + }, + ); + expect(yield* restricted.checkActionPermission(input)).toBe('denied'); + }), ); -it.effect( - 'fails closed for conditional, unspecified, malformed, and client failures', - () => - Effect.gen( - function* failsClosedForConditionalUnspecifiedMalformedAndClient() { - const failures = yield* Effect.forEach( - [ - makeClient([ - response( - v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION - ), - ]), - makeClient([ - response(v1.CheckPermissionResponse_Permissionship.UNSPECIFIED), - ]), - makeClient([Effect.fail(spiceDbPermissionClientError())]), - makeClient([ - Effect.fail( - spiceDbPermissionClientError( - new Error( - 'ontos-local-development-key unavailable at internal host' - ) - ) - ), - ]), - ], - (client) => - Effect.flip( - makeActionPermissionService(client).checkActionPermission(input) - ) - ); +it.effect('fails closed for conditional, unspecified, malformed, and client failures', () => + Effect.gen(function* failsClosedForConditionalUnspecifiedMalformedAndClient() { + const failures = yield* Effect.forEach( + [ + makeClient([response(v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION)]), + makeClient([response(v1.CheckPermissionResponse_Permissionship.UNSPECIFIED)]), + makeClient([Effect.fail(spiceDbPermissionClientError())]), + makeClient([ + Effect.fail( + spiceDbPermissionClientError(new Error('ontos-local-development-key unavailable at internal host')), + ), + ]), + ], + (client) => Effect.flip(makeActionPermissionService(client).checkActionPermission(input)), + ); - for (const failure of failures) { - expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); - expect(failure.code).toBe('action_permission_check_failed'); - expect(failure.reason.includes('ontos-local-development-key')).toBe( - false - ); - expect(failure.reason.includes('internal host')).toBe(false); - } - } - ) + for (const failure of failures) { + expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); + expect(failure.code).toBe('action_permission_check_failed'); + expect(failure.reason.includes('ontos-local-development-key')).toBe(false); + expect(failure.reason.includes('internal host')).toBe(false); + } + }), ); -it.effect( - 'constructs the live client with a bounded deadline and finalizes it with the scope', - () => - Effect.gen(function* constructsTheLiveClientWithABoundedDeadline() { - let finalized = false; - let observedTimeout = 0; - const configuration = { - endpoint: 'localhost:50051', - insecureLocal: true, - preSharedKey: 'test-key', - } as const; +it.effect('constructs the live client with a bounded deadline and finalizes it with the scope', () => + Effect.gen(function* constructsTheLiveClientWithABoundedDeadline() { + let finalized = false; + let observedTimeout = 0; + const configuration = { + endpoint: 'localhost:50051', + insecureLocal: true, + preSharedKey: 'test-key', + } as const; - yield* Effect.scoped( - makeActionPermissionLive( - (_configuration, timeoutMilliseconds) => { - observedTimeout = timeoutMilliseconds; - return { - checkPermission: () => - response( - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION - ), - close: () => { - finalized = true; - }, - }; - }, - () => Effect.succeed(configuration) - ).pipe( - Effect.flatMap((service) => service.checkActionPermission(input)) - ) - ); + yield* Effect.scoped( + makeActionPermissionLive( + (_configuration, timeoutMilliseconds) => { + observedTimeout = timeoutMilliseconds; + return { + checkPermission: () => response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + close: () => { + finalized = true; + }, + }; + }, + () => Effect.succeed(configuration), + ).pipe(Effect.flatMap((service) => service.checkActionPermission(input))), + ); - expect(observedTimeout).toBe(SPICEDB_CHECK_TIMEOUT_MS); - expect(finalized).toBe(true); - }) + expect(observedTimeout).toBe(SPICEDB_CHECK_TIMEOUT_MS); + expect(finalized).toBe(true); + }), ); -it.effect( - 'turns missing live configuration into a fail-closed permission service', - () => - Effect.gen( - function* turnsMissingLiveConfigurationIntoAFailclosedPermission() { - const failure = yield* Effect.scoped( - makeActionPermissionLive( - () => { - throw new Error('the client must not be constructed'); - }, - () => parseSpiceDbConfig({}) - ).pipe( - Effect.flatMap((service) => service.checkActionPermission(input)), - Effect.flip - ) - ); +it.effect('turns missing live configuration into a fail-closed permission service', () => + Effect.gen(function* turnsMissingLiveConfigurationIntoAFailclosedPermission() { + const failure = yield* Effect.scoped( + makeActionPermissionLive( + () => { + throw new Error('the client must not be constructed'); + }, + () => parseSpiceDbConfig({}), + ).pipe( + Effect.flatMap((service) => service.checkActionPermission(input)), + Effect.flip, + ), + ); - expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); - expect(failure.code).toBe('action_permission_check_failed'); - } - ) + expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); + expect(failure.code).toBe('action_permission_check_failed'); + }), ); it.effect('finalizes an acquired client even when its scoped use fails', () => @@ -416,16 +345,15 @@ it.effect('finalizes an acquired client even when its scoped use fails', () => const failure = yield* Effect.flip( Effect.scoped( acquirePermissionClientResource(() => ({ - checkPermission: () => - Effect.fail(spiceDbPermissionClientError(new Error('unavailable'))), + checkPermission: () => Effect.fail(spiceDbPermissionClientError(new Error('unavailable'))), close: () => { finalized = true; }, - })).pipe(Effect.flatMap(() => Effect.fail('test-failure'))) - ) + })).pipe(Effect.flatMap(() => Effect.fail('test-failure'))), + ), ); expect(failure).toBe('test-failure'); expect(finalized).toBe(true); - }) + }), ); diff --git a/app/packages/core-runtime/tests/unit/action-policy.test.ts b/app/packages/core-runtime/tests/unit/action-policy.test.ts index e8af6286b..f0da455aa 100644 --- a/app/packages/core-runtime/tests/unit/action-policy.test.ts +++ b/app/packages/core-runtime/tests/unit/action-policy.test.ts @@ -1,12 +1,7 @@ import { Effect, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - defineGlobalPolicy, - defineMicroverticalPolicy, - denyPolicy, - isActionPolicy, -} from '../../src/actions/policy.ts'; +import { defineGlobalPolicy, defineMicroverticalPolicy, denyPolicy, isActionPolicy } from '../../src/actions/policy.ts'; import type { ActionPolicyEvaluatorInput } from '../../src/actions/policy.ts'; const input = { @@ -34,10 +29,7 @@ it('defines immutable global and owner-local Policy references', () => { evaluate: () => Effect.void, policyKey: 'global.tenant-active.v1', }); - const modulePolicy = defineMicroverticalPolicy< - typeof input.payload, - 'inventory.stock' - >({ + const modulePolicy = defineMicroverticalPolicy({ evaluate: () => Effect.void, owningModuleKey: 'inventory.stock', policyKey: 'inventory.stock.available.v1', @@ -76,17 +68,8 @@ it.effect( }, policyKey: 'global.allowed.v1', }); - const denied = defineMicroverticalPolicy< - typeof input.payload, - 'inventory.stock' - >({ - evaluate: () => - Effect.fail( - denyPolicy( - 'stock_unavailable', - 'Requested stock is unavailable — retry later' - ) - ), + const denied = defineMicroverticalPolicy({ + evaluate: () => Effect.fail(denyPolicy('stock_unavailable', 'Requested stock is unavailable — retry later')), owningModuleKey: 'inventory.stock', policyKey: 'inventory.stock.available.v1', }); @@ -100,13 +83,11 @@ it.effect( expect(denial.reasonCode).toBe('stock_unavailable'); expect(denial.reason).toBe('Requested stock is unavailable — retry later'); expect(Object.isFrozen(denial)).toBe(true); - }) + }), ); it('rejects empty stable identifiers and denial messages', () => { - expect(() => - defineGlobalPolicy({ evaluate: () => Effect.void, policyKey: ' ' }) - ).toThrow(TypeError); + expect(() => defineGlobalPolicy({ evaluate: () => Effect.void, policyKey: ' ' })).toThrow(TypeError); expect(() => denyPolicy('', 'Safe message')).toThrow(TypeError); expect(() => denyPolicy('stable_code', '')).toThrow(TypeError); }); diff --git a/app/packages/core-runtime/tests/unit/action-public-surface.test.ts b/app/packages/core-runtime/tests/unit/action-public-surface.test.ts index b433b9870..63355065f 100644 --- a/app/packages/core-runtime/tests/unit/action-public-surface.test.ts +++ b/app/packages/core-runtime/tests/unit/action-public-surface.test.ts @@ -1,9 +1,6 @@ import { expect, it } from 'effect-rstest'; -import { - computeActionRequestHash, - computeCanonicalValueHash, -} from '../../src/actions/repository.ts'; +import { computeActionRequestHash, computeCanonicalValueHash } from '../../src/actions/repository.ts'; import type { ResolvedReadPermissionTarget } from '../../src/index.ts'; import * as publicSurface from '../../src/index.ts'; @@ -50,7 +47,7 @@ it('computes deterministic hashes independent of object key ordering', () => { computeCanonicalValueHash({ nested: { alpha: 1, beta: 3 }, values: ['first', 'second'], - }) + }), ); }); @@ -66,17 +63,13 @@ it('rejects cyclic values instead of producing an unstable request hash', () => principal, schemaVersion: '1', target: {}, - }) + }), ).toThrow(); }); it('canonical hashing distinguishes literal objects from internal value types', () => { - expect(computeCanonicalValueHash()).not.toBe( - computeCanonicalValueHash({ $undefined: true }) - ); - expect(computeCanonicalValueHash(Number.NaN)).not.toBe( - computeCanonicalValueHash({ $number: 'NaN' }) - ); + expect(computeCanonicalValueHash()).not.toBe(computeCanonicalValueHash({ $undefined: true })); + expect(computeCanonicalValueHash(Number.NaN)).not.toBe(computeCanonicalValueHash({ $number: 'NaN' })); expect(computeCanonicalValueHash(-0)).not.toBe(computeCanonicalValueHash(0)); }); diff --git a/app/packages/core-runtime/tests/unit/action-runtime.test.ts b/app/packages/core-runtime/tests/unit/action-runtime.test.ts index f8991275a..09b860196 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -1,33 +1,14 @@ -import { - Cause, - DateTime, - Deferred, - Effect, - Exit, - Fiber, - Option, - Predicate, - Schema, - Struct, -} from 'effect'; +import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Option, Predicate, Schema, Struct } from 'effect'; import { expect, it } from 'effect-rstest'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import { - defineAction, - defineActionResourcePermission, - getActionHandler, -} from '../../src/actions/definition.ts'; +import { defineAction, defineActionResourcePermission, getActionHandler } from '../../src/actions/definition.ts'; import { ActionInvocationPersistenceError, ActionPermissionCheckError, ActionTransactionError, } from '../../src/actions/errors.ts'; -import { - defineGlobalPolicy, - defineMicroverticalPolicy, - denyPolicy, -} from '../../src/actions/policy.ts'; +import { defineGlobalPolicy, defineMicroverticalPolicy, denyPolicy } from '../../src/actions/policy.ts'; import type { ActionInvocationRecord, ActionRepositoryService, @@ -43,10 +24,7 @@ import { makeActionRepository, } from '../../src/actions/repository.ts'; import type { ActionRuntimeStage } from '../../src/actions/runtime.ts'; -import { - ACTION_RUNTIME_STAGES, - makeActionRuntime, -} from '../../src/actions/runtime.ts'; +import { ACTION_RUNTIME_STAGES, makeActionRuntime } from '../../src/actions/runtime.ts'; import type { PrincipalManagementRepositoryService } from '../../src/auth/principal-management.ts'; import { PrincipalManagementRepository } from '../../src/auth/principal-management.ts'; import { supportRecoveryPrincipalContextResolverFromRepository } from '../../src/auth/support-recovery-principal-context.ts'; @@ -54,23 +32,14 @@ import { CoreDatabase } from '../../src/db/client.ts'; import { recordSupportImpersonationAction } from '../../src/modules/actions/record-support-impersonation.action.ts'; import { makeModuleEntrypointGateway } from '../../src/modules/module-entrypoint-gateway.ts'; import type { ModuleEntrypointDescriptor } from '../../src/modules/module-entrypoint.ts'; -import { - defineSystemModuleEntrypoint, - defineTenantModuleEntrypoint, -} from '../../src/modules/module-entrypoint.ts'; +import { defineSystemModuleEntrypoint, defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { ModuleStateCheckUnavailableError, ModuleStateDeniedError, } from '../../src/modules/module-state-gate-errors.ts'; -import { - checkModuleEntrypoint, - makeModuleStateSnapshot, -} from '../../src/modules/module-state-gate.ts'; +import { checkModuleEntrypoint, makeModuleStateSnapshot } from '../../src/modules/module-state-gate.ts'; import type { TenantModuleState } from '../../src/modules/tenant-module-state-service.ts'; -import type { - ActionPermissionDecision, - CheckActionPermissionInput, -} from '../../src/permissions/service.ts'; +import type { ActionPermissionDecision, CheckActionPermissionInput } from '../../src/permissions/service.ts'; import { testOperationalScopeResolver } from '../fixtures/operational-scope.ts'; import { makeTestDatabase } from '../support/database.ts'; @@ -93,46 +62,32 @@ const transport = (idempotencyKey = 'intent-1') => ({ const CounterpartyIdSchema = Schema.String.pipe(Schema.brand('CounterpartyId')); type CounterpartyId = typeof CounterpartyIdSchema.Type; -type RetainedCauseError = - | ActionTransactionError - | ActionInvocationPersistenceError; - -const expectSameJson = ( - actual: RetainedCauseError, - expected: RetainedCauseError -) => { +type RetainedCauseError = ActionTransactionError | ActionInvocationPersistenceError; + +const expectSameJson = (actual: RetainedCauseError, expected: RetainedCauseError) => { expect(JSON.stringify(actual)).toBe(JSON.stringify(expected)); }; const completionTime = () => DateTime.toDateUtc(DateTime.makeUnsafe(0)); -const forEachSequential = ( - items: readonly Item[], - run: (item: Item) => Effect.Effect -) => Effect.forEach(items, run, { discard: true }); +const forEachSequential = (items: readonly Item[], run: (item: Item) => Effect.Effect) => + Effect.forEach(items, run, { discard: true }); const unusedPrincipalManagementOperation = () => - Effect.die( - 'The ambient PrincipalManagementRepository must not be used by the Action runtime' - ); -const ambientPrincipalManagementRepository: PrincipalManagementRepositoryService = - { - bindApiKey: unusedPrincipalManagementOperation, - changePrincipalStatus: unusedPrincipalManagementOperation, - createNonHumanPrincipal: unusedPrincipalManagementOperation, - setApiKeyBindingStatus: unusedPrincipalManagementOperation, - validateSupportImpersonation: unusedPrincipalManagementOperation, - }; + Effect.die('The ambient PrincipalManagementRepository must not be used by the Action runtime'); +const ambientPrincipalManagementRepository: PrincipalManagementRepositoryService = { + bindApiKey: unusedPrincipalManagementOperation, + changePrincipalStatus: unusedPrincipalManagementOperation, + createNonHumanPrincipal: unusedPrincipalManagementOperation, + setApiKeyBindingStatus: unusedPrincipalManagementOperation, + validateSupportImpersonation: unusedPrincipalManagementOperation, +}; const providePrincipalManagementRepository = Effect.provideService( PrincipalManagementRepository, - ambientPrincipalManagementRepository + ambientPrincipalManagementRepository, ); -const PermissionDecisionSchema = Schema.Literals([ - 'allowed', - 'denied', - 'unavailable', -]); +const PermissionDecisionSchema = Schema.Literals(['allowed', 'denied', 'unavailable']); type PermissionDecision = typeof PermissionDecisionSchema.Type; interface HarnessOptions { @@ -149,16 +104,10 @@ interface HarnessOptions { readonly resolutionUnavailable?: boolean; readonly resourcePermissionDecision?: PermissionDecision; readonly tenantPermissionDecision?: PermissionDecision; - readonly transactionMode?: - | 'commit-definite' - | 'definite-failure' - | 'normal' - | 'uncertain'; + readonly transactionMode?: 'commit-definite' | 'definite-failure' | 'normal' | 'uncertain'; } -const makeHarness = Effect.fn(function* makeHarness( - options: HarnessOptions = {} -) { +const makeHarness = Effect.fn(function* makeHarness(options: HarnessOptions = {}) { const finalized: FinalizeActionPolicyDenialInput[] = []; const flushed: FlushActionSuccessInput[] = []; const legalEntityChecks: unknown[] = []; @@ -204,7 +153,7 @@ const makeHarness = Effect.fn(function* makeHarness( new ActionInvocationPersistenceError({ code: 'action_invocation_persistence_failed', reason: 'test rejection persistence failed', - }) + }), ); } finalized.push(input); @@ -234,7 +183,7 @@ const makeHarness = Effect.fn(function* makeHarness( new ActionTransactionError({ code: 'action_transaction_failed', reason: 'test denial evidence transaction failed', - }) + }), ); } currentInvocation = { @@ -250,14 +199,13 @@ const makeHarness = Effect.fn(function* makeHarness( new ActionInvocationPersistenceError({ code: 'action_invocation_persistence_failed', reason: 'test database unavailable', - }) + }), ) : Effect.succeed(currentInvocation), transitionInvocationToRunning: () => { transitionCount += 1; if ( - (currentInvocation.status === 'received' || - currentInvocation.status === 'running') && + (currentInvocation.status === 'received' || currentInvocation.status === 'running') && currentInvocation.completedAt === null ) { currentInvocation = { ...currentInvocation, status: 'running' }; @@ -278,8 +226,7 @@ const makeHarness = Effect.fn(function* makeHarness( uncertain: '08007', }; const defaultCode = - options.transactionMode === 'uncertain' || - options.transactionMode === 'commit-definite' + options.transactionMode === 'uncertain' || options.transactionMode === 'commit-definite' ? defaultCommitCodes[options.transactionMode] : undefined; const code = options.commitFailureCode ?? defaultCode; @@ -293,10 +240,7 @@ const makeHarness = Effect.fn(function* makeHarness( } return []; }); - const query = Effect.fn(function* executeQuery( - statement: string, - values: readonly unknown[] - ) { + const query = Effect.fn(function* executeQuery(statement: string, values: readonly unknown[]) { const text = statement.toLowerCase(); if (text.includes('set_config')) { const [tenantId, legalEntityId] = values; @@ -342,7 +286,7 @@ const makeHarness = Effect.fn(function* makeHarness( new ActionPermissionCheckError({ code: 'action_permission_check_failed', reason: 'test authorization service unavailable', - }) + }), ) : Effect.succeed(options.permissionDecision ?? 'allowed'); }, @@ -350,10 +294,7 @@ const makeHarness = Effect.fn(function* makeHarness( const moduleStateGate = { check: checkModuleEntrypoint, - prepareSnapshot: ( - tenantId: string, - entrypoints: readonly ModuleEntrypointDescriptor[] - ) => { + prepareSnapshot: (tenantId: string, entrypoints: readonly ModuleEntrypointDescriptor[]) => { const moduleKeys = entrypoints .filter((entrypoint) => entrypoint.scope === 'tenant') .map((entrypoint) => entrypoint.moduleKey); @@ -365,13 +306,11 @@ const makeHarness = Effect.fn(function* makeHarness( new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', reason: 'controlled unavailable state read', - }) + }), ); } const availableState: TenantModuleState = - options.moduleState === undefined || options.moduleState === 'missing' - ? 'active' - : options.moduleState; + options.moduleState === undefined || options.moduleState === 'missing' ? 'active' : options.moduleState; return Effect.succeed( makeModuleStateSnapshot( tenantId, @@ -381,8 +320,8 @@ const makeHarness = Effect.fn(function* makeHarness( : moduleKeys.map((moduleKey) => ({ moduleKey, state: availableState, - })) - ) + })), + ), ); }, recheckWrite: () => { @@ -392,7 +331,7 @@ const makeHarness = Effect.fn(function* makeHarness( new ModuleStateDeniedError({ code: 'module_state_denied', reason: 'controlled locked denial', - }) + }), ); } if (options.lockedModuleState === 'unavailable') { @@ -400,62 +339,53 @@ const makeHarness = Effect.fn(function* makeHarness( new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', reason: 'controlled locked unavailable check', - }) + }), ); } return Effect.void; }, } as const; - const runtime = makeActionRuntime( - database, - repository, - permission, - testOperationalScopeResolver, - { - contextAccess: { - legalEntities: (input) => { - legalEntityChecks.push(input); - return Effect.succeed( - input.legalEntityIds.map((key) => ({ - decision: - options.legalEntityPermissionDecision ?? ('allowed' as const), - key, - })) - ); - }, - modules: () => Effect.succeed([]), - resources: (input) => { - resourceChecks.push(input); - return Effect.succeed( - input.resources.map(({ moduleId, resourceId, resourceType }) => ({ - decision: - options.resourcePermissionDecision ?? ('allowed' as const), - key: `${moduleId}:${resourceType}:${resourceId}`, - })) - ); - }, - tenants: (input) => { - tenantChecks.push(input); - return Effect.succeed( - input.tenantIds.map((key) => ({ - decision: - options.tenantPermissionDecision ?? ('allowed' as const), - key, - })) - ); - }, + const runtime = makeActionRuntime(database, repository, permission, testOperationalScopeResolver, { + contextAccess: { + legalEntities: (input) => { + legalEntityChecks.push(input); + return Effect.succeed( + input.legalEntityIds.map((key) => ({ + decision: options.legalEntityPermissionDecision ?? ('allowed' as const), + key, + })), + ); }, - moduleEntrypointGateway: makeModuleEntrypointGateway(moduleStateGate), - moduleStateGate, - onStage: (stage) => { - stages.push(stage); + modules: () => Effect.succeed([]), + resources: (input) => { + resourceChecks.push(input); + return Effect.succeed( + input.resources.map(({ moduleId, resourceId, resourceType }) => ({ + decision: options.resourcePermissionDecision ?? ('allowed' as const), + key: `${moduleId}:${resourceType}:${resourceId}`, + })), + ); }, - resolveHandler: (action) => { - handlerResolutionCount += 1; - return getActionHandler(action); + tenants: (input) => { + tenantChecks.push(input); + return Effect.succeed( + input.tenantIds.map((key) => ({ + decision: options.tenantPermissionDecision ?? ('allowed' as const), + key, + })), + ); }, - } - ); + }, + moduleEntrypointGateway: makeModuleEntrypointGateway(moduleStateGate), + moduleStateGate, + onStage: (stage) => { + stages.push(stage); + }, + resolveHandler: (action) => { + handlerResolutionCount += 1; + return getActionHandler(action); + }, + }); return { counts: () => ({ @@ -498,23 +428,19 @@ const makeRepositoryFailures = Effect.fn(function* testProgram1() { principal, transport: transport('denied'), } as const; - const transactionFailure = yield* Effect.flip( - repository.rejectPermissionDenied(executor, input) - ); + const transactionFailure = yield* Effect.flip(repository.rejectPermissionDenied(executor, input)); const persistenceFailure = yield* Effect.flip( repository.finalizePolicyDenial(executor, { ...input, policy: { policyKey: 'global.counter-locked.v1', scope: 'global' }, reasonCode: 'counter_locked', - }) + }), ); expect(Schema.is(ActionTransactionError)(transactionFailure)).toBe(true); if (!Schema.is(ActionTransactionError)(transactionFailure)) { throw new Error('Expected typed test outcome'); } - expect(Schema.is(ActionInvocationPersistenceError)(persistenceFailure)).toBe( - true - ); + expect(Schema.is(ActionInvocationPersistenceError)(persistenceFailure)).toBe(true); if (!Schema.is(ActionInvocationPersistenceError)(persistenceFailure)) { throw new Error('Expected typed test outcome'); } @@ -524,30 +450,18 @@ const makeRepositoryFailures = Effect.fn(function* testProgram1() { it.effect( 'repository constructors retain original causes across Effect Cause propagation', Effect.fn(function* testProgram2() { - const { cause, persistenceFailure, transactionFailure } = - yield* makeRepositoryFailures(); - const propagatedTransaction = yield* Effect.flip( - Effect.failCause(Cause.fail(transactionFailure)) - ); - const propagatedPersistence = yield* Effect.flip( - Effect.failCause(Cause.fail(persistenceFailure)) - ); + const { cause, persistenceFailure, transactionFailure } = yield* makeRepositoryFailures(); + const propagatedTransaction = yield* Effect.flip(Effect.failCause(Cause.fail(transactionFailure))); + const propagatedPersistence = yield* Effect.flip(Effect.failCause(Cause.fail(persistenceFailure))); expect(propagatedTransaction).toBe(transactionFailure); expect(propagatedPersistence).toBe(persistenceFailure); - expect(getActionTransactionFailureCause(propagatedTransaction)).toEqual( - Cause.die(cause) - ); - expect( - getActionInvocationPersistenceFailureCause(propagatedPersistence) - ).toEqual(Cause.die(cause)); - }) + expect(getActionTransactionFailureCause(propagatedTransaction)).toEqual(Cause.die(cause)); + expect(getActionInvocationPersistenceFailureCause(propagatedPersistence)).toEqual(Cause.die(cause)); + }), ); it('public error classes expose no retained-cause accessors', () => { - for (const errorClass of [ - ActionTransactionError, - ActionInvocationPersistenceError, - ]) { + for (const errorClass of [ActionTransactionError, ActionInvocationPersistenceError]) { expect('withCause' in errorClass).toBe(false); expect('causeOf' in errorClass).toBe(false); } @@ -556,8 +470,7 @@ it('public error classes expose no retained-cause accessors', () => { it.effect( 'repository causes are absent from reflection, JSON, and Schema encoding', Effect.fn(function* testProgram3() { - const { persistenceFailure, transactionFailure } = - yield* makeRepositoryFailures(); + const { persistenceFailure, transactionFailure } = yield* makeRepositoryFailures(); const publicTransaction = new ActionTransactionError({ code: transactionFailure.code, reason: transactionFailure.reason, @@ -566,70 +479,46 @@ it.effect( code: persistenceFailure.code, reason: persistenceFailure.reason, }); - expect(Object.keys(transactionFailure)).toEqual( - Object.keys(publicTransaction) - ); - expect(Object.keys(persistenceFailure)).toEqual( - Object.keys(publicPersistence) - ); - expect(Reflect.ownKeys(transactionFailure)).toEqual( - Reflect.ownKeys(publicTransaction) - ); - expect(Reflect.ownKeys(persistenceFailure)).toEqual( - Reflect.ownKeys(publicPersistence) - ); + expect(Object.keys(transactionFailure)).toEqual(Object.keys(publicTransaction)); + expect(Object.keys(persistenceFailure)).toEqual(Object.keys(publicPersistence)); + expect(Reflect.ownKeys(transactionFailure)).toEqual(Reflect.ownKeys(publicTransaction)); + expect(Reflect.ownKeys(persistenceFailure)).toEqual(Reflect.ownKeys(publicPersistence)); expectSameJson(transactionFailure, publicTransaction); expectSameJson(persistenceFailure, publicPersistence); - const encodedTransactionFailure = yield* Schema.encodeEffect( - ActionTransactionError - )(transactionFailure); - expect( - Schema.is(Schema.toEncoded(ActionTransactionError))( - encodedTransactionFailure - ) - ).toBe(true); + const encodedTransactionFailure = yield* Schema.encodeEffect(ActionTransactionError)(transactionFailure); + expect(Schema.is(Schema.toEncoded(ActionTransactionError))(encodedTransactionFailure)).toBe(true); expect(Struct.omit(encodedTransactionFailure, ['_tag'])).toEqual({ code: transactionFailure.code, reason: transactionFailure.reason, }); - const encodedPersistenceFailure = yield* Schema.encodeEffect( - ActionInvocationPersistenceError - )(persistenceFailure); - expect( - Schema.is(Schema.toEncoded(ActionInvocationPersistenceError))( - encodedPersistenceFailure - ) - ).toBe(true); + const encodedPersistenceFailure = yield* Schema.encodeEffect(ActionInvocationPersistenceError)(persistenceFailure); + expect(Schema.is(Schema.toEncoded(ActionInvocationPersistenceError))(encodedPersistenceFailure)).toBe(true); expect(Struct.omit(encodedPersistenceFailure, ['_tag'])).toEqual({ code: persistenceFailure.code, reason: persistenceFailure.reason, }); - }) + }), ); it('repository cause readers reject foreign objects carrying the former cause property', () => { - const formerCauseProperty = ['ontos', 'Repository', 'Failure', 'Cause'].join( - '' - ); + const formerCauseProperty = ['ontos', 'Repository', 'Failure', 'Cause'].join(''); const cause = new Error('foreign defect'); const transactionFailure = Object.assign( new ActionTransactionError({ code: 'action_transaction_failed', reason: 'foreign failure', }), - { [formerCauseProperty]: cause } + { [formerCauseProperty]: cause }, ); const persistenceFailure = Object.assign( new ActionInvocationPersistenceError({ code: 'action_invocation_persistence_failed', reason: 'foreign failure', }), - { [formerCauseProperty]: cause } + { [formerCauseProperty]: cause }, ); expect(getActionTransactionFailureCause(transactionFailure)).toBe(undefined); - expect(getActionInvocationPersistenceFailureCause(persistenceFailure)).toBe( - undefined - ); + expect(getActionInvocationPersistenceFailureCause(persistenceFailure)).toBe(undefined); }); const registration = () => @@ -688,7 +577,7 @@ const registration = () => topic: 'counter.project', }); return { total: payload.amount }; - }) + }), ); it.effect( @@ -727,7 +616,7 @@ it.effect( moduleStateReadCount: 0, moduleStateRecheckCount: 0, }); - }) + }), ); it.effect( @@ -770,7 +659,7 @@ it.effect( expect(DateTime.formatIso(payload.occurredAt)).toBe(occurredAt); expect(Option.isNone(payload.note)).toBe(true); return Effect.succeed(payload); - } + }, ); const harness = yield* makeHarness(); @@ -797,31 +686,28 @@ it.effect( ]); expect(DateTime.formatIso(result.occurredAt)).toBe(occurredAt); expect(Option.isNone(result.note)).toBe(true); - expect(harness.flushed[0]?.resultHash).toBe( - computeCanonicalValueHash({ note: null, occurredAt }) - ); - }) + expect(harness.flushed[0]?.resultHash).toBe(computeCanonicalValueHash({ note: null, occurredAt })); + }), ); it.effect( 'uses a resolver-branded recovery only for the exact support-stop Action and still checks permission', Effect.fn(function* testProgram6() { - const recoveryPrincipal = - yield* supportRecoveryPrincipalContextResolverFromRepository({ - load: () => - Effect.succeedSome({ - bindingPrincipalId: principal.principalId, - bindingTenantId: principal.tenantId, - principalKind: 'human' as const, - principalTenantId: principal.tenantId, - tenantId: principal.tenantId, - }), - }).resolveStoppedImpersonation({ - originalAuthBindingId: principal.authBindingId, - originalPrincipalId: principal.principalId, - originalSessionId: 'expired-original-session', - tenantId: principal.tenantId, - }); + const recoveryPrincipal = yield* supportRecoveryPrincipalContextResolverFromRepository({ + load: () => + Effect.succeedSome({ + bindingPrincipalId: principal.principalId, + bindingTenantId: principal.tenantId, + principalKind: 'human' as const, + principalTenantId: principal.tenantId, + tenantId: principal.tenantId, + }), + }).resolveStoppedImpersonation({ + originalAuthBindingId: principal.authBindingId, + originalPrincipalId: principal.principalId, + originalSessionId: 'expired-original-session', + tenantId: principal.tenantId, + }); const harness = yield* makeHarness({ permissionDecision: 'allowed', tenantPermissionDecision: 'denied', @@ -863,7 +749,7 @@ it.effect( registration: recordSupportImpersonationAction, transport: transport('support-recovery-denied'), }) - .pipe(providePrincipalManagementRepository) + .pipe(providePrincipalManagementRepository), ); expect(Predicate.isTagged(denied, 'ActionPermissionDenied')).toBe(true); @@ -885,11 +771,9 @@ it.effect( registration: recordSupportImpersonationAction, transport: transport('support-recovery-wrong-checkpoint'), }) - .pipe(providePrincipalManagementRepository) + .pipe(providePrincipalManagementRepository), ); - expect( - Predicate.isTagged(wrongCheckpoint, 'ActionTrustedContextValidationError') - ).toBe(true); + expect(Predicate.isTagged(wrongCheckpoint, 'ActionTrustedContextValidationError')).toBe(true); const wrongAction = yield* Effect.flip( harness.runtime.runAction({ @@ -897,29 +781,19 @@ it.effect( principal: recoveryPrincipal, registration: registration(), transport: transport('support-recovery-wrong-action'), - }) + }), ); - expect( - Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError') - ).toBe(true); - }) + expect(Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError')).toBe(true); + }), ); it.effect( 'fails business Actions closed before invocation, permission, Policy, or handler access', Effect.fn(function* testProgram7() { yield* forEachSequential( - ( - [ - 'inactive', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - 'missing', - ] as const - ).map((state, index) => [index, state] as const), + (['inactive', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived', 'missing'] as const).map( + (state, index) => [index, state] as const, + ), Effect.fn(function* testProgram8([index, state]) { let handlerCalls = 0; let policyCalls = 0; @@ -960,7 +834,7 @@ it.effect( () => Effect.sync(() => { handlerCalls += 1; - }) + }), ); const failure = yield* Effect.flip( harness.runtime.runAction({ @@ -968,12 +842,9 @@ it.effect( principal, registration: action, transport: transport(`state-${state}`), - }) + }), ); - expect( - Predicate.isTagged(failure, 'ModuleStateDeniedError'), - state - ).toBe(true); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError'), state).toBe(true); expect(handlerCalls).toBe(0); expect(policyCalls).toBe(0); @@ -992,9 +863,9 @@ it.effect( moduleStateReadCount: 1, moduleStateRecheckCount: 0, }); - }) + }), ); - }) + }), ); it.effect( @@ -1028,7 +899,7 @@ it.effect( resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.void + () => Effect.void, ); const unavailable = yield* makeHarness({ moduleState: 'unavailable' }); @@ -1038,11 +909,9 @@ it.effect( principal, registration: action, transport: transport('state-unavailable'), - }) + }), ); - expect( - Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError') - ).toBe(true); + expect(Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError')).toBe(true); expect(unavailable.counts().createCount).toBe(0); @@ -1053,11 +922,9 @@ it.effect( principal, registration: action, transport: transport('state-locked-denied'), - }) - ); - expect(Predicate.isTagged(lockedFailure, 'ModuleStateDeniedError')).toBe( - true + }), ); + expect(Predicate.isTagged(lockedFailure, 'ModuleStateDeniedError')).toBe(true); expect(locked.gateCounts()).toEqual({ handlerResolutionCount: 0, @@ -1070,7 +937,7 @@ it.effect( transactionCount: 1, transitionCount: 1, }); - }) + }), ); it.effect( @@ -1085,13 +952,10 @@ it.effect( }); expect(result).toEqual({ total: 2 }); - expect( - harness.stages.indexOf('permission_checked') < - harness.stages.indexOf('policy_boundary') - ).toBe(true); + expect(harness.stages.indexOf('permission_checked') < harness.stages.indexOf('policy_boundary')).toBe(true); expect(harness.counts().transitionCount).toBe(1); expect(harness.counts().transactionCount).toBe(1); - }) + }), ); it.effect( @@ -1126,7 +990,7 @@ it.effect( schemaVersion: '1', tenantPermission: () => 'manage_identity', }, - () => Effect.void + () => Effect.void, ); yield* forEachSequential( @@ -1145,12 +1009,12 @@ it.effect( principal, registration: tenantAuthorizedRegistration, transport: transport(`tenant-${decision}`), - }) + }), ); expect(Predicate.isTagged(failure, expectedTag)).toBe(true); expect(harness.counts().transitionCount).toBe(0); - }) + }), ); const allowed = yield* makeHarness({ @@ -1164,7 +1028,7 @@ it.effect( transport: transport('tenant-allowed'), }); expect(allowed.counts().transitionCount).toBe(1); - }) + }), ); it.effect( @@ -1209,7 +1073,7 @@ it.effect( schemaVersion: '1', tenantPermission: () => permission, }, - () => Effect.void + () => Effect.void, ); const harness = yield* makeHarness(); yield* harness.runtime.runAction({ @@ -1229,9 +1093,9 @@ it.effect( correlationId: `correlation-${permission}`, idempotencyKey: permission, }); - }) + }), ); - }) + }), ); it.effect( @@ -1266,7 +1130,7 @@ it.effect( schemaVersion: '1', tenantPermission: () => 'manage_identity', }, - () => Effect.void + () => Effect.void, ); const first = yield* makeHarness(); const second = yield* makeHarness(); @@ -1299,7 +1163,7 @@ it.effect( idempotencyKey: 'same-idempotency-key', }); expect(second.flushed[0]?.transport).toEqual(first.flushed[0]?.transport); - }) + }), ); it.effect( @@ -1348,7 +1212,7 @@ it.effect( () => { handlerCalls += 1; return Effect.void; - } + }, ); const forgedTransport = { ...transport('legal-entity-denied'), @@ -1366,7 +1230,7 @@ it.effect( principal, registration: action, transport: forgedTransport, - }) + }), ); expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); @@ -1396,11 +1260,9 @@ it.effect( principal, registration: action, transport: forgedTransport, - }) + }), ); - expect( - Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError') - ).toBe(true); + expect(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')).toBe(true); expect(unavailable.rejections.length).toBe(0); expect(unavailable.counts().transactionCount).toBe(0); @@ -1422,11 +1284,8 @@ it.effect( correlationId: 'correlation-legal-entity-denied', idempotencyKey: 'legal-entity-allowed', }); - expect( - allowed.stages.indexOf('permission_checked') < - allowed.stages.indexOf('policy_boundary') - ).toBe(true); - }) + expect(allowed.stages.indexOf('permission_checked') < allowed.stages.indexOf('policy_boundary')).toBe(true); + }), ); it.effect( @@ -1491,7 +1350,7 @@ it.effect( () => { handlerCalls += 1; return Effect.void; - } + }, ); const denied = yield* makeHarness({ resourcePermissionDecision: 'denied' }); @@ -1506,7 +1365,7 @@ it.effect( targetResourceId: 'forged-resource', targetResourceType: 'forged-type', }, - }) + }), ); expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); @@ -1546,15 +1405,13 @@ it.effect( principal, registration: action, transport: transport('resource-unavailable'), - }) + }), ); - expect( - Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError') - ).toBe(true); + expect(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')).toBe(true); expect(unavailable.rejections.length).toBe(0); expect(unavailable.counts().transactionCount).toBe(0); - }) + }), ); it.effect( @@ -1607,7 +1464,7 @@ it.effect( () => { serviceFactoryCount += 1; return Effect.succeed({}); - } + }, ); const failure = yield* Effect.flip( @@ -1616,7 +1473,7 @@ it.effect( principal, registration: deniedRegistration, transport: transport('denied'), - }) + }), ); expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); @@ -1652,7 +1509,7 @@ it.effect( transport: transport('denied'), }, ]); - }) + }), ); it.effect( @@ -1665,12 +1522,10 @@ it.effect( principal, registration: registration(), transport: transport('unavailable'), - }) + }), ); - expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe( - true - ); + expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe(true); expect(harness.permissionCounts()).toEqual({ permissionCheckCount: 1, @@ -1689,7 +1544,7 @@ it.effect( 'invocation_prepared', 'authentication_boundary', ]); - }) + }), ); it.effect( @@ -1705,7 +1560,7 @@ it.effect( principal, registration: registration(), transport: transport('permission-denial-persistence-failure'), - }) + }), ); expect(Predicate.isTagged(failure, 'ActionTransactionError')).toBe(true); @@ -1716,7 +1571,7 @@ it.effect( }); expect(harness.counts().transitionCount).toBe(0); expect(harness.counts().transactionCount).toBe(0); - }) + }), ); it.effect( @@ -1730,10 +1585,7 @@ it.effect( }, policyKey: 'global.tenant-active.v1', }); - const modulePolicy = defineMicroverticalPolicy< - { readonly amount: number }, - 'inventory.stock' - >({ + const modulePolicy = defineMicroverticalPolicy<{ readonly amount: number }, 'inventory.stock'>({ evaluate: (input) => { observed.push(`module:${input.payload.amount}`); expect(input.principal.principalId).toBe(principal.principalId); @@ -1776,7 +1628,7 @@ it.effect( (payload) => { observed.push('handler'); return Effect.succeed(payload.amount); - } + }, ); const harness = yield* makeHarness(); @@ -1797,7 +1649,7 @@ it.effect( scope: 'microvertical', }, ]); - }) + }), ); it.effect( @@ -1816,12 +1668,7 @@ it.effect( defineGlobalPolicy<{ readonly amount: number }>({ evaluate: () => { observed.push('denied'); - return Effect.fail( - denyPolicy( - 'counter_locked', - 'Counter changes are locked — try later' - ) - ); + return Effect.fail(denyPolicy('counter_locked', 'Counter changes are locked — try later')); }, policyKey: 'global.counter-locked.v1', }), @@ -1864,7 +1711,7 @@ it.effect( () => { handlerExecutions += 1; return Effect.void; - } + }, ); const harness = yield* makeHarness(); @@ -1874,7 +1721,7 @@ it.effect( principal, registration: action, transport: transport('policy-denied'), - }) + }), ); expect(Predicate.isTagged(denial, 'ActionPolicyDenied')).toBe(true); @@ -1910,16 +1757,13 @@ it.effect( transport: transport('policy-denied'), }); expect(harness.flushed.length).toBe(0); - }) + }), ); it.effect( 'sanitizes Policy defects and interrupts without finalizing', Effect.fn(function* testProgram23() { - const evaluators = [ - () => Effect.die('secret evaluator defect'), - () => Effect.interrupt, - ] as const; + const evaluators = [() => Effect.die('secret evaluator defect'), () => Effect.interrupt] as const; yield* forEachSequential( evaluators.map((evaluate, index) => [index, evaluate] as const), @@ -1960,7 +1804,7 @@ it.effect( () => { handlerExecutions += 1; return Effect.void; - } + }, ); const harness = yield* makeHarness(); const error = yield* Effect.flip( @@ -1969,12 +1813,10 @@ it.effect( principal, registration: action, transport: transport(`policy-failure-${index}`), - }) + }), ); - expect(Predicate.isTagged(error, 'ActionPolicyEvaluationError')).toBe( - true - ); + expect(Predicate.isTagged(error, 'ActionPolicyEvaluationError')).toBe(true); expect(error.reason.includes('secret')).toBe(false); expect(handlerExecutions).toBe(0); @@ -1985,9 +1827,9 @@ it.effect( transactionCount: 0, transitionCount: 0, }); - }) + }), ); - }) + }), ); it.effect( @@ -1995,8 +1837,7 @@ it.effect( Effect.fn(function* testProgram25() { let handlerExecutions = 0; const policy = defineGlobalPolicy({ - evaluate: () => - Effect.fail(denyPolicy('blocked', 'This action is blocked')), + evaluate: () => Effect.fail(denyPolicy('blocked', 'This action is blocked')), policyKey: 'global.blocked.v1', }); const action = defineAction( @@ -2030,7 +1871,7 @@ it.effect( () => { handlerExecutions += 1; return Effect.void; - } + }, ); const harness = yield* makeHarness({ policyFinalizationFailure: true }); @@ -2040,17 +1881,15 @@ it.effect( principal, registration: action, transport: transport('policy-finalization-failure'), - }) + }), ); - expect(Predicate.isTagged(error, 'ActionInvocationPersistenceError')).toBe( - true - ); + expect(Predicate.isTagged(error, 'ActionInvocationPersistenceError')).toBe(true); expect(handlerExecutions).toBe(0); expect(harness.finalized.length).toBe(0); expect(harness.counts().transactionCount).toBe(0); - }) + }), ); it.effect( @@ -2069,15 +1908,13 @@ it.effect( registration: registration(), transport: transport(key), }); - }) + }), ); expect(harness.flushed.length).toBe(2); - expect( - harness.flushed.map((item) => item.evidence.domainEvents.length) - ).toEqual([1, 1]); + expect(harness.flushed.map((item) => item.evidence.domainEvents.length)).toEqual([1, 1]); expect(harness.flushed[0]?.evidence).not.toBe(harness.flushed[1]?.evidence); - }) + }), ); it.effect( @@ -2119,7 +1956,7 @@ it.effect( resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.void + () => Effect.void, ); yield* forEachSequential( ['fresh-first', 'fresh-second'], @@ -2138,11 +1975,11 @@ it.effect( registration: action, transport: transport(key), }); - }) + }), ); expect(evaluations).toBe(2); - }) + }), ); it.effect( @@ -2155,7 +1992,7 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); const invalidPrincipal = yield* Effect.flip( harness.runtime.runAction({ @@ -2163,7 +2000,7 @@ it.effect( principal: { ...principal, principalId: 'not-a-uuid' }, registration: registration(), transport: transport(), - }) + }), ); const missingKey = yield* Effect.flip( harness.runtime.runAction({ @@ -2171,7 +2008,7 @@ it.effect( principal, registration: registration(), transport: { correlationId: 'correlation-missing-key' }, - }) + }), ); const forgedSystemPrincipal = yield* Effect.flip( harness.runtime.runAction({ @@ -2184,31 +2021,17 @@ it.effect( }, registration: registration(), transport: transport('forged-system'), - }) + }), ); - expect( - Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError') - ).toBe(true); - - expect( - Predicate.isTagged( - invalidPrincipal, - 'ActionTrustedContextValidationError' - ) - ).toBe(true); - expect(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')).toBe( - true - ); + expect(Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError')).toBe(true); + + expect(Predicate.isTagged(invalidPrincipal, 'ActionTrustedContextValidationError')).toBe(true); + expect(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')).toBe(true); - expect( - Predicate.isTagged( - forgedSystemPrincipal, - 'ActionTrustedContextValidationError' - ) - ).toBe(true); + expect(Predicate.isTagged(forgedSystemPrincipal, 'ActionTrustedContextValidationError')).toBe(true); expect(harness.counts().createCount).toBe(0); - }) + }), ); it.effect( @@ -2218,12 +2041,9 @@ it.effect( reason: Schema.String, }); type DomainRejectedSelf = typeof DomainRejectedContract.Type; - const DomainRejected = Schema.TaggedError()( - 'DomainRejected', - { - reason: Schema.String, - } - ); + const DomainRejected = Schema.TaggedError()('DomainRejected', { + reason: Schema.String, + }); const harness = yield* makeHarness(); let policyEvaluations = 0; const allowedPolicy = defineGlobalPolicy({ @@ -2273,7 +2093,7 @@ it.effect( subjectResourceType: 'counter', }); return yield* new DomainRejected({ reason: 'counter_locked' }); - }) + }), ); const error = yield* Effect.flip( @@ -2282,7 +2102,7 @@ it.effect( principal, registration: rejected, transport: transport(), - }) + }), ); expect(Predicate.isTagged(error, 'DomainRejected')).toBe(true); @@ -2290,7 +2110,7 @@ it.effect( expect(error.reason).toBe('counter_locked'); expect(policyEvaluations).toBe(1); expect(harness.flushed.length).toBe(0); - }) + }), ); it.effect( @@ -2325,7 +2145,7 @@ it.effect( resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.die('secret database detail') + () => Effect.die('secret database detail'), ); const defect = yield* Effect.flip( defectHarness.runtime.runAction({ @@ -2333,7 +2153,7 @@ it.effect( principal, registration: defective, transport: transport(), - }) + }), ); const resultHarness = yield* makeHarness(); @@ -2369,7 +2189,7 @@ it.effect( const result = { total: 0 }; Object.defineProperty(result, 'total', { value: 'invalid' }); return Effect.succeed(result); - } + }, ); const resultError = yield* Effect.flip( resultHarness.runtime.runAction({ @@ -2377,39 +2197,29 @@ it.effect( principal, registration: invalidResult, transport: transport(), - }) + }), ); - expect(Predicate.isTagged(defect, 'ActionHandlerExecutionError')).toBe( - true - ); + expect(Predicate.isTagged(defect, 'ActionHandlerExecutionError')).toBe(true); expect(defect.reason.includes('secret')).toBe(false); - expect(Predicate.isTagged(resultError, 'ActionResultValidationError')).toBe( - true - ); + expect(Predicate.isTagged(resultError, 'ActionResultValidationError')).toBe(true); expect(defectHarness.flushed.length).toBe(0); expect(resultHarness.flushed.length).toBe(0); - }) + }), ); it.effect( 'sanitizes undeclared handler failures instead of widening the domain error contract', Effect.fn(function* testProgram33() { - const DeclaredDomainErrorContract = Schema.TaggedStruct( - 'DeclaredDomainError', - { - reason: Schema.String, - } - ); + const DeclaredDomainErrorContract = Schema.TaggedStruct('DeclaredDomainError', { + reason: Schema.String, + }); type DeclaredDomainErrorSelf = typeof DeclaredDomainErrorContract.Type; - const DeclaredDomainError = Schema.TaggedError()( - 'DeclaredDomainError', - { - reason: Schema.String, - } - ); + const DeclaredDomainError = Schema.TaggedError()('DeclaredDomainError', { + reason: Schema.String, + }); const undeclaredDomainError = new DeclaredDomainError({ reason: 'secret undeclared failure', }); @@ -2445,7 +2255,7 @@ it.effect( resultSchema: Schema.Void, schemaVersion: '1', }, - () => Effect.fail(undeclaredDomainError) + () => Effect.fail(undeclaredDomainError), ); const error = yield* Effect.flip( harness.runtime.runAction({ @@ -2453,14 +2263,14 @@ it.effect( principal, registration: action, transport: transport(), - }) + }), ); expect(Predicate.isTagged(error, 'ActionHandlerExecutionError')).toBe(true); expect(error.reason.includes('secret')).toBe(false); expect(harness.flushed.length).toBe(0); - }) + }), ); it.effect( @@ -2480,7 +2290,7 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); const conflict = yield* makeHarness({ @@ -2497,7 +2307,7 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); const definite = yield* makeHarness({ @@ -2509,7 +2319,7 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); const uncertain = yield* makeHarness({ transactionMode: 'uncertain' }); @@ -2519,7 +2329,7 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); const definiteCommit = yield* makeHarness({ @@ -2531,15 +2341,10 @@ it.effect( principal, registration: registration(), transport: transport('definite-commit'), - }) + }), ); - const acknowledgementFailureCodes = [ - 'ETIMEDOUT', - 'ECONNABORTED', - 'ENETRESET', - '08007', - ]; + const acknowledgementFailureCodes = ['ETIMEDOUT', 'ECONNABORTED', 'ENETRESET', '08007']; const acknowledgementErrors = yield* Effect.forEach( acknowledgementFailureCodes, Effect.fn(function* testProgram35(code) { @@ -2550,44 +2355,32 @@ it.effect( principal, registration: registration(), transport: transport(`uncertain-${code}`), - }) + }), ); }), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); - expect(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')).toBe( - true - ); + expect(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')).toBe(true); expect(committed.counts().transactionCount).toBe(0); expect(committed.permissionCounts().permissionCheckCount).toBe(0); - expect(Predicate.isTagged(conflictError, 'ActionRequestHashConflict')).toBe( - true - ); + expect(Predicate.isTagged(conflictError, 'ActionRequestHashConflict')).toBe(true); expect(conflict.counts().transactionCount).toBe(0); expect(conflict.permissionCounts().permissionCheckCount).toBe(0); - expect(Predicate.isTagged(definiteError, 'ActionTransactionError')).toBe( - true - ); + expect(Predicate.isTagged(definiteError, 'ActionTransactionError')).toBe(true); - expect( - Predicate.isTagged(definiteCommitError, 'ActionTransactionError') - ).toBe(true); + expect(Predicate.isTagged(definiteCommitError, 'ActionTransactionError')).toBe(true); - expect( - Predicate.isTagged(uncertainError, 'ActionCommitIndeterminate') - ).toBe(true); + expect(Predicate.isTagged(uncertainError, 'ActionCommitIndeterminate')).toBe(true); expect(uncertain.flushed.length).toBe(1); - expect(acknowledgementErrors.length).toBe( - acknowledgementFailureCodes.length - ); + expect(acknowledgementErrors.length).toBe(acknowledgementFailureCodes.length); for (const error of acknowledgementErrors) { expect(Predicate.isTagged(error, 'ActionCommitIndeterminate')).toBe(true); } - }) + }), ); it.effect( @@ -2596,9 +2389,7 @@ it.effect( const commitStarted = Deferred.makeUnsafe(); const commitSettlement = Deferred.makeUnsafe(); const harness = yield* makeHarness({ - commit: Deferred.succeed(commitStarted, null).pipe( - Effect.andThen(Deferred.await(commitSettlement)) - ), + commit: Deferred.succeed(commitStarted, null).pipe(Effect.andThen(Deferred.await(commitSettlement))), }); const actionFiber = yield* harness.runtime @@ -2610,9 +2401,7 @@ it.effect( }) .pipe(Effect.forkChild); yield* Deferred.await(commitStarted); - const interruption = yield* Fiber.interrupt(actionFiber).pipe( - Effect.forkChild - ); + const interruption = yield* Fiber.interrupt(actionFiber).pipe(Effect.forkChild); yield* Effect.yieldNow; const pending = actionFiber.pollUnsafe() === undefined; yield* Deferred.succeed(commitSettlement, []); @@ -2627,7 +2416,7 @@ it.effect( } expect(Cause.hasInterrupts(exit.cause)).toBe(true); expect(harness.flushed.length).toBe(1); - }) + }), ); it.effect( @@ -2655,9 +2444,7 @@ it.effect( status: 'succeeded', }, }); - const committedResolution = yield* Effect.flip( - committed.runtime.resolveActionCommit({ invocationId, principal }) - ); + const committedResolution = yield* Effect.flip(committed.runtime.resolveActionCommit({ invocationId, principal })); const unavailable = yield* makeHarness({ createRecord: { @@ -2669,27 +2456,21 @@ it.effect( resolutionUnavailable: true, }); const unavailableResolution = yield* Effect.flip( - unavailable.runtime.resolveActionCommit({ invocationId, principal }) + unavailable.runtime.resolveActionCommit({ invocationId, principal }), ); expect(Predicate.isTagged(openResolution, 'ActionCommitOpen')).toBe(true); expect(Struct.omit(openResolution, ['_tag'])).toEqual({ invocationId, }); - expect( - Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted') - ).toBe(true); - - expect( - Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate') - ).toBe(true); - if ( - !Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate') - ) { + expect(Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted')).toBe(true); + + expect(Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate')).toBe(true); + if (!Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate')) { throw new Error('Expected typed test outcome'); } expect(unavailableResolution.invocationId).toBe(invocationId); - }) + }), ); it.effect( @@ -2709,14 +2490,14 @@ it.effect( principal, registration: registration(), transport: transport(), - }) + }), ); expect(Predicate.isTagged(error, 'ActionInvocationStateError')).toBe(true); expect(terminal.counts().transitionCount).toBe(0); expect(terminal.counts().transactionCount).toBe(0); - }) + }), ); it.effect( @@ -2752,7 +2533,7 @@ it.effect( resultSchema: Schema.Struct({ reserved: Schema.Boolean }), schemaVersion: '1', }, - () => Effect.succeed({ reserved: true }) + () => Effect.succeed({ reserved: true }), ); const shellResult = yield* shell.runtime.runAction({ @@ -2773,7 +2554,7 @@ it.effect( expect(shellResult).toEqual({ total: 1 }); expect(moduleResult).toEqual({ reserved: true }); - }) + }), ); it('the Core database service identity remains server-only', () => { diff --git a/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts b/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts index 55825f4e2..05803ecbb 100644 --- a/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts +++ b/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts @@ -5,10 +5,7 @@ import { defineAction } from '../../src/actions/definition.ts'; import { defineGlobalPolicy, denyPolicy } from '../../src/actions/policy.ts'; import { ACTION_RUNTIME_STAGES } from '../../src/actions/runtime.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { - bindActionTestServices, - makeActionTestHarness, -} from '../../src/testing/actions.ts'; +import { bindActionTestServices, makeActionTestHarness } from '../../src/testing/actions.ts'; const principal = { authBindingId: '10000000-0000-4000-8000-000000000001', @@ -64,7 +61,7 @@ const lifecycleAction = defineAction( topic: 'test.counter.incremented.v1', }); return { total: payload.amount }; - }) + }), ); const request = { @@ -91,16 +88,14 @@ it.effect( expect(Predicate.isTagged(replay, 'ActionAlreadyCommitted')).toBe(true); - expect(snapshot.stages.slice(0, ACTION_RUNTIME_STAGES.length)).toEqual( - ACTION_RUNTIME_STAGES - ); + expect(snapshot.stages.slice(0, ACTION_RUNTIME_STAGES.length)).toEqual(ACTION_RUNTIME_STAGES); expect(snapshot.invocations.length).toBe(1); expect(snapshot.invocations[0]?.status).toBe('succeeded'); expect(snapshot.transactionCount).toBe(1); expect(snapshot.committed.length).toBe(1); expect(snapshot.committed[0]?.evidence.domainEvents.length).toBe(1); expect(snapshot.committed[0]?.evidence.outboxMessages.length).toBe(1); - }) + }), ); it.effect( @@ -119,7 +114,7 @@ it.effect( expect(snapshot.invocations[0]?.completedAt?.getTime()).toBe(0); expect(snapshot.transactionCount).toBe(0); expect(snapshot.stages.includes('handler_executed')).toBe(false); - }) + }), ); it.effect( @@ -157,8 +152,7 @@ it.effect( schemaVersion: '1', }, (payload, context) => context.services.increment(payload.amount), - (): Effect.Effect => - Effect.die('production owner services must not run in this test') + (): Effect.Effect => Effect.die('production owner services must not run in this test'), ); let calls = 0; const harness = yield* makeActionTestHarness({ @@ -187,7 +181,7 @@ it.effect( expect(result).toBe(5); expect(calls).toBe(1); expect(harness.snapshot().committed.length).toBe(1); - }) + }), ); it.effect( @@ -206,48 +200,39 @@ it.effect( }) .pipe(Effect.flip); - expect(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')).toBe( - true - ); + expect(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')).toBe(true); expect(harness.snapshot().invocations.length).toBe(0); - }) + }), ); -it.effect( - 'persists policy denials separately from permission denials before handler execution', - () => - Effect.gen(function* policyDenialSnapshot() { - const registration = defineAction( - { - ...lifecycleAction.descriptor, - policies: [ - defineGlobalPolicy({ - evaluate: () => - Effect.fail(denyPolicy('counter_locked', 'Counter is locked')), - policyKey: 'global.counter-locked.v1', - }), - ], - }, - () => Effect.die('A denied policy must not execute the handler') - ); - const harness = yield* makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - yield* harness.runtime - .runAction({ ...request, registration }) - .pipe(Effect.flip); - const snapshot = harness.snapshot(); - expect(snapshot.policyDenials.length).toBe(1); - expect(snapshot.permissionDenials.length).toBe(0); - expect(snapshot.invocations[0]?.status).toBe('rejected'); - expect(snapshot.invocations[0]?.completedAt?.getTime()).toBe(0); - expect(snapshot.policyDenials[0]?.actionInvocationId).toBe( - snapshot.invocations[0]?.actionInvocationId - ); - expect(snapshot.transactionCount).toBe(0); - expect(snapshot.committed.length).toBe(0); - expect(snapshot.stages.includes('handler_executed')).toBe(false); - }) +it.effect('persists policy denials separately from permission denials before handler execution', () => + Effect.gen(function* policyDenialSnapshot() { + const registration = defineAction( + { + ...lifecycleAction.descriptor, + policies: [ + defineGlobalPolicy({ + evaluate: () => Effect.fail(denyPolicy('counter_locked', 'Counter is locked')), + policyKey: 'global.counter-locked.v1', + }), + ], + }, + () => Effect.die('A denied policy must not execute the handler'), + ); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + yield* harness.runtime.runAction({ ...request, registration }).pipe(Effect.flip); + const snapshot = harness.snapshot(); + expect(snapshot.policyDenials.length).toBe(1); + expect(snapshot.permissionDenials.length).toBe(0); + expect(snapshot.invocations[0]?.status).toBe('rejected'); + expect(snapshot.invocations[0]?.completedAt?.getTime()).toBe(0); + expect(snapshot.policyDenials[0]?.actionInvocationId).toBe(snapshot.invocations[0]?.actionInvocationId); + expect(snapshot.transactionCount).toBe(0); + expect(snapshot.committed.length).toBe(0); + expect(snapshot.stages.includes('handler_executed')).toBe(false); + }), ); diff --git a/app/packages/core-runtime/tests/unit/application-composition.test.ts b/app/packages/core-runtime/tests/unit/application-composition.test.ts index 513a14179..b6cb4cb9e 100644 --- a/app/packages/core-runtime/tests/unit/application-composition.test.ts +++ b/app/packages/core-runtime/tests/unit/application-composition.test.ts @@ -12,7 +12,7 @@ const sha256 = (character: string) => character.repeat(64); const assertInvalid = Effect.fn(function* testProgram1( effect: Effect.Effect, - reason: RegExp + reason: RegExp, ) { const error = yield* Effect.flip(effect); expect(error.reason).toMatch(reason); @@ -26,10 +26,7 @@ const candidate = () => { return { modules: [ { - allowedContributions: [ - 'contacts.core.navigation.contacts', - 'contacts.core.page.contacts', - ], + allowedContributions: ['contacts.core.navigation.contacts', 'contacts.core.page.contacts'], contract: { sha256: sha256('a'), url: 'https://contacts.example/.well-known/ontos-module-manifest.json', @@ -81,12 +78,8 @@ const candidate = () => { const evidence = () => ({ contracts: { contacts: { - contractUrl: - 'https://contacts.example/.well-known/ontos-module-manifest.json', - contributionKeys: [ - 'contacts.core.navigation.contacts', - 'contacts.core.page.contacts', - ], + contractUrl: 'https://contacts.example/.well-known/ontos-module-manifest.json', + contributionKeys: ['contacts.core.navigation.contacts', 'contacts.core.page.contacts'], deployment: { appId: 'contacts', buildMarker: 'contacts-build-1' }, federationExposes: ['./Navigation', './PageContacts'], mfBoundaryId: 'contacts', @@ -121,38 +114,23 @@ const required = (value: Value | undefined): Value => { const onlyModule = (input: Candidate) => required(input.modules[0]); const federationManifest = (observations: Evidence) => - required( - observations.federationManifests[ - 'https://contacts.example/mf-manifest.json' - ] - ); + required(observations.federationManifests['https://contacts.example/mf-manifest.json']); -it.effect( - 'defaults the validation error code without changing its encoded contract', - () => - Effect.gen(function* encodeValidationError() { - const error = new ApplicationCompositionValidationError({ - reason: 'Invalid candidate', - }); - const encodedError = yield* Schema.encodeEffect( - ApplicationCompositionValidationError - )(error); - expect( - Schema.is(Schema.toEncoded(ApplicationCompositionValidationError))( - encodedError - ) - ).toBe(true); - expect(Struct.omit(encodedError, ['_tag'])).toEqual({ - code: 'application_composition_invalid', - reason: 'Invalid candidate', - }); - }) +it.effect('defaults the validation error code without changing its encoded contract', () => + Effect.gen(function* encodeValidationError() { + const error = new ApplicationCompositionValidationError({ + reason: 'Invalid candidate', + }); + const encodedError = yield* Schema.encodeEffect(ApplicationCompositionValidationError)(error); + expect(Schema.is(Schema.toEncoded(ApplicationCompositionValidationError))(encodedError)).toBe(true); + expect(Struct.omit(encodedError, ['_tag'])).toEqual({ + code: 'application_composition_invalid', + reason: 'Invalid candidate', + }); + }), ); -const addModuleCopy = ( - input: Candidate, - overrides: Partial> -): number => { +const addModuleCopy = (input: Candidate, overrides: Partial>): number => { const module = onlyModule(input); return input.modules.push({ ...structuredClone(module), @@ -177,23 +155,15 @@ it.effect( 'accepts one provider-neutral composition and produces deterministic canonical JSON', Effect.fn(function* testProgram2() { const input = candidate(); - const composition = yield* validateApplicationCompositionCandidate( - input, - evidence() - ); + const composition = yield* validateApplicationCompositionCandidate(input, evidence()); expect(composition).toEqual(input); expect(Object.isFrozen(composition)).toBe(true); - expect( - Object.isFrozen(required(composition.modules[0]).federation.exposes) - ).toBe(true); + expect(Object.isFrozen(required(composition.modules[0]).federation.exposes)).toBe(true); expect(Object.isFrozen(input)).toBe(false); yield* assertInvalid( - validateApplicationCompositionCandidate( - { ...input, provider: 'zephyr' }, - evidence() - ), - /supported .* schema/u + validateApplicationCompositionCandidate({ ...input, provider: 'zephyr' }, evidence()), + /supported .* schema/u, ); const reordered = structuredClone(input); @@ -205,42 +175,34 @@ it.effect( reordered.shell.coreCapabilities.reverse(); reordered.shell.sharedSingletons.reverse(); /* oxlint-disable perfectionist/sort-objects -- Deliberately reorder nested fields to test canonical encoding. expires: 2026-12-31. */ - reordered.shell.coreCapabilities = reordered.shell.coreCapabilities.map( - ({ id, version }) => ({ - version, - id, - }) - ); - reorderedModule.sharedSingletons = reorderedModule.sharedSingletons.map( - ({ packageName, version }) => ({ version, packageName }) - ); + reordered.shell.coreCapabilities = reordered.shell.coreCapabilities.map(({ id, version }) => ({ + version, + id, + })); + reorderedModule.sharedSingletons = reorderedModule.sharedSingletons.map(({ packageName, version }) => ({ + version, + packageName, + })); reorderedModule.contract = { url: reorderedModule.contract.url, sha256: reorderedModule.contract.sha256, }; /* oxlint-enable perfectionist/sort-objects */ expect( - canonicalizeApplicationComposition( - yield* validateApplicationCompositionCandidate(reordered, evidence()) - ) + canonicalizeApplicationComposition(yield* validateApplicationCompositionCandidate(reordered, evidence())), ).toBe(canonicalizeApplicationComposition(composition)); expect( - yield* Schema.decodeEffect( - Schema.fromJsonString(ApplicationCompositionSchema) - )(canonicalizeApplicationComposition(composition)) + yield* Schema.decodeEffect(Schema.fromJsonString(ApplicationCompositionSchema))( + canonicalizeApplicationComposition(composition), + ), ).toEqual(composition); - }) + }), ); it.effect( 'allows loopback HTTP only with trusted development evidence', Effect.fn(function* testProgram3() { - for (const host of [ - 'localhost', - '127.0.0.1', - '[::1]', - 'contacts.localhost', - ]) { + for (const host of ['localhost', '127.0.0.1', '[::1]', 'contacts.localhost']) { for (const artifact of ['contract', 'federation']) { const input = candidate(); const observations = evidence(); @@ -257,24 +219,20 @@ it.effect( [module.federation.manifest.url]: federationManifest(observations), }, }; - for (const environment of [ - {}, - { environment: 'stage' }, - { environment: 'production' }, - ]) { + for (const environment of [{}, { environment: 'stage' }, { environment: 'production' }]) { yield* assertInvalid( validateApplicationCompositionCandidate(input, { ...observed, ...environment, }), - /HTTPS outside development/u + /HTTPS outside development/u, ); } expect( yield* validateApplicationCompositionCandidate(input, { ...observed, environment: 'development', - }) + }), ).toEqual(input); } } @@ -285,72 +243,38 @@ it.effect( ...evidence(), environment: 'development', }), - /supported .* schema/u + /supported .* schema/u, ); - }) + }), ); it.effect( 'rejects candidate-wide ownership and compatibility contradictions', Effect.fn(function* testProgram4() { const cases: readonly [ - mutate: ( - input: Candidate, - observations: Evidence - ) => number | readonly string[] | string, + mutate: (input: Candidate, observations: Evidence) => number | readonly string[] | string, reason: RegExp, ][] = [ + [(input) => (onlyModule(input).federation.remoteName = 'anotherRemote'), /observed deployment contract/u], [ - (input) => (onlyModule(input).federation.remoteName = 'anotherRemote'), - /observed deployment contract/u, - ], - [ - (_input, observations) => - (observations.contracts.contacts.mfBoundaryId = 'anotherRemote'), + (_input, observations) => (observations.contracts.contacts.mfBoundaryId = 'anotherRemote'), /observed deployment contract/u, ], [ - (_input, observations) => - (federationManifest(observations).remoteName = 'anotherRemote'), + (_input, observations) => (federationManifest(observations).remoteName = 'anotherRemote'), /Module Federation manifest/u, ], + [(_input, observations) => (observations.contracts.contacts.contractUrl = 'invalid-url'), /observation schema/u], + [(input) => onlyModule(input).dependencies.push('billing.core'), /dependency billing\.core/u], + [(input) => onlyModule(input).dependencies.push('contacts.core'), /dependency cycle/u], + [(input) => onlyModule(input).dependencies.push('contacts.core', 'contacts.core'), /duplicate dependency/u], + [(input) => addModuleCopy(input, { moduleId: 'inventory.stock' }), /duplicate Shell contribution/u], + [(input) => addModuleCopy(input, { allowedContributions: [] }), /duplicate module ID contacts\.core/u], [ - (_input, observations) => - (observations.contracts.contacts.contractUrl = 'invalid-url'), - /observation schema/u, - ], - [ - (input) => onlyModule(input).dependencies.push('billing.core'), - /dependency billing\.core/u, - ], - [ - (input) => onlyModule(input).dependencies.push('contacts.core'), - /dependency cycle/u, - ], - [ - (input) => - onlyModule(input).dependencies.push('contacts.core', 'contacts.core'), - /duplicate dependency/u, - ], - [ - (input) => addModuleCopy(input, { moduleId: 'inventory.stock' }), - /duplicate Shell contribution/u, - ], - [ - (input) => addModuleCopy(input, { allowedContributions: [] }), - /duplicate module ID contacts\.core/u, - ], - [ - (input) => - (onlyModule(input).allowedContributions = [ - 'contacts.core.page.contacts', - ]), - /observed deployment contract/u, - ], - [ - (input) => (onlyModule(input).federation.exposes = ['./Navigation']), + (input) => (onlyModule(input).allowedContributions = ['contacts.core.page.contacts']), /observed deployment contract/u, ], + [(input) => (onlyModule(input).federation.exposes = ['./Navigation']), /observed deployment contract/u], [ (input) => { const module = onlyModule(input); @@ -399,12 +323,8 @@ it.effect( ], [ (input, observations) => { - const moduleSingleton = required( - onlyModule(input).sharedSingletons[0] - ); - const runtimeSingleton = required( - observations.runtime.sharedSingletons[0] - ); + const moduleSingleton = required(onlyModule(input).sharedSingletons[0]); + const runtimeSingleton = required(observations.runtime.sharedSingletons[0]); const shellSingleton = required(input.shell.sharedSingletons[0]); shellSingleton.packageName = 'foo'; shellSingleton.version = 'bar@baz'; @@ -416,14 +336,9 @@ it.effect( }, /incompatible shared singleton foo@bar/u, ], + [(input) => (onlyModule(input).requiredShellAbi.version = '2'), /Shell contribution ABI/u], [ - (input) => (onlyModule(input).requiredShellAbi.version = '2'), - /Shell contribution ABI/u, - ], - [ - (input) => - (required(onlyModule(input).requiredCoreCapabilities[0]).version = - '2'), + (input) => (required(onlyModule(input).requiredCoreCapabilities[0]).version = '2'), /Core capability core\.authorization/u, ], [ @@ -434,26 +349,15 @@ it.effect( }), /shared singleton react/u, ], + [(input) => (onlyModule(input).federation.execution = 'server'), /supported .* schema/u], [ - (input) => (onlyModule(input).federation.execution = 'server'), - /supported .* schema/u, - ], - [ - (input) => - (onlyModule(input).contract.url = - 'https://contacts.example/manifest.json?tag=live'), + (input) => (onlyModule(input).contract.url = 'https://contacts.example/manifest.json?tag=live'), /supported .* schema/u, ], - [ - (_input, observations) => - (federationManifest(observations).exposes = []), - /Module Federation manifest/u, - ], + [(_input, observations) => (federationManifest(observations).exposes = []), /Module Federation manifest/u], [ (_input, observations) => { - const singleton = required( - federationManifest(observations).sharedSingletons[0] - ); + const singleton = required(federationManifest(observations).sharedSingletons[0]); singleton.version = '18.3.1'; return singleton.version; }, @@ -465,10 +369,7 @@ it.effect( const input = candidate(); const observations = evidence(); mutate(input, observations); - yield* assertInvalid( - validateApplicationCompositionCandidate(input, observations), - reason - ); + yield* assertInvalid(validateApplicationCompositionCandidate(input, observations), reason); } - }) + }), ); diff --git a/app/packages/core-runtime/tests/unit/catalog-contract.test.ts b/app/packages/core-runtime/tests/unit/catalog-contract.test.ts index dd91b2eaa..febb6b4d6 100644 --- a/app/packages/core-runtime/tests/unit/catalog-contract.test.ts +++ b/app/packages/core-runtime/tests/unit/catalog-contract.test.ts @@ -1,27 +1,22 @@ import { expect, it } from 'effect-rstest'; -import { - compareApplicationCatalog, - expectedCoreTableCatalog, -} from '../../src/db/catalog.ts'; +import { compareApplicationCatalog, expectedCoreTableCatalog } from '../../src/db/catalog.ts'; import type { CatalogEntry } from '../../src/db/catalog.ts'; -const exactCatalog = expectedCoreTableCatalog.map( - (qualifiedName) => { - const [schemaName, tableName] = qualifiedName.split('.'); - expect((schemaName?.length ?? 0) > 0).toBe(true); - expect((tableName?.length ?? 0) > 0).toBe(true); - if (schemaName === undefined || tableName === undefined) { - throw new TypeError('Core catalog entries must be schema-qualified'); - } - - return { - kind: 'table', - schemaName, - tableName, - }; +const exactCatalog = expectedCoreTableCatalog.map((qualifiedName) => { + const [schemaName, tableName] = qualifiedName.split('.'); + expect((schemaName?.length ?? 0) > 0).toBe(true); + expect((tableName?.length ?? 0) > 0).toBe(true); + if (schemaName === undefined || tableName === undefined) { + throw new TypeError('Core catalog entries must be schema-qualified'); } -); + + return { + kind: 'table', + schemaName, + tableName, + }; +}); it('reports one missing expected Core table', () => { const difference = compareApplicationCatalog(exactCatalog.slice(1)); diff --git a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts index 7a2bc82a0..085b99966 100644 --- a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts +++ b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts @@ -52,7 +52,7 @@ it.effect( Effect.sync(() => { executions += 1; return { total: payload.amount * executions }; - }) + }), ); const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', @@ -68,169 +68,138 @@ it.effect( } as const; expect(yield* harness.runtime.runAction(request)).toEqual({ total: 2 }); - const invocationId = - harness.snapshot().invocations[0]?.actionInvocationId; + const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; expect(invocationId).toBeDefined(); if (invocationId === undefined) { throw new Error('Missing invocationId'); } - const replay = yield* harness.runtime - .runAction(request) - .pipe(Effect.flip); - const recovered = yield* harness.runtime - .resolveActionCommit({ invocationId, principal }) - .pipe(Effect.flip); + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + const recovered = yield* harness.runtime.resolveActionCommit({ invocationId, principal }).pipe(Effect.flip); for (const outcome of [replay, recovered]) { - expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe( - true - ); - expect( - 'invocationId' in outcome ? outcome.invocationId : undefined - ).toBe(invocationId); + expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe(true); + expect('invocationId' in outcome ? outcome.invocationId : undefined).toBe(invocationId); expect('total' in outcome).toBe(false); expect('result' in outcome).toBe(false); } expect(executions).toBe(1); expect(harness.snapshot().committed.length).toBe(1); expect(harness.snapshot().transactionCount).toBe(1); - }) + }), ); -it.effect( - 'committed error schema requires and preserves the recovery invocation identifier', - () => - Effect.gen(function* migratedTest() { - const encoded = { +it.effect('committed error schema requires and preserves the recovery invocation identifier', () => + Effect.gen(function* migratedTest() { + const encoded = { + _tag: 'ActionAlreadyCommitted', + code: 'action_already_committed', + invocationId: '40000000-0000-4000-8000-000000000001', + reason: 'This idempotency key already committed successfully', + } as const; + const decoded = yield* Schema.decodeEffect(ActionAlreadyCommitted)(encoded); + const reencoded = yield* decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted)); + expect(Schema.is(Schema.toEncoded(ActionAlreadyCommitted))(reencoded)).toBe(true); + expect(Struct.omit(reencoded, ['_tag'])).toEqual(Struct.omit(encoded, ['_tag'])); + expect( + Schema.is(ActionAlreadyCommitted)({ _tag: 'ActionAlreadyCommitted', code: 'action_already_committed', - invocationId: '40000000-0000-4000-8000-000000000001', reason: 'This idempotency key already committed successfully', - } as const; - const decoded = yield* Schema.decodeEffect(ActionAlreadyCommitted)( - encoded - ); - const reencoded = yield* decoded.pipe( - Schema.encodeEffect(ActionAlreadyCommitted) - ); - expect( - Schema.is(Schema.toEncoded(ActionAlreadyCommitted))(reencoded) - ).toBe(true); - expect(Struct.omit(reencoded, ['_tag'])).toEqual( - Struct.omit(encoded, ['_tag']) - ); - expect( - Schema.is(ActionAlreadyCommitted)({ - _tag: 'ActionAlreadyCommitted', - code: 'action_already_committed', - reason: 'This idempotency key already committed successfully', - }) - ).toBe(false); - expect('result' in decoded).toBe(false); - expect('status' in decoded).toBe(false); - }) + }), + ).toBe(false); + expect('result' in decoded).toBe(false); + expect('status' in decoded).toBe(false); + }), ); -it.effect( - 'lost commit acknowledgement recovers the committed invocation and faults only once', - () => - Effect.gen(function* migratedTest() { - let executions = 0; - const registration = defineAction( - { - accessEvidencePolicy: { - captureMode: 'metadata_only', - policyKey: 'test.recovery.read.v1', - }, - actionKey: 'test.recovery.acknowledgement', - auditProfile: 'minimal', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { - kind: 'action_execution', - provisioning: 'tenant_membership_default', - }, - entrypointKey: 'test.recovery.acknowledgement', - moduleKey: 'test.recovery', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'test.recovery', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Finite, - schemaVersion: '1', - }, - () => - Effect.sync(() => { - executions += 1; - return executions; - }) - ); - const harness = yield* makeActionTestHarness({ - actionPermission: 'allowed', - commitAcknowledgement: 'indeterminate-once', - }); - const request = { - payload: undefined, - principal, - registration, - transport: { - correlationId: 'lost-acknowledgement', - idempotencyKey: 'commit-once', +it.effect('lost commit acknowledgement recovers the committed invocation and faults only once', () => + Effect.gen(function* migratedTest() { + let executions = 0; + const registration = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'test.recovery.read.v1', }, - } as const; + actionKey: 'test.recovery.acknowledgement', + auditProfile: 'minimal', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'test.recovery.acknowledgement', + moduleKey: 'test.recovery', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'test.recovery', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Finite, + schemaVersion: '1', + }, + () => + Effect.sync(() => { + executions += 1; + return executions; + }), + ); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + commitAcknowledgement: 'indeterminate-once', + }); + const request = { + payload: undefined, + principal, + registration, + transport: { + correlationId: 'lost-acknowledgement', + idempotencyKey: 'commit-once', + }, + } as const; - const uncertain = yield* harness.runtime - .runAction(request) - .pipe(Effect.flip); - expect(Predicate.isTagged(uncertain, 'ActionCommitIndeterminate')).toBe( - true - ); - expect('invocationId' in uncertain).toBe(true); - if (!('invocationId' in uncertain)) { - throw new Error('Missing invocation identifier'); - } - expect(uncertain.invocationId).toBe( - harness.snapshot().invocations[0]?.actionInvocationId - ); - expect(harness.snapshot().invocations[0]?.status).toBe('succeeded'); + const uncertain = yield* harness.runtime.runAction(request).pipe(Effect.flip); + expect(Predicate.isTagged(uncertain, 'ActionCommitIndeterminate')).toBe(true); + expect('invocationId' in uncertain).toBe(true); + if (!('invocationId' in uncertain)) { + throw new Error('Missing invocation identifier'); + } + expect(uncertain.invocationId).toBe(harness.snapshot().invocations[0]?.actionInvocationId); + expect(harness.snapshot().invocations[0]?.status).toBe('succeeded'); - const recovered = yield* harness.runtime - .resolveActionCommit({ - invocationId: uncertain.invocationId, - principal, - }) - .pipe(Effect.flip); - const replay = yield* harness.runtime - .runAction(request) - .pipe(Effect.flip); - for (const outcome of [recovered, replay]) { - expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe( - true - ); - expect('invocationId' in outcome).toBe(true); - if (!('invocationId' in outcome)) { - throw new Error('Missing invocation identifier'); - } - expect(outcome.invocationId).toBe(uncertain.invocationId); + const recovered = yield* harness.runtime + .resolveActionCommit({ + invocationId: uncertain.invocationId, + principal, + }) + .pipe(Effect.flip); + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + for (const outcome of [recovered, replay]) { + expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe(true); + expect('invocationId' in outcome).toBe(true); + if (!('invocationId' in outcome)) { + throw new Error('Missing invocation identifier'); } - expect(executions).toBe(1); - expect(harness.snapshot().committed.length).toBe(1); - expect(harness.snapshot().transactionCount).toBe(1); + expect(outcome.invocationId).toBe(uncertain.invocationId); + } + expect(executions).toBe(1); + expect(harness.snapshot().committed.length).toBe(1); + expect(harness.snapshot().transactionCount).toBe(1); - expect( - yield* harness.runtime.runAction({ - ...request, - transport: { - correlationId: 'acknowledged-next', - idempotencyKey: 'next-invocation', - }, - }) - ).toBe(2); - expect(harness.snapshot().committed.length).toBe(2); - }) + expect( + yield* harness.runtime.runAction({ + ...request, + transport: { + correlationId: 'acknowledged-next', + idempotencyKey: 'next-invocation', + }, + }), + ).toBe(2); + expect(harness.snapshot().committed.length).toBe(2); + }), ); diff --git a/app/packages/core-runtime/tests/unit/config.test.ts b/app/packages/core-runtime/tests/unit/config.test.ts index f61859a5d..c43738440 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -9,29 +9,25 @@ import { parseDatabaseConnectionPair, } from '../../src/db/config.ts'; -it.effect( - 'loads the root environment independently of the invocation directory', - () => - Effect.gen(function* migratedTest() { - const originalDirectory = process.cwd(); - const rootExamplePath = ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); +it.effect('loads the root environment independently of the invocation directory', () => + Effect.gen(function* migratedTest() { + const originalDirectory = process.cwd(); + const rootExamplePath = ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); - yield* Effect.addFinalizer(() => - Effect.sync(() => { - process.chdir(originalDirectory); - }) - ); - process.chdir('/'); - const configuration = yield* loadDatabaseConfig({ - environment: {}, - envPath: rootExamplePath, - }); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + process.chdir(originalDirectory); + }), + ); + process.chdir('/'); + const configuration = yield* loadDatabaseConfig({ + environment: {}, + envPath: rootExamplePath, + }); - expect(ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); - expect(configuration.connectionString).toBe( - 'postgresql://ontos_runtime:ontos_runtime@localhost:5433/ontos' - ); - }) + expect(ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); + expect(configuration.connectionString).toBe('postgresql://ontos_runtime:ontos_runtime@localhost:5433/ontos'); + }), ); it.effect('parses valid local PostgreSQL connection settings', () => @@ -47,81 +43,66 @@ it.effect('parses valid local PostgreSQL connection settings', () => port: 5433, user: 'ontos', }); - }) + }), ); -it.effect( - 'keeps missing and malformed configuration in the typed error channel', - () => - Effect.gen(function* migratedTest() { - const missing = yield* Effect.flip(parseDatabaseConfig({})); - const malformed = yield* Effect.flip( - parseDatabaseConfig({ - DATABASE_URL: 'https://localhost/not-postgres', - }) - ); +it.effect('keeps missing and malformed configuration in the typed error channel', () => + Effect.gen(function* migratedTest() { + const missing = yield* Effect.flip(parseDatabaseConfig({})); + const malformed = yield* Effect.flip( + parseDatabaseConfig({ + DATABASE_URL: 'https://localhost/not-postgres', + }), + ); - expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); - expect(Predicate.isTagged(malformed, 'DatabaseConfigError')).toBe(true); - }) + expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(malformed, 'DatabaseConfigError')).toBe(true); + }), ); -it.effect( - 'requires distinct administrative and least-privilege runtime identities', - () => - Effect.gen(function* migratedTest() { - const valid = yield* parseDatabaseConnectionPair({ - DATABASE_ADMIN_URL: - 'postgresql://ontos_admin:admin@localhost:5433/ontos', +it.effect('requires distinct administrative and least-privilege runtime identities', () => + Effect.gen(function* migratedTest() { + const valid = yield* parseDatabaseConnectionPair({ + DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', + DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', + }); + const missing = yield* Effect.flip( + parseDatabaseConnectionPair({ DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', - }); - const missing = yield* Effect.flip( - parseDatabaseConnectionPair({ - DATABASE_URL: - 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', - }) - ); - const identical = yield* Effect.flip( - parseDatabaseConnectionPair({ - DATABASE_ADMIN_URL: 'postgresql://ontos:secret@localhost:5433/ontos', - DATABASE_URL: 'postgresql://ontos:secret@localhost:5433/ontos', - }) - ); - const superuserCompatible = yield* Effect.flip( - parseDatabaseConnectionPair({ - DATABASE_ADMIN_URL: - 'postgresql://ontos_admin:admin@localhost:5433/ontos', - DATABASE_URL: 'postgresql://postgres:secret@localhost:5433/ontos', - }) - ); - const queryParameterIdentities = yield* parseDatabaseConnectionPair({ - DATABASE_ADMIN_URL: - 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_admin', - DATABASE_URL: - 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_runtime', - }); - const queryParameterCollision = yield* Effect.flip( - parseDatabaseConnectionPair({ - DATABASE_ADMIN_URL: - 'postgresql://admin-authority@localhost:5433/ontos?user=effective_role', - DATABASE_URL: - 'postgresql://runtime-authority@localhost:5433/ontos?user=effective_role', - }) - ); + }), + ); + const identical = yield* Effect.flip( + parseDatabaseConnectionPair({ + DATABASE_ADMIN_URL: 'postgresql://ontos:secret@localhost:5433/ontos', + DATABASE_URL: 'postgresql://ontos:secret@localhost:5433/ontos', + }), + ); + const superuserCompatible = yield* Effect.flip( + parseDatabaseConnectionPair({ + DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', + DATABASE_URL: 'postgresql://postgres:secret@localhost:5433/ontos', + }), + ); + const queryParameterIdentities = yield* parseDatabaseConnectionPair({ + DATABASE_ADMIN_URL: 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_admin', + DATABASE_URL: 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_runtime', + }); + const queryParameterCollision = yield* Effect.flip( + parseDatabaseConnectionPair({ + DATABASE_ADMIN_URL: 'postgresql://admin-authority@localhost:5433/ontos?user=effective_role', + DATABASE_URL: 'postgresql://runtime-authority@localhost:5433/ontos?user=effective_role', + }), + ); - expect(valid.admin.user).toBe('ontos_admin'); - expect(valid.runtime.user).toBe('ontos_runtime'); - expect(queryParameterIdentities.admin.user).toBe('ontos_admin'); - expect(queryParameterIdentities.runtime.user).toBe('ontos_runtime'); - expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); - expect(Predicate.isTagged(identical, 'DatabaseConfigError')).toBe(true); - expect( - Predicate.isTagged(queryParameterCollision, 'DatabaseConfigError') - ).toBe(true); - expect( - Predicate.isTagged(superuserCompatible, 'DatabaseConfigError') - ).toBe(true); - }) + expect(valid.admin.user).toBe('ontos_admin'); + expect(valid.runtime.user).toBe('ontos_runtime'); + expect(queryParameterIdentities.admin.user).toBe('ontos_admin'); + expect(queryParameterIdentities.runtime.user).toBe('ontos_runtime'); + expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(identical, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(queryParameterCollision, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(superuserCompatible, 'DatabaseConfigError')).toBe(true); + }), ); it.effect('finalizes the pool resource when its Effect scope closes', () => @@ -134,9 +115,9 @@ it.effect('finalizes the pool resource when its Effect scope closes', () => finalized = true; return Promise.resolve(); }, - })) + })), ); expect(finalized).toBe(true); - }) + }), ); diff --git a/app/packages/core-runtime/tests/unit/context-access.test.ts b/app/packages/core-runtime/tests/unit/context-access.test.ts index ed80de0c2..c3d4cb89c 100644 --- a/app/packages/core-runtime/tests/unit/context-access.test.ts +++ b/app/packages/core-runtime/tests/unit/context-access.test.ts @@ -19,7 +19,7 @@ const principalId = '30000000-0000-4000-8000-000000000001'; const responseFor = ( request: v1.CheckBulkPermissionsRequest, - permissionships: readonly v1.CheckPermissionResponse_Permissionship[] + permissionships: readonly v1.CheckPermissionResponse_Permissionship[], ) => v1.CheckBulkPermissionsResponse.create({ pairs: request.items.map((item, index) => @@ -27,62 +27,54 @@ const responseFor = ( request: item, response: { item: v1.CheckBulkPermissionsResponseItem.create({ - permissionship: - permissionships[index] ?? - v1.CheckPermissionResponse_Permissionship.UNSPECIFIED, + permissionship: permissionships[index] ?? v1.CheckPermissionResponse_Permissionship.UNSPECIFIED, }), oneofKind: 'item', }, - }) + }), ), }); -const makeClient = ( - handle: SpiceDbPermissionClient['checkBulkPermissions'] -): SpiceDbPermissionClient => ({ +const makeClient = (handle: SpiceDbPermissionClient['checkBulkPermissions']): SpiceDbPermissionClient => ({ checkBulkPermissions: handle, checkPermission: () => Effect.die(new Error('Action check must not run')), close: () => {}, }); -it.effect( - 'uses one fully consistent batch and correlates allowed and denied module decisions', - () => - Effect.gen(function* correlatesModuleDecisions() { - const requests: v1.CheckBulkPermissionsRequest[] = []; - const access = makeContextAccess( - makeClient((request) => - Effect.sync(() => { - requests.push(request); - return responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, - ]); - }) - ) - ); +it.effect('uses one fully consistent batch and correlates allowed and denied module decisions', () => + Effect.gen(function* correlatesModuleDecisions() { + const requests: v1.CheckBulkPermissionsRequest[] = []; + const access = makeContextAccess( + makeClient((request) => + Effect.sync(() => { + requests.push(request); + return responseFor(request, [ + v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, + v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, + ]); + }), + ), + ); - const result = yield* access.modules({ - legalEntityId, - moduleIds: ['property.registry', 'billing.core'], - principalId, - tenantId, - }); - expect(result).toEqual([ - { decision: 'allowed', key: 'property.registry' }, - { decision: 'denied', key: 'billing.core' }, - ]); - expect(requests.length).toBe(1); - expect(requests[0]?.consistency?.requirement).toEqual({ - fullyConsistent: true, - oneofKind: 'fullyConsistent', - }); - expect(requests[0]?.items[0]?.resource?.objectType).toBe('module_access'); - expect(requests[0]?.items[0]?.permission).toBe('access'); - expect(requests[0]?.items[0]?.subject?.object?.objectId).toBe( - principalId - ); - }) + const result = yield* access.modules({ + legalEntityId, + moduleIds: ['property.registry', 'billing.core'], + principalId, + tenantId, + }); + expect(result).toEqual([ + { decision: 'allowed', key: 'property.registry' }, + { decision: 'denied', key: 'billing.core' }, + ]); + expect(requests.length).toBe(1); + expect(requests[0]?.consistency?.requirement).toEqual({ + fullyConsistent: true, + oneofKind: 'fullyConsistent', + }); + expect(requests[0]?.items[0]?.resource?.objectType).toBe('module_access'); + expect(requests[0]?.items[0]?.permission).toBe('access'); + expect(requests[0]?.items[0]?.subject?.object?.objectId).toBe(principalId); + }), ); it.effect('checks resource writes independently from resource reads', () => @@ -91,14 +83,10 @@ it.effect('checks resource writes independently from resource reads', () => const service = makeContextAccess( makeClient((request) => Effect.sync(() => { - permissions.push( - ...request.items.map(({ permission }) => permission) - ); - return responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, - ]); - }) - ) + permissions.push(...request.items.map(({ permission }) => permission)); + return responseFor(request, [v1.CheckPermissionResponse_Permissionship.NO_PERMISSION]); + }), + ), ); const target = { moduleId: 'property.registry', @@ -112,11 +100,9 @@ it.effect('checks resource writes independently from resource reads', () => resources: [target], tenantId: 'tenant-1', }); - expect(result).toEqual([ - { decision: 'denied', key: 'property.registry:unit:unit-1' }, - ]); + expect(result).toEqual([{ decision: 'denied', key: 'property.registry:unit:unit-1' }]); expect(permissions).toEqual(['write']); - }) + }), ); const makeAllowedPermissionRecorder = () => { @@ -127,65 +113,49 @@ const makeAllowedPermissionRecorder = () => { observed.push(...request.items.map(({ permission }) => permission)); return responseFor( request, - request.items.map( - () => v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION - ) + request.items.map(() => v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), ); - }) - ) + }), + ), ); return { observed, service }; }; -it.effect( - 'forwards every closed tenant permission key without widening it', - () => - Effect.gen(function* forwardsTenantPermissionKeys() { - const { observed, service } = makeAllowedPermissionRecorder(); +it.effect('forwards every closed tenant permission key without widening it', () => + Effect.gen(function* forwardsTenantPermissionKeys() { + const { observed, service } = makeAllowedPermissionRecorder(); - yield* Effect.forEach( - TENANT_PERMISSION_KEYS, - (permission) => - service - .tenants({ permission, principalId, tenantIds: [tenantId] }) - .pipe( - Effect.map((result) => - expect(result).toEqual([{ decision: 'allowed', key: tenantId }]) - ) - ), - { concurrency: 1 } - ); - expect(observed).toEqual(TENANT_PERMISSION_KEYS); - }) + yield* Effect.forEach( + TENANT_PERMISSION_KEYS, + (permission) => + service + .tenants({ permission, principalId, tenantIds: [tenantId] }) + .pipe(Effect.map((result) => expect(result).toEqual([{ decision: 'allowed', key: tenantId }]))), + { concurrency: 1 }, + ); + expect(observed).toEqual(TENANT_PERMISSION_KEYS); + }), ); -it.effect( - 'forwards every closed Legal Entity permission key without widening it', - () => - Effect.gen(function* forwardsLegalEntityPermissionKeys() { - const { observed, service } = makeAllowedPermissionRecorder(); +it.effect('forwards every closed Legal Entity permission key without widening it', () => + Effect.gen(function* forwardsLegalEntityPermissionKeys() { + const { observed, service } = makeAllowedPermissionRecorder(); - yield* Effect.forEach( - LEGAL_ENTITY_PERMISSION_KEYS, - (permission) => - service - .legalEntities({ - legalEntityIds: [legalEntityId], - permission, - principalId, - tenantId, - }) - .pipe( - Effect.map((result) => - expect(result).toEqual([ - { decision: 'allowed', key: legalEntityId }, - ]) - ) - ), - { concurrency: 1 } - ); - expect(observed).toEqual(LEGAL_ENTITY_PERMISSION_KEYS); - }) + yield* Effect.forEach( + LEGAL_ENTITY_PERMISSION_KEYS, + (permission) => + service + .legalEntities({ + legalEntityIds: [legalEntityId], + permission, + principalId, + tenantId, + }) + .pipe(Effect.map((result) => expect(result).toEqual([{ decision: 'allowed', key: legalEntityId }]))), + { concurrency: 1 }, + ); + expect(observed).toEqual(LEGAL_ENTITY_PERMISSION_KEYS); + }), ); it('creates lossless tenant and legal-entity-qualified object identities', () => { @@ -195,21 +165,16 @@ it('creates lossless tenant and legal-entity-qualified object identities', () => resourceType: 'property.unit', }; expect(toLegalEntityAccessObjectId(tenantId, legalEntityId)).not.toBe( - toLegalEntityAccessObjectId( - '10000000-0000-4000-8000-000000000002', - legalEntityId - ) + toLegalEntityAccessObjectId('10000000-0000-4000-8000-000000000002', legalEntityId), ); - expect( - toModuleAccessObjectId(tenantId, legalEntityId, 'property.registry') - ).not.toBe( - toModuleAccessObjectId(tenantId, legalEntityId, 'property-registry') + expect(toModuleAccessObjectId(tenantId, legalEntityId, 'property.registry')).not.toBe( + toModuleAccessObjectId(tenantId, legalEntityId, 'property-registry'), ); expect(toResourceAccessObjectId(tenantId, legalEntityId, resource)).not.toBe( toResourceAccessObjectId(tenantId, legalEntityId, { ...resource, resourceId: 'unit-with/slashes', - }) + }), ); }); @@ -224,12 +189,10 @@ it.effect('supports empty batches and exact resource filtering', () => v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, ]); - }) - ) + }), + ), ); - expect( - yield* access.legalEntities({ legalEntityIds: [], principalId, tenantId }) - ).toEqual([]); + expect(yield* access.legalEntities({ legalEntityIds: [], principalId, tenantId })).toEqual([]); expect( yield* access.resources({ legalEntityId, @@ -247,75 +210,55 @@ it.effect('supports empty batches and exact resource filtering', () => }, ], tenantId, - }) + }), ).toEqual([ { decision: 'allowed', key: 'property.registry:property.unit:unit-1' }, { decision: 'denied', key: 'property.registry:property.unit:unit-2' }, ]); expect(requests).toBe(1); - }) + }), ); -it.effect( - 'classifies client, partial, duplicate, malformed, and conditional results as unavailable', - () => - Effect.gen(function* classifiesUnavailableResults() { - const input = { legalEntityIds: [legalEntityId], principalId, tenantId }; - const failures = [ - makeClient(() => - Effect.fail( - spiceDbPermissionClientError(new Error('secret SpiceDB diagnostic')) - ) - ), - makeClient(() => - Effect.succeed(v1.CheckBulkPermissionsResponse.create({ pairs: [] })) - ), - makeClient((request) => - Effect.succeed( - responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION, - ]) - ) - ), - makeClient((request) => - Effect.sync(() => { - const response = responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, - ]); - const [pair] = response.pairs; - return v1.CheckBulkPermissionsResponse.create({ - pairs: pair === undefined ? [] : [{ response: pair.response }], - }); - }) - ), - ]; - const [failingClient] = failures; - expect(failingClient).toBeDefined(); - if (failingClient === undefined) { - throw new Error('Missing failingClient'); - } - yield* Effect.forEach( - failures, - (client) => - makeContextAccess(client) - .legalEntities(input) - .pipe( - Effect.map((result) => - expect(result).toEqual([ - { decision: 'unavailable', key: legalEntityId }, - ]) - ) - ), - { concurrency: 1 } - ); - expect( - yield* makeContextAccess(failingClient).legalEntities({ - ...input, - legalEntityIds: [legalEntityId, legalEntityId], - }) - ).toEqual([ - { decision: 'unavailable', key: legalEntityId }, - { decision: 'unavailable', key: legalEntityId }, - ]); - }) +it.effect('classifies client, partial, duplicate, malformed, and conditional results as unavailable', () => + Effect.gen(function* classifiesUnavailableResults() { + const input = { legalEntityIds: [legalEntityId], principalId, tenantId }; + const failures = [ + makeClient(() => Effect.fail(spiceDbPermissionClientError(new Error('secret SpiceDB diagnostic')))), + makeClient(() => Effect.succeed(v1.CheckBulkPermissionsResponse.create({ pairs: [] }))), + makeClient((request) => + Effect.succeed(responseFor(request, [v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION])), + ), + makeClient((request) => + Effect.sync(() => { + const response = responseFor(request, [v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION]); + const [pair] = response.pairs; + return v1.CheckBulkPermissionsResponse.create({ + pairs: pair === undefined ? [] : [{ response: pair.response }], + }); + }), + ), + ]; + const [failingClient] = failures; + expect(failingClient).toBeDefined(); + if (failingClient === undefined) { + throw new Error('Missing failingClient'); + } + yield* Effect.forEach( + failures, + (client) => + makeContextAccess(client) + .legalEntities(input) + .pipe(Effect.map((result) => expect(result).toEqual([{ decision: 'unavailable', key: legalEntityId }]))), + { concurrency: 1 }, + ); + expect( + yield* makeContextAccess(failingClient).legalEntities({ + ...input, + legalEntityIds: [legalEntityId, legalEntityId], + }), + ).toEqual([ + { decision: 'unavailable', key: legalEntityId }, + { decision: 'unavailable', key: legalEntityId }, + ]); + }), ); diff --git a/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts b/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts index a438d940c..a01484641 100644 --- a/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts +++ b/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts @@ -55,8 +55,8 @@ it('ignores a non-string constraint while retaining a valid code', () => { findPostgresFailure({ code: '23505', constraint: { private: 'diagnostic object' }, - }) - ) + }), + ), ).toEqual({ code: '23505' }); }); @@ -93,8 +93,8 @@ it('returns the first recognizable PostgreSQL metadata in root-to-cause order', cause: { code: '23505', constraint: 'nested_constraint' }, code: '40001', constraint: 'root_constraint', - }) - ) + }), + ), ).toEqual({ code: '40001', constraint: 'root_constraint' }); }); @@ -109,12 +109,8 @@ it('supports owner-local matching without changing default root precedence', () }); expect( Option.getOrThrow( - findPostgresFailure( - failure, - ({ code, constraint }) => - code === '23505' && constraint === 'owner_constraint' - ) - ) + findPostgresFailure(failure, ({ code, constraint }) => code === '23505' && constraint === 'owner_constraint'), + ), ).toEqual({ code: '23505', constraint: 'owner_constraint' }); }); @@ -153,31 +149,17 @@ it('distinguishes commit ambiguity from definite transaction failures', () => { expect( Option.isSome(connectionFailure) && - Predicate.isTagged( - connectionFailure.value, - 'DatabaseCommitAcknowledgementAmbiguous' - ) + Predicate.isTagged(connectionFailure.value, 'DatabaseCommitAcknowledgementAmbiguous'), ).toBe(true); expect( Option.isSome(administrativeShutdown) && - Predicate.isTagged( - administrativeShutdown.value, - 'DatabaseCommitAcknowledgementAmbiguous' - ) + Predicate.isTagged(administrativeShutdown.value, 'DatabaseCommitAcknowledgementAmbiguous'), ).toBe(true); expect( - Option.isSome(serializationFailure) && - Predicate.isTagged( - serializationFailure.value, - 'DatabaseTransactionFailure' - ) + Option.isSome(serializationFailure) && Predicate.isTagged(serializationFailure.value, 'DatabaseTransactionFailure'), ).toBe(true); - expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' })).toBe( - false - ); - expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' })).toBe( - false - ); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' })).toBe(false); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' })).toBe(false); }); it('classifies the exact commit-acknowledgement socket vocabulary', () => { @@ -196,9 +178,7 @@ it('classifies the exact commit-acknowledgement socket vocabulary', () => { expect(isDatabaseCommitAcknowledgementAmbiguous({ code })).toBe(true); } - expect( - isDatabaseCommitAcknowledgementAmbiguous({ code: 'ECONNREFUSED' }) - ).toBe(false); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: 'ECONNREFUSED' })).toBe(false); }); it('preserves the auth-facing unavailable socket vocabulary', () => { @@ -274,13 +254,9 @@ it('decodes native Drizzle and Effect SQL causes without exposing query data', ( it('walks native mixed Causes in order and skips unrelated failures', () => { const failure = Cause.combine( Cause.fail({ code: '40001' }), - Cause.die({ code: '23505', constraint: 'owned_unique' }) + Cause.die({ code: '23505', constraint: 'owned_unique' }), ); - expect( - Option.getOrThrow( - findPostgresFailure(failure, ({ code }) => code === '23505') - ) - ).toEqual({ + expect(Option.getOrThrow(findPostgresFailure(failure, ({ code }) => code === '23505'))).toEqual({ code: '23505', constraint: 'owned_unique', }); diff --git a/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts b/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts index aa818a988..c0d5263e4 100644 --- a/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts +++ b/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts @@ -5,10 +5,7 @@ import { EntrypointAuthorizationSchema, decodeEntrypointAuthorization, } from '../../src/authorization/entrypoint-classification.ts'; -import { - defineSystemModuleEntrypoint, - defineTenantModuleEntrypoint, -} from '../../src/modules/module-entrypoint.ts'; +import { defineSystemModuleEntrypoint, defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; it('decodes every closed authorization classification', () => { const classifications = [ @@ -23,9 +20,7 @@ it('decodes every closed authorization classification', () => { ] as const; for (const classification of classifications) { - expect(decodeEntrypointAuthorization(classification)).toEqual( - classification - ); + expect(decodeEntrypointAuthorization(classification)).toEqual(classification); } }); @@ -44,7 +39,7 @@ it('rejects omitted, unknown, excessive, and incompatible authorization fields', expect(() => Schema.decodeUnknownSync(EntrypointAuthorizationSchema, { onExcessProperty: 'error', - })(value) + })(value), ).toThrow(); } }); @@ -77,7 +72,7 @@ it('requires role-compatible authorization and freezes nested classification', ( entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action', - }) + }), ).toThrow(); expect(() => defineTenantModuleEntrypoint({ @@ -86,7 +81,7 @@ it('requires role-compatible authorization and freezes nested classification', ( entrypointKey: 'inventory.stock.project', moduleKey: 'inventory.stock', role: 'worker', - }) + }), ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts b/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts index 8ab07d81f..3058c9f44 100644 --- a/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts +++ b/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts @@ -3,10 +3,7 @@ import { expect, it } from 'effect-rstest'; import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; -class FixtureDeletionError extends Schema.TaggedError()( - 'FixtureDeletionError', - {} -) {} +class FixtureDeletionError extends Schema.TaggedError()('FixtureDeletionError', {}) {} it.effect('purges fixture rows sequentially in child-before-parent order', () => Effect.gen(function* verifyDeletionOrder() { @@ -16,8 +13,8 @@ it.effect('purges fixture rows sequentially in child-before-parent order', () => Effect.andThen( Effect.sync(() => { deleted.push('child'); - }) - ) + }), + ), ), Effect.sync(() => { expect(deleted).toEqual(['child']); @@ -25,7 +22,7 @@ it.effect('purges fixture rows sequentially in child-before-parent order', () => }), ]); expect(deleted).toEqual(['child', 'parent']); - }) + }), ); it.effect('stops fixture cleanup at the first failed deletion', () => @@ -43,12 +40,12 @@ it.effect('stops fixture cleanup at the first failed deletion', () => ]).pipe(Effect.flip); expect(error).toBe(failure); expect(deleted).toEqual(['child']); - }) + }), ); it.effect('accepts an empty fixture cleanup', () => Effect.gen(function* verifyEmptyCleanup() { const result = yield* purgeFixtureRows([]); expect(result).toBe(undefined); - }) + }), ); diff --git a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts index f899392b0..ddc35bff7 100644 --- a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts +++ b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts @@ -4,10 +4,7 @@ import { expect, it } from 'effect-rstest'; import { Headers, HttpServerRequest } from 'effect/unstable/http'; import { TrustedPrincipalContextSchema } from '../../src/actions/principal-context.ts'; -import { - classifyReadCoreError, - makeGovernedReadHttpHandler, -} from '../../src/http/governed-read.ts'; +import { classifyReadCoreError, makeGovernedReadHttpHandler } from '../../src/http/governed-read.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { ModuleStateCheckUnavailableError } from '../../src/modules/module-state-check-unavailable-error.ts'; import { ModuleStateDeniedError } from '../../src/modules/module-state-denied-error.ts'; @@ -31,10 +28,10 @@ import { ReadResultValidationError } from '../../src/reads/read-result-validatio import { ReadRuntime } from '../../src/reads/runtime.ts'; import type { ReadRuntimeService } from '../../src/reads/runtime.ts'; -const problem = ( - kind: Kind, - status: Status -) => ({ kind, status }); +const problem = (kind: Kind, status: Status) => ({ + kind, + status, +}); const problems = { authentication: () => problem('authentication', 401), @@ -55,10 +52,7 @@ const capturedLoggerLayer = (entries: string[]) => ]); const reason = 'safe reason'; -const coreFailures: readonly [ - ReadCoreError, - ReturnType<(typeof problems)[keyof typeof problems]>, -][] = [ +const coreFailures: readonly [ReadCoreError, ReturnType<(typeof problems)[keyof typeof problems]>][] = [ [ new ModuleStateCheckUnavailableError({ code: 'module_state_check_unavailable', @@ -66,10 +60,7 @@ const coreFailures: readonly [ }), problems.unavailable(), ], - [ - new ModuleStateDeniedError({ code: 'module_state_denied', reason }), - problems.forbidden(), - ], + [new ModuleStateDeniedError({ code: 'module_state_denied', reason }), problems.forbidden()], [ new OperationAuthenticationRequired({ code: 'operation_authentication_required', @@ -77,14 +68,8 @@ const coreFailures: readonly [ }), problems.authentication(), ], - [ - new OperationContextDenied({ code: 'operation_context_denied', reason }), - problems.forbidden(), - ], - [ - new OperationContextInvalid({ code: 'operation_context_invalid', reason }), - problems.forbidden(), - ], + [new OperationContextDenied({ code: 'operation_context_denied', reason }), problems.forbidden()], + [new OperationContextInvalid({ code: 'operation_context_invalid', reason }), problems.forbidden()], [ new OperationContextUnavailable({ code: 'operation_context_unavailable', @@ -99,10 +84,7 @@ const coreFailures: readonly [ }), problems.unavailable(), ], - [ - new ReadEvidenceValidationError({ code: 'read_evidence_invalid', reason }), - problems.internal(), - ], + [new ReadEvidenceValidationError({ code: 'read_evidence_invalid', reason }), problems.internal()], [ new ReadHandlerExecutionError({ code: 'read_handler_execution_failed', @@ -110,22 +92,10 @@ const coreFailures: readonly [ }), problems.internal(), ], - [ - new ReadHandlerNotFound({ code: 'read_handler_not_found', reason }), - problems.notFound(), - ], - [ - new ReadHandlerUnavailable({ code: 'read_handler_unavailable', reason }), - problems.unavailable(), - ], - [ - new ReadInputValidationError({ code: 'read_input_invalid', reason }), - problems.invalid(), - ], - [ - new ReadPermissionDenied({ code: 'read_permission_denied', reason }), - problems.forbidden(), - ], + [new ReadHandlerNotFound({ code: 'read_handler_not_found', reason }), problems.notFound()], + [new ReadHandlerUnavailable({ code: 'read_handler_unavailable', reason }), problems.unavailable()], + [new ReadInputValidationError({ code: 'read_input_invalid', reason }), problems.invalid()], + [new ReadPermissionDenied({ code: 'read_permission_denied', reason }), problems.forbidden()], [ new ReadPermissionUnavailable({ code: 'read_permission_unavailable', @@ -140,10 +110,7 @@ const coreFailures: readonly [ }), problems.unavailable(), ], - [ - new ReadResultValidationError({ code: 'read_result_invalid', reason }), - problems.internal(), - ], + [new ReadResultValidationError({ code: 'read_result_invalid', reason }), problems.internal()], ]; it('classifies every Core governed-read failure through the endpoint problem set', () => { @@ -203,7 +170,7 @@ const registration = defineRead( result: { ok: true as const }, }), () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) + () => ({ kind: 'module', moduleId: 'core.shell' }), ); const principal = Schema.decodeSync(TrustedPrincipalContextSchema)({ @@ -224,205 +191,174 @@ const readRuntime = { return Effect.succeed({ ok: true as const }); }, } as ReadRuntimeService; -const requestService = HttpServerRequest.fromWeb( - new Request('https://ontos.test/reads/fixture') -); +const requestService = HttpServerRequest.fromWeb(new Request('https://ontos.test/reads/fixture')); -it.effect( - 'validates correlation before authentication or ReadRuntime acquisition', - () => - Effect.gen(function* validateCorrelationFirst() { - let authenticationCalls = 0; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: () => { - authenticationCalls += 1; - return Effect.succeed(principal); - }, - problems, - registration, - }); - const exit = yield* Effect.exit( - handler({ - payload: { query: 'fixture' }, - request: { - headers: Headers.fromInput({ - authorization: 'Bearer private', - 'x-correlation-id': ' ', - }), - }, - }) - ).pipe( - Effect.provideService(ReadRuntime, readRuntime), - Effect.provideService( - HttpServerRequest.HttpServerRequest, - requestService - ) - ); - expect(authenticationCalls).toBe(0); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(Cause.squash(exit.cause)).toEqual(problems.invalid()); - } - }) -); - -it.effect( - 'sanitizes synchronous defects across correlation validation and authentication', - () => - Effect.gen(function* sanitizeSynchronousDefects() { - const cases = [ - { - handler: makeGovernedReadHttpHandler({ - authenticatePrincipal: () => Effect.succeed(principal), - problems: { - ...problems, - invalid: () => { - throw new Error('private invalid-problem factory detail'); - }, - }, - registration, - }), - headers: Headers.empty, - }, - { - handler: makeGovernedReadHttpHandler({ - authenticatePrincipal: (): Effect.Effect => { - throw new Error('private authentication adapter detail'); - }, - problems, - registration, - }), - headers: Headers.fromInput({ - 'x-correlation-id': 'synchronous-defect', - }), - }, - ]; - const exits = yield* Effect.forEach( - cases, - ({ handler, headers }) => - Effect.exit( - handler({ payload: { query: 'fixture' }, request: { headers } }) - ).pipe( - Effect.provideService(ReadRuntime, readRuntime), - Effect.provideService( - HttpServerRequest.HttpServerRequest, - requestService - ) - ), - { concurrency: 'unbounded' } - ); - for (const exit of exits) { - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - const publicFailure = Cause.squash(exit.cause); - expect(publicFailure).toEqual(problems.internal()); - expect( - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - publicFailure - ) - ).not.toMatch(/private/u); - } - } - }) -); - -it.effect( - 'passes only payload, trusted principal, registration, and correlation to ReadRuntime', - () => - Effect.gen(function* forwardTrustedReadInputs() { - const payload = { query: 'fixture' }; - observed.length = 0; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: (authorization) => { - expect(Redacted.value(authorization)).toBe('Bearer private'); - return Effect.succeed(principal); - }, - problems, - registration, - }); - const assertDecodedPayloadInput = () => - handler({ - payload: { - // @ts-expect-error The HTTP framework must pass the schema-decoded payload shape. - query: 123, - }, - request: { headers: Headers.empty }, - }); - void assertDecodedPayloadInput; - const result = yield* handler({ - payload, +it.effect('validates correlation before authentication or ReadRuntime acquisition', () => + Effect.gen(function* validateCorrelationFirst() { + let authenticationCalls = 0; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: () => { + authenticationCalls += 1; + return Effect.succeed(principal); + }, + problems, + registration, + }); + const exit = yield* Effect.exit( + handler({ + payload: { query: 'fixture' }, request: { headers: Headers.fromInput({ authorization: 'Bearer private', - 'x-correlation-id': 'correlation-test', + 'x-correlation-id': ' ', }), }, - }).pipe( - Effect.provideService(ReadRuntime, readRuntime), - Effect.provideService( - HttpServerRequest.HttpServerRequest, - requestService - ) - ); - expect(result).toEqual({ ok: true }); - expect(observed).toEqual([ - { - input: payload, - principal, - registration, - transport: { correlationId: 'correlation-test' }, - }, - ]); - }) + }), + ).pipe( + Effect.provideService(ReadRuntime, readRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), + ); + expect(authenticationCalls).toBe(0); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.squash(exit.cause)).toEqual(problems.invalid()); + } + }), ); -it.effect( - 'sanitizes unexpected defects at the complete governed handler boundary', - () => - Effect.gen(function* sanitizeHandlerDefects() { - // SAFETY: This test double exercises only the handler's runRead call and deliberately omits no - // other ReadRuntimeService member; remove when the generic runtime interface exposes a test port. - const defectRuntime = { - runRead: () => - Effect.die(new Error('private database connection detail')), - } as ReadRuntimeService; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: () => Effect.succeed(principal), - problems, - registration, - }); - const logEntries: string[] = []; - const exit = yield* Effect.exit( - handler({ - payload: { query: 'fixture' }, - request: { - headers: Headers.fromInput({ - 'x-correlation-id': 'correlation-defect', - }), +it.effect('sanitizes synchronous defects across correlation validation and authentication', () => + Effect.gen(function* sanitizeSynchronousDefects() { + const cases = [ + { + handler: makeGovernedReadHttpHandler({ + authenticatePrincipal: () => Effect.succeed(principal), + problems: { + ...problems, + invalid: () => { + throw new Error('private invalid-problem factory detail'); + }, }, - }) - ).pipe( - Effect.provideService(ReadRuntime, defectRuntime), - Effect.provideService( - HttpServerRequest.HttpServerRequest, - requestService + registration, + }), + headers: Headers.empty, + }, + { + handler: makeGovernedReadHttpHandler({ + authenticatePrincipal: (): Effect.Effect => { + throw new Error('private authentication adapter detail'); + }, + problems, + registration, + }), + headers: Headers.fromInput({ + 'x-correlation-id': 'synchronous-defect', + }), + }, + ]; + const exits = yield* Effect.forEach( + cases, + ({ handler, headers }) => + Effect.exit(handler({ payload: { query: 'fixture' }, request: { headers } })).pipe( + Effect.provideService(ReadRuntime, readRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), ), - Effect.provide(capturedLoggerLayer(logEntries)) - ); + { concurrency: 'unbounded' }, + ); + for (const exit of exits) { expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { const publicFailure = Cause.squash(exit.cause); expect(publicFailure).toEqual(problems.internal()); - expect( - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - publicFailure - ) - ).not.toMatch(/private database connection detail/u); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(publicFailure)).not.toMatch( + /private/u, + ); } - expect(logEntries.length).toBe(1); - expect(logEntries.join('\n')).not.toMatch( - /private database connection detail/u + } + }), +); + +it.effect('passes only payload, trusted principal, registration, and correlation to ReadRuntime', () => + Effect.gen(function* forwardTrustedReadInputs() { + const payload = { query: 'fixture' }; + observed.length = 0; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: (authorization) => { + expect(Redacted.value(authorization)).toBe('Bearer private'); + return Effect.succeed(principal); + }, + problems, + registration, + }); + const assertDecodedPayloadInput = () => + handler({ + payload: { + // @ts-expect-error The HTTP framework must pass the schema-decoded payload shape. + query: 123, + }, + request: { headers: Headers.empty }, + }); + void assertDecodedPayloadInput; + const result = yield* handler({ + payload, + request: { + headers: Headers.fromInput({ + authorization: 'Bearer private', + 'x-correlation-id': 'correlation-test', + }), + }, + }).pipe( + Effect.provideService(ReadRuntime, readRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), + ); + expect(result).toEqual({ ok: true }); + expect(observed).toEqual([ + { + input: payload, + principal, + registration, + transport: { correlationId: 'correlation-test' }, + }, + ]); + }), +); + +it.effect('sanitizes unexpected defects at the complete governed handler boundary', () => + Effect.gen(function* sanitizeHandlerDefects() { + // SAFETY: This test double exercises only the handler's runRead call and deliberately omits no + // other ReadRuntimeService member; remove when the generic runtime interface exposes a test port. + const defectRuntime = { + runRead: () => Effect.die(new Error('private database connection detail')), + } as ReadRuntimeService; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: () => Effect.succeed(principal), + problems, + registration, + }); + const logEntries: string[] = []; + const exit = yield* Effect.exit( + handler({ + payload: { query: 'fixture' }, + request: { + headers: Headers.fromInput({ + 'x-correlation-id': 'correlation-defect', + }), + }, + }), + ).pipe( + Effect.provideService(ReadRuntime, defectRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), + Effect.provide(capturedLoggerLayer(logEntries)), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const publicFailure = Cause.squash(exit.cause); + expect(publicFailure).toEqual(problems.internal()); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(publicFailure)).not.toMatch( + /private database connection detail/u, ); - expect(logEntries[0] ?? '').toMatch(/correlation-defect/u); - }) + } + expect(logEntries.length).toBe(1); + expect(logEntries.join('\n')).not.toMatch(/private database connection detail/u); + expect(logEntries[0] ?? '').toMatch(/correlation-defect/u); + }), ); diff --git a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts index f652b5e77..242d611db 100644 --- a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts +++ b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts @@ -23,9 +23,7 @@ const principal = { tenantId: 'a3000000-0000-4000-8000-000000000001', }; -const verificationFailure = ( - _tag: (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag'] -) => +const verificationFailure = (_tag: (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag']) => Schema.decodeEffect(OperationPrincipalVerificationErrorSchema)({ _tag, reason: 'Private verifier diagnostic', @@ -47,23 +45,17 @@ const unavailableProblem = () => ({ type: 'https://ontos.dev/problems/fixture-unavailable', }); const ProblemResponseSchema = Schema.Struct({ - _tag: Schema.Literals([ - 'FixtureAuthenticationProblem', - 'FixtureUnavailableProblem', - ]), + _tag: Schema.Literals(['FixtureAuthenticationProblem', 'FixtureUnavailableProblem']), status: Schema.Literals([401, 503]), }); const SuccessResponseSchema = Schema.Struct({ principal: Schema.Unknown }); -const problemResponse = ( - problem: ReturnType -) => +const problemResponse = (problem: ReturnType) => HttpServerResponse.jsonUnsafe(problem, { status: problem.status }).pipe( - HttpServerResponse.setHeader('content-type', 'application/problem+json') + HttpServerResponse.setHeader('content-type', 'application/problem+json'), ); -const respondWithProblem = ( - error: ReturnType -) => Effect.succeed(problemResponse(error)); +const respondWithProblem = (error: ReturnType) => + Effect.succeed(problemResponse(error)); it.live( 'mounted HTTP authentication maps verifier classes, challenges unusable credentials, and stops before private logic', @@ -80,21 +72,19 @@ it.live( ['Bearer misconfigured', 'ActionPrincipalConfigurationError'], ['Bearer unavailable', 'ActionPrincipalUnavailableError'], ]); - const authenticate = makeMicroverticalHttpPrincipalAuthentication( - (authorization) => { - const raw = Redacted.value(authorization); - const failure = failureByCredential.get(raw); - return failure === undefined - ? Effect.succeed(principal) - : verificationFailure(failure).pipe(Effect.flatMap(Effect.fail)); - } - ); + const authenticate = makeMicroverticalHttpPrincipalAuthentication((authorization) => { + const raw = Redacted.value(authorization); + const failure = failureByCredential.get(raw); + return failure === undefined + ? Effect.succeed(principal) + : verificationFailure(failure).pipe(Effect.flatMap(Effect.fail)); + }); return Effect.gen(function* mountedAuthenticationHandler() { - const server = yield* NodeHttpServer.make( - () => process.getBuiltinModule('http').createServer(), - { host: '127.0.0.1', port: 0 } - ); + const server = yield* NodeHttpServer.make(() => process.getBuiltinModule('http').createServer(), { + host: '127.0.0.1', + port: 0, + }); const application = HttpServerRequest.HttpServerRequest.use((request) => authenticate(Redacted.make(request.headers['authorization']), { authentication: authenticationProblem, @@ -106,17 +96,15 @@ it.live( return HttpServerResponse.jsonUnsafe({ principal: trustedPrincipal, }); - }) + }), ), - Effect.catch(respondWithProblem) - ) + Effect.catch(respondWithProblem), + ), ); yield* server.serve(application); const address = yield* Match.value(server.address).pipe( Match.tag('TcpAddress', (tcpAddress) => Effect.succeed(tcpAddress)), - Match.orElse(() => - Effect.die('HTTP authentication fixture did not bind to TCP') - ) + Match.orElse(() => Effect.die('HTTP authentication fixture did not bind to TCP')), ); const client = yield* HttpClient.HttpClient; const url = `http://127.0.0.1:${address.port}/operation`; @@ -131,49 +119,31 @@ it.live( const request = authorization === undefined ? HttpClientRequest.get(url) - : HttpClientRequest.get(url).pipe( - HttpClientRequest.setHeader('authorization', authorization) - ); + : HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader('authorization', authorization)); const response = yield* client.execute(request); expect(response.status).toBe(expectedStatus); - expect(response.headers['content-type']).toBe( - 'application/problem+json' - ); - expect(response.headers['www-authenticate']).toBe( - expectedStatus === 401 ? 'Bearer' : undefined - ); + expect(response.headers['content-type']).toBe('application/problem+json'); + expect(response.headers['www-authenticate']).toBe(expectedStatus === 401 ? 'Bearer' : undefined); const rawBody = yield* response.json; - expect(rawBody).toEqual( - expectedStatus === 401 - ? authenticationProblem() - : unavailableProblem() - ); - const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)( - rawBody - ); + expect(rawBody).toEqual(expectedStatus === 401 ? authenticationProblem() : unavailableProblem()); + const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)(rawBody); expect( Predicate.isTagged( body, - expectedStatus === 401 - ? 'FixtureAuthenticationProblem' - : 'FixtureUnavailableProblem' - ) + expectedStatus === 401 ? 'FixtureAuthenticationProblem' : 'FixtureUnavailableProblem', + ), ).toBe(true); expect(body.status).toBe(expectedStatus); } expect(privateOperationReached).toBe(0); const success = yield* client.execute( - HttpClientRequest.get(url).pipe( - HttpClientRequest.setHeader('authorization', 'Bearer valid') - ) + HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader('authorization', 'Bearer valid')), ); expect(success.status).toBe(200); - const successBody = yield* success.json.pipe( - Effect.flatMap(Schema.decodeUnknownEffect(SuccessResponseSchema)) - ); + const successBody = yield* success.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SuccessResponseSchema))); expect(successBody.principal).toEqual(principal); expect(privateOperationReached).toBe(1); }).pipe(Effect.provide(FetchHttpClient.layer)); - } + }, ); diff --git a/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts b/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts index ac7242cfa..037f6f2c8 100644 --- a/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts +++ b/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts @@ -19,98 +19,78 @@ const activeRecord: LegalEntityContextRecord = { tenantId, }; -it.effect( - 'lists zero, one, and many active legal entities in deterministic safe order', - () => - Effect.gen(function* listsActiveLegalEntities() { - expect(yield* classifyActiveLegalEntities([], tenantId)).toEqual([]); - expect( - yield* classifyActiveLegalEntities([activeRecord], tenantId) - ).toEqual([ - { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, - ]); - expect( - yield* classifyActiveLegalEntities( - [ - activeRecord, - { - ...activeRecord, - legalEntityId: '20000000-0000-4000-8000-000000000003', - legalName: 'Alpha s.r.o.', - }, - { - ...activeRecord, - legalEntityId: '20000000-0000-4000-8000-000000000002', - legalName: 'Alpha s.r.o.', - }, - { - ...activeRecord, - legalEntityId: '20000000-0000-4000-8000-000000000004', - legalName: 'Suspended s.r.o.', - status: 'suspended', - }, - { - ...activeRecord, - legalEntityId: '20000000-0000-4000-8000-000000000005', - legalName: 'Archived s.r.o.', - status: 'archived', - }, - ], - tenantId - ) - ).toEqual([ - { - legalEntityId: '20000000-0000-4000-8000-000000000002', - legalName: 'Alpha s.r.o.', - }, - { - legalEntityId: '20000000-0000-4000-8000-000000000003', - legalName: 'Alpha s.r.o.', - }, - { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, - ]); - }) +it.effect('lists zero, one, and many active legal entities in deterministic safe order', () => + Effect.gen(function* listsActiveLegalEntities() { + expect(yield* classifyActiveLegalEntities([], tenantId)).toEqual([]); + expect(yield* classifyActiveLegalEntities([activeRecord], tenantId)).toEqual([ + { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, + ]); + expect( + yield* classifyActiveLegalEntities( + [ + activeRecord, + { + ...activeRecord, + legalEntityId: '20000000-0000-4000-8000-000000000003', + legalName: 'Alpha s.r.o.', + }, + { + ...activeRecord, + legalEntityId: '20000000-0000-4000-8000-000000000002', + legalName: 'Alpha s.r.o.', + }, + { + ...activeRecord, + legalEntityId: '20000000-0000-4000-8000-000000000004', + legalName: 'Suspended s.r.o.', + status: 'suspended', + }, + { + ...activeRecord, + legalEntityId: '20000000-0000-4000-8000-000000000005', + legalName: 'Archived s.r.o.', + status: 'archived', + }, + ], + tenantId, + ), + ).toEqual([ + { + legalEntityId: '20000000-0000-4000-8000-000000000002', + legalName: 'Alpha s.r.o.', + }, + { + legalEntityId: '20000000-0000-4000-8000-000000000003', + legalName: 'Alpha s.r.o.', + }, + { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, + ]); + }), ); -it.effect( - 'validates exactly one active selection and rejects missing or inactive selections', - () => - Effect.gen(function* validatesLegalEntitySelection() { - expect( - yield* classifySelectedLegalEntity( - [activeRecord], - tenantId, - activeRecord.legalEntityId - ) - ).toEqual({ - legalEntityId: activeRecord.legalEntityId, - legalName: activeRecord.legalName, - }); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifySelectedLegalEntity( - [activeRecord], - tenantId, - '20000000-0000-4000-8000-000000000099' - ) - ), - 'LegalEntityContextMissingError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifySelectedLegalEntity( - [{ ...activeRecord, status: 'suspended' }], - tenantId, - activeRecord.legalEntityId - ) - ), - 'LegalEntityContextInactiveError' - ) - ).toBe(true); - }) +it.effect('validates exactly one active selection and rejects missing or inactive selections', () => + Effect.gen(function* validatesLegalEntitySelection() { + expect(yield* classifySelectedLegalEntity([activeRecord], tenantId, activeRecord.legalEntityId)).toEqual({ + legalEntityId: activeRecord.legalEntityId, + legalName: activeRecord.legalName, + }); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifySelectedLegalEntity([activeRecord], tenantId, '20000000-0000-4000-8000-000000000099'), + ), + 'LegalEntityContextMissingError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifySelectedLegalEntity([{ ...activeRecord, status: 'suspended' }], tenantId, activeRecord.legalEntityId), + ), + 'LegalEntityContextInactiveError', + ), + ).toBe(true); + }), ); it.effect('rejects cross-tenant, malformed, and duplicate records', () => @@ -125,60 +105,44 @@ it.effect('rejects cross-tenant, malformed, and duplicate records', () => tenantId: '10000000-0000-4000-8000-000000000002', }, ], - tenantId - ) + tenantId, + ), ), - 'LegalEntityContextInvalidError' - ) + 'LegalEntityContextInvalidError', + ), ).toBe(true); expect( Predicate.isTagged( - yield* Effect.flip( - classifyActiveLegalEntities( - [{ ...activeRecord, legalName: '' }], - tenantId - ) - ), - 'LegalEntityContextInvalidError' - ) + yield* Effect.flip(classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId)), + 'LegalEntityContextInvalidError', + ), ).toBe(true); expect( Predicate.isTagged( - yield* Effect.flip( - classifyActiveLegalEntities( - [activeRecord, { ...activeRecord }], - tenantId - ) - ), - 'LegalEntityContextAmbiguousError' - ) + yield* Effect.flip(classifyActiveLegalEntities([activeRecord, { ...activeRecord }], tenantId)), + 'LegalEntityContextAmbiguousError', + ), ).toBe(true); - }) + }), ); -it.effect( - 'types database failures as sanitized legal-entity context unavailability', - () => - Effect.gen(function* sanitizesLegalEntityDatabaseFailure() { - const context = makeLegalEntityContext({ - executor: yield* makeTestDatabase(() => - Effect.fail( - new SqlError({ - reason: new ConnectionError({ - cause: new Error('secret database diagnostic'), - }), - }) - ) +it.effect('types database failures as sanitized legal-entity context unavailability', () => + Effect.gen(function* sanitizesLegalEntityDatabaseFailure() { + const context = makeLegalEntityContext({ + executor: yield* makeTestDatabase(() => + Effect.fail( + new SqlError({ + reason: new ConnectionError({ + cause: new Error('secret database diagnostic'), + }), + }), ), - }); - const error = yield* Effect.flip(context.listActiveForTenant(tenantId)); - expect( - Predicate.isTagged(error, 'LegalEntityContextUnavailableError') - ).toBe(true); - expect( - Predicate.isTagged(error, 'LegalEntityContextUnavailableError') - ? error.reason - : undefined - ).not.toMatch(/secret database diagnostic/u); - }) + ), + }); + const error = yield* Effect.flip(context.listActiveForTenant(tenantId)); + expect(Predicate.isTagged(error, 'LegalEntityContextUnavailableError')).toBe(true); + expect(Predicate.isTagged(error, 'LegalEntityContextUnavailableError') ? error.reason : undefined).not.toMatch( + /secret database diagnostic/u, + ); + }), ); diff --git a/app/packages/core-runtime/tests/unit/module-catalog.test.ts b/app/packages/core-runtime/tests/unit/module-catalog.test.ts index 4138b605a..2fb8a9341 100644 --- a/app/packages/core-runtime/tests/unit/module-catalog.test.ts +++ b/app/packages/core-runtime/tests/unit/module-catalog.test.ts @@ -1,9 +1,6 @@ import { expect, it } from 'effect-rstest'; -import { - buildInstalledModuleCatalog, - resolveInstalledModuleCatalog, -} from '../../src/modules/catalog.ts'; +import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog } from '../../src/modules/catalog.ts'; import type { OntosOutboxSubscriptionContract } from '../../src/modules/manifest.ts'; import { validateOutboxWorkerSubscriptions } from '../../src/outbox/definition.ts'; import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; @@ -11,7 +8,7 @@ import { makeModuleContractFixture } from '../../src/testing/module-contract.ts' const contract = ( appId: string, moduleId: string, - outboxSubscriptions: readonly OntosOutboxSubscriptionContract[] = [] + outboxSubscriptions: readonly OntosOutboxSubscriptionContract[] = [], ) => makeModuleContractFixture({ appId, @@ -32,17 +29,10 @@ it('builds immutable deterministic dual indexes for distinct deployment and modu }, ]); - expect(catalog.deploymentAppIds).toEqual([ - 'documents-center', - 'property-registry', - ]); + expect(catalog.deploymentAppIds).toEqual(['documents-center', 'property-registry']); expect(catalog.moduleIds).toEqual(['documents.center', 'property.registry']); - expect( - catalog.getByDeploymentAppId('property-registry')?.manifest.module.id - ).toBe('property.registry'); - expect(catalog.getByModuleId('property.registry')?.deployment.appId).toBe( - 'property-registry' - ); + expect(catalog.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe('property.registry'); + expect(catalog.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); expect(Object.isFrozen(catalog)).toBe(true); expect(Object.isFrozen(catalog.contracts)).toBe(true); expect(Object.isFrozen(catalog.outboxSubscriptions)).toBe(true); @@ -65,19 +55,13 @@ it('accepts a valid owner-local subscription whose producer is not installed', ( } as const; const catalog = buildInstalledModuleCatalog([ { - contract: contract('property-registry', 'property.registry', [ - subscription, - ]), + contract: contract('property-registry', 'property.registry', [subscription]), expectedAppId: 'property-registry', }, ]); expect(catalog.outboxSubscriptions).toEqual([subscription]); - expect(() => - validateOutboxWorkerSubscriptions(catalog.outboxSubscriptions) - ).not.toThrow(); - expect(Object.isFrozen(catalog.outboxSubscriptions[0]?.entrypoint)).toBe( - true - ); + expect(() => validateOutboxWorkerSubscriptions(catalog.outboxSubscriptions)).not.toThrow(); + expect(Object.isFrozen(catalog.outboxSubscriptions[0]?.entrypoint)).toBe(true); expect(Object.isFrozen(subscription.entrypoint)).toBe(false); }); @@ -99,12 +83,10 @@ it('rejects contradictory or incomplete Outbox subscription snapshots', () => { expect(() => buildInstalledModuleCatalog([ { - contract: contract('property-registry', 'property.registry', [ - invalidSubscription, - ]), + contract: contract('property-registry', 'property.registry', [invalidSubscription]), expectedAppId: 'property-registry', }, - ]) + ]), ).toThrow(); expect(() => buildInstalledModuleCatalog([ @@ -117,7 +99,7 @@ it('rejects contradictory or incomplete Outbox subscription snapshots', () => { ]), expectedAppId: 'property-registry', }, - ]) + ]), ).toThrow(); const duplicateWorkerKey = 'shared.projector'; @@ -161,7 +143,7 @@ it('rejects contradictory or incomplete Outbox subscription snapshots', () => { ]), expectedAppId: 'documents-center', }, - ]) + ]), ).toThrow(); }); @@ -172,7 +154,7 @@ it('rejects deployment mismatch, duplicate deployment IDs, and duplicate module contract: contract('property-registry', 'property.registry'), expectedAppId: 'different-app', }, - ]) + ]), ).toThrow(); expect(() => buildInstalledModuleCatalog([ @@ -184,7 +166,7 @@ it('rejects deployment mismatch, duplicate deployment IDs, and duplicate module contract: contract('property-registry', 'property.other'), expectedAppId: 'property-registry', }, - ]) + ]), ).toThrow(); expect(() => buildInstalledModuleCatalog([ @@ -196,7 +178,7 @@ it('rejects deployment mismatch, duplicate deployment IDs, and duplicate module contract: contract('property-other', 'property.registry'), expectedAppId: 'property-other', }, - ]) + ]), ).toThrow(); }); @@ -210,7 +192,7 @@ it('rejects unsupported contract versions without weakening catalog safety', () }, expectedAppId: 'property-registry', }, - ]) + ]), ).toThrow(); }); @@ -308,7 +290,7 @@ for (const scenario of [ contract: contract(appId, moduleId), expectedAppId: appId, outcome: 'fetched', - })) + })), ); expect(catalog.moduleIds).toEqual(scenario.moduleIds); expect(catalog.deploymentStatuses).toEqual(scenario.statuses); diff --git a/app/packages/core-runtime/tests/unit/module-manifest.test.ts b/app/packages/core-runtime/tests/unit/module-manifest.test.ts index d6adf79cc..c8e4a436c 100644 --- a/app/packages/core-runtime/tests/unit/module-manifest.test.ts +++ b/app/packages/core-runtime/tests/unit/module-manifest.test.ts @@ -1,10 +1,6 @@ import { Effect, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - HttpApi, - HttpApiEndpoint, - HttpApiGroup, -} from 'effect/unstable/httpapi'; +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'; import { defineAction } from '../../src/actions/definition.ts'; import { @@ -54,7 +50,7 @@ const createAction = (owner = 'property.registry') => resultSchema: Schema.Struct({ id: Schema.String }), schemaVersion: '1', }, - ({ name }) => Effect.succeed({ id: name }) + ({ name }) => Effect.succeed({ id: name }), ); const emptyManifestInput = () => ({ @@ -62,15 +58,7 @@ const emptyManifestInput = () => ({ defaultState: 'inactive' as const, preservesHistoryWhenInactive: true, scope: 'tenant' as const, - supportedStates: [ - 'inactive', - 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ] as const, + supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'] as const, }, module: { description: 'Property capability', @@ -105,147 +93,132 @@ it('defines a valid empty manifest, preserves literals, and freezes its public s const literal: 'property.registry' = manifest.module.id; expect(literal).toBe('property.registry'); - expect(Object.keys(manifest)).toEqual([ - 'activation', - 'module', - 'publicSurface', - ]); + expect(Object.keys(manifest)).toEqual(['activation', 'module', 'publicSurface']); expect(Object.isFrozen(manifest)).toBe(true); expect(Object.isFrozen(manifest.activation.supportedStates)).toBe(true); expect(Object.isFrozen(manifest.publicSurface.actions)).toBe(true); expect(() => Object.defineProperty(manifest.publicSurface.actions, 0, { value: 'private', - }) + }), ).toThrow(); }); -it.effect( - 'accepts populated typed surfaces and keeps executable values out of safe descriptors', - () => - Effect.gen(function* verifySafeDescriptors() { - const action = createAction(); - const apiValue = HttpApi.make('PropertyApi').add( - HttpApiGroup.make('property').add( - HttpApiEndpoint.get('listUnits', '/units') - ) - ); - const parameterizedApiValue = HttpApi.make('PropertyDetailApi').add( - HttpApiGroup.make('propertyDetail').add( - HttpApiEndpoint.get('getUnit', '/units/:unitId', { - headers: {}, - params: { unitId: UnitId }, - query: {}, - }) - ) - ); - const manifest = defineOntosModuleManifest({ - ...emptyManifestInput(), - publicSurface: { - actions: [action], - api: { - PropertyClient: apiValue, - PropertyDetail: parameterizedApiValue, +it.effect('accepts populated typed surfaces and keeps executable values out of safe descriptors', () => + Effect.gen(function* verifySafeDescriptors() { + const action = createAction(); + const apiValue = HttpApi.make('PropertyApi').add( + HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')), + ); + const parameterizedApiValue = HttpApi.make('PropertyDetailApi').add( + HttpApiGroup.make('propertyDetail').add( + HttpApiEndpoint.get('getUnit', '/units/:unitId', { + headers: {}, + params: { unitId: UnitId }, + query: {}, + }), + ), + ); + const manifest = defineOntosModuleManifest({ + ...emptyManifestInput(), + publicSurface: { + actions: [action], + api: { + PropertyClient: apiValue, + PropertyDetail: parameterizedApiValue, + }, + components: { PropertyUnitCard: componentValue }, + events: [ + { + key: 'property.unit-created', + owningModuleId: 'property.registry', + payloadSchema: Schema.Struct({ unitId: UnitId }), + referencesResourceTypes: ['property.unit'], + tense: 'past', + visibility: 'public_module_event', }, - components: { PropertyUnitCard: componentValue }, - events: [ - { - key: 'property.unit-created', - owningModuleId: 'property.registry', - payloadSchema: Schema.Struct({ unitId: UnitId }), - referencesResourceTypes: ['property.unit'], - tense: 'past', - visibility: 'public_module_event', - }, - ], - reports: [ - { - accessFiltering: 'legal_entity_scope', - dimensions: ['legal_entity'], - key: 'property.unit-inventory', - label: 'Unit inventory', - owningModuleId: 'property.registry', - resourceTypes: ['property.unit'], - }, - ], - resourceTypes: [ - { - capabilities: { - graphVisible: true, - linkable: true, - mediaAttachable: true, - searchable: true, - timelineVisible: true, - }, - description: 'A physical unit', - key: 'property.unit', - label: 'Unit', - owningModuleId: 'property.registry', - }, - ], - search: [ - { - accessFiltering: 'legal_entity_scope', - key: 'property.unit-search', - owningModuleId: 'property.registry', - resourceType: 'property.unit', + ], + reports: [ + { + accessFiltering: 'legal_entity_scope', + dimensions: ['legal_entity'], + key: 'property.unit-inventory', + label: 'Unit inventory', + owningModuleId: 'property.registry', + resourceTypes: ['property.unit'], + }, + ], + resourceTypes: [ + { + capabilities: { + graphVisible: true, + linkable: true, + mediaAttachable: true, + searchable: true, + timelineVisible: true, }, - ], - shellContributions: - emptyManifestInput().publicSurface.shellContributions, - }, - }); - const registration = defineVerticalRuntimeRegistration({ - actions: [action], - entrypoints: { - api: { resource: () => Promise.resolve(apiValue) }, - components: { - dashboard: () => Promise.resolve(componentValue), + description: 'A physical unit', + key: 'property.unit', + label: 'Unit', + owningModuleId: 'property.registry', }, - pages: {}, - reports: {}, - search: {}, - }, - manifest, - outboxWorkers: [], - }); - const descriptors = extractVerticalRuntimeSafeDescriptors(registration); - - expect(manifest.publicSurface.actions[0]).toBe(action); - expect(manifest.publicSurface.api.PropertyClient).toBe(apiValue); - expect(manifest.publicSurface.api.PropertyDetail).toBe( - parameterizedApiValue - ); - expect(manifest.publicSurface.components.PropertyUnitCard).toBe( - componentValue - ); - expect(Object.keys(registration)).toEqual(['moduleId']); - expect(getVerticalRuntimeActions(registration)[0]).toBe(action); - const loadDashboard = - getVerticalRuntimeEntrypoints(registration).components['dashboard']; - expect(loadDashboard).toBeDefined(); - if (loadDashboard === undefined) { - throw new Error('Expected assertion to hold'); - } - expect(yield* Effect.promise(loadDashboard)).toBe(componentValue); - expect(descriptors).toEqual({ - actions: [ + ], + search: [ { - actionKey: 'property.registry.create-unit', - auditProfile: 'standard', - entrypoint: action.descriptor.entrypoint, - idempotency: 'required', - legalEntityScope: 'optional', + accessFiltering: 'legal_entity_scope', + key: 'property.unit-search', owningModuleId: 'property.registry', - schemaVersion: '1', + resourceType: 'property.unit', }, ], - moduleId: 'property.registry', - outboxSubscriptions: [], - shellContributions: - emptyManifestInput().publicSurface.shellContributions, - }); - }) + shellContributions: emptyManifestInput().publicSurface.shellContributions, + }, + }); + const registration = defineVerticalRuntimeRegistration({ + actions: [action], + entrypoints: { + api: { resource: () => Promise.resolve(apiValue) }, + components: { + dashboard: () => Promise.resolve(componentValue), + }, + pages: {}, + reports: {}, + search: {}, + }, + manifest, + outboxWorkers: [], + }); + const descriptors = extractVerticalRuntimeSafeDescriptors(registration); + + expect(manifest.publicSurface.actions[0]).toBe(action); + expect(manifest.publicSurface.api.PropertyClient).toBe(apiValue); + expect(manifest.publicSurface.api.PropertyDetail).toBe(parameterizedApiValue); + expect(manifest.publicSurface.components.PropertyUnitCard).toBe(componentValue); + expect(Object.keys(registration)).toEqual(['moduleId']); + expect(getVerticalRuntimeActions(registration)[0]).toBe(action); + const loadDashboard = getVerticalRuntimeEntrypoints(registration).components['dashboard']; + expect(loadDashboard).toBeDefined(); + if (loadDashboard === undefined) { + throw new Error('Expected assertion to hold'); + } + expect(yield* Effect.promise(loadDashboard)).toBe(componentValue); + expect(descriptors).toEqual({ + actions: [ + { + actionKey: 'property.registry.create-unit', + auditProfile: 'standard', + entrypoint: action.descriptor.entrypoint, + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleId: 'property.registry', + schemaVersion: '1', + }, + ], + moduleId: 'property.registry', + outboxSubscriptions: [], + shellContributions: emptyManifestInput().publicSurface.shellContributions, + }); + }), ); it('rejects invalid identities, private fields, duplicates, cross-owner values, and undeclared references', () => { @@ -253,28 +226,18 @@ it('rejects invalid identities, private fields, duplicates, cross-owner values, defineOntosModuleManifest({ ...emptyManifestInput(), module: { ...emptyManifestInput().module, id: 'property-registry' }, - }) + }), ).toThrow(); const privateRoutesInput = { ...emptyManifestInput(), privateRoutes: [], }; - expect(() => - validateOntosModuleManifestFields( - privateRoutesInput, - privateRoutesInput.publicSurface - ) - ).toThrow(); + expect(() => validateOntosModuleManifestFields(privateRoutesInput, privateRoutesInput.publicSurface)).toThrow(); const dependenciesInput = { ...emptyManifestInput(), dependencies: { core: [], externalSystems: [], modules: [] }, }; - expect(() => - validateOntosModuleManifestFields( - dependenciesInput, - dependenciesInput.publicSurface - ) - ).toThrow(); + expect(() => validateOntosModuleManifestFields(dependenciesInput, dependenciesInput.publicSurface)).toThrow(); expect(() => defineOntosModuleManifest({ ...emptyManifestInput(), @@ -282,7 +245,7 @@ it('rejects invalid identities, private fields, duplicates, cross-owner values, ...emptyManifestInput().activation, supportedStates: ['inactive', 'inactive'], }, - }) + }), ).toThrow(); expect(() => defineOntosModuleManifest({ @@ -291,7 +254,7 @@ it('rejects invalid identities, private fields, duplicates, cross-owner values, ...emptyManifestInput().publicSurface, actions: [createAction('billing.invoice')], }, - }) + }), ).toThrow(); expect(() => defineOntosModuleManifest({ @@ -307,7 +270,7 @@ it('rejects invalid identities, private fields, duplicates, cross-owner values, }, ], }, - }) + }), ).toThrow(); expect(() => validateOntosModuleExecutableReferences( @@ -326,36 +289,18 @@ it('rejects invalid identities, private fields, duplicates, cross-owner values, [], [], [], - 'property.registry' - ) + 'property.registry', + ), ).toThrow(/real values created by defineAction/u); - expect(() => - validateOntosModuleExecutableReferences( - [], - [42], - [], - [], - 'property.registry' - ) - ).toThrow(/real Effect HttpApi/u); - expect(() => - validateOntosModuleExecutableReferences( - [], - [], - ['not-a-component'], - [], - 'property.registry' - ) - ).toThrow(/callable component/u); - expect(() => - validateOntosModuleExecutableReferences( - [], - [], - [], - [{}], - 'property.registry' - ) - ).toThrow(/Effect Schema value/u); + expect(() => validateOntosModuleExecutableReferences([], [42], [], [], 'property.registry')).toThrow( + /real Effect HttpApi/u, + ); + expect(() => validateOntosModuleExecutableReferences([], [], ['not-a-component'], [], 'property.registry')).toThrow( + /callable component/u, + ); + expect(() => validateOntosModuleExecutableReferences([], [], [], [{}], 'property.registry')).toThrow( + /Effect Schema value/u, + ); }); it('deployment contract decoding is exact and versioned', () => { @@ -371,8 +316,7 @@ it('deployment contract decoding is exact and versioned', () => { reports: [], resourceTypes: [], search: [], - shellContributions: - emptyManifestInput().publicSurface.shellContributions, + shellContributions: emptyManifestInput().publicSurface.shellContributions, }, }, runtime: { outboxSubscriptions: [] }, @@ -385,7 +329,7 @@ it('deployment contract decoding is exact and versioned', () => { decodeOntosModuleDeploymentContract({ ...contract, sourcePath: './private.ts', - }) + }), ).toThrow(); expect(() => decodeOntosModuleDeploymentContract({ @@ -394,12 +338,8 @@ it('deployment contract decoding is exact and versioned', () => { ...contract.manifest, dependencies: { core: [], externalSystems: [], modules: [] }, }, - }) - ).toThrow(); - expect(() => - decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '0' }) - ).toThrow(); - expect(() => - decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '999' }) + }), ).toThrow(); + expect(() => decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '0' })).toThrow(); + expect(() => decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '999' })).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/module-state-gate.test.ts b/app/packages/core-runtime/tests/unit/module-state-gate.test.ts index ae21755c4..d91c6f250 100644 --- a/app/packages/core-runtime/tests/unit/module-state-gate.test.ts +++ b/app/packages/core-runtime/tests/unit/module-state-gate.test.ts @@ -79,9 +79,8 @@ const makeRecordingTracer = (spans: Tracer.Span[]): Tracer.Tracer => }, }); -const accessSet = ( - ...accesses: readonly ModuleEntrypointAccess[] -): ReadonlySet => new Set(accesses); +const accessSet = (...accesses: readonly ModuleEntrypointAccess[]): ReadonlySet => + new Set(accesses); const expectedAllowed = { active: accessSet('background', 'historical_read', 'read', 'write'), archived: accessSet('historical_read'), @@ -90,24 +89,20 @@ const expectedAllowed = { quarantined: accessSet(), read_only: accessSet('historical_read', 'read'), suspended: accessSet('historical_read'), -} satisfies Readonly< - Record> ->; +} satisfies Readonly>>; it('encodes the exhaustive state/access matrix once, including missing state', () => { for (const state of TENANT_MODULE_STATES) { for (const access of MODULE_ENTRYPOINT_ACCESSES) { expect(decideModuleStateAccess(state, access), `${state}/${access}`).toBe( - expectedAllowed[state].has(access) ? 'allow' : 'deny' + expectedAllowed[state].has(access) ? 'allow' : 'deny', ); } } for (const access of MODULE_ENTRYPOINT_ACCESSES) { expect(decideModuleStateAccess(null, access)).toBe('deny'); expect(tenantStatesAllowingAccess(access)).toEqual( - TENANT_MODULE_STATES.filter((state) => - expectedAllowed[state].has(access) - ).toSorted() + TENANT_MODULE_STATES.filter((state) => expectedAllowed[state].has(access)).toSorted(), ); } }); @@ -142,7 +137,7 @@ it('constructs frozen tenant and explicit system entrypoints and rejects forged entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action', - }) + }), ).toThrow(); expect(() => defineSystemModuleEntrypoint({ @@ -154,7 +149,7 @@ it('constructs frozen tenant and explicit system entrypoints and rejects forged entrypointKey: 'Invalid', moduleKey: 'inventory.stock', role: 'action', - }) + }), ).toThrow(); for (const [role, access] of [ ['page', 'read'], @@ -174,183 +169,146 @@ it('constructs frozen tenant and explicit system entrypoints and rejects forged entrypointKey: `inventory.stock.${role.replace('_', '-')}`, moduleKey: 'inventory.stock', role, - }).role + }).role, ).toBe(role); } }); -it.effect( - 'deduplicates one batch, reuses an immutable snapshot, and fails undeclared keys closed', - () => - Effect.gen(function* reuseSnapshot() { - let reads = 0; - let observedKeys: readonly string[] = []; - const service: TenantModuleStateServiceContract = { - getTenantModuleStates: (_tenantId, moduleKeys) => { - reads += 1; - observedKeys = moduleKeys; - return Effect.succeed( - moduleKeys.map((moduleKey) => ({ - moduleKey, - state: - moduleKey === 'billing.invoice' - ? ('read_only' as const) - : ('active' as const), - })) - ); - }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }; - const descriptors = [ - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', - role: 'page', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'inventory.stock.report', - moduleKey: 'inventory.stock', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'billing.invoice.search', - moduleKey: 'billing.invoice', - role: 'search', - }), - ] as const; - const snapshot = yield* prepareModuleStateSnapshot( - service, - 'tenant-1', - descriptors - ); - expect(observedKeys).toEqual(['billing.invoice', 'inventory.stock']); - expect(reads).toBe(1); - expect(Object.isFrozen(snapshot)).toBe(true); - expect(Object.isFrozen(snapshot.entrypointKeys)).toBe(true); - expect(Object.isFrozen(snapshot.moduleKeys)).toBe(true); - yield* checkModuleEntrypoint(snapshot, descriptors[0]); - yield* checkModuleEntrypoint(snapshot, descriptors[0]); - expect(reads).toBe(1); - - const undeclared = defineTenantModuleEntrypoint({ +it.effect('deduplicates one batch, reuses an immutable snapshot, and fails undeclared keys closed', () => + Effect.gen(function* reuseSnapshot() { + let reads = 0; + let observedKeys: readonly string[] = []; + const service: TenantModuleStateServiceContract = { + getTenantModuleStates: (_tenantId, moduleKeys) => { + reads += 1; + observedKeys = moduleKeys; + return Effect.succeed( + moduleKeys.map((moduleKey) => ({ + moduleKey, + state: moduleKey === 'billing.invoice' ? ('read_only' as const) : ('active' as const), + })), + ); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }; + const descriptors = [ + defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access', }, - entrypointKey: 'people.directory.page', - moduleKey: 'people.directory', + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', role: 'page', - }); - const failure = yield* Effect.flip( - checkModuleEntrypoint(snapshot, undeclared) - ); - expect( - Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError') - ).toBe(true); - const undeclaredSameModule = defineTenantModuleEntrypoint({ - access: 'write', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', authorization: { - kind: 'action_execution', - provisioning: 'tenant_membership_default', + kind: 'context_permission', + permission: 'module.access', }, - entrypointKey: 'inventory.stock.undeclared-action', + entrypointKey: 'inventory.stock.report', moduleKey: 'inventory.stock', - role: 'action', - }); - const sameModuleFailure = yield* Effect.flip( - checkModuleEntrypoint(snapshot, undeclaredSameModule) - ); - expect( - Predicate.isTagged( - sameModuleFailure, - 'ModuleStateCheckUnavailableError' - ) - ).toBe(true); - expect(reads).toBe(1); - }) -); - -it.effect( - 'records safe acquisition and evaluation telemetry including snapshot reuse', - () => - Effect.gen(function* recordTelemetry() { - const spans: Tracer.Span[] = []; - const tracer = makeRecordingTracer(spans); - const descriptor = defineTenantModuleEntrypoint({ + role: 'report', + }), + defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access', }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', - role: 'page', - }); - const service: TenantModuleStateServiceContract = { - getTenantModuleStates: () => - Effect.succeed([{ moduleKey: 'inventory.stock', state: 'active' }]), - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }; + entrypointKey: 'billing.invoice.search', + moduleKey: 'billing.invoice', + role: 'search', + }), + ] as const; + const snapshot = yield* prepareModuleStateSnapshot(service, 'tenant-1', descriptors); + expect(observedKeys).toEqual(['billing.invoice', 'inventory.stock']); + expect(reads).toBe(1); + expect(Object.isFrozen(snapshot)).toBe(true); + expect(Object.isFrozen(snapshot.entrypointKeys)).toBe(true); + expect(Object.isFrozen(snapshot.moduleKeys)).toBe(true); + yield* checkModuleEntrypoint(snapshot, descriptors[0]); + yield* checkModuleEntrypoint(snapshot, descriptors[0]); + expect(reads).toBe(1); - yield* Effect.gen(function* telemetryEffect() { - const snapshot = yield* prepareModuleStateSnapshot( - service, - 'tenant-1', - [descriptor] - ); - yield* checkModuleEntrypoint(snapshot, descriptor); - yield* checkModuleEntrypoint(snapshot, descriptor); - yield* Effect.exit( - prepareModuleStateSnapshot(service, '', [descriptor]) - ); - }).pipe(Effect.provideService(Tracer.Tracer, tracer)); + const undeclared = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'people.directory.page', + moduleKey: 'people.directory', + role: 'page', + }); + const failure = yield* Effect.flip(checkModuleEntrypoint(snapshot, undeclared)); + expect(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')).toBe(true); + const undeclaredSameModule = defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.undeclared-action', + moduleKey: 'inventory.stock', + role: 'action', + }); + const sameModuleFailure = yield* Effect.flip(checkModuleEntrypoint(snapshot, undeclaredSameModule)); + expect(Predicate.isTagged(sameModuleFailure, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(reads).toBe(1); + }), +); - const acquisitions = spans.filter( - (span) => span.name === 'ModuleStateGate.acquire' - ); - const evaluations = spans.filter( - (span) => span.name === 'ModuleStateGate.evaluate' - ); - expect(acquisitions.length).toBe(2); - expect(evaluations.length).toBe(2); - expect(acquisitions[0]?.attributes.get('batchSize')).toBe(1); - expect(acquisitions[0]?.attributes.get('outcome')).toBe('available'); - expect( - Predicate.isNumber(acquisitions[0]?.attributes.get('elapsedMs')) - ).toBe(true); - expect(acquisitions[1]?.attributes.get('outcome')).toBe('unavailable'); - expect(evaluations[0]?.attributes.get('access')).toBe('read'); - expect(evaluations[0]?.attributes.get('outcome')).toBe('allow'); - expect(evaluations[0]?.attributes.get('scope')).toBe('tenant'); - expect(evaluations[0]?.attributes.get('snapshotReuse')).toBe(false); - expect(evaluations[1]?.attributes.get('snapshotReuse')).toBe(true); +it.effect('records safe acquisition and evaluation telemetry including snapshot reuse', () => + Effect.gen(function* recordTelemetry() { + const spans: Tracer.Span[] = []; + const tracer = makeRecordingTracer(spans); + const descriptor = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }); + const service: TenantModuleStateServiceContract = { + getTenantModuleStates: () => Effect.succeed([{ moduleKey: 'inventory.stock', state: 'active' }]), + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }; + + yield* Effect.gen(function* telemetryEffect() { + const snapshot = yield* prepareModuleStateSnapshot(service, 'tenant-1', [descriptor]); + yield* checkModuleEntrypoint(snapshot, descriptor); + yield* checkModuleEntrypoint(snapshot, descriptor); + yield* Effect.exit(prepareModuleStateSnapshot(service, '', [descriptor])); + }).pipe(Effect.provideService(Tracer.Tracer, tracer)); + + const acquisitions = spans.filter((span) => span.name === 'ModuleStateGate.acquire'); + const evaluations = spans.filter((span) => span.name === 'ModuleStateGate.evaluate'); + expect(acquisitions.length).toBe(2); + expect(evaluations.length).toBe(2); + expect(acquisitions[0]?.attributes.get('batchSize')).toBe(1); + expect(acquisitions[0]?.attributes.get('outcome')).toBe('available'); + expect(Predicate.isNumber(acquisitions[0]?.attributes.get('elapsedMs'))).toBe(true); + expect(acquisitions[1]?.attributes.get('outcome')).toBe('unavailable'); + expect(evaluations[0]?.attributes.get('access')).toBe('read'); + expect(evaluations[0]?.attributes.get('outcome')).toBe('allow'); + expect(evaluations[0]?.attributes.get('scope')).toBe('tenant'); + expect(evaluations[0]?.attributes.get('snapshotReuse')).toBe(false); + expect(evaluations[1]?.attributes.get('snapshotReuse')).toBe(true); - for (const span of spans) { - for (const key of span.attributes.keys()) { - expect(key).not.toMatch( - /entrypoint|module|payload|principal|tenant/u - ); - } + for (const span of spans) { + for (const key of span.attributes.keys()) { + expect(key).not.toMatch(/entrypoint|module|payload|principal|tenant/u); } - }) + } + }), ); it.effect('empty and system-only compositions perform zero reads', () => @@ -375,315 +333,283 @@ it.effect('empty and system-only compositions perform zero reads', () => role: 'page', }); const empty = yield* prepareModuleStateSnapshot(service, 'tenant-1', []); - const systemOnly = yield* prepareModuleStateSnapshot(service, 'tenant-1', [ - system, - ]); + const systemOnly = yield* prepareModuleStateSnapshot(service, 'tenant-1', [system]); yield* checkModuleEntrypoint(systemOnly, system); expect(empty.moduleKeys).toEqual([]); expect(reads).toBe(0); - }) + }), ); -it.effect( - 'the gateway rejects missing trusted principal context before state acquisition', - () => - Effect.gen(function* rejectMissingPrincipal() { - let reads = 0; - const descriptor = defineTenantModuleEntrypoint({ +it.effect('the gateway rejects missing trusted principal context before state acquisition', () => + Effect.gen(function* rejectMissingPrincipal() { + let reads = 0; + const descriptor = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }); + const gate = makeModuleStateGate({ + getTenantModuleStates: () => { + reads += 1; + return Effect.succeed([]); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }); + const failure = yield* Effect.flip(makeModuleEntrypointGateway(gate).prepareSnapshotInput({}, [descriptor])); + expect(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(reads).toBe(0); + }), +); + +it.effect('gates every future entrypoint category before its fake implementation load', () => + Effect.gen(function* gateEntrypoints() { + let reads = 0; + let authorizationCalls = 0; + let loadCalls = 0; + const records = [ + { moduleKey: 'module.active', state: 'active' }, + { moduleKey: 'module.archived', state: 'archived' }, + { moduleKey: 'module.deprecated', state: 'deprecated' }, + { moduleKey: 'module.inactive', state: 'inactive' }, + { moduleKey: 'module.read-only', state: 'read_only' }, + { moduleKey: 'module.suspended', state: 'suspended' }, + ] as const; + const gateway = makeModuleEntrypointGateway( + makeModuleStateGate({ + getTenantModuleStates: (_tenantId, moduleKeys) => { + reads += 1; + return Effect.succeed(records.filter((record) => moduleKeys.includes(record.moduleKey))); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }), + ); + const allowed = [ + defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access', }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', + entrypointKey: 'module.active.page', + moduleKey: 'module.active', role: 'page', - }); - const gate = makeModuleStateGate({ - getTenantModuleStates: () => { - reads += 1; - return Effect.succeed([]); + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }); - const failure = yield* Effect.flip( - makeModuleEntrypointGateway(gate).prepareSnapshotInput({}, [descriptor]) - ); - expect( - Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError') - ).toBe(true); - expect(reads).toBe(0); - }) -); - -it.effect( - 'gates every future entrypoint category before its fake implementation load', - () => - Effect.gen(function* gateEntrypoints() { - let reads = 0; - let authorizationCalls = 0; - let loadCalls = 0; - const records = [ - { moduleKey: 'module.active', state: 'active' }, - { moduleKey: 'module.archived', state: 'archived' }, - { moduleKey: 'module.deprecated', state: 'deprecated' }, - { moduleKey: 'module.inactive', state: 'inactive' }, - { moduleKey: 'module.read-only', state: 'read_only' }, - { moduleKey: 'module.suspended', state: 'suspended' }, - ] as const; - const gateway = makeModuleEntrypointGateway( - makeModuleStateGate({ - getTenantModuleStates: (_tenantId, moduleKeys) => { - reads += 1; - return Effect.succeed( - records.filter((record) => moduleKeys.includes(record.moduleKey)) - ); - }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }) - ); - const allowed = [ - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.active.page', - moduleKey: 'module.active', - role: 'page', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.active.component', - moduleKey: 'module.active', - role: 'public_component', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.read-only.api', - moduleKey: 'module.read-only', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.suspended.api-history', - moduleKey: 'module.suspended', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.deprecated.search', - moduleKey: 'module.deprecated', - role: 'search', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.inactive.search-history', - moduleKey: 'module.inactive', - role: 'search', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.deprecated.report', - moduleKey: 'module.deprecated', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.archived.report-history', - moduleKey: 'module.archived', - role: 'report', - }), - defineSystemModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'core.audit.page', - moduleKey: 'core.audit', - role: 'page', - }), - ] as const; - const denied = [ - defineTenantModuleEntrypoint({ - access: 'write', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.read-only.api-write', - moduleKey: 'module.read-only', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.inactive.search', - moduleKey: 'module.inactive', - role: 'search', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.archived.report', - moduleKey: 'module.archived', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { - kind: 'context_permission', - permission: 'module.access', - }, - entrypointKey: 'module.missing.report-history', - moduleKey: 'module.missing', - role: 'report', - }), - ] as const; - const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [ - ...allowed, - ...denied, - ]); - expect(reads).toBe(1); - expect(snapshot.moduleKeys.includes('core.audit')).toBe(false); - const run = ( - entrypoint: (typeof allowed)[number] | (typeof denied)[number] - ) => - gateway.run({ - authorize: Effect.sync(() => { - authorizationCalls += 1; - }), - entrypoint, - load: Effect.sync(() => { - loadCalls += 1; - }), - snapshot, - }); - yield* Effect.forEach(allowed, run, { concurrency: 'unbounded' }); - const deniedFailures = yield* Effect.forEach( - denied, - (entrypoint) => Effect.flip(run(entrypoint)), - { concurrency: 'unbounded' } - ); - for (const failure of deniedFailures) { - expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe( - true - ); - } - expect(authorizationCalls).toBe(allowed.length); - expect(loadCalls).toBe(allowed.length); - expect(reads).toBe(1); - }) -); - -it.effect( - 'the gateway never evaluates authorization or lazy implementation on denial', - () => - Effect.gen(function* denyBeforeLoading() { - const gate = makeModuleStateGate({ - getTenantModuleStates: () => - Effect.succeed([ - { moduleKey: 'inventory.stock', state: 'read_only' }, - ]), - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }); - const gateway = makeModuleEntrypointGateway(gate); - const descriptor = defineTenantModuleEntrypoint({ - access: 'write', + entrypointKey: 'module.active.component', + moduleKey: 'module.active', + role: 'public_component', + }), + defineTenantModuleEntrypoint({ + access: 'read', authorization: { - kind: 'action_execution', - provisioning: 'tenant_membership_default', + kind: 'context_permission', + permission: 'module.access', }, - entrypointKey: 'inventory.stock.reserve', - moduleKey: 'inventory.stock', - role: 'action', - }); - const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [ - descriptor, - ]); - let authorizationCalls = 0; - let loadFactoryCalls = 0; - let loadCalls = 0; - const failure = yield* Effect.flip( - gateway.run({ - authorize: Effect.sync(() => { - authorizationCalls += 1; - }), - entrypoint: descriptor, - load: Effect.suspend(() => { - loadFactoryCalls += 1; - return Effect.sync(() => { - loadCalls += 1; - return 'loaded'; - }); - }), - snapshot, - }) - ); - expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); - expect(authorizationCalls).toBe(0); - expect(loadFactoryCalls).toBe(0); - expect(loadCalls).toBe(0); - }) -); - -it.effect( - 'a missing row is a definite denial rather than an unavailable read', - () => - Effect.gen(function* denyMissingRow() { - const descriptor = defineTenantModuleEntrypoint({ + entrypointKey: 'module.read-only.api', + moduleKey: 'module.read-only', + role: 'api', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.suspended.api-history', + moduleKey: 'module.suspended', + role: 'api', + }), + defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access', }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', + entrypointKey: 'module.deprecated.search', + moduleKey: 'module.deprecated', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.inactive.search-history', + moduleKey: 'module.inactive', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.deprecated.report', + moduleKey: 'module.deprecated', + role: 'report', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.archived.report-history', + moduleKey: 'module.archived', + role: 'report', + }), + defineSystemModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'core.audit.page', + moduleKey: 'core.audit', role: 'page', + }), + ] as const; + const denied = [ + defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.read-only.api-write', + moduleKey: 'module.read-only', + role: 'api', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.inactive.search', + moduleKey: 'module.inactive', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.archived.report', + moduleKey: 'module.archived', + role: 'report', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'module.missing.report-history', + moduleKey: 'module.missing', + role: 'report', + }), + ] as const; + const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [...allowed, ...denied]); + expect(reads).toBe(1); + expect(snapshot.moduleKeys.includes('core.audit')).toBe(false); + const run = (entrypoint: (typeof allowed)[number] | (typeof denied)[number]) => + gateway.run({ + authorize: Effect.sync(() => { + authorizationCalls += 1; + }), + entrypoint, + load: Effect.sync(() => { + loadCalls += 1; + }), + snapshot, }); - const snapshot = makeModuleStateSnapshot('tenant-1', [descriptor], []); - const failure = yield* Effect.flip( - checkModuleEntrypoint(snapshot, descriptor) - ); + yield* Effect.forEach(allowed, run, { concurrency: 'unbounded' }); + const deniedFailures = yield* Effect.forEach(denied, (entrypoint) => Effect.flip(run(entrypoint)), { + concurrency: 'unbounded', + }); + for (const failure of deniedFailures) { expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); - }) + } + expect(authorizationCalls).toBe(allowed.length); + expect(loadCalls).toBe(allowed.length); + expect(reads).toBe(1); + }), +); + +it.effect('the gateway never evaluates authorization or lazy implementation on denial', () => + Effect.gen(function* denyBeforeLoading() { + const gate = makeModuleStateGate({ + getTenantModuleStates: () => Effect.succeed([{ moduleKey: 'inventory.stock', state: 'read_only' }]), + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }); + const gateway = makeModuleEntrypointGateway(gate); + const descriptor = defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.reserve', + moduleKey: 'inventory.stock', + role: 'action', + }); + const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [descriptor]); + let authorizationCalls = 0; + let loadFactoryCalls = 0; + let loadCalls = 0; + const failure = yield* Effect.flip( + gateway.run({ + authorize: Effect.sync(() => { + authorizationCalls += 1; + }), + entrypoint: descriptor, + load: Effect.suspend(() => { + loadFactoryCalls += 1; + return Effect.sync(() => { + loadCalls += 1; + return 'loaded'; + }); + }), + snapshot, + }), + ); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); + expect(authorizationCalls).toBe(0); + expect(loadFactoryCalls).toBe(0); + expect(loadCalls).toBe(0); + }), +); + +it.effect('a missing row is a definite denial rather than an unavailable read', () => + Effect.gen(function* denyMissingRow() { + const descriptor = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { + kind: 'context_permission', + permission: 'module.access', + }, + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }); + const snapshot = makeModuleStateSnapshot('tenant-1', [descriptor], []); + const failure = yield* Effect.flip(checkModuleEntrypoint(snapshot, descriptor)); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts b/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts index f9f323884..4e742fdae 100644 --- a/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts +++ b/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts @@ -1,40 +1,22 @@ -import { - Clock, - Config, - ConfigProvider, - Context, - Effect, - Layer, - Logger, - Option, - References, - Tracer, -} from 'effect'; +import { Clock, Config, ConfigProvider, Context, Effect, Layer, Logger, Option, References, Tracer } from 'effect'; import { expect, it } from 'effect-rstest'; import { TestClock } from 'effect/testing'; import { makeTestDatabase } from '../support/database.ts'; -it.effect( - 'preserves a caller Context.Reference override instead of its default', - () => - Effect.gen(function* preserveReference() { - const executor = yield* makeTestDatabase(() => Effect.succeed([])); - const fallback = { source: 'default' }; - const override = { source: 'caller' }; - const reference = Context.Reference( - 'native-transaction-context/reference', - { - defaultValue: () => fallback, - } - ); - expect(yield* reference).toBe(fallback); - const actual = yield* executor - .transaction(() => reference) - .pipe(Effect.provideService(reference, override)); - expect(actual).toBe(override); - expect(yield* reference).toBe(fallback); - }) +it.effect('preserves a caller Context.Reference override instead of its default', () => + Effect.gen(function* preserveReference() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const fallback = { source: 'default' }; + const override = { source: 'caller' }; + const reference = Context.Reference('native-transaction-context/reference', { + defaultValue: () => fallback, + }); + expect(yield* reference).toBe(fallback); + const actual = yield* executor.transaction(() => reference).pipe(Effect.provideService(reference, override)); + expect(actual).toBe(override); + expect(yield* reference).toBe(fallback); + }), ); it.effect('uses caller Clock operations inside the transaction', () => @@ -58,12 +40,12 @@ it.effect('uses caller Clock operations inside the transaction', () => Effect.gen(function* callerProgram() { yield* Effect.sleep('1 millis'); return yield* Clock.currentTimeMillis; - }) + }), ) .pipe(Effect.provideService(Clock.Clock, clock)); expect(actual).toBe(1234); expect(sleeps).toBe(1); - }) + }), ); it.effect('preserves a caller TestClock inside the transaction', () => @@ -72,27 +54,23 @@ it.effect('preserves a caller TestClock inside the transaction', () => const actual = yield* Effect.gen(function* virtualClockProgram() { const clock = yield* TestClock.make(); yield* clock.setTime(1234); - return yield* executor - .transaction(() => Clock.currentTimeMillis) - .pipe(Effect.provideService(Clock.Clock, clock)); + return yield* executor.transaction(() => Clock.currentTimeMillis).pipe(Effect.provideService(Clock.Clock, clock)); }).pipe(Effect.scoped); expect(actual).toBe(1234); - }) + }), ); -it.effect( - 'loads configuration from the caller provider inside the transaction', - () => - Effect.gen(function* preserveConfig() { - const executor = yield* makeTestDatabase(() => Effect.succeed([])); - const provider = ConfigProvider.fromUnknown({ - NATIVE_CONTEXT_TEST_VALUE: 'caller-config', - }); - const actual = yield* executor - .transaction(() => Config.string('NATIVE_CONTEXT_TEST_VALUE')) - .pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)); - expect(actual).toBe('caller-config'); - }) +it.effect('loads configuration from the caller provider inside the transaction', () => + Effect.gen(function* preserveConfig() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const provider = ConfigProvider.fromUnknown({ + NATIVE_CONTEXT_TEST_VALUE: 'caller-config', + }); + const actual = yield* executor + .transaction(() => Config.string('NATIVE_CONTEXT_TEST_VALUE')) + .pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)); + expect(actual).toBe('caller-config'); + }), ); const spanPreservation = Effect.gen(function* preserveSpans() { @@ -102,11 +80,9 @@ const spanPreservation = Effect.gen(function* preserveSpans() { const inner = yield* executor.transaction(() => Effect.gen(function* transactionProgram() { const parent = yield* Effect.currentParentSpan; - const child = yield* Effect.currentSpan.pipe( - Effect.withSpan('transaction-child') - ); + const child = yield* Effect.currentSpan.pipe(Effect.withSpan('transaction-child')); return { child, parent }; - }) + }), ); return { caller, ...inner }; }).pipe(Effect.withSpan('transaction-caller')); @@ -127,23 +103,17 @@ const spanPreservation = Effect.gen(function* preserveSpans() { expect(actual.child.parent.value).toBe(actual.parent); }); -it.layer( - Layer.succeed( - Tracer.Tracer, - Tracer.make({ span: (options) => new Tracer.NativeSpan(options) }) - ) -)('native transaction tracing', (tracingIt) => { - tracingIt.effect( - 'preserves the caller span and parents transaction child spans to it', - () => spanPreservation - ); -}); +it.layer(Layer.succeed(Tracer.Tracer, Tracer.make({ span: (options) => new Tracer.NativeSpan(options) })))( + 'native transaction tracing', + (tracingIt) => { + tracingIt.effect('preserves the caller span and parents transaction child spans to it', () => spanPreservation); + }, +); it.effect('emits transaction logs with caller annotations and logger', () => Effect.gen(function* preserveLogging() { const executor = yield* makeTestDatabase(() => Effect.succeed([])); - const records: Effect.Success[] = - []; + const records: Effect.Success[] = []; const logger = Logger.make(({ fiber }) => { records.push(fiber.getRef(References.CurrentLogAnnotations)); }); @@ -154,10 +124,8 @@ it.effect('emits transaction logs with caller annotations and logger', () => operation: 'context-test', requestId: 'native-request', }), - Effect.provideService(Logger.CurrentLoggers, new Set([logger])) + Effect.provideService(Logger.CurrentLoggers, new Set([logger])), ); - expect(records).toEqual([ - { operation: 'context-test', requestId: 'native-request' }, - ]); - }) + expect(records).toEqual([{ operation: 'context-test', requestId: 'native-request' }]); + }), ); diff --git a/app/packages/core-runtime/tests/unit/native-transaction.test.ts b/app/packages/core-runtime/tests/unit/native-transaction.test.ts index 3352b1ca1..c94884692 100644 --- a/app/packages/core-runtime/tests/unit/native-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/native-transaction.test.ts @@ -6,8 +6,7 @@ import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; import { makeTestDatabase } from '../support/database.ts'; const harness = Effect.fn(function* makeHarness( - settle: (statement: string) => Effect.Effect = () => - Effect.void + settle: (statement: string) => Effect.Effect = () => Effect.void, ) { const events: string[] = []; const executor = yield* makeTestDatabase((statement) => @@ -15,32 +14,29 @@ const harness = Effect.fn(function* makeHarness( events.push(statement); yield* settle(statement); return []; - }) + }), ); return { events, executor }; }); -it.effect( - 'native transactions preserve caller services and execute the body once', - () => - Effect.gen(function* preserveCallerServices() { - class Service extends Context.Service< - Service, - { readonly value: object } - >()('@app/core-runtime/tests/unit/native-transaction.test/Service') {} - const service = { value: {} }; - const h = yield* harness(); - let calls = 0; - const value = yield* h.executor - .transaction(() => { - calls += 1; - return Service.pipe(Effect.map((current) => current.value)); - }) - .pipe(Effect.provideService(Service, service)); - expect(value).toBe(service.value); - expect(calls).toBe(1); - expect(h.events).toEqual(['BEGIN', 'COMMIT']); - }) +it.effect('native transactions preserve caller services and execute the body once', () => + Effect.gen(function* preserveCallerServices() { + class Service extends Context.Service()( + '@app/core-runtime/tests/unit/native-transaction.test/Service', + ) {} + const service = { value: {} }; + const h = yield* harness(); + let calls = 0; + const value = yield* h.executor + .transaction(() => { + calls += 1; + return Service.pipe(Effect.map((current) => current.value)); + }) + .pipe(Effect.provideService(Service, service)); + expect(value).toBe(service.value); + expect(calls).toBe(1); + expect(h.events).toEqual(['BEGIN', 'COMMIT']); + }), ); it.effect('native isolation configuration precedes transaction queries', () => @@ -53,7 +49,7 @@ it.effect('native isolation configuration precedes transaction queries', () => isolationLevel: 'repeatable read', }); yield* transaction.execute(sql`select 1`, 'objects'); - }) + }), ); expect(h.events).toEqual([ 'BEGIN', @@ -61,7 +57,7 @@ it.effect('native isolation configuration precedes transaction queries', () => 'select 1', 'COMMIT', ]); - }) + }), ); for (const [name, cause] of [ @@ -71,16 +67,14 @@ for (const [name, cause] of [ it.effect(`native ${name} rolls back with the original cause`, () => Effect.gen(function* rollbackOriginalCause() { const h = yield* harness(); - const exit = yield* Effect.exit( - h.executor.transaction(() => Effect.failCause(cause)) - ); + const exit = yield* Effect.exit(h.executor.transaction(() => Effect.failCause(cause))); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) { throw new Error('Expected assertion to hold'); } expect(exit.cause).toEqual(cause); expect(h.events).toEqual(['BEGIN', 'ROLLBACK']); - }) + }), ); } @@ -91,7 +85,7 @@ it.effect('a synchronous body construction throw rolls back', () => const exit = yield* Effect.exit( h.executor.transaction((): Effect.Effect => { throw defect; - }) + }), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) { @@ -99,7 +93,7 @@ it.effect('a synchronous body construction throw rolls back', () => } expect(exit.cause).toEqual(Cause.die(defect)); expect(h.events).toEqual(['BEGIN', 'ROLLBACK']); - }) + }), ); for (const phase of ['COMMIT', 'ROLLBACK']) { @@ -108,25 +102,17 @@ for (const phase of ['COMMIT', 'ROLLBACK']) { const failure = new SqlError({ reason: new ConnectionError({ cause: new Error(`${phase} failed`) }), }); - const h = yield* harness((statement) => - statement === phase ? Effect.fail(failure) : Effect.void - ); + const h = yield* harness((statement) => (statement === phase ? Effect.fail(failure) : Effect.void)); const exit = yield* Effect.exit( - h.executor.transaction(() => - phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('body failure') - ) + h.executor.transaction(() => (phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('body failure'))), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) { throw new Error('Expected assertion to hold'); } - expect( - exit.cause.reasons.some( - (reason) => Cause.isDieReason(reason) && reason.defect === failure - ) - ).toBe(true); + expect(exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === failure)).toBe(true); expect(h.events).toEqual(['BEGIN', phase]); - }) + }), ); } @@ -141,21 +127,15 @@ it.effect( const releaseRollback = yield* Deferred.make(); const h = yield* harness((statement) => statement === 'ROLLBACK' - ? Deferred.succeed(rollingBack, null).pipe( - Effect.andThen(Deferred.await(releaseRollback)) - ) - : Effect.void + ? Deferred.succeed(rollingBack, null).pipe(Effect.andThen(Deferred.await(releaseRollback))) + : Effect.void, ); const fiber = yield* h.executor .transaction(() => Deferred.succeed(started, null).pipe( Effect.andThen(Effect.never), - Effect.ensuring( - Deferred.succeed(finalizing, null).pipe( - Effect.andThen(Deferred.await(releaseFinalizer)) - ) - ) - ) + Effect.ensuring(Deferred.succeed(finalizing, null).pipe(Effect.andThen(Deferred.await(releaseFinalizer)))), + ), ) .pipe(Effect.forkChild); yield* Deferred.await(started); @@ -176,7 +156,7 @@ it.effect( expect(Cause.hasInterrupts(result.exit.cause)).toBe(true); expect(result.events).toEqual(['BEGIN', 'ROLLBACK']); }), - 2000 + 2000, ); for (const phase of ['COMMIT', 'ROLLBACK']) { @@ -195,16 +175,12 @@ for (const phase of ['COMMIT', 'ROLLBACK']) { statement === phase ? Deferred.succeed(started, null).pipe( Effect.andThen(Deferred.await(release)), - Effect.andThen(Effect.fail(failure)) + Effect.andThen(Effect.fail(failure)), ) - : Effect.void + : Effect.void, ); const fiber = yield* h.executor - .transaction(() => - phase === 'COMMIT' - ? Effect.succeed(42) - : Effect.fail('domain failure') - ) + .transaction(() => (phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('domain failure'))) .pipe(Effect.forkChild); yield* Deferred.await(started); const interrupt = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild); @@ -218,12 +194,8 @@ for (const phase of ['COMMIT', 'ROLLBACK']) { throw new Error('Expected assertion to hold'); } // Native settlement defects take precedence over pending interruption. - expect( - exit.cause.reasons.some( - (reason) => Cause.isDieReason(reason) && reason.defect === failure - ) - ).toBe(true); + expect(exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === failure)).toBe(true); }), - 2000 + 2000, ); } diff --git a/app/packages/core-runtime/tests/unit/operation-context.test.ts b/app/packages/core-runtime/tests/unit/operation-context.test.ts index 897d27b95..cd8287aed 100644 --- a/app/packages/core-runtime/tests/unit/operation-context.test.ts +++ b/app/packages/core-runtime/tests/unit/operation-context.test.ts @@ -39,221 +39,182 @@ const active = { tenantStatus: 'active', }; const access = (decision: 'allowed' | 'denied' | 'unavailable') => ({ - legalEntities: ({ - legalEntityIds, - }: { - readonly legalEntityIds: readonly string[]; - }) => Effect.succeed(legalEntityIds.map((key) => ({ decision, key }))), + legalEntities: ({ legalEntityIds }: { readonly legalEntityIds: readonly string[] }) => + Effect.succeed(legalEntityIds.map((key) => ({ decision, key }))), modules: () => Effect.succeed([]), resources: () => Effect.succeed([]), }); -const InactiveContextError = Schema.Union([ - OperationAuthenticationRequired, - OperationContextDenied, -]); +const InactiveContextError = Schema.Union([OperationAuthenticationRequired, OperationContextDenied]); -it.effect( - 'classifies required, optional, forbidden, denied, unavailable, and valid scope before handlers', - () => - Effect.gen(function* scopeClassification() { - const repository = { load: () => Effect.succeed(active) }; - const allowed = makeOperationalScopeResolver( - repository, - access('allowed') - ); - const valid = yield* allowed.resolve({ +it.effect('classifies required, optional, forbidden, denied, unavailable, and valid scope before handlers', () => + Effect.gen(function* scopeClassification() { + const repository = { load: () => Effect.succeed(active) }; + const allowed = makeOperationalScopeResolver(repository, access('allowed')); + const valid = yield* allowed.resolve({ + correlationId: 'c-1', + legalEntityScope: 'required', + principal, + }); + const { legalEntityId: _legalEntityId, ...principalWithoutLegalEntity } = principal; + const missing = yield* Effect.flip( + allowed.resolve({ correlationId: 'c-1', legalEntityScope: 'required', + principal: principalWithoutLegalEntity, + }), + ); + const forbidden = yield* Effect.flip( + allowed.resolve({ + correlationId: 'c-1', + legalEntityScope: 'forbidden', principal, - }); - const { legalEntityId: _legalEntityId, ...principalWithoutLegalEntity } = - principal; - const missing = yield* Effect.flip( - allowed.resolve({ - correlationId: 'c-1', - legalEntityScope: 'required', - principal: principalWithoutLegalEntity, - }) - ); - const forbidden = yield* Effect.flip( - allowed.resolve({ - correlationId: 'c-1', - legalEntityScope: 'forbidden', - principal, - }) - ); - const denied = yield* Effect.flip( - makeOperationalScopeResolver(repository, access('denied')).resolve({ - correlationId: 'c-1', - legalEntityScope: 'optional', - principal, - }) - ); - const unavailable = yield* Effect.flip( - makeOperationalScopeResolver(repository, access('unavailable')).resolve( - { - correlationId: 'c-1', - legalEntityScope: 'optional', - principal, - } - ) - ); + }), + ); + const denied = yield* Effect.flip( + makeOperationalScopeResolver(repository, access('denied')).resolve({ + correlationId: 'c-1', + legalEntityScope: 'optional', + principal, + }), + ); + const unavailable = yield* Effect.flip( + makeOperationalScopeResolver(repository, access('unavailable')).resolve({ + correlationId: 'c-1', + legalEntityScope: 'optional', + principal, + }), + ); - expect(Object.isFrozen(valid)).toBe(true); - expect(Schema.is(OperationContextDenied)(missing)).toBe(true); - expect(Schema.is(OperationContextInvalid)(forbidden)).toBe(true); - expect(Schema.is(OperationContextDenied)(denied)).toBe(true); - expect(Schema.is(OperationContextUnavailable)(unavailable)).toBe(true); - }) + expect(Object.isFrozen(valid)).toBe(true); + expect(Schema.is(OperationContextDenied)(missing)).toBe(true); + expect(Schema.is(OperationContextInvalid)(forbidden)).toBe(true); + expect(Schema.is(OperationContextDenied)(denied)).toBe(true); + expect(Schema.is(OperationContextUnavailable)(unavailable)).toBe(true); + }), ); -it.effect( - 'rejects stale tenant, principal, revoked auth binding, and cross-tenant entity records', - () => - Effect.gen(function* staleContextRecords() { - const records = [ - { ...active, tenantStatus: 'suspended' }, - { ...active, principalStatus: 'disabled' }, - { - ...active, - bindingRevokedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-01-01T00:00:00.000Z') - ), - }, - { ...active, bindingTenantId: '00000000-0000-4000-8000-000000000099' }, - { - ...active, - legalEntityTenantId: '00000000-0000-4000-8000-000000000099', - }, - ]; - const errors = yield* Effect.forEach( - records, - (record) => { - const resolver = makeOperationalScopeResolver( - { load: () => Effect.succeed(record) }, - access('allowed') - ); - return Effect.flip( - resolver.resolve({ - correlationId: 'c-1', - legalEntityScope: 'required', - principal, - }) - ); - }, - { concurrency: 1 } - ); - for (const error of errors) { - expect(Schema.is(InactiveContextError)(error)).toBe(true); - } - }) +it.effect('rejects stale tenant, principal, revoked auth binding, and cross-tenant entity records', () => + Effect.gen(function* staleContextRecords() { + const records = [ + { ...active, tenantStatus: 'suspended' }, + { ...active, principalStatus: 'disabled' }, + { + ...active, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }, + { ...active, bindingTenantId: '00000000-0000-4000-8000-000000000099' }, + { + ...active, + legalEntityTenantId: '00000000-0000-4000-8000-000000000099', + }, + ]; + const errors = yield* Effect.forEach( + records, + (record) => { + const resolver = makeOperationalScopeResolver({ load: () => Effect.succeed(record) }, access('allowed')); + return Effect.flip( + resolver.resolve({ + correlationId: 'c-1', + legalEntityScope: 'required', + principal, + }), + ); + }, + { concurrency: 1 }, + ); + for (const error of errors) { + expect(Schema.is(InactiveContextError)(error)).toBe(true); + } + }), ); -it.effect( - 'preserves resolver-issued system provenance across operational scope construction', - () => - Effect.gen(function* systemProvenance() { - const systemContext = yield* systemPrincipalContextResolverFromRepository( - { - load: () => - Effect.succeed({ - kind: 'system' as const, - principalStatus: 'active' as const, - tenantStatus: 'active' as const, - }).pipe(Effect.asSome), - } - ).resolve({ - principalId: principal.principalId, - registration: registerSystemWorkload({ - jobKey: 'operation-scope-test', - }), - runReference: 'run-1', - tenantId: principal.tenantId, - }); - const resolver = makeOperationalScopeResolver( - { - load: () => - Effect.succeed({ - ...active, - bindingPrincipalId: null, - bindingStatus: null, - bindingTenantId: null, - legalEntityStatus: null, - legalEntityTenantId: null, - }), - }, - access('allowed') - ); +it.effect('preserves resolver-issued system provenance across operational scope construction', () => + Effect.gen(function* systemProvenance() { + const systemContext = yield* systemPrincipalContextResolverFromRepository({ + load: () => + Effect.succeed({ + kind: 'system' as const, + principalStatus: 'active' as const, + tenantStatus: 'active' as const, + }).pipe(Effect.asSome), + }).resolve({ + principalId: principal.principalId, + registration: registerSystemWorkload({ + jobKey: 'operation-scope-test', + }), + runReference: 'run-1', + tenantId: principal.tenantId, + }); + const resolver = makeOperationalScopeResolver( + { + load: () => + Effect.succeed({ + ...active, + bindingPrincipalId: null, + bindingStatus: null, + bindingTenantId: null, + legalEntityStatus: null, + legalEntityTenantId: null, + }), + }, + access('allowed'), + ); - const scope = yield* resolver.resolve({ - correlationId: 'system-correlation', - legalEntityScope: 'forbidden', - principal: systemContext, - }); + const scope = yield* resolver.resolve({ + correlationId: 'system-correlation', + legalEntityScope: 'forbidden', + principal: systemContext, + }); - expect(scope.authMethod).toBe('system'); - expect(scope.correlationId).toBe('system-correlation'); - const decoded = yield* decodeTrustedPrincipalContext(scope); - expect(decoded.authMethod).toBe('system'); - expect(decoded.principalId).toBe(scope.principalId); - const untrusted = yield* Effect.exit( - decodeTrustedPrincipalContext({ ...scope }) - ); - expect(Exit.isFailure(untrusted)).toBe(true); - }) + expect(scope.authMethod).toBe('system'); + expect(scope.correlationId).toBe('system-correlation'); + const decoded = yield* decodeTrustedPrincipalContext(scope); + expect(decoded.authMethod).toBe('system'); + expect(decoded.principalId).toBe(scope.principalId); + const untrusted = yield* Effect.exit(decodeTrustedPrincipalContext({ ...scope })); + expect(Exit.isFailure(untrusted)).toBe(true); + }), ); -it.effect( - 'permits only a resolver-branded support-stop recovery through inactive historical scope', - () => - Effect.gen(function* supportRecovery() { - const recoveryPrincipal = - yield* supportRecoveryPrincipalContextResolverFromRepository({ - load: () => - Effect.succeed({ - bindingPrincipalId: principal.principalId, - bindingTenantId: principal.tenantId, - principalKind: 'human' as const, - principalTenantId: principal.tenantId, - tenantId: principal.tenantId, - }).pipe(Effect.asSome), - }).resolveStoppedImpersonation({ - originalAuthBindingId: principal.authBindingId, - originalPrincipalId: principal.principalId, - originalSessionId: 'expired-original-session', +it.effect('permits only a resolver-branded support-stop recovery through inactive historical scope', () => + Effect.gen(function* supportRecovery() { + const recoveryPrincipal = yield* supportRecoveryPrincipalContextResolverFromRepository({ + load: () => + Effect.succeed({ + bindingPrincipalId: principal.principalId, + bindingTenantId: principal.tenantId, + principalKind: 'human' as const, + principalTenantId: principal.tenantId, tenantId: principal.tenantId, - }); - const resolver = makeOperationalScopeResolver( - { - load: () => - Effect.succeed({ - ...active, - bindingRevokedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-08-09T00:00:00.000Z') - ), - bindingStatus: 'revoked', - principalStatus: 'disabled', - tenantStatus: 'suspended', - }), - }, - access('allowed') - ); + }).pipe(Effect.asSome), + }).resolveStoppedImpersonation({ + originalAuthBindingId: principal.authBindingId, + originalPrincipalId: principal.principalId, + originalSessionId: 'expired-original-session', + tenantId: principal.tenantId, + }); + const resolver = makeOperationalScopeResolver( + { + load: () => + Effect.succeed({ + ...active, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), + bindingStatus: 'revoked', + principalStatus: 'disabled', + tenantStatus: 'suspended', + }), + }, + access('allowed'), + ); - const scope = yield* resolver.resolve({ - correlationId: 'support-recovery', - legalEntityScope: 'optional', - principal: recoveryPrincipal, - }); + const scope = yield* resolver.resolve({ + correlationId: 'support-recovery', + legalEntityScope: 'optional', + principal: recoveryPrincipal, + }); - expect( - isTrustedSupportRecoveryPrincipalContext( - scope, - recordSupportImpersonationAction - ) - ).toBe(true); - expect(isTrustedSupportRecoveryPrincipalContext(scope, {})).toBe(false); - expect(isTrustedSupportRecoveryPrincipalContext({ ...scope })).toBe( - false - ); - }) + expect(isTrustedSupportRecoveryPrincipalContext(scope, recordSupportImpersonationAction)).toBe(true); + expect(isTrustedSupportRecoveryPrincipalContext(scope, {})).toBe(false); + expect(isTrustedSupportRecoveryPrincipalContext({ ...scope })).toBe(false); + }), ); diff --git a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts index 97ffa6f0c..edbdc9ce3 100644 --- a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts @@ -36,59 +36,54 @@ const makeWorker = (workerKey = 'consumer.message-logger') => topic: 'producer.message-created', workerKey, }, - (payload) => - Effect.sync(() => - expect(Predicate.isString(payload.messageKey)).toBe(true) - ) + (payload) => Effect.sync(() => expect(Predicate.isString(payload.messageKey)).toBe(true)), ); -it.effect( - 'defines an exact immutable registration while keeping the handler opaque', - () => - Effect.gen(function* immutableRegistration() { - const worker = makeWorker(); - expect(worker.descriptor).toEqual({ - consumerModuleKey: 'consumer', - entrypoint: { - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.message-logger', - moduleKey: 'consumer', - role: 'worker', - scope: 'tenant', - }, - leaseDurationMs: 30_000, - payloadSchema, - producerModuleKey: 'producer', - retryPolicy: { - initialBackoffMs: 1000, - maxAttempts: 5, - maxBackoffMs: 10_000, - multiplier: 2, - }, - topic: 'producer.message-created', - workerKey: 'consumer.message-logger', - }); - expect(Object.isFrozen(worker)).toBe(true); - expect(Object.isFrozen(worker.descriptor)).toBe(true); - expect(Object.isFrozen(worker.descriptor.retryPolicy)).toBe(true); - expect('handler' in worker).toBe(false); - expect(Object.keys(worker)).toEqual(['descriptor']); - const payload = yield* Schema.decodeEffect(payloadSchema)({ - messageKey: 'message-1', - }); - yield* getOutboxWorkerHandler(worker)(payload, { - attemptNumber: 1, - claimId: 'claim-1', - deliveryId: 'delivery-1', - domainEventId: 'event-1', - messageId: 'message-1', - producerModuleKey: 'producer', - tenantId: 'tenant-1', - tenantSequenceNo: 1n, - topic: 'producer.message-created', - workerKey: 'consumer.message-logger', - }); - }) +it.effect('defines an exact immutable registration while keeping the handler opaque', () => + Effect.gen(function* immutableRegistration() { + const worker = makeWorker(); + expect(worker.descriptor).toEqual({ + consumerModuleKey: 'consumer', + entrypoint: { + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.message-logger', + moduleKey: 'consumer', + role: 'worker', + scope: 'tenant', + }, + leaseDurationMs: 30_000, + payloadSchema, + producerModuleKey: 'producer', + retryPolicy: { + initialBackoffMs: 1000, + maxAttempts: 5, + maxBackoffMs: 10_000, + multiplier: 2, + }, + topic: 'producer.message-created', + workerKey: 'consumer.message-logger', + }); + expect(Object.isFrozen(worker)).toBe(true); + expect(Object.isFrozen(worker.descriptor)).toBe(true); + expect(Object.isFrozen(worker.descriptor.retryPolicy)).toBe(true); + expect('handler' in worker).toBe(false); + expect(Object.keys(worker)).toEqual(['descriptor']); + const payload = yield* Schema.decodeEffect(payloadSchema)({ + messageKey: 'message-1', + }); + yield* getOutboxWorkerHandler(worker)(payload, { + attemptNumber: 1, + claimId: 'claim-1', + deliveryId: 'delivery-1', + domainEventId: 'event-1', + messageId: 'message-1', + producerModuleKey: 'producer', + tenantId: 'tenant-1', + tenantSequenceNo: 1n, + topic: 'producer.message-created', + workerKey: 'consumer.message-logger', + }); + }), ); it('preserves schema inference for a typed handler payload', () => { defineOutboxWorker( @@ -116,7 +111,7 @@ it('preserves schema inference for a typed handler payload', () => { (payload) => { const key: string = payload.messageKey; return Effect.sync(() => expect(key).toBe(payload.messageKey)); - } + }, ); }); it('rejects invalid identities, retry policies, and lease policies', () => { @@ -143,9 +138,7 @@ it('rejects invalid identities, retry policies, and lease policies', () => { { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, ]; for (const descriptor of invalidDescriptors) { - expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow( - OutboxWorkerDescriptorError - ); + expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow(OutboxWorkerDescriptorError); } }); it('rejects duplicate worker keys and calculates bounded exponential backoff', () => { @@ -154,7 +147,7 @@ it('rejects duplicate worker keys and calculates bounded exponential backoff', ( expect.objectContaining({ name: 'OutboxWorkerDescriptorError', reason: expect.stringMatching(/duplicate Outbox Worker key/u), - }) + }), ); expect(validateOutboxWorkerRegistrations([worker])).toEqual([worker]); expect(retryBackoffMs(worker.descriptor.retryPolicy, 1)).toBe(1000); @@ -174,12 +167,10 @@ it('validates and freezes the schema-free installed subscription catalog', () => expect(validated).toEqual([subscription]); expect(Object.isFrozen(validated)).toBe(true); expect(Object.isFrozen(validated[0])).toBe(true); - expect(() => - validateOutboxWorkerSubscriptions([subscription, subscription]) - ).toThrow( + expect(() => validateOutboxWorkerSubscriptions([subscription, subscription])).toThrow( expect.objectContaining({ name: 'OutboxWorkerDescriptorError', reason: expect.stringMatching(/duplicate Outbox Worker key/u), - }) + }), ); }); diff --git a/app/packages/core-runtime/tests/unit/outbox-errors.test.ts b/app/packages/core-runtime/tests/unit/outbox-errors.test.ts index e90948ec8..084baa69b 100644 --- a/app/packages/core-runtime/tests/unit/outbox-errors.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-errors.test.ts @@ -23,64 +23,49 @@ const errorSchemas = [ ]; const checkErrorContract = ( - schema: Schema.Codec< - Failure, - { readonly _tag: string; readonly code: string; readonly reason: string } - >, + schema: Schema.Codec, failure: Failure, encoded: { readonly _tag: string; readonly code: string; readonly reason: string; - } + }, ): void => { - it.effect( - `${encoded._tag} preserves its schema and yieldable failure contract`, - () => - Effect.gen(function* errorContract() { - expect(Schema.is(schema)(failure)).toBeTruthy(); - expect(yield* Schema.encodeEffect(schema)(failure)).toEqual(encoded); - const decoded = yield* Schema.decodeEffect(schema)(encoded); - expect(Schema.is(schema)(decoded)).toBeTruthy(); - expect(yield* Schema.encodeEffect(schema)(decoded)).toEqual(encoded); - for (const otherSchema of errorSchemas) { - expect(Schema.is(otherSchema)(failure)).toBe( - Object.is(otherSchema, schema) - ); - expect(Schema.is(otherSchema)(decoded)).toBe( - Object.is(otherSchema, schema) - ); - } - expect(() => - Schema.decodeSync(schema)({ ...encoded, _tag: 'WrongError' }) - ).toThrow(); - expect(() => - Schema.decodeSync(schema)({ ...encoded, code: 'wrong_code' }) - ).toThrow(); - expect(() => - Schema.decodeUnknownSync(schema)({ ...encoded, reason: 42 }) - ).toThrow(); - expect(() => - Schema.decodeUnknownSync(schema)({ - _tag: encoded._tag, - code: encoded.code, - }) - ).toThrow(); - const yielded = yield* Effect.flip( - Effect.gen(function* yieldFailure() { - expect(Schema.is(schema)(failure)).toBeTruthy(); - return yield* failure; - }) - ); - expect(yielded).toBe(failure); - const decodedFailure = yield* Effect.flip( - Effect.gen(function* yieldDecodedFailure() { - expect(Schema.is(schema)(decoded)).toBeTruthy(); - return yield* decoded; - }) - ); - expect(decodedFailure).toBe(decoded); - }) + it.effect(`${encoded._tag} preserves its schema and yieldable failure contract`, () => + Effect.gen(function* errorContract() { + expect(Schema.is(schema)(failure)).toBeTruthy(); + expect(yield* Schema.encodeEffect(schema)(failure)).toEqual(encoded); + const decoded = yield* Schema.decodeEffect(schema)(encoded); + expect(Schema.is(schema)(decoded)).toBeTruthy(); + expect(yield* Schema.encodeEffect(schema)(decoded)).toEqual(encoded); + for (const otherSchema of errorSchemas) { + expect(Schema.is(otherSchema)(failure)).toBe(Object.is(otherSchema, schema)); + expect(Schema.is(otherSchema)(decoded)).toBe(Object.is(otherSchema, schema)); + } + expect(() => Schema.decodeSync(schema)({ ...encoded, _tag: 'WrongError' })).toThrow(); + expect(() => Schema.decodeSync(schema)({ ...encoded, code: 'wrong_code' })).toThrow(); + expect(() => Schema.decodeUnknownSync(schema)({ ...encoded, reason: 42 })).toThrow(); + expect(() => + Schema.decodeUnknownSync(schema)({ + _tag: encoded._tag, + code: encoded.code, + }), + ).toThrow(); + const yielded = yield* Effect.flip( + Effect.gen(function* yieldFailure() { + expect(Schema.is(schema)(failure)).toBeTruthy(); + return yield* failure; + }), + ); + expect(yielded).toBe(failure); + const decodedFailure = yield* Effect.flip( + Effect.gen(function* yieldDecodedFailure() { + expect(Schema.is(schema)(decoded)).toBeTruthy(); + return yield* decoded; + }), + ); + expect(decodedFailure).toBe(decoded); + }), ); }; @@ -94,7 +79,7 @@ checkErrorContract( _tag: 'OutboxWorkerDescriptorError', code: 'outbox_worker_descriptor_invalid', reason: 'detail', - } + }, ); checkErrorContract( OutboxPayloadDecodeError, @@ -106,7 +91,7 @@ checkErrorContract( _tag: 'OutboxPayloadDecodeError', code: 'outbox_payload_invalid', reason: 'detail', - } + }, ); checkErrorContract( OutboxPersistenceError, @@ -118,13 +103,13 @@ checkErrorContract( _tag: 'OutboxPersistenceError', code: 'outbox_persistence_failed', reason: 'detail', - } -); -checkErrorContract( - OutboxClaimLostError, - new OutboxClaimLostError({ code: 'outbox_claim_lost', reason: 'detail' }), - { _tag: 'OutboxClaimLostError', code: 'outbox_claim_lost', reason: 'detail' } + }, ); +checkErrorContract(OutboxClaimLostError, new OutboxClaimLostError({ code: 'outbox_claim_lost', reason: 'detail' }), { + _tag: 'OutboxClaimLostError', + code: 'outbox_claim_lost', + reason: 'detail', +}); checkErrorContract( OutboxHandlerExecutionError, new OutboxHandlerExecutionError({ @@ -135,7 +120,7 @@ checkErrorContract( _tag: 'OutboxHandlerExecutionError', code: 'outbox_handler_execution_failed', reason: 'detail', - } + }, ); checkErrorContract( OutboxPollerConfigError, @@ -147,28 +132,21 @@ checkErrorContract( _tag: 'OutboxPollerConfigError', code: 'outbox_poller_config_invalid', reason: 'detail', - } + }, ); it('persistence errors keep the original cause private and immutable', () => { const cause = { secret: 'database credential' }; const failure = outboxPersistenceError(cause); expect(Schema.is(OutboxPersistenceError)(failure)).toBeTruthy(); - expect( - Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause') - ).toEqual({ + expect(Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause')).toEqual({ configurable: false, enumerable: false, value: cause, writable: false, }); - expect( - Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause') - ?.value - ).toBe(cause); - expect(Object.keys(failure).includes('ontosOutboxPersistenceCause')).toBe( - false - ); + expect(Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause')?.value).toBe(cause); + expect(Object.keys(failure).includes('ontosOutboxPersistenceCause')).toBe(false); expect(JSON.stringify(failure).includes('database credential')).toBe(false); const encoded = Schema.encodeSync(OutboxPersistenceError)(failure); expect(Predicate.isTagged(encoded, 'OutboxPersistenceError')).toBe(true); @@ -176,26 +154,13 @@ it('persistence errors keep the original cause private and immutable', () => { code: 'outbox_persistence_failed', reason: 'The Outbox Worker persistence operation failed', }); - expect( - Object.hasOwn( - Schema.decodeSync(OutboxPersistenceError)(encoded), - 'ontosOutboxPersistenceCause' - ) - ).toBe(false); + expect(Object.hasOwn(Schema.decodeSync(OutboxPersistenceError)(encoded), 'ontosOutboxPersistenceCause')).toBe(false); }); it('sanitizer normalizes control whitespace, trims, truncates and falls back', () => { - expect(sanitizeOutboxErrorMessage(' \r\nfirst\r\n\tsecond\t third \n')).toBe( - 'first second third' - ); + expect(sanitizeOutboxErrorMessage(' \r\nfirst\r\n\tsecond\t third \n')).toBe('first second third'); expect(sanitizeOutboxErrorMessage(' plain detail ')).toBe('plain detail'); - expect(sanitizeOutboxErrorMessage(` ${'x'.repeat(501)} `)).toBe( - 'x'.repeat(500) - ); - expect(sanitizeOutboxErrorMessage(' \r\n\t ')).toBe( - 'Outbox Worker processing failed' - ); - expect(sanitizeOutboxErrorMessage('')).toBe( - 'Outbox Worker processing failed' - ); + expect(sanitizeOutboxErrorMessage(` ${'x'.repeat(501)} `)).toBe('x'.repeat(500)); + expect(sanitizeOutboxErrorMessage(' \r\n\t ')).toBe('Outbox Worker processing failed'); + expect(sanitizeOutboxErrorMessage('')).toBe('Outbox Worker processing failed'); }); diff --git a/app/packages/core-runtime/tests/unit/outbox-health.test.ts b/app/packages/core-runtime/tests/unit/outbox-health.test.ts index 38fc4c487..ed1272bab 100644 --- a/app/packages/core-runtime/tests/unit/outbox-health.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-health.test.ts @@ -2,113 +2,89 @@ import { ConfigProvider, Effect, Layer, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; import { FetchHttpClient, HttpClient } from 'effect/unstable/http'; -import { - createOutboxWorkerHealth, - serveOutboxWorkerHealth, -} from '../../src/outbox/health.ts'; +import { createOutboxWorkerHealth, serveOutboxWorkerHealth } from '../../src/outbox/health.ts'; import { runOutboxWorkerProcess } from '../../src/outbox/process.ts'; import { OutboxRuntime } from '../../src/outbox/runtime.ts'; -it.live( - 'production health binds all IPv4 interfaces for external-container probes', - () => - Effect.gen(function* externallyReachableHealth() { - const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); - expect(server.hostname).toBe('0.0.0.0'); - }) +it.live('production health binds all IPv4 interfaces for external-container probes', () => + Effect.gen(function* externallyReachableHealth() { + const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + expect(server.hostname).toBe('0.0.0.0'); + }), ); -it.live( - 'readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', - () => { - let now = 1000; - return Effect.gen(function* healthLifecycle() { - const services = yield* Layer.build(FetchHttpClient.layer); - const client = yield* Effect.provide(HttpClient.HttpClient, services); - const health = yield* createOutboxWorkerHealth({ - now: Effect.sync(() => now), - staleAfterMs: 100, - }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); - const ready = client.get(`http://127.0.0.1:${server.port}/ready`); - const startingResponse = yield* ready; - expect(startingResponse.status).toBe(503); - expect(yield* startingResponse.json).toEqual({ ready: false }); - expect( - (yield* client.get(`http://127.0.0.1:${server.port}/unknown`)).status - ).toBe(404); - yield* health.cycleSucceeded; - const readyResponse = yield* ready; - expect(readyResponse.status).toBe(200); - expect(yield* readyResponse.json).toEqual({ ready: true }); - now = 1101; - expect((yield* ready).status).toBe(503); - yield* health.cycleSucceeded; - yield* health.cycleFailed; - expect((yield* ready).status).toBe(503); - yield* health.cycleSucceeded; - yield* health.shuttingDown; - expect((yield* ready).status).toBe(503); +it.live('readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', () => { + let now = 1000; + return Effect.gen(function* healthLifecycle() { + const services = yield* Layer.build(FetchHttpClient.layer); + const client = yield* Effect.provide(HttpClient.HttpClient, services); + const health = yield* createOutboxWorkerHealth({ + now: Effect.sync(() => now), + staleAfterMs: 100, }); - } + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + const ready = client.get(`http://127.0.0.1:${server.port}/ready`); + const startingResponse = yield* ready; + expect(startingResponse.status).toBe(503); + expect(yield* startingResponse.json).toEqual({ ready: false }); + expect((yield* client.get(`http://127.0.0.1:${server.port}/unknown`)).status).toBe(404); + yield* health.cycleSucceeded; + const readyResponse = yield* ready; + expect(readyResponse.status).toBe(200); + expect(yield* readyResponse.json).toEqual({ ready: true }); + now = 1101; + expect((yield* ready).status).toBe(503); + yield* health.cycleSucceeded; + yield* health.cycleFailed; + expect((yield* ready).status).toBe(503); + yield* health.cycleSucceeded; + yield* health.shuttingDown; + expect((yield* ready).status).toBe(503); + }); +}); + +it.live('closing the health scope marks it unavailable and releases its dynamically allocated port', () => + Effect.gen(function* releasedPort() { + const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); + yield* health.cycleSucceeded; + const server = yield* Effect.scoped(serveOutboxWorkerHealth(health, { port: 0 })); + expect(yield* health.isReady).toBe(false); + const rebound = yield* Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })); + expect(rebound.port).toBe(server.port); + }), ); -it.live( - 'closing the health scope marks it unavailable and releases its dynamically allocated port', - () => - Effect.gen(function* releasedPort() { - const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); - yield* health.cycleSucceeded; - const server = yield* Effect.scoped( - serveOutboxWorkerHealth(health, { port: 0 }) - ); - expect(yield* health.isReady).toBe(false); - const rebound = yield* Effect.scoped( - serveOutboxWorkerHealth(health, { port: server.port }) - ); - expect(rebound.port).toBe(server.port); - }) +it.live('a health port already in use produces a typed server startup failure', () => + Effect.gen(function* occupiedPort() { + const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + const failure = yield* Effect.flip(Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port }))); + expect(Predicate.isTagged(failure, 'ServeError')).toBe(true); + }), ); -it.live( - 'a health port already in use produces a typed server startup failure', - () => - Effect.gen(function* occupiedPort() { - const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); +it.effect('invalid configured health ports fail startup with a typed configuration error before polling', () => + Effect.gen(function* invalidPortConfiguration() { + for (const port of ['0', '65536', '4102.5', 'invalid']) { const failure = yield* Effect.flip( - Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })) + runOutboxWorkerProcess({ + claimOwnerPrefix: 'health-config-test', + health: true, + registrations: [], + subscriptions: [], + }).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromUnknown({ OUTBOX_WORKER_HEALTH_PORT: port }), + ), + Effect.provideService(OutboxRuntime, { + matchMessages: () => Effect.die('Invalid configuration must prevent matching'), + runCycle: () => Effect.die('Invalid configuration must prevent polling'), + }), + ), ); - expect(Predicate.isTagged(failure, 'ServeError')).toBe(true); - }) -); - -it.effect( - 'invalid configured health ports fail startup with a typed configuration error before polling', - () => - Effect.gen(function* invalidPortConfiguration() { - for (const port of ['0', '65536', '4102.5', 'invalid']) { - const failure = yield* Effect.flip( - runOutboxWorkerProcess({ - claimOwnerPrefix: 'health-config-test', - health: true, - registrations: [], - subscriptions: [], - }).pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown({ OUTBOX_WORKER_HEALTH_PORT: port }) - ), - Effect.provideService(OutboxRuntime, { - matchMessages: () => - Effect.die('Invalid configuration must prevent matching'), - runCycle: () => - Effect.die('Invalid configuration must prevent polling'), - }) - ) - ); - expect(Predicate.isTagged(failure, 'ConfigError')).toBe(true); - } - }) + expect(Predicate.isTagged(failure, 'ConfigError')).toBe(true); + } + }), ); diff --git a/app/packages/core-runtime/tests/unit/outbox-poller.test.ts b/app/packages/core-runtime/tests/unit/outbox-poller.test.ts index 55234e561..80f94257f 100644 --- a/app/packages/core-runtime/tests/unit/outbox-poller.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-poller.test.ts @@ -4,14 +4,8 @@ import { TestClock } from 'effect/testing'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { defineOutboxWorker } from '../../src/outbox/definition.ts'; -import { - OutboxPersistenceError, - OutboxPollerConfigError, -} from '../../src/outbox/errors.ts'; -import { - parseOutboxPollingConfig, - runOutboxPollingLoop, -} from '../../src/outbox/poller.ts'; +import { OutboxPersistenceError, OutboxPollerConfigError } from '../../src/outbox/errors.ts'; +import { parseOutboxPollingConfig, runOutboxPollingLoop } from '../../src/outbox/poller.ts'; import type { OutboxCycleRunner } from '../../src/outbox/poller.ts'; const registration = defineOutboxWorker( @@ -38,7 +32,7 @@ const registration = defineOutboxWorker( topic: 'producer.message-created', workerKey: 'consumer.logger', }, - () => Effect.void + () => Effect.void, ); const emptyResult = { @@ -51,90 +45,84 @@ const emptyResult = { succeeded: 0, } as const; -it.effect( - 'uses safe one-second defaults and accepts bounded scalar overrides', - () => - Effect.gen(function* validPollingConfiguration() { - expect( - yield* parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: {}, - }) - ).toEqual({ - claimOwner: 'consumer:default', - maxDeliveries: 100, - pollIntervalMs: 1000, - }); +it.effect('uses safe one-second defaults and accepts bounded scalar overrides', () => + Effect.gen(function* validPollingConfiguration() { + expect( + yield* parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: {}, + }), + ).toEqual({ + claimOwner: 'consumer:default', + maxDeliveries: 100, + pollIntervalMs: 1000, + }); - expect( - yield* parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: { - OUTBOX_WORKER_CLAIM_OWNER: 'consumer:configured', - OUTBOX_WORKER_MAX_DELIVERIES: '25', - OUTBOX_WORKER_POLL_INTERVAL_MS: '250', - }, - }) - ).toEqual({ - claimOwner: 'consumer:configured', - maxDeliveries: 25, - pollIntervalMs: 250, - }); - }) + expect( + yield* parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: { + OUTBOX_WORKER_CLAIM_OWNER: 'consumer:configured', + OUTBOX_WORKER_MAX_DELIVERIES: '25', + OUTBOX_WORKER_POLL_INTERVAL_MS: '250', + }, + }), + ).toEqual({ + claimOwner: 'consumer:configured', + maxDeliveries: 25, + pollIntervalMs: 250, + }); + }), ); -it.effect( - 'rejects invalid polling values instead of falling back to a busy loop', - () => - Effect.gen(function* invalidPollingConfiguration() { - const error = yield* Effect.flip( - parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: { OUTBOX_WORKER_POLL_INTERVAL_MS: '0' }, - }) - ); - expect(Schema.is(OutboxPollerConfigError)(error)).toBe(true); - }) +it.effect('rejects invalid polling values instead of falling back to a busy loop', () => + Effect.gen(function* invalidPollingConfiguration() { + const error = yield* Effect.flip( + parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: { OUTBOX_WORKER_POLL_INTERVAL_MS: '0' }, + }), + ); + expect(Schema.is(OutboxPollerConfigError)(error)).toBe(true); + }), ); -it.effect( - 'runs immediately, survives a typed cycle failure, and continues polling', - () => - Effect.gen(function* pollingContinuesAfterFailure() { - let calls = 0; - const healthTransitions: string[] = []; - const runCycle: OutboxCycleRunner = () => - Effect.suspend(() => { - calls += 1; - return calls === 1 - ? Effect.fail( - new OutboxPersistenceError({ - code: 'outbox_persistence_failed', - reason: 'controlled test failure', - }) - ) - : Effect.succeed(emptyResult); - }); - const running = yield* runOutboxPollingLoop( - { - config: { - claimOwner: 'consumer:test', - maxDeliveries: 10, - pollIntervalMs: 10, - }, - health: { - cycleFailed: Effect.sync(() => healthTransitions.push('failed')), - cycleSucceeded: Effect.sync(() => healthTransitions.push('ready')), - }, - registrations: [registration], - subscriptions: [registration.descriptor], +it.effect('runs immediately, survives a typed cycle failure, and continues polling', () => + Effect.gen(function* pollingContinuesAfterFailure() { + let calls = 0; + const healthTransitions: string[] = []; + const runCycle: OutboxCycleRunner = () => + Effect.suspend(() => { + calls += 1; + return calls === 1 + ? Effect.fail( + new OutboxPersistenceError({ + code: 'outbox_persistence_failed', + reason: 'controlled test failure', + }), + ) + : Effect.succeed(emptyResult); + }); + const running = yield* runOutboxPollingLoop( + { + config: { + claimOwner: 'consumer:test', + maxDeliveries: 10, + pollIntervalMs: 10, + }, + health: { + cycleFailed: Effect.sync(() => healthTransitions.push('failed')), + cycleSucceeded: Effect.sync(() => healthTransitions.push('ready')), }, - runCycle - ).pipe(Effect.forkChild); + registrations: [registration], + subscriptions: [registration.descriptor], + }, + runCycle, + ).pipe(Effect.forkChild); - yield* TestClock.adjust('20 millis'); - yield* Fiber.interrupt(running); - expect(calls, 'polling loop did not continue').toBe(3); - expect(healthTransitions).toEqual(['failed', 'ready', 'ready']); - }).pipe(Effect.scoped) + yield* TestClock.adjust('20 millis'); + yield* Fiber.interrupt(running); + expect(calls, 'polling loop did not continue').toBe(3); + expect(healthTransitions).toEqual(['failed', 'ready', 'ready']); + }).pipe(Effect.scoped), ); diff --git a/app/packages/core-runtime/tests/unit/outbox-process.test.ts b/app/packages/core-runtime/tests/unit/outbox-process.test.ts index 26eea7abe..11416fe50 100644 --- a/app/packages/core-runtime/tests/unit/outbox-process.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-process.test.ts @@ -7,10 +7,7 @@ const gracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => Effect.gen(function* gracefulShutdownEffect() { const child = yield* ChildProcess.make( process.execPath, - [ - '--experimental-strip-types', - 'tests/fixtures/outbox-worker-process.fixture.ts', - ], + ['--experimental-strip-types', 'tests/fixtures/outbox-worker-process.fixture.ts'], { cwd: new URL('../..', import.meta.url).pathname, env: { @@ -22,31 +19,25 @@ const gracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => stderr: 'pipe', stdin: 'ignore', stdout: 'pipe', - } + }, ); const readyToStop = yield* Deferred.make(); const outputFiber = yield* child.stdout.pipe( Stream.decodeText(), Stream.splitLines, Stream.tap((line) => - line === 'cycle:1' - ? Deferred.succeed(readyToStop, null).pipe(Effect.asVoid) - : Effect.void + line === 'cycle:1' ? Deferred.succeed(readyToStop, null).pipe(Effect.asVoid) : Effect.void, ), Stream.runCollect, - Effect.forkChild - ); - const errorsFiber = yield* child.stderr.pipe( - Stream.decodeText(), - Stream.mkString, - Effect.forkChild + Effect.forkChild, ); + const errorsFiber = yield* child.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkChild); yield* Deferred.await(readyToStop); yield* child.kill({ killSignal: signal }); const [code, outputLines, errors] = yield* Effect.all( [child.exitCode, Fiber.join(outputFiber), Fiber.join(errorsFiber)], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const output = outputLines.join('\n'); @@ -57,16 +48,14 @@ const gracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => const assertGracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => Layer.build(NodeServices.layer).pipe( - Effect.flatMap((nodeServices) => - gracefulShutdown(signal).pipe(Effect.provide(nodeServices)) - ), - Effect.scoped + Effect.flatMap((nodeServices) => gracefulShutdown(signal).pipe(Effect.provide(nodeServices))), + Effect.scoped, ); for (const signal of ['SIGINT', 'SIGTERM'] as const) { it.live( `${signal} interrupts polling and disposes the managed worker runtime`, () => assertGracefulShutdown(signal), - 5000 + 5000, ); } diff --git a/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts b/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts index 4858dba66..104632e20 100644 --- a/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts @@ -3,31 +3,18 @@ import { expect, it } from 'effect-rstest'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { defineOutboxWorker } from '../../src/outbox/definition.ts'; -import type { - OutboxWorkerHandler, - OutboxWorkerRegistration, -} from '../../src/outbox/definition.ts'; -import { - OutboxClaimLostError, - OutboxWorkerDescriptorError, -} from '../../src/outbox/errors.ts'; -import type { - OutboxClaim, - OutboxFailureStatus, - OutboxRepositoryService, -} from '../../src/outbox/repository.ts'; +import type { OutboxWorkerHandler, OutboxWorkerRegistration } from '../../src/outbox/definition.ts'; +import { OutboxClaimLostError, OutboxWorkerDescriptorError } from '../../src/outbox/errors.ts'; +import type { OutboxClaim, OutboxFailureStatus, OutboxRepositoryService } from '../../src/outbox/repository.ts'; import { makeOutboxRuntime } from '../../src/outbox/runtime.ts'; const TestHandlerFailureContract = Schema.TaggedStruct('TestHandlerFailure', { reason: Schema.String, }); type TestHandlerFailureSelf = typeof TestHandlerFailureContract.Type; -const TestHandlerFailure = Schema.TaggedError()( - 'TestHandlerFailure', - { - reason: Schema.String, - } -); +const TestHandlerFailure = Schema.TaggedError()('TestHandlerFailure', { + reason: Schema.String, +}); class TestWorkerDependency extends Context.Service< TestWorkerDependency, @@ -43,10 +30,7 @@ const retryPolicy = { multiplier: 1, } as const; -const claim = ( - attemptNumber = 1, - payloadJson?: OutboxClaim['payloadJson'] -): OutboxClaim => ({ +const claim = (attemptNumber = 1, payloadJson?: OutboxClaim['payloadJson']): OutboxClaim => ({ attemptId: `attempt-${attemptNumber}`, attemptNumber, claimId: `runtime:claim-${attemptNumber}`, @@ -55,8 +39,7 @@ const claim = ( deliveryId: 'delivery-1', domainEventId: 'event-1', messageId: 'message-1', - payloadJson: - payloadJson === undefined ? { messageKey: 'message-1' } : payloadJson, + payloadJson: payloadJson === undefined ? { messageKey: 'message-1' } : payloadJson, producerModuleKey: 'producer', retryPolicy, tenantId: 'tenant-1', @@ -66,11 +49,7 @@ const claim = ( }); const worker = ( - handler: OutboxWorkerHandler< - { readonly messageKey: typeof MessageKey.Type }, - HandlerError, - HandlerRequirements - > + handler: OutboxWorkerHandler<{ readonly messageKey: typeof MessageKey.Type }, HandlerError, HandlerRequirements>, ) => defineOutboxWorker( { @@ -89,7 +68,7 @@ const worker = ( topic: 'producer.message-created', workerKey: 'consumer.logger', }, - handler + handler, ); interface RepositoryProbe { @@ -111,7 +90,7 @@ const repository = ( readonly deliveriesCreated: number; readonly messagesMatched: number; }; - } = {} + } = {}, ): ControlledRepository => { const claims = [...(options.claims ?? [])]; const failureStatuses = [...(options.failureStatuses ?? [])]; @@ -124,41 +103,26 @@ const repository = ( if (options.completeError !== undefined) { return Effect.fail(options.completeError); } - return Effect.sync(() => probe.completed.push(claimed)).pipe( - Effect.asVoid - ); + return Effect.sync(() => probe.completed.push(claimed)).pipe(Effect.asVoid); }, fail: (claimed, message) => Effect.sync(() => { probe.failed.push({ claim: claimed, message }); return failureStatuses.shift() ?? 'pending'; }), - matchUnmatched: () => - Effect.succeed( - options.match ?? { deliveriesCreated: 0, messagesMatched: 0 } - ), + matchUnmatched: () => Effect.succeed(options.match ?? { deliveriesCreated: 0, messagesMatched: 0 }), }, }; }; -type NoRequirementsWorker = OutboxWorkerRegistration< - Schema.ConstraintDecoder, - string, - string, - unknown ->; +type NoRequirementsWorker = OutboxWorkerRegistration, string, string, unknown>; interface WorkerInvocation { - readonly context: Parameters< - OutboxWorkerHandler<{ readonly messageKey: string }, never> - >[1]; + readonly context: Parameters>[1]; readonly payload: { readonly messageKey: string }; } -const run = ( - service: OutboxRepositoryService, - registration: NoRequirementsWorker = worker(() => Effect.void) -) => +const run = (service: OutboxRepositoryService, registration: NoRequirementsWorker = worker(() => Effect.void)) => makeOutboxRuntime(service).runCycle({ claimOwner: 'unit-runtime', registrations: [registration], @@ -182,246 +146,210 @@ it.effect('owner-local cycles do not perform global matching', () => }); expect(controlled.probe.completed).toEqual([]); expect(controlled.probe.failed).toEqual([]); - }) + }), ); -it.effect( - 'matches messages only through the explicit Core matcher snapshot', - () => - Effect.gen(function* explicitMatcherSnapshot() { - const controlled = repository({ - match: { deliveriesCreated: 3, messagesMatched: 2 }, - }); - const registration = worker(() => Effect.void); - const result = yield* makeOutboxRuntime(controlled.service).matchMessages( - { - subscriptions: [registration.descriptor], - } - ); +it.effect('matches messages only through the explicit Core matcher snapshot', () => + Effect.gen(function* explicitMatcherSnapshot() { + const controlled = repository({ + match: { deliveriesCreated: 3, messagesMatched: 2 }, + }); + const registration = worker(() => Effect.void); + const result = yield* makeOutboxRuntime(controlled.service).matchMessages({ + subscriptions: [registration.descriptor], + }); - expect(result).toEqual({ deliveriesCreated: 3, messagesMatched: 2 }); - }) + expect(result).toEqual({ deliveriesCreated: 3, messagesMatched: 2 }); + }), ); -it.effect( - 'rejects an owner-local worker missing from the installed subscription catalog', - () => - Effect.gen(function* missingInstalledSubscription() { - const controlled = repository(); - const registration = worker(() => Effect.void); - const error = yield* Effect.flip( - makeOutboxRuntime(controlled.service).runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [], - }) - ); +it.effect('rejects an owner-local worker missing from the installed subscription catalog', () => + Effect.gen(function* missingInstalledSubscription() { + const controlled = repository(); + const registration = worker(() => Effect.void); + const error = yield* Effect.flip( + makeOutboxRuntime(controlled.service).runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [], + }), + ); - expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); - expect(error.reason).toMatch( - /absent from the installed subscription catalog/u - ); - }) + expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); + expect(error.reason).toMatch(/absent from the installed subscription catalog/u); + }), ); -it.effect( - 'rejects deployed owner descriptors without a matching local worker registration', - () => - Effect.gen(function* missingLocalRegistration() { - const controlled = repository(); - const registration = worker(() => Effect.void); - const error = yield* Effect.flip( - makeOutboxRuntime(controlled.service).runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [ - registration.descriptor, - { - ...registration.descriptor, - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.second-worker', - moduleKey: registration.descriptor.consumerModuleKey, - role: 'worker', - }), - workerKey: 'consumer.second-worker', - }, - ], - }) - ); +it.effect('rejects deployed owner descriptors without a matching local worker registration', () => + Effect.gen(function* missingLocalRegistration() { + const controlled = repository(); + const registration = worker(() => Effect.void); + const error = yield* Effect.flip( + makeOutboxRuntime(controlled.service).runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [ + registration.descriptor, + { + ...registration.descriptor, + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.second-worker', + moduleKey: registration.descriptor.consumerModuleKey, + role: 'worker', + }), + workerKey: 'consumer.second-worker', + }, + ], + }), + ); - expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); - expect(error.reason).toMatch( - /contradicts its deployed descriptor snapshot/u - ); - }) + expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); + expect(error.reason).toMatch(/contradicts its deployed descriptor snapshot/u); + }), ); -it.effect( - 'decodes a published payload, supplies exact context, and completes success', - () => - Effect.gen(function* successfulDelivery() { - const selected = claim(); - const controlled = repository({ claims: [selected] }); - let observed: WorkerInvocation | undefined; - const registration = worker((payload, context) => - Effect.sync(() => { - observed = { context, payload }; - }) - ); +it.effect('decodes a published payload, supplies exact context, and completes success', () => + Effect.gen(function* successfulDelivery() { + const selected = claim(); + const controlled = repository({ claims: [selected] }); + let observed: WorkerInvocation | undefined; + const registration = worker((payload, context) => + Effect.sync(() => { + observed = { context, payload }; + }), + ); + + const result = yield* run(controlled.service, registration); - const result = yield* run(controlled.service, registration); - - expect(result.succeeded).toBe(1); - expect(controlled.probe.completed).toEqual([selected]); - expect(controlled.probe.failed).toEqual([]); - expect(observed).toEqual({ - context: { - attemptNumber: 1, - claimId: 'runtime:claim-1', - correlationId: 'correlation-1', - deliveryId: 'delivery-1', - domainEventId: 'event-1', - messageId: 'message-1', - producerModuleKey: 'producer', - tenantId: 'tenant-1', - tenantSequenceNo: 7n, - topic: 'producer.message-created', - workerKey: 'consumer.logger', - }, - payload: { messageKey: 'message-1' }, - }); - }) + expect(result.succeeded).toBe(1); + expect(controlled.probe.completed).toEqual([selected]); + expect(controlled.probe.failed).toEqual([]); + expect(observed).toEqual({ + context: { + attemptNumber: 1, + claimId: 'runtime:claim-1', + correlationId: 'correlation-1', + deliveryId: 'delivery-1', + domainEventId: 'event-1', + messageId: 'message-1', + producerModuleKey: 'producer', + tenantId: 'tenant-1', + tenantSequenceNo: 7n, + topic: 'producer.message-created', + workerKey: 'consumer.logger', + }, + payload: { messageKey: 'message-1' }, + }); + }), ); -it.effect( - 'runs a worker with Effect services provided by its owning MicroVertical host', - () => - Effect.gen(function* ownerProvidedServices() { - const selected = { ...claim(), workerKey: 'consumer.layered-logger' }; - const controlled = repository({ claims: [selected] }); - const observed: string[] = []; - const registration = defineOutboxWorker( - { - consumerModuleKey: 'consumer', - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.layered-logger', - moduleKey: 'consumer', - role: 'worker', - }), - leaseDurationMs: 30_000, - payloadSchema: Schema.Struct({ messageKey: MessageKey }), - producerModuleKey: 'producer', - retryPolicy, - topic: 'producer.message-created', - workerKey: 'consumer.layered-logger', - }, - (_payload, context) => - TestWorkerDependency.pipe( - Effect.flatMap(({ record }) => - Effect.sync(() => record(context.messageId)) - ) - ) +it.effect('runs a worker with Effect services provided by its owning MicroVertical host', () => + Effect.gen(function* ownerProvidedServices() { + const selected = { ...claim(), workerKey: 'consumer.layered-logger' }; + const controlled = repository({ claims: [selected] }); + const observed: string[] = []; + const registration = defineOutboxWorker( + { + consumerModuleKey: 'consumer', + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.layered-logger', + moduleKey: 'consumer', + role: 'worker', + }), + leaseDurationMs: 30_000, + payloadSchema: Schema.Struct({ messageKey: MessageKey }), + producerModuleKey: 'producer', + retryPolicy, + topic: 'producer.message-created', + workerKey: 'consumer.layered-logger', + }, + (_payload, context) => + TestWorkerDependency.pipe(Effect.flatMap(({ record }) => Effect.sync(() => record(context.messageId)))), + ); + + const result = yield* makeOutboxRuntime(controlled.service) + .runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [registration.descriptor], + }) + .pipe( + Effect.provideService(TestWorkerDependency, { + record: (messageId) => observed.push(messageId), + }), ); - const result = yield* makeOutboxRuntime(controlled.service) - .runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [registration.descriptor], - }) - .pipe( - Effect.provideService(TestWorkerDependency, { - record: (messageId) => observed.push(messageId), - }) - ); - - expect(result.succeeded).toBe(1); - expect(observed).toEqual(['message-1']); - }) + expect(result.succeeded).toBe(1); + expect(observed).toEqual(['message-1']); + }), ); -it.effect( - 'records decode failures as retries without calling the handler or completion', - () => - Effect.gen(function* decodeFailure() { - const controlled = repository({ - claims: [claim(1, { messageKey: 42 })], - failureStatuses: ['pending'], - }); - let calls = 0; - - const result = yield* run( - controlled.service, - worker(() => Effect.sync(() => (calls += 1))) - ); +it.effect('records decode failures as retries without calling the handler or completion', () => + Effect.gen(function* decodeFailure() { + const controlled = repository({ + claims: [claim(1, { messageKey: 42 })], + failureStatuses: ['pending'], + }); + let calls = 0; - expect(calls).toBe(0); - expect(result.failed).toBe(1); - expect(result.retried).toBe(1); - expect(controlled.probe.completed).toEqual([]); - expect(controlled.probe.failed[0]?.message).toBe( - 'The Outbox Message payload does not match its published schema' - ); - }) -); + const result = yield* run( + controlled.service, + worker(() => Effect.sync(() => (calls += 1))), + ); -it.effect( - 'classifies declared failures, defects, retry exhaustion, and never completes them', - () => - Effect.gen(function* failureClassification() { - const declared = repository({ - claims: [claim()], - failureStatuses: ['pending'], - }); - const declaredResult = yield* run( - declared.service, - worker(() => - Effect.fail(new TestHandlerFailure({ reason: 'secret typed detail' })) - ) - ); - expect(declaredResult.retried).toBe(1); - expect(declared.probe.failed[0]?.message).toBe( - 'The Outbox Worker handler returned a declared failure' - ); + expect(calls).toBe(0); + expect(result.failed).toBe(1); + expect(result.retried).toBe(1); + expect(controlled.probe.completed).toEqual([]); + expect(controlled.probe.failed[0]?.message).toBe('The Outbox Message payload does not match its published schema'); + }), +); - const defect = repository({ - claims: [claim(2)], - failureStatuses: ['dead'], - }); - const defectResult = yield* run( - defect.service, - worker(() => - Effect.die(new Error('database password must not be stored')) - ) - ); - expect(defectResult.dead).toBe(1); - expect(defect.probe.failed[0]?.message).toBe( - 'The Outbox Worker handler failed unexpectedly' - ); - expect(defect.probe.failed[0]?.message ?? '').not.toMatch(/password/u); - expect(declared.probe.completed).toEqual([]); - expect(defect.probe.completed).toEqual([]); - }) +it.effect('classifies declared failures, defects, retry exhaustion, and never completes them', () => + Effect.gen(function* failureClassification() { + const declared = repository({ + claims: [claim()], + failureStatuses: ['pending'], + }); + const declaredResult = yield* run( + declared.service, + worker(() => Effect.fail(new TestHandlerFailure({ reason: 'secret typed detail' }))), + ); + expect(declaredResult.retried).toBe(1); + expect(declared.probe.failed[0]?.message).toBe('The Outbox Worker handler returned a declared failure'); + + const defect = repository({ + claims: [claim(2)], + failureStatuses: ['dead'], + }); + const defectResult = yield* run( + defect.service, + worker(() => Effect.die(new Error('database password must not be stored'))), + ); + expect(defectResult.dead).toBe(1); + expect(defect.probe.failed[0]?.message).toBe('The Outbox Worker handler failed unexpectedly'); + expect(defect.probe.failed[0]?.message ?? '').not.toMatch(/password/u); + expect(declared.probe.completed).toEqual([]); + expect(defect.probe.completed).toEqual([]); + }), ); -it.effect( - 'surfaces stale-claim finalization and leaves checkpoint responsibility with the repository', - () => - Effect.gen(function* staleClaimFinalization() { - const controlled = repository({ - claims: [claim()], - completeError: new OutboxClaimLostError({ - code: 'outbox_claim_lost', - reason: 'stale test claim', - }), - }); +it.effect('surfaces stale-claim finalization and leaves checkpoint responsibility with the repository', () => + Effect.gen(function* staleClaimFinalization() { + const controlled = repository({ + claims: [claim()], + completeError: new OutboxClaimLostError({ + code: 'outbox_claim_lost', + reason: 'stale test claim', + }), + }); - const error = yield* Effect.flip(run(controlled.service)); - expect(Schema.is(OutboxClaimLostError)(error)).toBe(true); - expect(controlled.probe.failed).toEqual([]); - }) + const error = yield* Effect.flip(run(controlled.service)); + expect(Schema.is(OutboxClaimLostError)(error)).toBe(true); + expect(controlled.probe.failed).toEqual([]); + }), ); diff --git a/app/packages/core-runtime/tests/unit/permission-client.test.ts b/app/packages/core-runtime/tests/unit/permission-client.test.ts index 0812ea01c..110456392 100644 --- a/app/packages/core-runtime/tests/unit/permission-client.test.ts +++ b/app/packages/core-runtime/tests/unit/permission-client.test.ts @@ -10,9 +10,7 @@ import { } from '../../src/permissions/client.ts'; type PermissionRpcError = NonNullable< - Parameters< - NonNullable[3]> - >[0] + Parameters[3]>>[0] >; const configuration = { @@ -23,19 +21,14 @@ const configuration = { it.effect('permission RPCs are lazy and execute again on each Effect run', () => Effect.gen(function* checksLazyRpcExecution() { - yield* Effect.addFinalizer(() => - Effect.sync(() => rstest.restoreAllMocks()) - ); + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); const check = rstest .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') .mockImplementation((request, metadata) => { if (Predicate.isFunction(metadata)) { metadata(null, v1.CheckPermissionResponse.create({})); } - return rstest.fn()( - request, - metadata - ); + return rstest.fn()(request, metadata); }); const bulk = rstest .spyOn(v1.PermissionsServiceClient.prototype, 'checkBulkPermissions') @@ -43,16 +36,11 @@ it.effect('permission RPCs are lazy and execute again on each Effect run', () => if (Predicate.isFunction(metadata)) { metadata(null, v1.CheckBulkPermissionsResponse.create({})); } - return rstest.fn()( - request, - metadata - ); + return rstest.fn()(request, metadata); }); const client = yield* Effect.acquireRelease( - Effect.sync(() => - createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS) - ), - (acquiredClient) => Effect.sync(() => acquiredClient.close()) + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), ); const request = v1.CheckPermissionRequest.create({}); const bulkRequest = v1.CheckBulkPermissionsRequest.create({}); @@ -68,80 +56,52 @@ it.effect('permission RPCs are lazy and execute again on each Effect run', () => expect(bulk.mock.calls.length).toBe(2); expect(check.mock.calls[0]?.[0]).toBe(request); expect(bulk.mock.calls[0]?.[0]).toBe(bulkRequest); - }) + }), ); -it.effect( - 'SDK rejections become typed permission failures without leaking diagnostics', - () => - Effect.gen(function* checksTypedSdkFailure() { - yield* Effect.addFinalizer(() => - Effect.sync(() => rstest.restoreAllMocks()) - ); - const cause = Object.assign(new Error('private transport diagnostic'), { - code: 13, - details: 'private transport diagnostic', - metadata: rstest.fn<() => PermissionRpcError['metadata']>()(), - }); - rstest - .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') - .mockImplementation((request, metadata) => { - if (Predicate.isFunction(metadata)) { - metadata(cause); - } - return rstest.fn()( - request, - metadata - ); - }); - const client = yield* Effect.acquireRelease( - Effect.sync(() => - createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS) - ), - (acquiredClient) => Effect.sync(() => acquiredClient.close()) - ); - const failure = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})) - ); - expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); - expect(failure.reason.includes(cause.message)).toBe(false); - expect(Object.getOwnPropertyDescriptor(failure, 'cause')?.value).toBe( - cause - ); - }) +it.effect('SDK rejections become typed permission failures without leaking diagnostics', () => + Effect.gen(function* checksTypedSdkFailure() { + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); + const cause = Object.assign(new Error('private transport diagnostic'), { + code: 13, + details: 'private transport diagnostic', + metadata: rstest.fn<() => PermissionRpcError['metadata']>()(), + }); + rstest.spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission').mockImplementation((request, metadata) => { + if (Predicate.isFunction(metadata)) { + metadata(cause); + } + return rstest.fn()(request, metadata); + }); + const client = yield* Effect.acquireRelease( + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), + ); + const failure = yield* Effect.flip(client.checkPermission(v1.CheckPermissionRequest.create({}))); + expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); + expect(failure.reason.includes(cause.message)).toBe(false); + expect(Object.getOwnPropertyDescriptor(failure, 'cause')?.value).toBe(cause); + }), ); -it.effect( - 'an SDK call that never replies is bounded by the permission deadline', - () => - Effect.gen(function* checksPermissionDeadline() { - yield* Effect.addFinalizer(() => - Effect.sync(() => rstest.restoreAllMocks()) - ); - rstest - .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') - .mockImplementation((request, metadata) => - rstest.fn()( - request, - metadata - ) - ); - const client = yield* Effect.acquireRelease( - Effect.sync(() => - createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS) - ), - (acquiredClient) => Effect.sync(() => acquiredClient.close()) +it.effect('an SDK call that never replies is bounded by the permission deadline', () => + Effect.gen(function* checksPermissionDeadline() { + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); + rstest + .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') + .mockImplementation((request, metadata) => + rstest.fn()(request, metadata), ); - const fiber = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})) - ).pipe(Effect.forkChild); - yield* TestClock.adjust(SPICEDB_CHECK_TIMEOUT_MS); - const failure = yield* Fiber.join(fiber); - expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); - expect( - Cause.isTimeoutError( - Object.getOwnPropertyDescriptor(failure, 'cause')?.value - ) - ).toBe(true); - }) + const client = yield* Effect.acquireRelease( + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), + ); + const fiber = yield* Effect.flip(client.checkPermission(v1.CheckPermissionRequest.create({}))).pipe( + Effect.forkChild, + ); + yield* TestClock.adjust(SPICEDB_CHECK_TIMEOUT_MS); + const failure = yield* Fiber.join(fiber); + expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); + expect(Cause.isTimeoutError(Object.getOwnPropertyDescriptor(failure, 'cause')?.value)).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/unit/pool-configuration.test.ts b/app/packages/core-runtime/tests/unit/pool-configuration.test.ts index 4e0204393..f32f777df 100644 --- a/app/packages/core-runtime/tests/unit/pool-configuration.test.ts +++ b/app/packages/core-runtime/tests/unit/pool-configuration.test.ts @@ -1,31 +1,20 @@ import { Effect, Redacted, Predicate } from 'effect'; import { expect, it } from 'effect-rstest'; -import { - DEFAULT_DATABASE_POOL_DEADLINES, - configureDatabasePool, -} from '../../src/db/pool-configuration.ts'; +import { DEFAULT_DATABASE_POOL_DEADLINES, configureDatabasePool } from '../../src/db/pool-configuration.ts'; const runtimeUrl = 'postgresql://runtime:secret@localhost:5432/ontos'; -it.effect( - 'uses acquisition and statement deadlines without opting into a lock deadline', - () => - Effect.gen(function* verifyDefaults() { - const connectionString = Redacted.make(`${runtimeUrl}?sslmode=require`); - const configuration = yield* configureDatabasePool(connectionString); +it.effect('uses acquisition and statement deadlines without opting into a lock deadline', () => + Effect.gen(function* verifyDefaults() { + const connectionString = Redacted.make(`${runtimeUrl}?sslmode=require`); + const configuration = yield* configureDatabasePool(connectionString); - expect(configuration.connectionTimeoutMillis).toBe( - DEFAULT_DATABASE_POOL_DEADLINES.connectionTimeoutMillis - ); - expect(configuration.statement_timeout).toBe( - DEFAULT_DATABASE_POOL_DEADLINES.statement_timeout - ); - expect(Object.hasOwn(configuration, 'lock_timeout')).toBe(false); - expect(configuration.connectionString).toBe( - `${runtimeUrl}?sslmode=require` - ); - }) + expect(configuration.connectionTimeoutMillis).toBe(DEFAULT_DATABASE_POOL_DEADLINES.connectionTimeoutMillis); + expect(configuration.statement_timeout).toBe(DEFAULT_DATABASE_POOL_DEADLINES.statement_timeout); + expect(Object.hasOwn(configuration, 'lock_timeout')).toBe(false); + expect(configuration.connectionString).toBe(`${runtimeUrl}?sslmode=require`); + }), ); it.effect('includes an explicitly opted-in lock deadline', () => @@ -36,50 +25,38 @@ it.effect('includes an explicitly opted-in lock deadline', () => }); expect(configuration.lock_timeout).toBe(250); - }) + }), ); -it.effect( - 'rejects URL deadline overrides with a typed configuration failure', - () => - Effect.forEach( - [ - 'connectionTimeoutMillis=1', - 'connect_timeout=1', - 'lock_timeout=1', - 'statement_timeout=1', - 'query_timeout=1', - 'options=-c%20statement_timeout%3D1', - ], - (parameter) => - Effect.gen(function* verifyParameter() { - const connectionString = Redacted.make(`${runtimeUrl}?${parameter}`); - const error = yield* Effect.flip( - configureDatabasePool(connectionString) - ); - expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe( - true - ); - expect(error.reason).toBe( - 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines' - ); - }), - { concurrency: 'unbounded' } - ) +it.effect('rejects URL deadline overrides with a typed configuration failure', () => + Effect.forEach( + [ + 'connectionTimeoutMillis=1', + 'connect_timeout=1', + 'lock_timeout=1', + 'statement_timeout=1', + 'query_timeout=1', + 'options=-c%20statement_timeout%3D1', + ], + (parameter) => + Effect.gen(function* verifyParameter() { + const connectionString = Redacted.make(`${runtimeUrl}?${parameter}`); + const error = yield* Effect.flip(configureDatabasePool(connectionString)); + expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe(true); + expect(error.reason).toBe( + 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', + ); + }), + { concurrency: 'unbounded' }, + ), ); -it.effect( - 'rejects invalid deadline values with a typed configuration failure', - () => - Effect.gen(function* verifyInvalidDeadline() { - const connectionString = Redacted.make(runtimeUrl); - const error = yield* Effect.flip( - configureDatabasePool(connectionString, { statement_timeout: 0 }) - ); +it.effect('rejects invalid deadline values with a typed configuration failure', () => + Effect.gen(function* verifyInvalidDeadline() { + const connectionString = Redacted.make(runtimeUrl); + const error = yield* Effect.flip(configureDatabasePool(connectionString, { statement_timeout: 0 })); - expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe(true); - expect(error.reason).toBe( - 'Database pool deadlines must be positive 32-bit millisecond integers' - ); - }) + expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe(true); + expect(error.reason).toBe('Database pool deadlines must be positive 32-bit millisecond integers'); + }), ); diff --git a/app/packages/core-runtime/tests/unit/principal-management.test.ts b/app/packages/core-runtime/tests/unit/principal-management.test.ts index 95d2b7dfc..a89ef3aff 100644 --- a/app/packages/core-runtime/tests/unit/principal-management.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-management.test.ts @@ -18,8 +18,7 @@ const tenantId = '10000000-0000-4000-8000-000000000001'; const principalId = '20000000-0000-4000-8000-000000000001'; const authBindingId = '30000000-0000-4000-8000-000000000001'; -const unconfigured = (operation: string) => - Effect.die(`${operation} is not configured in this test`); +const unconfigured = (operation: string) => Effect.die(`${operation} is not configured in this test`); const repositoryDefaults: PrincipalManagementPersistence = { createPrincipal: () => unconfigured('createPrincipal'), insertApiKeyBinding: () => unconfigured('insertApiKeyBinding'), @@ -29,23 +28,14 @@ const repositoryDefaults: PrincipalManagementPersistence = { updateApiKeyBindingStatus: () => unconfigured('updateApiKeyBindingStatus'), updatePrincipalStatus: () => unconfigured('updatePrincipalStatus'), }; -const repository = ( - overrides: Partial -): PrincipalManagementRepositoryService => +const repository = (overrides: Partial): PrincipalManagementRepositoryService => principalManagementRepositoryFromPersistence({ ...repositoryDefaults, ...overrides, }); -type OptionValue = - Outcome extends Option.Option ? Value : never; -type PrincipalRecord = OptionValue< - Effect.Success> ->; -type ApiKeyBindingRecord = OptionValue< - Effect.Success< - ReturnType - > ->; +type OptionValue = Outcome extends Option.Option ? Value : never; +type PrincipalRecord = OptionValue>>; +type ApiKeyBindingRecord = OptionValue>>; const selectingPrincipal = (record: PrincipalRecord | undefined) => repository({ loadPrincipal: () => Effect.succeed(Option.fromNullishOr(record)), @@ -54,9 +44,7 @@ const selectingBinding = (record: ApiKeyBindingRecord | undefined) => repository({ loadApiKeyBinding: () => Effect.succeed(Option.fromNullishOr(record)), }); -const repositoryForSupportParticipants = ( - results: readonly (readonly { readonly authBindingId: string }[])[] -) => { +const repositoryForSupportParticipants = (results: readonly (readonly { readonly authBindingId: string }[])[]) => { let call = 0; return repository({ loadSupportBindings: () => @@ -69,186 +57,162 @@ const repositoryForSupportParticipants = ( }; const provideRepository = (service: PrincipalManagementRepositoryService) => Effect.provideService(PrincipalManagementRepository, service); -it.effect( - 'rejects human principal administration and managed keys targeting humans', - () => - Effect.gen(function* rejectsHumanPrincipalAdministration() { - const transaction = selectingPrincipal({ - kind: 'human', - status: 'active', - }); - const principalError = yield* Effect.flip( - changePrincipalStatus({ - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - reason: 'Offboarding', - tenantId, - }).pipe(provideRepository(transaction)) - ); - const bindingError = yield* Effect.flip( - bindApiKey({ - managed: true, - principalId, - providerSubjectId: 'provider-key-id', - tenantId, - }).pipe(provideRepository(transaction)) - ); - - expect( - Predicate.isTagged(principalError, 'IdentityTargetInvalidError') - ).toBe(true); - expect( - Predicate.isTagged(bindingError, 'IdentityTargetInvalidError') - ).toBe(true); - }) -); -it.effect( - 'enforces expected state, terminal revocation, and revocation reasons', - () => - Effect.gen(function* enforcesBindingLifecycle() { - const conflictError = yield* Effect.flip( - setApiKeyBindingStatus({ - authBindingId, - expectedStatus: 'active', - managed: true, - newStatus: 'revoked', - principalId, - reason: 'Rotate', - tenantId, - }).pipe( - provideRepository( - selectingBinding({ - bindingStatus: 'disabled', - principalKind: 'service', - principalStatus: 'active', - }) - ) - ) - ); - const terminalError = yield* Effect.flip( - setApiKeyBindingStatus({ - authBindingId, - expectedStatus: 'revoked', - managed: true, - newStatus: 'active', - principalId, - tenantId, - }).pipe( - provideRepository( - selectingBinding({ - bindingStatus: 'revoked', - principalKind: 'service', - principalStatus: 'active', - }) - ) - ) - ); - const reasonError = yield* Effect.flip( - setApiKeyBindingStatus({ - authBindingId, - expectedStatus: 'active', - managed: true, - newStatus: 'revoked', - principalId, - reason: ' ', - tenantId, - }).pipe( - provideRepository( - selectingBinding({ - bindingStatus: 'active', - principalKind: 'service', - principalStatus: 'active', - }) - ) - ) - ); +it.effect('rejects human principal administration and managed keys targeting humans', () => + Effect.gen(function* rejectsHumanPrincipalAdministration() { + const transaction = selectingPrincipal({ + kind: 'human', + status: 'active', + }); + const principalError = yield* Effect.flip( + changePrincipalStatus({ + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + reason: 'Offboarding', + tenantId, + }).pipe(provideRepository(transaction)), + ); + const bindingError = yield* Effect.flip( + bindApiKey({ + managed: true, + principalId, + providerSubjectId: 'provider-key-id', + tenantId, + }).pipe(provideRepository(transaction)), + ); - expect( - Predicate.isTagged(conflictError, 'IdentityLifecycleConflictError') - ).toBe(true); - expect( - Predicate.isTagged(terminalError, 'IdentityLifecycleConflictError') - ).toBe(true); - expect( - Predicate.isTagged(reasonError, 'IdentityTargetInvalidError') - ).toBe(true); - }) + expect(Predicate.isTagged(principalError, 'IdentityTargetInvalidError')).toBe(true); + expect(Predicate.isTagged(bindingError, 'IdentityTargetInvalidError')).toBe(true); + }), ); -it.effect( - 'rejects managed binding transitions for human or inactive targets', - () => - Effect.gen(function* rejectsIneligibleBindingTargets() { - const records = [ - { - bindingStatus: 'active', - principalKind: 'human', - principalStatus: 'active', - }, - { - bindingStatus: 'active', - principalKind: 'service', - principalStatus: 'disabled', - }, - ] satisfies readonly ApiKeyBindingRecord[]; - yield* Effect.forEach( - records, - (record) => - Effect.gen(function* rejectsIneligibleBindingTarget() { - const error = yield* Effect.flip( - setApiKeyBindingStatus({ - authBindingId, - expectedStatus: 'active', - managed: true, - newStatus: 'disabled', - principalId, - tenantId, - }).pipe(provideRepository(selectingBinding(record))) - ); - expect( - Predicate.isTagged(error, 'IdentityTargetInvalidError') - ).toBe(true); +it.effect('enforces expected state, terminal revocation, and revocation reasons', () => + Effect.gen(function* enforcesBindingLifecycle() { + const conflictError = yield* Effect.flip( + setApiKeyBindingStatus({ + authBindingId, + expectedStatus: 'active', + managed: true, + newStatus: 'revoked', + principalId, + reason: 'Rotate', + tenantId, + }).pipe( + provideRepository( + selectingBinding({ + bindingStatus: 'disabled', + principalKind: 'service', + principalStatus: 'active', }), - { concurrency: 1 } - ); - }) -); -it.effect( - 'binds only eligible active self and managed principal kinds without secret material', - () => - Effect.gen(function* bindsEligiblePrincipal() { - let inserted: - | Parameters[0] - | undefined; - const transaction = repository({ - insertApiKeyBinding: (value) => - Effect.sync(() => { - inserted = value; - return Option.some({ authBindingId }); + ), + ), + ); + const terminalError = yield* Effect.flip( + setApiKeyBindingStatus({ + authBindingId, + expectedStatus: 'revoked', + managed: true, + newStatus: 'active', + principalId, + tenantId, + }).pipe( + provideRepository( + selectingBinding({ + bindingStatus: 'revoked', + principalKind: 'service', + principalStatus: 'active', }), - loadPrincipal: () => - Effect.succeedSome({ kind: 'service', status: 'active' }), - }); - const result = yield* bindApiKey({ + ), + ), + ); + const reasonError = yield* Effect.flip( + setApiKeyBindingStatus({ + authBindingId, + expectedStatus: 'active', managed: true, + newStatus: 'revoked', principalId, - providerSubjectId: 'provider-key-id', + reason: ' ', tenantId, - }).pipe(provideRepository(transaction)); + }).pipe( + provideRepository( + selectingBinding({ + bindingStatus: 'active', + principalKind: 'service', + principalStatus: 'active', + }), + ), + ), + ); - expect(result).toEqual({ authBindingId, status: 'active' }); - expect(inserted?.providerSubjectId).toBe('provider-key-id'); - expect('key' in (inserted ?? {})).toBe(false); - expect('secret' in (inserted ?? {})).toBe(false); - expect('hash' in (inserted ?? {})).toBe(false); - }) + expect(Predicate.isTagged(conflictError, 'IdentityLifecycleConflictError')).toBe(true); + expect(Predicate.isTagged(terminalError, 'IdentityLifecycleConflictError')).toBe(true); + expect(Predicate.isTagged(reasonError, 'IdentityTargetInvalidError')).toBe(true); + }), +); +it.effect('rejects managed binding transitions for human or inactive targets', () => + Effect.gen(function* rejectsIneligibleBindingTargets() { + const records = [ + { + bindingStatus: 'active', + principalKind: 'human', + principalStatus: 'active', + }, + { + bindingStatus: 'active', + principalKind: 'service', + principalStatus: 'disabled', + }, + ] satisfies readonly ApiKeyBindingRecord[]; + yield* Effect.forEach( + records, + (record) => + Effect.gen(function* rejectsIneligibleBindingTarget() { + const error = yield* Effect.flip( + setApiKeyBindingStatus({ + authBindingId, + expectedStatus: 'active', + managed: true, + newStatus: 'disabled', + principalId, + tenantId, + }).pipe(provideRepository(selectingBinding(record))), + ); + expect(Predicate.isTagged(error, 'IdentityTargetInvalidError')).toBe(true); + }), + { concurrency: 1 }, + ); + }), +); +it.effect('binds only eligible active self and managed principal kinds without secret material', () => + Effect.gen(function* bindsEligiblePrincipal() { + let inserted: Parameters[0] | undefined; + const transaction = repository({ + insertApiKeyBinding: (value) => + Effect.sync(() => { + inserted = value; + return Option.some({ authBindingId }); + }), + loadPrincipal: () => Effect.succeedSome({ kind: 'service', status: 'active' }), + }); + const result = yield* bindApiKey({ + managed: true, + principalId, + providerSubjectId: 'provider-key-id', + tenantId, + }).pipe(provideRepository(transaction)); + + expect(result).toEqual({ authBindingId, status: 'active' }); + expect(inserted?.providerSubjectId).toBe('provider-key-id'); + expect('key' in (inserted ?? {})).toBe(false); + expect('secret' in (inserted ?? {})).toBe(false); + expect('hash' in (inserted ?? {})).toBe(false); + }), ); it.effect('maps an existing API key binding to a lifecycle conflict', () => Effect.gen(function* mapsExistingBindingToConflict() { const transaction = repository({ insertApiKeyBinding: () => Effect.succeedNone, - loadPrincipal: () => - Effect.succeedSome({ kind: 'service', status: 'active' }), + loadPrincipal: () => Effect.succeedSome({ kind: 'service', status: 'active' }), }); const error = yield* Effect.flip( @@ -257,48 +221,36 @@ it.effect('maps an existing API key binding to a lifecycle conflict', () => principalId, providerSubjectId: 'duplicate-provider-key-id', tenantId, - }).pipe(provideRepository(transaction)) + }).pipe(provideRepository(transaction)), ); - expect(Predicate.isTagged(error, 'IdentityLifecycleConflictError')).toBe( - true - ); - }) + expect(Predicate.isTagged(error, 'IdentityLifecycleConflictError')).toBe(true); + }), ); -it.effect( - 'requires exactly one active tenant-local user binding for both impersonation participants', - () => - Effect.gen(function* validatesSupportParticipants() { - const original = [{ authBindingId }]; - const target = [ - { authBindingId: '30000000-0000-4000-8000-000000000002' }, - ]; - const input: Parameters[0] = { - checkpoint: 'requested', - originalAuthBindingId: authBindingId, - originalPrincipalId: principalId, - targetPrincipalId: '20000000-0000-4000-8000-000000000002', - tenantId, - }; +it.effect('requires exactly one active tenant-local user binding for both impersonation participants', () => + Effect.gen(function* validatesSupportParticipants() { + const original = [{ authBindingId }]; + const target = [{ authBindingId: '30000000-0000-4000-8000-000000000002' }]; + const input: Parameters[0] = { + checkpoint: 'requested', + originalAuthBindingId: authBindingId, + originalPrincipalId: principalId, + targetPrincipalId: '20000000-0000-4000-8000-000000000002', + tenantId, + }; - yield* validateSupportImpersonation(input).pipe( - provideRepository(repositoryForSupportParticipants([original, target])) - ); - const error = yield* Effect.flip( - validateSupportImpersonation(input).pipe( - provideRepository(repositoryForSupportParticipants([original, []])) - ) - ); + yield* validateSupportImpersonation(input).pipe( + provideRepository(repositoryForSupportParticipants([original, target])), + ); + const error = yield* Effect.flip( + validateSupportImpersonation(input).pipe(provideRepository(repositoryForSupportParticipants([original, []]))), + ); - expect(Predicate.isTagged(error, 'IdentityTargetInvalidError')).toBe( - true - ); + expect(Predicate.isTagged(error, 'IdentityTargetInvalidError')).toBe(true); - yield* validateSupportImpersonation({ - ...input, - checkpoint: 'stopped', - }).pipe( - provideRepository(repositoryForSupportParticipants([original, target])) - ); - }) + yield* validateSupportImpersonation({ + ...input, + checkpoint: 'stopped', + }).pipe(provideRepository(repositoryForSupportParticipants([original, target]))); + }), ); diff --git a/app/packages/core-runtime/tests/unit/principal-resolver.test.ts b/app/packages/core-runtime/tests/unit/principal-resolver.test.ts index ebb3756f3..8db0e5daa 100644 --- a/app/packages/core-runtime/tests/unit/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-resolver.test.ts @@ -14,9 +14,7 @@ import { makeTestDatabase } from '../support/database.ts'; const activeRecord: PrincipalResolutionRecord = { authBindingId: 'binding-1', - bindingCreatedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-01-01T00:00:00.000Z') - ), + bindingCreatedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), bindingRevokedAt: null, bindingStatus: 'active', displayName: 'Ada Lovelace', @@ -33,9 +31,7 @@ it.effect('lists safe eligible tenants by name and tenant ID', () => activeRecord, { ...activeRecord, - bindingCreatedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-02-01T00:00:00.000Z') - ), + bindingCreatedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-02-01T00:00:00.000Z')), displayName: 'Grace Hopper', principalId: 'principal-2', tenantId: 'tenant-2', @@ -76,13 +72,11 @@ it.effect('lists safe eligible tenants by name and tenant ID', () => { name: 'Alpha tenant', tenantId: 'tenant-3' }, { name: 'Zeta tenant', tenantId: 'tenant-1' }, ]); - }) + }), ); it.effect('lists and resolves one active tenant binding', () => Effect.gen(function* listsAndResolvesActiveTenant() { - expect(yield* classifyAvailableTenants([activeRecord])).toEqual([ - { name: 'Zeta tenant', tenantId: 'tenant-1' }, - ]); + expect(yield* classifyAvailableTenants([activeRecord])).toEqual([{ name: 'Zeta tenant', tenantId: 'tenant-1' }]); expect(yield* classifyDefaultPrincipal([activeRecord])).toEqual({ authBindingId: 'binding-1', displayName: 'Ada Lovelace', @@ -90,47 +84,41 @@ it.effect('lists and resolves one active tenant binding', () => principalKind: 'human', tenantId: 'tenant-1', }); - expect( - yield* classifySelectedPrincipal([activeRecord], 'tenant-1') - ).toEqual({ + expect(yield* classifySelectedPrincipal([activeRecord], 'tenant-1')).toEqual({ authBindingId: 'binding-1', displayName: 'Ada Lovelace', principalId: 'principal-1', principalKind: 'human', tenantId: 'tenant-1', }); - }) + }), ); -it.effect( - 'chooses the oldest eligible binding and breaks creation ties by tenant ID', - () => - Effect.gen(function* choosesOldestBinding() { - const result = yield* classifyDefaultPrincipal([ - activeRecord, - { - ...activeRecord, - displayName: 'Tie winner', - principalId: 'principal-0', - tenantId: 'tenant-0', - }, - { - ...activeRecord, - bindingCreatedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-02-01T00:00:00.000Z') - ), - principalId: 'principal-2', - tenantId: 'tenant-2', - }, - ]); - - expect(result).toEqual({ - authBindingId: 'binding-1', +it.effect('chooses the oldest eligible binding and breaks creation ties by tenant ID', () => + Effect.gen(function* choosesOldestBinding() { + const result = yield* classifyDefaultPrincipal([ + activeRecord, + { + ...activeRecord, displayName: 'Tie winner', principalId: 'principal-0', - principalKind: 'human', tenantId: 'tenant-0', - }); - }) + }, + { + ...activeRecord, + bindingCreatedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-02-01T00:00:00.000Z')), + principalId: 'principal-2', + tenantId: 'tenant-2', + }, + ]); + + expect(result).toEqual({ + authBindingId: 'binding-1', + displayName: 'Tie winner', + principalId: 'principal-0', + principalKind: 'human', + tenantId: 'tenant-0', + }); + }), ); it.effect('resolves only the exact eligible selected tenant', () => Effect.gen(function* resolvesExactTenant() { @@ -140,9 +128,7 @@ it.effect('resolves only the exact eligible selected tenant', () => principalId: 'principal-2', tenantId: 'tenant-2', }; - expect( - yield* classifySelectedPrincipal([activeRecord, selected], 'tenant-2') - ).toEqual({ + expect(yield* classifySelectedPrincipal([activeRecord, selected], 'tenant-2')).toEqual({ authBindingId: 'binding-1', displayName: 'Grace Hopper', principalId: 'principal-2', @@ -151,139 +137,97 @@ it.effect('resolves only the exact eligible selected tenant', () => }); expect( Predicate.isTagged( - yield* Effect.flip( - classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant') - ), - 'PrincipalBindingMissingError' - ) + yield* Effect.flip(classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant')), + 'PrincipalBindingMissingError', + ), ).toBe(true); - }) + }), ); it.effect('rejects Better Auth user bindings to non-human principals', () => Effect.all( - (['service', 'integration', 'agent', 'system'] as const).map( - (principalKind) => - Effect.gen(function* rejectsNonHumanPrincipal() { - const record = { ...activeRecord, principalKind }; - expect( - Predicate.isTagged( - yield* Effect.flip(classifyDefaultPrincipal([record])), - 'PrincipalInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifySelectedPrincipal([record], record.tenantId) - ), - 'PrincipalInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip(classifyAvailableTenants([record])), - 'PrincipalInactiveError' - ) - ).toBe(true); - }) - ) - ) -); -it.effect( - 'resolves exactly one API-key subject for human, service, or integration principals', - () => - Effect.gen(function* resolvesApiKeySubject() { - yield* Effect.all( - (['human', 'service', 'integration'] as const).map((principalKind) => - Effect.gen(function* resolvesPrincipalKind() { - const resolved = yield* classifyApiKeyPrincipal([ - { ...activeRecord, principalKind }, - ]); - expect(resolved.principalKind).toBe(principalKind); - expect(resolved.authBindingId).toBe(activeRecord.authBindingId); - }) - ) - ); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyApiKeyPrincipal([ - activeRecord, - { ...activeRecord, tenantId: 't-2' }, - ]) + (['service', 'integration', 'agent', 'system'] as const).map((principalKind) => + Effect.gen(function* rejectsNonHumanPrincipal() { + const record = { ...activeRecord, principalKind }; + expect( + Predicate.isTagged(yield* Effect.flip(classifyDefaultPrincipal([record])), 'PrincipalInactiveError'), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip(classifySelectedPrincipal([record], record.tenantId)), + 'PrincipalInactiveError', ), - 'PrincipalBindingAmbiguousError' - ) - ).toBe(true); - }) + ).toBe(true); + expect( + Predicate.isTagged(yield* Effect.flip(classifyAvailableTenants([record])), 'PrincipalInactiveError'), + ).toBe(true); + }), + ), + ), ); -it.effect( - 'fails closed for empty, inactive, and duplicate eligible resolver states', - () => - Effect.gen(function* rejectsInvalidResolverStates() { - expect( - Predicate.isTagged( - yield* Effect.flip(classifyAvailableTenants([])), - 'PrincipalBindingMissingError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyAvailableTenants([ - { ...activeRecord, bindingStatus: 'revoked' }, - ]) - ), - 'PrincipalBindingInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyAvailableTenants([ - { - ...activeRecord, - bindingRevokedAt: DateTime.toDateUtc( - DateTime.makeUnsafe('2026-03-01T00:00:00.000Z') - ), - }, - ]) - ), - 'PrincipalBindingInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyAvailableTenants([ - { ...activeRecord, principalStatus: 'disabled' }, - ]) - ), - 'PrincipalInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyAvailableTenants([ - { ...activeRecord, tenantStatus: 'suspended' }, - ]) - ), - 'TenantInactiveError' - ) - ).toBe(true); - expect( - Predicate.isTagged( - yield* Effect.flip( - classifyAvailableTenants([ - activeRecord, - { ...activeRecord, principalId: 'duplicate-principal' }, - ]) - ), - 'PrincipalBindingAmbiguousError' - ) - ).toBe(true); - }) +it.effect('resolves exactly one API-key subject for human, service, or integration principals', () => + Effect.gen(function* resolvesApiKeySubject() { + yield* Effect.all( + (['human', 'service', 'integration'] as const).map((principalKind) => + Effect.gen(function* resolvesPrincipalKind() { + const resolved = yield* classifyApiKeyPrincipal([{ ...activeRecord, principalKind }]); + expect(resolved.principalKind).toBe(principalKind); + expect(resolved.authBindingId).toBe(activeRecord.authBindingId); + }), + ), + ); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }])), + 'PrincipalBindingAmbiguousError', + ), + ).toBe(true); + }), +); +it.effect('fails closed for empty, inactive, and duplicate eligible resolver states', () => + Effect.gen(function* rejectsInvalidResolverStates() { + expect(Predicate.isTagged(yield* Effect.flip(classifyAvailableTenants([])), 'PrincipalBindingMissingError')).toBe( + true, + ); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([{ ...activeRecord, bindingStatus: 'revoked' }])), + 'PrincipalBindingInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([ + { + ...activeRecord, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-03-01T00:00:00.000Z')), + }, + ]), + ), + 'PrincipalBindingInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }])), + 'PrincipalInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }])), + 'TenantInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([activeRecord, { ...activeRecord, principalId: 'duplicate-principal' }]), + ), + 'PrincipalBindingAmbiguousError', + ), + ).toBe(true); + }), ); it.effect('types database failures as resolver unavailability', () => Effect.gen(function* sanitizesResolverDatabaseFailure() { @@ -295,14 +239,14 @@ it.effect('types database failures as resolver unavailability', () => reason: new ConnectionError({ cause: new Error('secret database error'), }), - }) - ) + }), + ), ), - }).listAvailableTenants('subject') + }).listAvailableTenants('subject'), ); if (!Predicate.isTagged(error, 'PrincipalResolverUnavailableError')) { expect.unreachable('Expected resolver unavailability'); } expect(error.reason).not.toMatch(/secret database error/u); - }) + }), ); diff --git a/app/packages/core-runtime/tests/unit/read-definition.test.ts b/app/packages/core-runtime/tests/unit/read-definition.test.ts index f870bb8a8..41502e9d9 100644 --- a/app/packages/core-runtime/tests/unit/read-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/read-definition.test.ts @@ -5,13 +5,9 @@ import { Effect, FileSystem, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { - defineRead, - validateReadDescriptorInput, -} from '../../src/reads/definition.ts'; +import { defineRead, validateReadDescriptorInput } from '../../src/reads/definition.ts'; -const modulePermissionTarget = () => - ({ kind: 'module', moduleId: 'core.shell' }) as const; +const modulePermissionTarget = () => ({ kind: 'module', moduleId: 'core.shell' }) as const; it('defines immutable read metadata while keeping handler and service factory private', () => { const registration = defineRead( { @@ -41,7 +37,7 @@ it('defines immutable read metadata while keeping handler and service factory pr }, () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), () => Effect.succeed(Object.freeze({})), - modulePermissionTarget + modulePermissionTarget, ); expect(Object.keys(registration)).toEqual(['descriptor']); expect(Object.isFrozen(registration.descriptor)).toBe(true); @@ -62,18 +58,11 @@ it('requires an explicit valid owner-scoped read entrypoint', () => { }), legalEntityScope: 'forbidden', owningModuleKey: 'core.shell', - }) + }), ).toThrow(); }); it('supports every governed access kind and rejects forged scope metadata', () => { - for (const accessKind of [ - 'detail', - 'download', - 'export', - 'list', - 'report', - 'search', - ] as const) { + for (const accessKind of ['detail', 'download', 'export', 'list', 'report', 'search'] as const) { expect(() => defineRead( { @@ -101,12 +90,11 @@ it('supports every governed access kind and rejects forged scope metadata', () = resultSchema: Schema.Void, schemaVersion: '1', }, - () => - Effect.succeed({ evidence: { resultCount: 0 }, result: undefined }), + () => Effect.succeed({ evidence: { resultCount: 0 }, result: undefined }), () => Effect.succeed({}), modulePermissionTarget, - accessKind === 'search' ? () => [] : undefined - ) + accessKind === 'search' ? () => [] : undefined, + ), ).not.toThrow(); } expect(() => @@ -123,28 +111,22 @@ it('supports every governed access kind and rejects forged scope metadata', () = }), legalEntityScope: 'implicit', owningModuleKey: 'core.shell', - }) + }), ).toThrow(); }); it.layer(NodeFileSystem.layer)('read package boundary', (suite) => { - suite.effect( - 'keeps low-level read runtime construction and Core schema out of package exports', - () => - Effect.gen(function* readPackageBoundary() { - const fs = yield* FileSystem.FileSystem; - const [indexSource, packageSource] = yield* Effect.all( - [ - fs.readFileString( - fileURLToPath(new URL('../../src/index.ts', import.meta.url)) - ), - fs.readFileString( - fileURLToPath(new URL('../../package.json', import.meta.url)) - ), - ], - { concurrency: 'unbounded' } - ); - expect(indexSource).not.toMatch(/\bmakeReadRuntime,?$/mu); - expect(packageSource).not.toMatch(/"\.\/db\/schema"/u); - }) + suite.effect('keeps low-level read runtime construction and Core schema out of package exports', () => + Effect.gen(function* readPackageBoundary() { + const fs = yield* FileSystem.FileSystem; + const [indexSource, packageSource] = yield* Effect.all( + [ + fs.readFileString(fileURLToPath(new URL('../../src/index.ts', import.meta.url))), + fs.readFileString(fileURLToPath(new URL('../../package.json', import.meta.url))), + ], + { concurrency: 'unbounded' }, + ); + expect(indexSource).not.toMatch(/\bmakeReadRuntime,?$/mu); + expect(packageSource).not.toMatch(/"\.\/db\/schema"/u); + }), ); }); diff --git a/app/packages/core-runtime/tests/unit/read-runtime.test.ts b/app/packages/core-runtime/tests/unit/read-runtime.test.ts index 521fc2875..c9419f37e 100644 --- a/app/packages/core-runtime/tests/unit/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/read-runtime.test.ts @@ -1,13 +1,4 @@ -import { - Cause, - Deferred, - Effect, - Exit, - Fiber, - Option, - Predicate, - Schema, -} from 'effect'; +import { Cause, Deferred, Effect, Exit, Fiber, Option, Predicate, Schema } from 'effect'; /* oxlint-disable sonarjs/use-type-alias, typescript/no-unsafe-type-assertion -- Existing compatibility boundary; expires: 2026-12-31. */ import { expect, it } from 'effect-rstest'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; @@ -22,10 +13,7 @@ import { ReadPermissionDenied, ReadPolicyDenied, } from '../../src/reads/errors.ts'; -import { - makeReadRuntime, - READ_RUNTIME_STAGES, -} from '../../src/reads/runtime.ts'; +import { makeReadRuntime, READ_RUNTIME_STAGES } from '../../src/reads/runtime.ts'; import { makeTestDatabase } from '../support/database.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; @@ -61,13 +49,10 @@ const makeHarness = Effect.fn(function* makeHarness( readonly permissionDecision?: 'allowed' | 'denied' | 'unavailable'; readonly resolvedScope?: typeof scope & { readonly legalEntityId?: string }; readonly resultPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; - readonly resultTenantPermissionDecision?: - | 'allowed' - | 'denied' - | 'unavailable'; + readonly resultTenantPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; readonly tenantPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; readonly transactionEvents?: string[]; - } = {} + } = {}, ) { let evidence = 0; let tenantPermissionChecks = 0; @@ -82,9 +67,7 @@ const makeHarness = Effect.fn(function* makeHarness( }), }); } - const queryHash = values - .filter(Predicate.isString) - .find((value) => /^[\da-f]{64}$/u.test(value)); + const queryHash = values.filter(Predicate.isString).find((value) => /^[\da-f]{64}$/u.test(value)); evidenceRows.push(queryHash === undefined ? {} : { queryHash }); evidence += 1; } @@ -104,8 +87,8 @@ const makeHarness = Effect.fn(function* makeHarness( Effect.ensuring( Effect.sync(() => { options.transactionEvents?.push('transaction_settled'); - }) - ) + }), + ), ); Object.defineProperty(database.executor, 'transaction', { value: transaction, @@ -122,7 +105,7 @@ const makeHarness = Effect.fn(function* makeHarness( legalEntityIds.map((key) => ({ decision: options.permissionDecision ?? ('unavailable' as const), key, - })) + })), ); }, modules: ({ moduleIds }) => @@ -130,7 +113,7 @@ const makeHarness = Effect.fn(function* makeHarness( moduleIds.map((key) => ({ decision: options.permissionDecision ?? ('unavailable' as const), key, - })) + })), ), resources: ({ resources }) => { const [target] = resources; @@ -139,12 +122,9 @@ const makeHarness = Effect.fn(function* makeHarness( } return Effect.succeed( resources.map((resource) => ({ - decision: - options.resultPermissionDecision ?? - options.permissionDecision ?? - ('unavailable' as const), + decision: options.resultPermissionDecision ?? options.permissionDecision ?? ('unavailable' as const), key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, - })) + })), ); }, tenants: ({ permission, tenantIds }) => { @@ -158,15 +138,13 @@ const makeHarness = Effect.fn(function* makeHarness( options.tenantPermissionDecision ?? options.permissionDecision ?? ('unavailable' as const)) - : (options.tenantPermissionDecision ?? - options.permissionDecision ?? - ('unavailable' as const)), + : (options.tenantPermissionDecision ?? options.permissionDecision ?? ('unavailable' as const)), key, - })) + })), ); }, }, - { onStage: (stage) => stages.push(stage) } + { onStage: (stage) => stages.push(stage) }, ); return { evidence: () => evidence, @@ -208,55 +186,49 @@ const registration = (items: readonly string[] = []) => result: context.services.items, }), () => Effect.succeed({ items }), - () => ({ kind: 'module', moduleId: 'core.shell' }) + () => ({ kind: 'module', moduleId: 'core.shell' }), ); -it.effect( - 'runs every gate before the handler and persists evidence before releasing zero results', - () => - Effect.gen(function* migratedTest1() { - const harness = yield* makeHarness(); - const result = yield* harness.runtime.runRead({ - input: {}, - principal: scope, - registration: registration(), - transport: { correlationId: scope.correlationId }, - }); - expect(result).toEqual([]); - expect(harness.evidence()).toBe(1); - expect(harness.stages).toEqual(READ_RUNTIME_STAGES); - }) +it.effect('runs every gate before the handler and persists evidence before releasing zero results', () => + Effect.gen(function* migratedTest1() { + const harness = yield* makeHarness(); + const result = yield* harness.runtime.runRead({ + input: {}, + principal: scope, + registration: registration(), + transport: { correlationId: scope.correlationId }, + }); + expect(result).toEqual([]); + expect(harness.evidence()).toBe(1); + expect(harness.stages).toEqual(READ_RUNTIME_STAGES); + }), ); -it.effect( - 'validates decoded transformed results and preserves their nullable JSON encoding', - () => - Effect.gen(function* migratedTest2() { - const harness = yield* makeHarness(); - const ResultSchema = Schema.Struct({ - value: Schema.OptionFromNullOr(Schema.String), - }); - const transformedRegistration = defineRead( - { ...registration().descriptor, resultSchema: ResultSchema }, - () => - Effect.succeed({ - evidence: { resultCount: 1 }, - result: { value: Option.none() }, - }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); - const result = yield* harness.runtime.runRead({ - input: {}, - principal: scope, - registration: transformedRegistration, - transport: { correlationId: scope.correlationId }, - }); - const encoded = yield* Schema.encodeUnknownEffect( - Schema.toCodecJson(ResultSchema) - )(result); +it.effect('validates decoded transformed results and preserves their nullable JSON encoding', () => + Effect.gen(function* migratedTest2() { + const harness = yield* makeHarness(); + const ResultSchema = Schema.Struct({ + value: Schema.OptionFromNullOr(Schema.String), + }); + const transformedRegistration = defineRead( + { ...registration().descriptor, resultSchema: ResultSchema }, + () => + Effect.succeed({ + evidence: { resultCount: 1 }, + result: { value: Option.none() }, + }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const result = yield* harness.runtime.runRead({ + input: {}, + principal: scope, + registration: transformedRegistration, + transport: { correlationId: scope.correlationId }, + }); + const encoded = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(ResultSchema))(result); - expect(Option.isNone(result.value)).toBe(true); - expect(encoded).toEqual({ value: null }); - }) + expect(Option.isNone(result.value)).toBe(true); + expect(encoded).toEqual({ value: null }); + }), ); it.effect('uses each denying Policy reference own declared HTTP status', () => Effect.all( @@ -264,10 +236,7 @@ it.effect('uses each denying Policy reference own declared HTTP status', () => Effect.gen(function* migratedTest4() { const harness = yield* makeHarness(); const policy = defineGlobalPolicy>>({ - evaluate: () => - Effect.fail( - denyPolicy(`policy-${denialStatus}`, 'Denied by test Policy') - ), + evaluate: () => Effect.fail(denyPolicy(`policy-${denialStatus}`, 'Denied by test Policy')), policyKey: `global.read-policy-${denialStatus}.v1`, }); const governed = defineRead( @@ -279,7 +248,7 @@ it.effect('uses each denying Policy reference own declared HTTP status', () => () => Effect.succeed({}), () => ({ kind: 'module', moduleId: 'core.shell' }), undefined, - [policy] + [policy], ); const error = yield* Effect.flip( harness.runtime.runRead({ @@ -287,183 +256,159 @@ it.effect('uses each denying Policy reference own declared HTTP status', () => principal: scope, registration: governed, transport: { correlationId: scope.correlationId }, - }) + }), ); expect(Predicate.isTagged(error, 'ReadPolicyDenied')).toBe(true); expect( (yield* Schema.decodeEffect(ReadPolicyDenied)( - yield* Effect.filterOrFail( - Effect.succeed(error), - Schema.is(ReadPolicyDenied) - ) - )).httpStatus + yield* Effect.filterOrFail(Effect.succeed(error), Schema.is(ReadPolicyDenied)), + )).httpStatus, ).toBe(denialStatus); - }) + }), ), - { concurrency: 'unbounded' } - ) + { concurrency: 'unbounded' }, + ), ); -it.effect( - 'executes every governed access kind and computes hash-only query evidence inside Core', - () => - Effect.all( - ( - ['detail', 'download', 'export', 'list', 'report', 'search'] as const - ).map((accessKind) => - Effect.gen(function* migratedTest6() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const harness = yield* makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - }); - const governed = defineRead( - { - ...registration().descriptor, - accessKind, - evidencePolicy: { - captureMode: 'hash_only', - policyKey: `core.shell.${accessKind}.hash.v1`, - }, - legalEntityScope: 'required', +it.effect('executes every governed access kind and computes hash-only query evidence inside Core', () => + Effect.all( + (['detail', 'download', 'export', 'list', 'report', 'search'] as const).map((accessKind) => + Effect.gen(function* migratedTest6() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + }); + const governed = defineRead( + { + ...registration().descriptor, + accessKind, + evidencePolicy: { + captureMode: 'hash_only', + policyKey: `core.shell.${accessKind}.hash.v1`, }, - () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - accessKind === 'search' ? () => [] : undefined - ); - expect( - yield* harness.runtime.runRead({ - input: {}, - principal: { - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - legalEntityId, - principalId: scope.principalId, - tenantId: scope.tenantId, - }, - registration: governed, - transport: { correlationId: scope.correlationId }, - }) - ).toEqual([]); - expect(String(harness.evidenceRows()[0]?.queryHash)).toMatch( - /^[\da-f]{64}$/u - ); - }) - ), - { concurrency: 'unbounded' } - ) + legalEntityScope: 'required', + }, + () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + accessKind === 'search' ? () => [] : undefined, + ); + expect( + yield* harness.runtime.runRead({ + input: {}, + principal: { + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + legalEntityId, + principalId: scope.principalId, + tenantId: scope.tenantId, + }, + registration: governed, + transport: { correlationId: scope.correlationId }, + }), + ).toEqual([]); + expect(String(harness.evidenceRows()[0]?.queryHash)).toMatch(/^[\da-f]{64}$/u); + }), + ), + { concurrency: 'unbounded' }, + ), ); -it.effect( - 'rejects invalid input before opening a transaction or executing a handler', - () => - Effect.gen(function* migratedTest7() { - const harness = yield* makeHarness(); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: { unexpected: Symbol('invalid') }, - principal: {}, - registration: registration(), - transport: {}, - }) - ); - expect(Predicate.isTagged(error, 'ReadInputValidationError')).toBe(true); - expect(harness.evidence()).toBe(0); - }) +it.effect('rejects invalid input before opening a transaction or executing a handler', () => + Effect.gen(function* migratedTest7() { + const harness = yield* makeHarness(); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: { unexpected: Symbol('invalid') }, + principal: {}, + registration: registration(), + transport: {}, + }), + ); + expect(Predicate.isTagged(error, 'ReadInputValidationError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), ); -it.effect( - 'preserves typed result-validation failure across transaction rollback', - () => - Effect.gen(function* migratedTest8() { - const harness = yield* makeHarness(); - const invalidRegistration = defineRead( - registration().descriptor, - () => { - const result: string[] = []; - const handlerResult = { evidence: { resultCount: 1 }, result }; - Object.defineProperty(handlerResult, 'result', { value: 42 }); - return Effect.succeed(handlerResult); +it.effect('preserves typed result-validation failure across transaction rollback', () => + Effect.gen(function* migratedTest8() { + const harness = yield* makeHarness(); + const invalidRegistration = defineRead( + registration().descriptor, + () => { + const result: string[] = []; + const handlerResult = { evidence: { resultCount: 1 }, result }; + Object.defineProperty(handlerResult, 'result', { value: 42 }); + return Effect.succeed(handlerResult); + }, + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: { + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + principalId: scope.principalId, + tenantId: scope.tenantId, }, - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: { - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - principalId: scope.principalId, - tenantId: scope.tenantId, - }, - registration: invalidRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'ReadResultValidationError')).toBe(true); - expect(harness.evidence()).toBe(0); - }) + registration: invalidRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadResultValidationError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), ); -it.effect( - 'never releases an allowed result when required evidence persistence fails', - () => - Effect.gen(function* migratedTest9() { - const harness = yield* makeHarness({ failEvidence: true }); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: registration(), - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'ReadEvidencePersistenceError')).toBe( - true - ); - expect(harness.evidence()).toBe(0); - }) +it.effect('never releases an allowed result when required evidence persistence fails', () => + Effect.gen(function* migratedTest9() { + const harness = yield* makeHarness({ failEvidence: true }); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: registration(), + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadEvidencePersistenceError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), ); -it.effect( - 'preserves scoped service-factory unavailability and never invokes the handler', - () => - Effect.gen(function* migratedTest10() { - const harness = yield* makeHarness(); - let handlerCalls = 0; - const unavailableRegistration = defineRead( - registration().descriptor, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => - Effect.fail( - new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'The owner repository scope is temporarily unavailable', - }) - ), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: unavailableRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe( - true - ); - expect(handlerCalls).toBe(0); - expect(harness.evidence()).toBe(0); - }) +it.effect('preserves scoped service-factory unavailability and never invokes the handler', () => + Effect.gen(function* migratedTest10() { + const harness = yield* makeHarness(); + let handlerCalls = 0; + const unavailableRegistration = defineRead( + registration().descriptor, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => + Effect.fail( + new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'The owner repository scope is temporarily unavailable', + }), + ), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: unavailableRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(0); + }), ); -const counterpartyReadRegistration = ( - legalEntityScope: 'required' | 'optional', - onHandler: () => void -) => +const counterpartyReadRegistration = (legalEntityScope: 'required' | 'optional', onHandler: () => void) => defineRead( { ...registration().descriptor, @@ -475,7 +420,7 @@ const counterpartyReadRegistration = ( return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); }, () => Effect.succeed({}), - () => ({ kind: 'legal_entity', permission: 'read_counterparty' }) + () => ({ kind: 'legal_entity', permission: 'read_counterparty' }), ); const counterpartyReadPrincipal = (legalEntityId: string) => ({ @@ -487,303 +432,282 @@ const counterpartyReadPrincipal = (legalEntityId: string) => ({ tenantId: scope.tenantId, }); -it.effect( - 'persists sanitized permission denial and never invokes the private handler', - () => - Effect.gen(function* migratedTest11() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const legalEntityPermissions: (string | undefined)[] = []; - const harness = yield* makeHarness({ - onLegalEntityPermission: (permission) => - legalEntityPermissions.push(permission), - permissionDecision: 'denied', - resolvedScope: { ...scope, legalEntityId }, - }); - let handlerCalls = 0; - const deniedRegistration = counterpartyReadRegistration( - 'required', - () => { - handlerCalls += 1; - } - ); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: counterpartyReadPrincipal(legalEntityId), - registration: deniedRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); - expect(legalEntityPermissions).toEqual(['read_counterparty']); - expect(handlerCalls).toBe(0); - expect(harness.evidence()).toBe(1); - }) +it.effect('persists sanitized permission denial and never invokes the private handler', () => + Effect.gen(function* migratedTest11() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const legalEntityPermissions: (string | undefined)[] = []; + const harness = yield* makeHarness({ + onLegalEntityPermission: (permission) => legalEntityPermissions.push(permission), + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + }); + let handlerCalls = 0; + const deniedRegistration = counterpartyReadRegistration('required', () => { + handlerCalls += 1; + }); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: counterpartyReadPrincipal(legalEntityId), + registration: deniedRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); + expect(legalEntityPermissions).toEqual(['read_counterparty']); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(1); + }), ); -it.effect( - 'fails closed when explicit Counterparty read authority is unavailable', - () => - Effect.gen(function* migratedTest12() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let handlerCalls = 0; - const harness = yield* makeHarness({ - permissionDecision: 'unavailable', - resolvedScope: { ...scope, legalEntityId }, - }); - const counterpartyRead = counterpartyReadRegistration('optional', () => { - handlerCalls += 1; - }); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: counterpartyReadPrincipal(legalEntityId), - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'ReadPermissionUnavailable')).toBe(true); - expect(handlerCalls).toBe(0); - expect(harness.evidence()).toBe(0); - }) +it.effect('fails closed when explicit Counterparty read authority is unavailable', () => + Effect.gen(function* migratedTest12() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let handlerCalls = 0; + const harness = yield* makeHarness({ + permissionDecision: 'unavailable', + resolvedScope: { ...scope, legalEntityId }, + }); + const counterpartyRead = counterpartyReadRegistration('optional', () => { + handlerCalls += 1; + }); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: counterpartyReadPrincipal(legalEntityId), + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadPermissionUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(0); + }), ); -it.effect( - 'derives the authorized resource from decoded input and ignores conflicting transport hints', - () => - Effect.gen(function* migratedTest13() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let authorizedTarget: - | { moduleId: string; resourceId: string; resourceType: string } - | undefined; - const harness = yield* makeHarness({ - onResourceTarget: (target) => { - authorizedTarget = target; - }, - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - }); - const target = { - moduleId: 'inventory.stock', - resourceId: 'stock-1', - resourceType: 'inventory.stock.item', - }; - const targetRegistration = defineRead( - { - ...registration().descriptor, - inputSchema: ResourceTargetSchema, - legalEntityScope: 'required', - permissionTarget: 'resource', - resultSchema: Schema.String, - }, - () => - Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }), - () => Effect.succeed({}), - (input) => ({ kind: 'resource', resource: input }) - ); - const result = yield* harness.runtime.runRead({ +it.effect('derives the authorized resource from decoded input and ignores conflicting transport hints', () => + Effect.gen(function* migratedTest13() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let authorizedTarget: { moduleId: string; resourceId: string; resourceType: string } | undefined; + const harness = yield* makeHarness({ + onResourceTarget: (target) => { + authorizedTarget = target; + }, + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + }); + const target = { + moduleId: 'inventory.stock', + resourceId: 'stock-1', + resourceType: 'inventory.stock.item', + }; + const targetRegistration = defineRead( + { + ...registration().descriptor, + inputSchema: ResourceTargetSchema, + legalEntityScope: 'required', + permissionTarget: 'resource', + resultSchema: Schema.String, + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }), + () => Effect.succeed({}), + (input) => ({ kind: 'resource', resource: input }), + ); + const result = yield* harness.runtime.runRead({ + input: target, + principal: { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + legalEntityId, + }, + registration: targetRegistration, + transport: { + correlationId: scope.correlationId, + targetModuleKey: 'forged.module', + targetResourceId: 'forged-resource', + targetResourceType: 'forged.type', + }, + }); + expect(result).toBe('visible'); + expect(authorizedTarget).toEqual(target); + }), +); +it.effect('authorizes a canonical Resource through explicit tenant Party administration alternatives', () => + Effect.gen(function* migratedTest14() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const target = { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'counterparty', + }; + const policyTargets: unknown[] = []; + const policy = defineGlobalPolicy({ + evaluate: ({ target: policyTarget }) => { + policyTargets.push(policyTarget); + return Effect.void; + }, + policyKey: 'party.registry.counterparty-read.v1', + }); + let handlerCalls = 0; + const counterpartyRead = defineRead( + { + ...registration().descriptor, + inputSchema: ResourceTargetSchema, + legalEntityScope: 'required', + permissionTarget: 'resource', + policies: [{ denialStatus: 422, policyKey: policy.policyKey }], + resultSchema: Schema.String, + }, + () => { + handlerCalls += 1; + return Effect.succeed({ + evidence: { resultCount: 1 }, + result: 'visible', + }); + }, + () => Effect.succeed({}), + (input) => ({ + kind: 'any_of', + targets: [ + { kind: 'resource', resource: input }, + { kind: 'tenant', permission: 'manage_party_identity' }, + ], + }), + undefined, + [policy], + ); + const principal = { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session' as const, + legalEntityId, + }; + + const tenantAdmin = yield* makeHarness({ + permissionDecision: 'denied', + tenantPermissionDecision: 'allowed', + }); + expect( + yield* tenantAdmin.runtime.runRead({ input: target, - principal: { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - legalEntityId, - }, - registration: targetRegistration, + principal: scope, + registration: counterpartyRead, transport: { correlationId: scope.correlationId, targetModuleKey: 'forged.module', targetResourceId: 'forged-resource', targetResourceType: 'forged.type', }, - }); - expect(result).toBe('visible'); - expect(authorizedTarget).toEqual(target); - }) + }), + ).toBe('visible'); + expect(policyTargets).toEqual([ + { + targetModuleKey: 'party.registry', + targetResourceId: 'counterparty-1', + targetResourceType: 'counterparty', + }, + ]); + + const resourceAuthority = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'denied', + }); + expect( + yield* resourceAuthority.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ).toBe('visible'); + + const indeterminate = yield* makeHarness({ + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'unavailable', + }); + const unavailable = yield* Effect.flip( + indeterminate.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(unavailable, 'ReadPermissionUnavailable')).toBe(true); + expect(indeterminate.evidence()).toBe(0); + + const denied = yield* makeHarness({ + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'denied', + }); + const denial = yield* Effect.flip( + denied.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(denial, 'ReadPermissionDenied')).toBe(true); + expect(denied.evidence()).toBe(1); + expect(handlerCalls).toBe(2); + }), ); -it.effect( - 'authorizes a canonical Resource through explicit tenant Party administration alternatives', - () => - Effect.gen(function* migratedTest14() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const target = { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'counterparty', - }; - const policyTargets: unknown[] = []; - const policy = defineGlobalPolicy({ - evaluate: ({ target: policyTarget }) => { - policyTargets.push(policyTarget); - return Effect.void; - }, - policyKey: 'party.registry.counterparty-read.v1', - }); - let handlerCalls = 0; - const counterpartyRead = defineRead( - { - ...registration().descriptor, - inputSchema: ResourceTargetSchema, - legalEntityScope: 'required', - permissionTarget: 'resource', - policies: [{ denialStatus: 422, policyKey: policy.policyKey }], - resultSchema: Schema.String, - }, - () => { - handlerCalls += 1; - return Effect.succeed({ - evidence: { resultCount: 1 }, - result: 'visible', - }); - }, - () => Effect.succeed({}), - (input) => ({ +it.effect('rejects generic tenant access as an alternative permission target', () => + Effect.gen(function* migratedTest15() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let handlerCalls = 0; + const invalid = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'required', + permissionTarget: 'resource', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => + // SAFETY: This intentionally forges a runtime-invalid alternative to prove validation fails closed. + ({ kind: 'any_of', targets: [ - { kind: 'resource', resource: input }, - { kind: 'tenant', permission: 'manage_party_identity' }, + { + kind: 'resource', + resource: { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'counterparty', + }, + }, + { kind: 'tenant', permission: 'access' }, ], - }), - undefined, - [policy] - ); - const principal = { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session' as const, - legalEntityId, - }; - - const tenantAdmin = yield* makeHarness({ - permissionDecision: 'denied', - tenantPermissionDecision: 'allowed', - }); - expect( - yield* tenantAdmin.runtime.runRead({ - input: target, - principal: scope, - registration: counterpartyRead, - transport: { - correlationId: scope.correlationId, - targetModuleKey: 'forged.module', - targetResourceId: 'forged-resource', - targetResourceType: 'forged.type', - }, - }) - ).toBe('visible'); - expect(policyTargets).toEqual([ - { - targetModuleKey: 'party.registry', - targetResourceId: 'counterparty-1', - targetResourceType: 'counterparty', - }, - ]); - - const resourceAuthority = yield* makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'denied', - }); - expect( - yield* resourceAuthority.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }) - ).toBe('visible'); - - const indeterminate = yield* makeHarness({ - permissionDecision: 'denied', - resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'unavailable', - }); - const unavailable = yield* Effect.flip( - indeterminate.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(unavailable, 'ReadPermissionUnavailable')).toBe( - true - ); - expect(indeterminate.evidence()).toBe(0); - - const denied = yield* makeHarness({ - permissionDecision: 'denied', + }) as never, + ); + const failure = yield* Effect.flip( + (yield* makeHarness({ resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'denied', - }); - const denial = yield* Effect.flip( - denied.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(denial, 'ReadPermissionDenied')).toBe(true); - expect(denied.evidence()).toBe(1); - expect(handlerCalls).toBe(2); - }) -); -it.effect( - 'rejects generic tenant access as an alternative permission target', - () => - Effect.gen(function* migratedTest15() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let handlerCalls = 0; - const invalid = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'required', - permissionTarget: 'resource', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + })).runtime.runRead({ + input: {}, + principal: { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + legalEntityId, }, - () => Effect.succeed({}), - () => - // SAFETY: This intentionally forges a runtime-invalid alternative to prove validation fails closed. - ({ - kind: 'any_of', - targets: [ - { - kind: 'resource', - resource: { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'counterparty', - }, - }, - { kind: 'tenant', permission: 'access' }, - ], - }) as never - ); - const failure = yield* Effect.flip( - (yield* makeHarness({ - resolvedScope: { ...scope, legalEntityId }, - })).runtime.runRead({ - input: {}, - principal: { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - legalEntityId, - }, - registration: invalid, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(failure, 'ReadHandlerExecutionError')).toBe( - true - ); - expect(handlerCalls).toBe(0); - }) + registration: invalid, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(failure, 'ReadHandlerExecutionError')).toBe(true); + expect(handlerCalls).toBe(0); + }), ); for (const scenario of [ { @@ -818,7 +742,7 @@ for (const scenario of [ { kind: 'module', moduleId: 'party.registry' }, ], }), - scenario.resultTargets ?? undefined + scenario.resultTargets ?? undefined, ); const failure = yield* Effect.flip( (yield* makeHarness({ @@ -828,11 +752,11 @@ for (const scenario of [ principal: scope, registration: alternativeRead, transport: { correlationId: scope.correlationId }, - }) + }), ); expect(Predicate.isTagged(failure, scenario.expectedFailure)).toBe(true); expect(handlerCalls).toBe(0); - }) + }), ); } @@ -854,7 +778,7 @@ for (const scenario of [ new ReadPermissionDenied({ code: 'read_permission_denied', reason: 'A late provider target check denied this read', - }) + }), ), }, ]) { @@ -865,7 +789,7 @@ for (const scenario of [ registration().descriptor, () => scenario.outcome, () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) + () => ({ kind: 'module', moduleId: 'core.shell' }), ); const error = yield* Effect.flip( harness.runtime.runRead({ @@ -873,197 +797,177 @@ for (const scenario of [ principal: scope, registration: failingRead, transport: { correlationId: scope.correlationId }, - }) + }), ); expect(Predicate.isTagged(error, scenario.expectedFailure)).toBe(true); expect(harness.evidence()).toBe(scenario.expectedEvidence); - }) + }), ); } -it.effect( - 'does not release generated search candidates denied by result-level authorization', - () => - Effect.gen(function* migratedTest20() { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ - moduleId: 'inventory.stock', - resourceId: 'stock-1', - resourceType: 'inventory.stock.item', - }); - const harness = yield* makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - resultPermissionDecision: 'denied', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'required', - resultSchema: Schema.Array(ResourceTargetSchema), +it.effect('does not release generated search candidates denied by result-level authorization', () => + Effect.gen(function* migratedTest20() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ + moduleId: 'inventory.stock', + resourceId: 'stock-1', + resourceType: 'inventory.stock.item', + }); + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + resultPermissionDecision: 'denied', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'required', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + (result) => result, + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + legalEntityId, }, - () => - Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - (result) => result - ); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - legalEntityId, - }, - registration: searchRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); - expect(harness.evidence()).toBe(1); - }) + registration: searchRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); + expect(harness.evidence()).toBe(1); + }), ); -it.effect( - 'authorizes tenant-scoped Party search results without fabricating a Legal Entity', - () => - Effect.gen(function* migratedTest21() { - const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ - moduleId: 'party.registry', - resourceId: 'party-1', - resourceType: 'party.registry.party', - }); - let resourceChecks = 0; - const tenantPermissions: string[] = []; - const harness = yield* makeHarness({ - onResourceTarget: () => { - resourceChecks += 1; - }, - onTenantPermission: (permission) => tenantPermissions.push(permission), - permissionDecision: 'allowed', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - accessKind: 'search', - legalEntityScope: 'optional', - permissionTarget: 'tenant', - resultSchema: Schema.Array(ResourceTargetSchema), - }, - () => - Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'tenant', permission: 'read_party_identity' }), - (result) => result - ); +it.effect('authorizes tenant-scoped Party search results without fabricating a Legal Entity', () => + Effect.gen(function* migratedTest21() { + const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ + moduleId: 'party.registry', + resourceId: 'party-1', + resourceType: 'party.registry.party', + }); + let resourceChecks = 0; + const tenantPermissions: string[] = []; + const harness = yield* makeHarness({ + onResourceTarget: () => { + resourceChecks += 1; + }, + onTenantPermission: (permission) => tenantPermissions.push(permission), + permissionDecision: 'allowed', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + accessKind: 'search', + legalEntityScope: 'optional', + permissionTarget: 'tenant', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'tenant', permission: 'read_party_identity' }), + (result) => result, + ); - expect( - yield* harness.runtime.runRead({ - input: {}, - principal: scope, - registration: searchRegistration, - transport: { correlationId: scope.correlationId }, - }) - ).toEqual([candidate]); - expect(tenantPermissions).toEqual([ - 'read_party_identity', - 'read_party_identity', - ]); - expect(resourceChecks).toBe(0); - }) + expect( + yield* harness.runtime.runRead({ + input: {}, + principal: scope, + registration: searchRegistration, + transport: { correlationId: scope.correlationId }, + }), + ).toEqual([candidate]); + expect(tenantPermissions).toEqual(['read_party_identity', 'read_party_identity']); + expect(resourceChecks).toBe(0); + }), ); -it.effect( - 'fails closed when tenant-scoped Party result authorization becomes unavailable', - () => - Effect.gen(function* migratedTest22() { - const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ - moduleId: 'party.registry', - resourceId: 'party-1', - resourceType: 'party.registry.party', - }); - const harness = yield* makeHarness({ - permissionDecision: 'allowed', - resultTenantPermissionDecision: 'unavailable', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - accessKind: 'search', - legalEntityScope: 'optional', - permissionTarget: 'tenant', - resultSchema: Schema.Array(ResourceTargetSchema), - }, - () => - Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'tenant', permission: 'read_party_identity' }), - (result) => result - ); - const failure = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: searchRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - expect(Predicate.isTagged(failure, 'ReadPermissionUnavailable')).toBe( - true - ); - }) +it.effect('fails closed when tenant-scoped Party result authorization becomes unavailable', () => + Effect.gen(function* migratedTest22() { + const candidate = yield* Schema.decodeEffect(ResourceTargetSchema)({ + moduleId: 'party.registry', + resourceId: 'party-1', + resourceType: 'party.registry.party', + }); + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resultTenantPermissionDecision: 'unavailable', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + accessKind: 'search', + legalEntityScope: 'optional', + permissionTarget: 'tenant', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'tenant', permission: 'read_party_identity' }), + (result) => result, + ); + const failure = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: searchRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(failure, 'ReadPermissionUnavailable')).toBe(true); + }), ); -it.effect( - 'preserves declared owner read availability and not-found failures but sanitizes defects', - () => - Effect.gen(function* migratedTest23() { - const failures = [ - new ReadHandlerUnavailable({ - code: 'read_handler_unavailable', - reason: 'A provider is temporarily unavailable', - }), - new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'The resource does not exist', +it.effect('preserves declared owner read availability and not-found failures but sanitizes defects', () => + Effect.gen(function* migratedTest23() { + const failures = [ + new ReadHandlerUnavailable({ + code: 'read_handler_unavailable', + reason: 'A provider is temporarily unavailable', + }), + new ReadHandlerNotFound({ + code: 'read_handler_not_found', + reason: 'The resource does not exist', + }), + new Error('secret owner defect'), + ] as const; + const expectedTags = ['ReadHandlerUnavailable', 'ReadHandlerNotFound', 'ReadHandlerExecutionError']; + yield* Effect.forEach( + failures, + (failure, index) => + Effect.gen(function* migratedTest24() { + const harness = yield* makeHarness(); + const failingRegistration = defineRead( + registration().descriptor, + () => Effect.fail(failure), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: failingRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + const expectedTag = expectedTags[index]; + if (expectedTag === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(Predicate.isTagged(error, expectedTag)).toBe(true); + expect(error.reason).not.toMatch(/secret/u); + expect(harness.evidence()).toBe(0); }), - new Error('secret owner defect'), - ] as const; - const expectedTags = [ - 'ReadHandlerUnavailable', - 'ReadHandlerNotFound', - 'ReadHandlerExecutionError', - ]; - yield* Effect.forEach( - failures, - (failure, index) => - Effect.gen(function* migratedTest24() { - const harness = yield* makeHarness(); - const failingRegistration = defineRead( - registration().descriptor, - () => Effect.fail(failure), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); - const error = yield* Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: failingRegistration, - transport: { correlationId: scope.correlationId }, - }) - ); - const expectedTag = expectedTags[index]; - if (expectedTag === undefined) { - expect.unreachable('Expected value to be present'); - } - expect(Predicate.isTagged(error, expectedTag)).toBe(true); - expect(error.reason).not.toMatch(/secret/u); - expect(harness.evidence()).toBe(0); - }), - { concurrency: 'unbounded' } - ); - }) + { concurrency: 'unbounded' }, + ); + }), ); it.effect('keeps read interruption and waits for transaction settlement', () => Effect.gen(function* migratedTest25() { @@ -1081,7 +985,7 @@ it.effect('keeps read interruption and waits for transaction settlement', () => return { evidence: { resultCount: 0 }, result: [] }; }), () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) + () => ({ kind: 'module', moduleId: 'core.shell' }), ); const fiber = yield* harness.runtime .runRead({ @@ -1101,44 +1005,36 @@ it.effect('keeps read interruption and waits for transaction settlement', () => expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true); expect(events).toEqual(['transaction_settled', 'read_completed']); expect(harness.evidence()).toBe(0); - }) + }), ); -it.effect( - 'prioritizes failed denial evidence while retaining permission denial in the cause', - () => - Effect.gen(function* migratedTest26() { - const harness = yield* makeHarness({ failEvidence: true }); - const denied = new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'Denied by read handler', - }); - const governed = defineRead( - registration().descriptor, - () => Effect.fail(denied), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }) - ); - const exit = yield* Effect.exit( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: governed, - transport: { correlationId: scope.correlationId }, - }) - ); - if (!Exit.isFailure(exit)) { - expect.unreachable('Expected value to be present'); - } - const failures = exit.cause.reasons - .filter(Cause.isFailReason) - .map((reason) => reason.error); - expect(failures.length).toBe(2); - expect( - Predicate.isTagged(failures[0], 'ReadEvidencePersistenceError') - ).toBe(true); - expect(failures[1]).toBe(denied); - expect(Predicate.isTagged(failures[1], 'ReadPermissionDenied')).toBe( - true - ); - }) +it.effect('prioritizes failed denial evidence while retaining permission denial in the cause', () => + Effect.gen(function* migratedTest26() { + const harness = yield* makeHarness({ failEvidence: true }); + const denied = new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'Denied by read handler', + }); + const governed = defineRead( + registration().descriptor, + () => Effect.fail(denied), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const exit = yield* Effect.exit( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: governed, + transport: { correlationId: scope.correlationId }, + }), + ); + if (!Exit.isFailure(exit)) { + expect.unreachable('Expected value to be present'); + } + const failures = exit.cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error); + expect(failures.length).toBe(2); + expect(Predicate.isTagged(failures[0], 'ReadEvidencePersistenceError')).toBe(true); + expect(failures[1]).toBe(denied); + expect(Predicate.isTagged(failures[1], 'ReadPermissionDenied')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/unit/schema-contract.test.ts b/app/packages/core-runtime/tests/unit/schema-contract.test.ts index 9594fc4ee..c20df0724 100644 --- a/app/packages/core-runtime/tests/unit/schema-contract.test.ts +++ b/app/packages/core-runtime/tests/unit/schema-contract.test.ts @@ -16,13 +16,9 @@ import { const actionConfig = getTableConfig(actionInvocations); const dialect = new PgDialect(); type SchemaExport = (typeof schemaExports)[keyof typeof schemaExports]; -const isPgTable = ( - value: SchemaExport -): value is Extract => isTable(value); +const isPgTable = (value: SchemaExport): value is Extract => isTable(value); const getColumn = (name: string) => { - const column = actionConfig.columns.find( - (candidate) => candidate.name === name - ); + const column = actionConfig.columns.find((candidate) => candidate.name === name); if (column === undefined) { expect.unreachable(`Expected action_invocations.${name}`); } @@ -41,18 +37,12 @@ it('exports exactly the 18 Core tables in PostgreSQL schema core', () => { return `${config.schema}.${config.name}`; }) .toSorted(); - const expectedQualifiedNames = CORE_TABLE_INVENTORY.map( - (tableName) => `${CORE_SCHEMA_NAME}.${tableName}` - ).toSorted(); + const expectedQualifiedNames = CORE_TABLE_INVENTORY.map((tableName) => `${CORE_SCHEMA_NAME}.${tableName}`).toSorted(); expect(qualifiedNames).toEqual(expectedQualifiedNames); expect(new Set(qualifiedNames).size).toBe(CORE_TABLE_INVENTORY.length); expect(qualifiedNames.some((name) => name.startsWith('public.'))).toBe(false); - expect( - qualifiedNames.some((name) => - /^(?:auth|ticketing|properties|property|accounting)\./u.test(name) - ) - ).toBe(false); + expect(qualifiedNames.some((name) => /^(?:auth|ticketing|properties|property|accounting)\./u.test(name))).toBe(false); }); it('supports pre-authentication Action Invocation rows and indeterminate outcomes', () => { expect(getColumn('principal_id').notNull).toBe(false); @@ -72,9 +62,7 @@ it('supports pre-authentication Action Invocation rows and indeterminate outcome 'replayed', ]); - const statusCheck = actionConfig.checks.find( - (candidate) => candidate.name === 'core_action_invocations_status_ck' - ); + const statusCheck = actionConfig.checks.find((candidate) => candidate.name === 'core_action_invocations_status_ck'); if (statusCheck === undefined) { expect.unreachable('Expected value to be present'); } @@ -86,41 +74,33 @@ it('supports pre-authentication Action Invocation rows and indeterminate outcome }); it('preserves critical Action foreign keys and unique idempotency index', () => { const principalForeignKey = actionConfig.foreignKeys.find((foreignKey) => - foreignKey - .reference() - .columns.some((column) => column.name === 'principal_id') + foreignKey.reference().columns.some((column) => column.name === 'principal_id'), ); if (principalForeignKey === undefined) { expect.unreachable('Expected value to be present'); } - expect(getTableName(principalForeignKey.reference().foreignTable)).toBe( - getTableName(principals) - ); + expect(getTableName(principalForeignKey.reference().foreignTable)).toBe(getTableName(principals)); expect(principalForeignKey.onDelete).toBe('restrict'); - expect( - principalForeignKey.reference().columns.map((column) => column.name) - ).toEqual(['tenant_id', 'principal_id']); + expect(principalForeignKey.reference().columns.map((column) => column.name)).toEqual(['tenant_id', 'principal_id']); const idempotencyIndex = actionConfig.indexes.find( - (candidate) => - candidate.config.name === 'core_action_invocations_idempotency_uk' + (candidate) => candidate.config.name === 'core_action_invocations_idempotency_uk', ); if (idempotencyIndex === undefined) { expect.unreachable('Expected value to be present'); } expect(idempotencyIndex.config.unique).toBe(true); expect(idempotencyIndex.config.where).toBeDefined(); - expect( - idempotencyIndex.config.columns.map((column) => - 'name' in column ? column.name : false - ) - ).toEqual(['tenant_id', 'action_key', 'principal_id', 'idempotency_key']); + expect(idempotencyIndex.config.columns.map((column) => ('name' in column ? column.name : false))).toEqual([ + 'tenant_id', + 'action_key', + 'principal_id', + 'idempotency_key', + ]); }); it('allocates Domain Event order through a database-owned monotonic sequence', () => { const domainEventConfig = getTableConfig(domainEvents); - const sequenceColumn = domainEventConfig.columns.find( - (candidate) => candidate.name === 'tenant_sequence_no' - ); + const sequenceColumn = domainEventConfig.columns.find((candidate) => candidate.name === 'tenant_sequence_no'); if (sequenceColumn === undefined) { expect.unreachable('Expected value to be present'); @@ -130,18 +110,16 @@ it('allocates Domain Event order through a database-owned monotonic sequence', ( expect(sequenceColumn.getSQLType()).toBe('bigint'); const sequenceIndex = domainEventConfig.indexes.find( - (candidate) => - candidate.config.name === 'core_domain_events_tenant_sequence_uk' + (candidate) => candidate.config.name === 'core_domain_events_tenant_sequence_uk', ); if (sequenceIndex === undefined) { expect.unreachable('Expected value to be present'); } expect(sequenceIndex.config.unique).toBe(true); - expect( - sequenceIndex.config.columns.map((column) => - 'name' in column ? column.name : false - ) - ).toEqual(['tenant_id', 'tenant_sequence_no']); + expect(sequenceIndex.config.columns.map((column) => ('name' in column ? column.name : false))).toEqual([ + 'tenant_id', + 'tenant_sequence_no', + ]); }); it('keeps the inferred Action status type aligned with the lifecycle union', () => { type ActionInsert = typeof actionInvocations.$inferInsert; diff --git a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts index 7a7d7e5af..df81fc567 100644 --- a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts @@ -15,7 +15,7 @@ const unusedOperation = (): never => { }; const transactionService = ( install: OperationalScopeTransactionService['install'], - verify: OperationalScopeTransactionService['verify'] + verify: OperationalScopeTransactionService['verify'], ): OperationalScopeTransactionService => ({ delete: unusedOperation, insert: unusedOperation, @@ -24,44 +24,42 @@ const transactionService = ( update: unusedOperation, verify, }); -it.effect( - 'installs and verifies transaction-local scope and exposes no transaction controls', - () => - Effect.gen(function* migratedTest1() { - let calls = 0; - const transaction = transactionService( - () => - Effect.sync(() => { - calls += 1; - }), +it.effect('installs and verifies transaction-local scope and exposes no transaction controls', () => + Effect.gen(function* migratedTest1() { + let calls = 0; + const transaction = transactionService( + () => Effect.sync(() => { calls += 1; - return Option.some({ - legal_entity_id: 'entity', - tenant_id: 'tenant', - }); - }) - ); - const capability = yield* installOperationalScopeFromTransactionService({ - authContextRef: 'job:test:run:scoped-transaction', - authMethod: 'system', - correlationId: 'c-1', - legalEntityId: 'entity', - principalId: 'principal', - tenantId: 'tenant', - }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)); - expect(calls).toBe(2); - expect('commit' in capability).toBe(false); - expect('query' in capability).toBe(false); - expect('rollback' in capability).toBe(false); - expect('transaction' in capability).toBe(false); - }) + }), + Effect.sync(() => { + calls += 1; + return Option.some({ + legal_entity_id: 'entity', + tenant_id: 'tenant', + }); + }), + ); + const capability = yield* installOperationalScopeFromTransactionService({ + authContextRef: 'job:test:run:scoped-transaction', + authMethod: 'system', + correlationId: 'c-1', + legalEntityId: 'entity', + principalId: 'principal', + tenantId: 'tenant', + }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)); + expect(calls).toBe(2); + expect('commit' in capability).toBe(false); + expect('query' in capability).toBe(false); + expect('rollback' in capability).toBe(false); + expect('transaction' in capability).toBe(false); + }), ); it.effect('fails closed when transaction settings do not match', () => Effect.gen(function* migratedTest2() { const transaction = transactionService( () => Effect.void, - Effect.succeedSome({ legal_entity_id: '', tenant_id: 'foreign' }) + Effect.succeedSome({ legal_entity_id: '', tenant_id: 'foreign' }), ); const error = yield* Effect.flip( installOperationalScopeFromTransactionService({ @@ -70,10 +68,10 @@ it.effect('fails closed when transaction settings do not match', () => correlationId: 'c-1', principalId: 'principal', tenantId: 'tenant', - }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)) + }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)), ); expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe(true); - }) + }), ); it('creates complete CRUD RLS policies with update using and with-check predicates', () => { const fixture = pgTable.withRLS('fixture', { @@ -83,18 +81,9 @@ it('creates complete CRUD RLS policies with update using and with-check predicat expect(getTableConfig(fixture).enableRLS).toBe(true); for (const policies of [ tenantRlsPolicies('tenant_fixture', fixture.tenantId), - tenantLegalEntityRlsPolicies( - 'entity_fixture', - fixture.tenantId, - fixture.legalEntityId - ), + tenantLegalEntityRlsPolicies('entity_fixture', fixture.tenantId, fixture.legalEntityId), ]) { - expect(policies.map((policy) => policy.for)).toEqual([ - 'select', - 'insert', - 'update', - 'delete', - ]); + expect(policies.map((policy) => policy.for)).toEqual(['select', 'insert', 'update', 'delete']); expect(policies[2].using).toBeDefined(); expect(policies[2].withCheck).toBeDefined(); } diff --git a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts index 4a582e5d1..42b89d8e1 100644 --- a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts +++ b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts @@ -41,110 +41,89 @@ const observation = (projectionVersion: string, title: string) => ({ }); it('declares one immutable Core registration for every closed Party lifecycle topic', () => { - expect(CORE_SEARCH_INGESTION_REGISTRATIONS.map(({ topic }) => topic)).toEqual( - CORE_SEARCH_PARTY_LIFECYCLE_TOPICS - ); + expect(CORE_SEARCH_INGESTION_REGISTRATIONS.map(({ topic }) => topic)).toEqual(CORE_SEARCH_PARTY_LIFECYCLE_TOPICS); expect(Object.isFrozen(CORE_SEARCH_INGESTION_REGISTRATIONS)).toBe(true); expect( CORE_SEARCH_INGESTION_REGISTRATIONS.every( (registration) => Object.isFrozen(registration) && registration.consumerModuleKey === 'party.registry' && - registration.producerModuleKey === 'party.registry' - ) + registration.producerModuleKey === 'party.registry', + ), ).toBe(true); }); -it.effect( - 'ingests duplicate and out-of-order post-commit observations idempotently', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const ingestion = makeCoreSearchIngestion(store); +it.effect('ingests duplicate and out-of-order post-commit observations idempotently', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const ingestion = makeCoreSearchIngestion(store); - return Effect.gen(function* ingestObservationsIdempotently() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* ingestion.ingest(observation('2', 'Current title')); - yield* ingestion.ingest(observation('2', 'Current title')); - yield* ingestion.ingest(observation('1', 'Stale title')); + return Effect.gen(function* ingestObservationsIdempotently() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* ingestion.ingest(observation('2', 'Current title')); + yield* ingestion.ingest(observation('2', 'Current title')); + yield* ingestion.ingest(observation('1', 'Stale title')); - const hits = yield* runtime.search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'current', - resourceType: 'party.registry.party', - tenantId, - }); - expect(hits.map(({ title }) => title)).toEqual(['Current title']); + const hits = yield* runtime.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'current', + resourceType: 'party.registry.party', + tenantId, }); - } -); - -it.effect( - 'identifier updates accept only their generated self-consumer worker', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const ingestion = makeCoreSearchIngestion(store); - const update = { - ...observation('3', 'Updated identifier projection'), - topic: 'party.registry.official-identifier-updated.v1', - workerKey: 'party.registry.project-official-identifier-updated-to-search', - }; - return Effect.gen(function* acceptOnlyGeneratedWorker() { - yield* ingestion.ingest(update); - yield* ingestion.ingest(update); - const denied = yield* Effect.flip( - ingestion.ingest({ - ...update, - workerKey: - 'party.registry.project-official-identifier-added-to-search', - }) - ); - expect(Predicate.isTagged(denied, 'CoreSearchProjectionInvalid')).toBe( - true - ); - }); - } -); + expect(hits.map(({ title }) => title)).toEqual(['Current title']); + }); +}); -it.effect( - 'rejects undeclared topics and sequence/document identity mismatches', - () => { - const ingestion = makeCoreSearchIngestion( - makeInMemoryCoreSearchProjectionStore() +it.effect('identifier updates accept only their generated self-consumer worker', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const ingestion = makeCoreSearchIngestion(store); + const update = { + ...observation('3', 'Updated identifier projection'), + topic: 'party.registry.official-identifier-updated.v1', + workerKey: 'party.registry.project-official-identifier-updated-to-search', + }; + return Effect.gen(function* acceptOnlyGeneratedWorker() { + yield* ingestion.ingest(update); + yield* ingestion.ingest(update); + const denied = yield* Effect.flip( + ingestion.ingest({ + ...update, + workerKey: 'party.registry.project-official-identifier-added-to-search', + }), ); - const invalidObservations = [ - { ...observation('1', 'Party'), topic: 'party.registry.undeclared.v1' }, - { ...observation('1', 'Party'), producerModuleKey: 'foreign.module' }, - { - ...observation('1', 'Party'), - workerKey: 'party.registry.project-party-created-to-search', - }, - { ...observation('1', 'Party'), projectionVersion: '2' }, - { - ...observation('1', 'Party'), - mutation: { - document: { - ...document('1', 'Party'), - ref: { ...ref, moduleId: 'foreign.module' }, - }, - kind: 'upsert', + expect(Predicate.isTagged(denied, 'CoreSearchProjectionInvalid')).toBe(true); + }); +}); + +it.effect('rejects undeclared topics and sequence/document identity mismatches', () => { + const ingestion = makeCoreSearchIngestion(makeInMemoryCoreSearchProjectionStore()); + const invalidObservations = [ + { ...observation('1', 'Party'), topic: 'party.registry.undeclared.v1' }, + { ...observation('1', 'Party'), producerModuleKey: 'foreign.module' }, + { + ...observation('1', 'Party'), + workerKey: 'party.registry.project-party-created-to-search', + }, + { ...observation('1', 'Party'), projectionVersion: '2' }, + { + ...observation('1', 'Party'), + mutation: { + document: { + ...document('1', 'Party'), + ref: { ...ref, moduleId: 'foreign.module' }, }, + kind: 'upsert', }, - ]; - return Effect.gen(function* testInvalidObservations() { - const failures = yield* Effect.forEach( - invalidObservations, - (invalidObservation) => - Effect.flip(ingestion.ingest(invalidObservation)), - { concurrency: 'unbounded' } - ); - for (const failure of failures) { - expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe( - true - ); - } - }); - } -); + }, + ]; + return Effect.gen(function* testInvalidObservations() { + const failures = yield* Effect.forEach( + invalidObservations, + (invalidObservation) => Effect.flip(ingestion.ingest(invalidObservation)), + { concurrency: 'unbounded' }, + ); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + }); +}); diff --git a/app/packages/core-runtime/tests/unit/search-projection.test.ts b/app/packages/core-runtime/tests/unit/search-projection.test.ts index 36ce98ccf..9c43d9509 100644 --- a/app/packages/core-runtime/tests/unit/search-projection.test.ts +++ b/app/packages/core-runtime/tests/unit/search-projection.test.ts @@ -10,10 +10,7 @@ import { createCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, } from '../../src/search/projection.ts'; -import type { - CoreSearchProjectionHit, - CoreSearchProjectionStoreService, -} from '../../src/search/projection.ts'; +import type { CoreSearchProjectionHit, CoreSearchProjectionStoreService } from '../../src/search/projection.ts'; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Any)); @@ -73,9 +70,7 @@ it.effect( tenantId, }; return Effect.gen(function* testProjectionRebuildFloor() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); yield* store.replace(rebuild); yield* store.apply({ document: party(), kind: 'upsert' }); yield* store.replace({ @@ -90,15 +85,11 @@ it.effect( query: 'acme', resourceType: partyRef.resourceType, tenantId, - }) + }), ).toEqual([]); yield* store.replace(rebuild); - const divergent = yield* Effect.flip( - store.replace({ ...rebuild, documents: [party()] }) - ); - expect(Predicate.isTagged(divergent, 'CoreSearchProjectionInvalid')).toBe( - true - ); + const divergent = yield* Effect.flip(store.replace({ ...rebuild, documents: [party()] })); + expect(Predicate.isTagged(divergent, 'CoreSearchProjectionInvalid')).toBe(true); yield* store.apply({ document: party({ projectionVersion: '3' }), kind: 'upsert', @@ -113,75 +104,112 @@ it.effect( }); expect(searchResults.length).toBe(1); }); - } -); - -it.effect( - 'Core Search identifies alias-only matches while canonical evidence takes precedence', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - return Effect.gen(function* testAliasMatches() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* store.apply({ - document: party({ - aliases: [ - { - kind: 'resource', - ref: aliasRef, - searchableText: ['Former Company', 'Acme'], - }, - ], - matchedRef: aliasRef, - }), - kind: 'upsert', - }); - const search = (query: string) => - runtime.search({ - includeArchived: false, - moduleId: partyRef.moduleId, - query, - resourceType: partyRef.resourceType, - tenantId, - }); - const aliasHits = yield* search('former'); - expect(aliasHits.length).toBe(1); - expect(aliasHits[0]?.ref).toEqual(partyRef); - expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); - expect(aliasHits[0]?.matchedSubjectRef).toBe(undefined); - expect(encodeJson(aliasHits)).not.toMatch( - /Former Company|searchableText|aliases/u - ); - const canonicalHits = yield* search('acme'); - expect(canonicalHits[0]?.matchedRef).toBe(undefined); - }); - } + }, ); -it.effect( - 'Core Search rejects cross-tenant aliases and malformed or oversized temporal evidence', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const invalidEvidence = [ - { +it.effect('Core Search identifies alias-only matches while canonical evidence takes precedence', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testAliasMatches() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* store.apply({ + document: party({ aliases: [ { kind: 'resource', - ref: { ...aliasRef, tenantId: otherTenantId }, - searchableText: ['foreign'], + ref: aliasRef, + searchableText: ['Former Company', 'Acme'], }, ], - }, - { - temporalSearchableText: [{ validFrom: 'not-a-date', value: 'private' }], - }, - { - temporalSearchableText: [ - { validFrom: '2026-02-01', validTo: '2026-02-01', value: 'private' }, - ], - }, - { + matchedRef: aliasRef, + }), + kind: 'upsert', + }); + const search = (query: string) => + runtime.search({ + includeArchived: false, + moduleId: partyRef.moduleId, + query, + resourceType: partyRef.resourceType, + tenantId, + }); + const aliasHits = yield* search('former'); + expect(aliasHits.length).toBe(1); + expect(aliasHits[0]?.ref).toEqual(partyRef); + expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); + expect(aliasHits[0]?.matchedSubjectRef).toBe(undefined); + expect(encodeJson(aliasHits)).not.toMatch(/Former Company|searchableText|aliases/u); + const canonicalHits = yield* search('acme'); + expect(canonicalHits[0]?.matchedRef).toBe(undefined); + }); +}); + +it.effect('Core Search rejects cross-tenant aliases and malformed or oversized temporal evidence', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const invalidEvidence = [ + { + aliases: [ + { + kind: 'resource', + ref: { ...aliasRef, tenantId: otherTenantId }, + searchableText: ['foreign'], + }, + ], + }, + { + temporalSearchableText: [{ validFrom: 'not-a-date', value: 'private' }], + }, + { + temporalSearchableText: [{ validFrom: '2026-02-01', validTo: '2026-02-01', value: 'private' }], + }, + { + aliases: [ + { + kind: 'subject', + ref: aliasRef, + searchableText: [], + temporalSearchableText: [ + { + validFrom: '2026-02-01', + validTo: '2026-01-01', + value: 'private', + }, + ], + }, + ], + }, + { + aliases: Array.from({ length: 101 }, () => ({ + kind: 'resource', + ref: aliasRef, + searchableText: ['private'], + })), + }, + { + temporalSearchableText: Array.from({ length: 101 }, () => ({ + validFrom: '2026-01-01', + value: 'private', + })), + }, + ]; + return Effect.gen(function* testInvalidEvidence() { + const failures = yield* Effect.forEach( + invalidEvidence, + (evidence) => Effect.flip(store.apply({ document: party(evidence), kind: 'upsert' })), + { concurrency: 'unbounded' }, + ); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + }); +}); + +it.effect('Core Search honors half-open evidence periods for canonical and subject aliases', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testHalfOpenEvidencePeriods() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* TestClock.setTime(Date.parse('2026-09-03T00:00:00Z')); + yield* store.apply({ + document: party({ aliases: [ { kind: 'subject', @@ -189,256 +217,129 @@ it.effect( searchableText: [], temporalSearchableText: [ { - validFrom: '2026-02-01', - validTo: '2026-01-01', - value: 'private', + validFrom: '2026-01-01T00:00:00Z', + validTo: '2026-02-01T00:00:00Z', + value: 'old-private@example.test', }, ], }, ], - }, - { - aliases: Array.from({ length: 101 }, () => ({ - kind: 'resource', - ref: aliasRef, - searchableText: ['private'], - })), - }, - { - temporalSearchableText: Array.from({ length: 101 }, () => ({ - validFrom: '2026-01-01', - value: 'private', - })), - }, - ]; - return Effect.gen(function* testInvalidEvidence() { - const failures = yield* Effect.forEach( - invalidEvidence, - (evidence) => - Effect.flip( - store.apply({ document: party(evidence), kind: 'upsert' }) - ), - { concurrency: 'unbounded' } - ); - for (const failure of failures) { - expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe( - true - ); - } + temporalSearchableText: [ + { + validFrom: '2026-02-01T00:00:00Z', + value: 'current-private@example.test', + }, + { + validFrom: '2000-01-01T00:00:00Z', + validTo: '2100-01-01T00:00:00Z', + value: 'long-lived@example.test', + }, + ], + }), + kind: 'upsert', }); - } -); - -it.effect( - 'Core Search honors half-open evidence periods for canonical and subject aliases', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - return Effect.gen(function* testHalfOpenEvidencePeriods() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* TestClock.setTime(Date.parse('2026-09-03T00:00:00Z')); - yield* store.apply({ - document: party({ - aliases: [ - { - kind: 'subject', - ref: aliasRef, - searchableText: [], - temporalSearchableText: [ - { - validFrom: '2026-01-01T00:00:00Z', - validTo: '2026-02-01T00:00:00Z', - value: 'old-private@example.test', - }, - ], - }, - ], - temporalSearchableText: [ - { - validFrom: '2026-02-01T00:00:00Z', - value: 'current-private@example.test', - }, - { - validFrom: '2000-01-01T00:00:00Z', - validTo: '2100-01-01T00:00:00Z', - value: 'long-lived@example.test', - }, - ], - }), - kind: 'upsert', - }); - const search = (query: string, effectiveAt?: string) => { - const request = { - includeArchived: false, - moduleId: partyRef.moduleId, - query, - resourceType: partyRef.resourceType, - tenantId, - }; - return runtime.search( - effectiveAt === undefined ? request : { ...request, effectiveAt } - ); + const search = (query: string, effectiveAt?: string) => { + const request = { + includeArchived: false, + moduleId: partyRef.moduleId, + query, + resourceType: partyRef.resourceType, + tenantId, }; - const historicalHits = yield* search( - 'old-private', - '2026-01-01T00:00:00Z' - ); - expect(historicalHits[0]?.matchedSubjectRef).toEqual(aliasRef); - expect(yield* search('old-private', '2026-02-01T00:00:00Z')).toEqual([]); - expect(yield* search('current-private', '2026-01-31T23:59:59Z')).toEqual( - [] - ); - const current = yield* search('current-private', '2026-02-01T00:00:00Z'); - expect(current.length).toBe(1); - expect(current[0]?.matchedSubjectRef).toBe(undefined); - expect(encodeJson(current)).not.toMatch( - /private@example|temporalSearchableText/u - ); - const longLivedHits = yield* search('long-lived'); - expect(longLivedHits.length).toBe(1); - }); - } -); - -it.effect( - 'Core Search rebuilds one owned projection atomically and isolates tenants', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); + return runtime.search(effectiveAt === undefined ? request : { ...request, effectiveAt }); + }; + const historicalHits = yield* search('old-private', '2026-01-01T00:00:00Z'); + expect(historicalHits[0]?.matchedSubjectRef).toEqual(aliasRef); + expect(yield* search('old-private', '2026-02-01T00:00:00Z')).toEqual([]); + expect(yield* search('current-private', '2026-01-31T23:59:59Z')).toEqual([]); + const current = yield* search('current-private', '2026-02-01T00:00:00Z'); + expect(current.length).toBe(1); + expect(current[0]?.matchedSubjectRef).toBe(undefined); + expect(encodeJson(current)).not.toMatch(/private@example|temporalSearchableText/u); + const longLivedHits = yield* search('long-lived'); + expect(longLivedHits.length).toBe(1); + }); +}); - return Effect.gen(function* testOwnedProjectionRebuild() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* store.replace({ - documents: [party()], - moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.party', - tenantId, - }); - yield* store.replace({ - documents: [ - party({ - ref: { ...partyRef, tenantId: otherTenantId }, - title: 'Other tenant', - }), - ], - moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.party', - tenantId: otherTenantId, - }); +it.effect('Core Search rebuilds one owned projection atomically and isolates tenants', () => { + const store = makeInMemoryCoreSearchProjectionStore(); - const result = yield* runtime.search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'acme', - resourceType: 'party.registry.party', - tenantId, - }); - expect(result).toEqual([ - { - archived: false, - facets: [], - metadata: [ - { key: 'party-kind', kind: 'string', value: 'ORGANIZATION' }, - ], - ref: partyRef, - title: 'Acme, s.r.o.', - }, - ]); - expect(encodeJson(result)).not.toMatch(/private@example\.test/u); + return Effect.gen(function* testOwnedProjectionRebuild() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* store.replace({ + documents: [party()], + moduleId: 'party.registry', + rebuildVersion: '1', + resourceType: 'party.registry.party', + tenantId, + }); + yield* store.replace({ + documents: [ + party({ + ref: { ...partyRef, tenantId: otherTenantId }, + title: 'Other tenant', + }), + ], + moduleId: 'party.registry', + rebuildVersion: '1', + resourceType: 'party.registry.party', + tenantId: otherTenantId, + }); - yield* store.replace({ - documents: [ - party({ - archived: true, - projectionVersion: '2', - title: 'Replacement', - }), - ], - moduleId: 'party.registry', - rebuildVersion: '2', - resourceType: 'party.registry.party', - tenantId, - }); - const hits = yield* runtime.search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'acme', - resourceType: 'party.registry.party', - tenantId, - }); - expect(hits).toEqual([]); + const result = yield* runtime.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'acme', + resourceType: 'party.registry.party', + tenantId, }); - } -); + expect(result).toEqual([ + { + archived: false, + facets: [], + metadata: [{ key: 'party-kind', kind: 'string', value: 'ORGANIZATION' }], + ref: partyRef, + title: 'Acme, s.r.o.', + }, + ]); + expect(encodeJson(result)).not.toMatch(/private@example\.test/u); -it.effect( - 'Core Search applies typed Legal Entity and role facets without returning match evidence', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const counterpartyRef = { + yield* store.replace({ + documents: [ + party({ + archived: true, + projectionVersion: '2', + title: 'Replacement', + }), + ], moduleId: 'party.registry', - resourceId: '40000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.counterparty', + rebuildVersion: '2', + resourceType: 'party.registry.party', tenantId, - } as const; - return Effect.gen(function* testTypedLegalEntityAndRoleFacets() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* store.replace({ - documents: [ - { - archived: false, - facets: [], - matchedSubjectRef: aliasRef, - metadata: [ - { - key: 'current-roles', - kind: 'strings', - value: ['CUSTOMER', 'SUPPLIER'], - }, - ], - projectionVersion: '1', - ref: counterpartyRef, - searchableText: ['Acme', 'private@example.test'], - selectedLegalEntityId: legalEntityId, - subjectRef: partyRef, - temporalFacets: [ - { - key: 'current-role', - validFrom: '2026-01-01T00:00:00.000Z', - value: 'CUSTOMER', - }, - { - key: 'current-role', - validFrom: '2026-02-01T00:00:00.000Z', - value: 'SUPPLIER', - }, - ], - title: 'Acme', - }, - ], - moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.counterparty', - tenantId, - }); + }); + const hits = yield* runtime.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'acme', + resourceType: 'party.registry.party', + tenantId, + }); + expect(hits).toEqual([]); + }); +}); - const result = yield* runtime.search({ - effectiveAt: '2026-09-03T00:00:00.000Z', - facets: [{ key: 'current-role', values: ['SUPPLIER'] }], - includeArchived: false, - moduleId: 'party.registry', - query: 'private@example.test', - resourceType: 'party.registry.counterparty', - selectedLegalEntityId: legalEntityId, - tenantId, - }); - expect(result).toEqual([ +it.effect('Core Search applies typed Legal Entity and role facets without returning match evidence', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const counterpartyRef = { + moduleId: 'party.registry', + resourceId: '40000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.counterparty', + tenantId, + } as const; + return Effect.gen(function* testTypedLegalEntityAndRoleFacets() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* store.replace({ + documents: [ { archived: false, facets: [], @@ -450,7 +351,9 @@ it.effect( value: ['CUSTOMER', 'SUPPLIER'], }, ], + projectionVersion: '1', ref: counterpartyRef, + searchableText: ['Acme', 'private@example.test'], selectedLegalEntityId: legalEntityId, subjectRef: partyRef, temporalFacets: [ @@ -467,156 +370,169 @@ it.effect( ], title: 'Acme', }, - ]); - expect(encodeJson(result)).not.toMatch(/private@example\.test/u); - expect( - yield* runtime.search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'acme', - resourceType: 'party.registry.counterparty', - tenantId, - }) - ).toEqual([]); + ], + moduleId: 'party.registry', + rebuildVersion: '1', + resourceType: 'party.registry.counterparty', + tenantId, }); - } -); -it.effect( - 'Core Search rejects malformed or cross-owner rebuild documents without partial replacement', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - return Effect.gen(function* testMalformedRebuildDocuments() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* store.replace({ - documents: [party()], + const result = yield* runtime.search({ + effectiveAt: '2026-09-03T00:00:00.000Z', + facets: [{ key: 'current-role', values: ['SUPPLIER'] }], + includeArchived: false, + moduleId: 'party.registry', + query: 'private@example.test', + resourceType: 'party.registry.counterparty', + selectedLegalEntityId: legalEntityId, + tenantId, + }); + expect(result).toEqual([ + { + archived: false, + facets: [], + matchedSubjectRef: aliasRef, + metadata: [ + { + key: 'current-roles', + kind: 'strings', + value: ['CUSTOMER', 'SUPPLIER'], + }, + ], + ref: counterpartyRef, + selectedLegalEntityId: legalEntityId, + subjectRef: partyRef, + temporalFacets: [ + { + key: 'current-role', + validFrom: '2026-01-01T00:00:00.000Z', + value: 'CUSTOMER', + }, + { + key: 'current-role', + validFrom: '2026-02-01T00:00:00.000Z', + value: 'SUPPLIER', + }, + ], + title: 'Acme', + }, + ]); + expect(encodeJson(result)).not.toMatch(/private@example\.test/u); + expect( + yield* runtime.search({ + includeArchived: false, moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.party', + query: 'acme', + resourceType: 'party.registry.counterparty', tenantId, - }); + }), + ).toEqual([]); + }); +}); - const failure = yield* Effect.flip( - store.replace({ - documents: [ - party({ ref: { ...partyRef, moduleId: 'foreign.module' } }), - ], - moduleId: 'party.registry', - rebuildVersion: '1', - resourceType: 'party.registry.party', - tenantId, - }) - ); - expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe( - true - ); - const result = yield* runtime.search({ - includeArchived: false, +it.effect('Core Search rejects malformed or cross-owner rebuild documents without partial replacement', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testMalformedRebuildDocuments() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* store.replace({ + documents: [party()], + moduleId: 'party.registry', + rebuildVersion: '1', + resourceType: 'party.registry.party', + tenantId, + }); + + const failure = yield* Effect.flip( + store.replace({ + documents: [party({ ref: { ...partyRef, moduleId: 'foreign.module' } })], moduleId: 'party.registry', - query: 'acme', + rebuildVersion: '1', resourceType: 'party.registry.party', tenantId, - }); - expect(result.length).toBe(1); + }), + ); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + const result = yield* runtime.search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'acme', + resourceType: 'party.registry.party', + tenantId, }); - } -); + expect(result.length).toBe(1); + }); +}); -it.effect( - 'Core Search makes duplicate and out-of-order lifecycle observations harmless', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const versionTwo = party({ - projectionVersion: '2', - title: 'Current title', +it.effect('Core Search makes duplicate and out-of-order lifecycle observations harmless', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const versionTwo = party({ + projectionVersion: '2', + title: 'Current title', + }); + return Effect.gen(function* testDuplicateLifecycleObservations() { + const runtime = yield* createCoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchProjectionStore, store)); + yield* store.apply({ document: versionTwo, kind: 'upsert' }); + yield* store.apply({ document: versionTwo, kind: 'upsert' }); + yield* store.apply({ + document: party({ projectionVersion: '1', title: 'Stale title' }), + kind: 'upsert', }); - return Effect.gen(function* testDuplicateLifecycleObservations() { - const runtime = yield* createCoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchProjectionStore, store) - ); - yield* store.apply({ document: versionTwo, kind: 'upsert' }); - yield* store.apply({ document: versionTwo, kind: 'upsert' }); - yield* store.apply({ - document: party({ projectionVersion: '1', title: 'Stale title' }), - kind: 'upsert', - }); - yield* store.apply({ - kind: 'delete', - projectionVersion: '3', - ref: partyRef, - }); - yield* store.apply({ document: versionTwo, kind: 'upsert' }); - - expect( - yield* runtime.search({ - includeArchived: true, - moduleId: 'party.registry', - query: 'current', - resourceType: 'party.registry.party', - tenantId, - }) - ).toEqual([]); + yield* store.apply({ + kind: 'delete', + projectionVersion: '3', + ref: partyRef, }); - } -); + yield* store.apply({ document: versionTwo, kind: 'upsert' }); -it.effect( - 'native projection services preserve keys and provided identity', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - return Effect.gen(function* testNativeServiceIdentity() { - const runtime = yield* createCoreSearchQueryRuntime; - expect(CoreSearchProjectionStore.key).toBe( - '@app/core-runtime/search/projection/CoreSearchProjectionStore' - ); - expect(CoreSearchQueryRuntime.key).toBe( - '@app/core-runtime/search/projection/CoreSearchQueryRuntime' - ); - expect(yield* CoreSearchProjectionStore).toBe(store); - expect( - yield* CoreSearchQueryRuntime.pipe( - Effect.provideService(CoreSearchQueryRuntime, runtime) - ) - ).toBe(runtime); - }).pipe(Effect.provideService(CoreSearchProjectionStore, store)); - } -); - -it.effect( - 'native projection services compose store writes with query reads', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - return Effect.gen(function* testNativeProjectionComposition() { - const providedStore = yield* CoreSearchProjectionStore; - const runtime = yield* CoreSearchQueryRuntime; - yield* providedStore.apply({ document: party(), kind: 'upsert' }); - const hits = yield* runtime.search({ - includeArchived: false, - moduleId: partyRef.moduleId, - query: 'acme', - resourceType: partyRef.resourceType, + expect( + yield* runtime.search({ + includeArchived: true, + moduleId: 'party.registry', + query: 'current', + resourceType: 'party.registry.party', tenantId, - }); - expect(hits.length).toBe(1); - expect(hits[0]?.ref).toEqual(partyRef); - }).pipe( - Effect.provideServiceEffect( - CoreSearchQueryRuntime, - createCoreSearchQueryRuntime - ), - Effect.provideService(CoreSearchProjectionStore, store) - ); - } -); + }), + ).toEqual([]); + }); +}); + +it.effect('native projection services preserve keys and provided identity', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testNativeServiceIdentity() { + const runtime = yield* createCoreSearchQueryRuntime; + expect(CoreSearchProjectionStore.key).toBe('@app/core-runtime/search/projection/CoreSearchProjectionStore'); + expect(CoreSearchQueryRuntime.key).toBe('@app/core-runtime/search/projection/CoreSearchQueryRuntime'); + expect(yield* CoreSearchProjectionStore).toBe(store); + expect(yield* CoreSearchQueryRuntime.pipe(Effect.provideService(CoreSearchQueryRuntime, runtime))).toBe(runtime); + }).pipe(Effect.provideService(CoreSearchProjectionStore, store)); +}); + +it.effect('native projection services compose store writes with query reads', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + return Effect.gen(function* testNativeProjectionComposition() { + const providedStore = yield* CoreSearchProjectionStore; + const runtime = yield* CoreSearchQueryRuntime; + yield* providedStore.apply({ document: party(), kind: 'upsert' }); + const hits = yield* runtime.search({ + includeArchived: false, + moduleId: partyRef.moduleId, + query: 'acme', + resourceType: partyRef.resourceType, + tenantId, + }); + expect(hits.length).toBe(1); + expect(hits[0]?.ref).toEqual(partyRef); + }).pipe( + Effect.provideServiceEffect(CoreSearchQueryRuntime, createCoreSearchQueryRuntime), + Effect.provideService(CoreSearchProjectionStore, store), + ); +}); const searchThroughNativeService = ( - input: Parameters[0] + input: Parameters[0], ): Effect.Effect< readonly CoreSearchProjectionHit[], - | CoreSearchProjectionInvalid - | InstanceType, + CoreSearchProjectionInvalid | InstanceType, CoreSearchQueryRuntime > => Effect.gen(function* searchNativeProjection() { @@ -624,41 +540,33 @@ const searchThroughNativeService = ( return yield* runtime.search(input); }); -it.effect( - 'native projection services retain invalid and unavailable failures', - () => { - const unavailable = new CoreSearchProjectionUnavailable({ - code: 'core_search_projection_unavailable', - reason: 'Projection test store unavailable', - }); - const store: CoreSearchProjectionStoreService = { - ...makeInMemoryCoreSearchProjectionStore(), - queryCandidates: () => Effect.fail(unavailable), - }; - return Effect.gen(function* testNativeProjectionFailures() { - const providedStore = yield* CoreSearchProjectionStore; - const invalid = yield* Effect.flip( - providedStore.apply({ kind: 'invalid' }) - ); - expect(Schema.is(CoreSearchProjectionInvalid)(invalid)).toBe(true); - const invalidQuery = yield* Effect.flip(searchThroughNativeService({})); - expect(Schema.is(CoreSearchProjectionInvalid)(invalidQuery)).toBe(true); - const failedQuery = yield* Effect.flip( - searchThroughNativeService({ - includeArchived: false, - moduleId: partyRef.moduleId, - query: 'acme', - resourceType: partyRef.resourceType, - tenantId, - }) - ); - expect(failedQuery).toBe(unavailable); - }).pipe( - Effect.provideServiceEffect( - CoreSearchQueryRuntime, - createCoreSearchQueryRuntime - ), - Effect.provideService(CoreSearchProjectionStore, store) +it.effect('native projection services retain invalid and unavailable failures', () => { + const unavailable = new CoreSearchProjectionUnavailable({ + code: 'core_search_projection_unavailable', + reason: 'Projection test store unavailable', + }); + const store: CoreSearchProjectionStoreService = { + ...makeInMemoryCoreSearchProjectionStore(), + queryCandidates: () => Effect.fail(unavailable), + }; + return Effect.gen(function* testNativeProjectionFailures() { + const providedStore = yield* CoreSearchProjectionStore; + const invalid = yield* Effect.flip(providedStore.apply({ kind: 'invalid' })); + expect(Schema.is(CoreSearchProjectionInvalid)(invalid)).toBe(true); + const invalidQuery = yield* Effect.flip(searchThroughNativeService({})); + expect(Schema.is(CoreSearchProjectionInvalid)(invalidQuery)).toBe(true); + const failedQuery = yield* Effect.flip( + searchThroughNativeService({ + includeArchived: false, + moduleId: partyRef.moduleId, + query: 'acme', + resourceType: partyRef.resourceType, + tenantId, + }), ); - } -); + expect(failedQuery).toBe(unavailable); + }).pipe( + Effect.provideServiceEffect(CoreSearchQueryRuntime, createCoreSearchQueryRuntime), + Effect.provideService(CoreSearchProjectionStore, store), + ); +}); diff --git a/app/packages/core-runtime/tests/unit/search-schema.test.ts b/app/packages/core-runtime/tests/unit/search-schema.test.ts index 14bd92237..fe422a658 100644 --- a/app/packages/core-runtime/tests/unit/search-schema.test.ts +++ b/app/packages/core-runtime/tests/unit/search-schema.test.ts @@ -1,11 +1,7 @@ import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; import { expect, it } from 'effect-rstest'; -import { - searchIndexEntries, - searchProjectionGenerations, - searchProjectionRebuilds, -} from '../../src/db/schema.ts'; +import { searchIndexEntries, searchProjectionGenerations, searchProjectionRebuilds } from '../../src/db/schema.ts'; const config = getTableConfig(searchIndexEntries); @@ -17,11 +13,7 @@ it('Core Search rebuild floors are tenant/resource-scoped and cannot be deleted 'source_module_key', 'source_resource_type', ]); - expect(rebuilds.policies.map(({ for: operation }) => operation)).toEqual([ - 'select', - 'insert', - 'update', - ]); + expect(rebuilds.policies.map(({ for: operation }) => operation)).toEqual(['select', 'insert', 'update']); expect(rebuilds.checks.map(({ name }) => name)).toEqual([ 'core_search_projection_rebuilds_version_ck', 'core_search_projection_rebuilds_fingerprint_ck', @@ -31,21 +23,10 @@ it('Core Search rebuild floors are tenant/resource-scoped and cannot be deleted it('Core Search snapshot generations are independent tenant/source-scoped infrastructure', () => { const generations = getTableConfig(searchProjectionGenerations); expect(generations.enableRLS).toBe(true); - expect(generations.primaryKeys[0]?.columns.map(({ name }) => name)).toEqual([ - 'tenant_id', - 'source_module_key', - ]); - expect(generations.policies.map(({ for: operation }) => operation)).toEqual([ - 'select', - 'insert', - 'update', - ]); - expect(generations.columns.some(({ name }) => name === 'generation')).toBe( - true - ); - expect( - generations.columns.some(({ name }) => name === 'event_watermark') - ).toBe(true); + expect(generations.primaryKeys[0]?.columns.map(({ name }) => name)).toEqual(['tenant_id', 'source_module_key']); + expect(generations.policies.map(({ for: operation }) => operation)).toEqual(['select', 'insert', 'update']); + expect(generations.columns.some(({ name }) => name === 'generation')).toBe(true); + expect(generations.columns.some(({ name }) => name === 'event_watermark')).toBe(true); }); it('Core Search physical projection has versioned tenant-qualified lookup keys', () => { @@ -53,30 +34,22 @@ it('Core Search physical projection has versioned tenant-qualified lookup keys', expect( config.columns .filter(({ name }) => ['deleted', 'projection_version'].includes(name)) - .map(({ name, notNull }) => ({ name, notNull })) + .map(({ name, notNull }) => ({ name, notNull })), ).toEqual([ { name: 'deleted', notNull: true }, { name: 'projection_version', notNull: true }, ]); - const source = config.indexes.find( - ({ config: index }) => index.name === 'core_search_index_entries_source_uk' - ); + const source = config.indexes.find(({ config: index }) => index.name === 'core_search_index_entries_source_uk'); expect(source?.config.unique).toBe(true); - expect( - source?.config.columns.map((column) => 'name' in column && column.name) - ).toEqual([ + expect(source?.config.columns.map((column) => 'name' in column && column.name)).toEqual([ 'tenant_id', 'source_module_key', 'source_resource_type', 'source_resource_id', ]); - const query = config.indexes.find( - ({ config: index }) => index.name === 'core_search_index_entries_query_idx' - ); + const query = config.indexes.find(({ config: index }) => index.name === 'core_search_index_entries_query_idx'); expect(query).toBeDefined(); - expect( - query?.config.columns.map((column) => 'name' in column && column.name) - ).toEqual([ + expect(query?.config.columns.map((column) => 'name' in column && column.name)).toEqual([ 'tenant_id', 'source_module_key', 'source_resource_type', @@ -92,24 +65,15 @@ it('Core Search projection declares complete tenant RLS and bounded document che 'core_search_index_entries_tenant_update', 'core_search_index_entries_tenant_delete', ]); - expect(config.policies.map((policy) => policy.for)).toEqual([ - 'select', - 'insert', - 'update', - 'delete', - ]); + expect(config.policies.map((policy) => policy.for)).toEqual(['select', 'insert', 'update', 'delete']); expect(config.policies.every(({ to }) => to === 'ontos_runtime')).toBe(true); const dialect = new PgDialect(); const checks = config.checks.map(({ name, value }) => ({ name, sql: dialect.sqlToQuery(value).sql, })); - expect( - checks.find(({ name }) => name === 'core_search_index_entries_document_ck') - ?.sql ?? '' - ).toMatch(/body_text/u); - expect( - checks.find(({ name }) => name === 'core_search_index_entries_version_ck') - ?.sql ?? '' - ).toMatch(/projection_version/u); + expect(checks.find(({ name }) => name === 'core_search_index_entries_document_ck')?.sql ?? '').toMatch(/body_text/u); + expect(checks.find(({ name }) => name === 'core_search_index_entries_version_ck')?.sql ?? '').toMatch( + /projection_version/u, + ); }); diff --git a/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts index 254c5d88d..392aa50bb 100644 --- a/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts +++ b/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts @@ -3,10 +3,7 @@ import { expect, it } from 'effect-rstest'; import { attestOutboxWorkerHandlerContext } from '../../src/outbox/definition.ts'; import { CoreSearchProjectionUnavailable } from '../../src/search/projection.ts'; -import { - makeCoreSearchWorkerSnapshot, - retryCoreSearchSnapshot, -} from '../../src/search/worker-snapshot.ts'; +import { makeCoreSearchWorkerSnapshot, retryCoreSearchSnapshot } from '../../src/search/worker-snapshot.ts'; import type { CoreSearchSnapshotBackend, CoreSearchSnapshotReadExecutor, @@ -35,14 +32,11 @@ const executor: CoreSearchSnapshotReadExecutor = { }, }; -class SnapshotRetryFailure extends Schema.TaggedError()( - 'SnapshotRetryFailure', - { - cause: Schema.optional(Schema.Unknown), - code: Schema.optional(Schema.String), - message: Schema.String, - } -) {} +class SnapshotRetryFailure extends Schema.TaggedError()('SnapshotRetryFailure', { + cause: Schema.optional(Schema.Unknown), + code: Schema.optional(Schema.String), + message: Schema.String, +}) {} const readParty = (readExecutor: CoreSearchSnapshotReadExecutor) => { expect(Object.keys(readExecutor)).toEqual(['select']); @@ -57,317 +51,251 @@ const readCounterparty = (readExecutor: CoreSearchSnapshotReadExecutor) => { const readInvalid = () => Effect.succeed('invalid'); const readStillInvalid = () => Effect.succeed('still invalid'); -it.effect( - 'worker snapshot rejects caller-created and unregistered contexts before opening persistence', - () => { - let calls = 0; - const backend: CoreSearchSnapshotBackend = { - run: () => { - calls += 1; - return Effect.die(new Error('unreachable')); - }, - }; - const snapshot = makeCoreSearchWorkerSnapshot(backend); - return Effect.gen(function* rejectInvalidWorkerContexts() { - const failures = yield* Effect.forEach( - [ - context, - attestOutboxWorkerHandlerContext({ - ...context, - workerKey: 'party.registry.unregistered', - }), - attestOutboxWorkerHandlerContext({ - ...context, - producerModuleKey: 'foreign.module', - }), - ], - (candidate) => - Effect.flip( - snapshot.read(candidate, () => Effect.succeed('unreachable')) - ), - { concurrency: 'unbounded' } - ); - expect(failures.length).toBe(3); - for (const failure of failures) { - expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe( - true - ); - } - expect(calls).toBe(0); - }); - } -); - -it.effect( - 'identifier-update worker receives the verified Core snapshot capability', - () => { - const reader = makeCoreSearchWorkerSnapshot({ - run: (_context, readSnapshot) => - readSnapshot( - { - eventWatermark: '3', - legalEntityIds: [], - projectionVersion: '1', - tenantId, - }, - executor, - () => Effect.void - ), - }); - return Effect.gen(function* readVerifiedWorkerSnapshot() { - const version = yield* reader.read( +it.effect('worker snapshot rejects caller-created and unregistered contexts before opening persistence', () => { + let calls = 0; + const backend: CoreSearchSnapshotBackend = { + run: () => { + calls += 1; + return Effect.die(new Error('unreachable')); + }, + }; + const snapshot = makeCoreSearchWorkerSnapshot(backend); + return Effect.gen(function* rejectInvalidWorkerContexts() { + const failures = yield* Effect.forEach( + [ + context, attestOutboxWorkerHandlerContext({ ...context, - topic: 'party.registry.official-identifier-updated.v1', - workerKey: - 'party.registry.project-official-identifier-updated-to-search', + workerKey: 'party.registry.unregistered', }), - (snapshot) => Effect.succeed(snapshot.projectionVersion) - ); - expect(version).toBe('1'); - }); - } -); + attestOutboxWorkerHandlerContext({ + ...context, + producerModuleKey: 'foreign.module', + }), + ], + (candidate) => Effect.flip(snapshot.read(candidate, () => Effect.succeed('unreachable'))), + { concurrency: 'unbounded' }, + ); + expect(failures.length).toBe(3); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + expect(calls).toBe(0); + }); +}); -it.effect( - 'worker snapshot exposes select-only owner reads at one current watermark and restores scope', - () => { - const installedScopes: (string | undefined)[] = []; - const backend: CoreSearchSnapshotBackend = { - run: (_context, readSnapshot) => - readSnapshot( - { - eventWatermark: '100', - legalEntityIds: [legalEntityId], - projectionVersion: '42', - tenantId, - }, - executor, - (scope) => { - installedScopes.push(scope); - return Effect.void; - } - ), - }; - const snapshot = makeCoreSearchWorkerSnapshot(backend); - return Effect.gen(function* readCurrentOwnerSnapshot() { - const result = yield* snapshot.read( - attestOutboxWorkerHandlerContext(context), - (view) => - Effect.gen(function* readOwnerProjection() { - expect(view.projectionVersion).toBe('42'); - expect(view.eventWatermark).toBe('100'); - expect(view.tenantId).toBe(tenantId); - expect(view.legalEntityIds).toEqual([legalEntityId]); - const party = yield* view.tenant(readParty); - const counterparty = yield* view.forLegalEntity( - legalEntityId, - readCounterparty - ); - return { - counterparty, - party, - projectionVersion: view.projectionVersion, - }; - }) - ); - expect(result).toEqual({ - counterparty: 'counterparty', - party: 'party', - projectionVersion: '42', - }); - expect(installedScopes).toEqual([ - undefined, - undefined, - legalEntityId, - undefined, - ]); - }); - } -); +it.effect('identifier-update worker receives the verified Core snapshot capability', () => { + const reader = makeCoreSearchWorkerSnapshot({ + run: (_context, readSnapshot) => + readSnapshot( + { + eventWatermark: '3', + legalEntityIds: [], + projectionVersion: '1', + tenantId, + }, + executor, + () => Effect.void, + ), + }); + return Effect.gen(function* readVerifiedWorkerSnapshot() { + const version = yield* reader.read( + attestOutboxWorkerHandlerContext({ + ...context, + topic: 'party.registry.official-identifier-updated.v1', + workerKey: 'party.registry.project-official-identifier-updated-to-search', + }), + (snapshot) => Effect.succeed(snapshot.projectionVersion), + ); + expect(version).toBe('1'); + }); +}); -it.effect( - 'worker snapshot rejects a Legal Entity outside its tenant enumeration and preserves owner failures', - () => { - const installedScopes: (string | undefined)[] = []; - const backend: CoreSearchSnapshotBackend = { - run: (_context, readSnapshot) => - readSnapshot( - { - eventWatermark: '100', - legalEntityIds: [legalEntityId], - projectionVersion: '42', - tenantId, - }, - executor, - (scope) => { - installedScopes.push(scope); - return Effect.void; - } - ), - }; - const snapshot = makeCoreSearchWorkerSnapshot(backend); - const verified = attestOutboxWorkerHandlerContext(context); - return Effect.gen(function* rejectInvalidAndPreserveOwnerFailure() { - const invalidScope = yield* Effect.flip( - snapshot.read(verified, (view) => - view.forLegalEntity('20000000-0000-4000-8000-000000000002', () => - Effect.succeed('no') - ) - ) - ); - expect( - Predicate.isTagged(invalidScope, 'CoreSearchProjectionInvalid') - ).toBe(true); - expect(installedScopes).toEqual([]); - const failure = yield* Effect.flip( - snapshot.read(verified, (view) => - view.forLegalEntity(legalEntityId, () => - Effect.fail('owner-unavailable') - ) - ) - ); - expect(failure).toBe('owner-unavailable'); - expect(installedScopes).toEqual([legalEntityId, undefined]); +it.effect('worker snapshot exposes select-only owner reads at one current watermark and restores scope', () => { + const installedScopes: (string | undefined)[] = []; + const backend: CoreSearchSnapshotBackend = { + run: (_context, readSnapshot) => + readSnapshot( + { + eventWatermark: '100', + legalEntityIds: [legalEntityId], + projectionVersion: '42', + tenantId, + }, + executor, + (scope) => { + installedScopes.push(scope); + return Effect.void; + }, + ), + }; + const snapshot = makeCoreSearchWorkerSnapshot(backend); + return Effect.gen(function* readCurrentOwnerSnapshot() { + const result = yield* snapshot.read(attestOutboxWorkerHandlerContext(context), (view) => + Effect.gen(function* readOwnerProjection() { + expect(view.projectionVersion).toBe('42'); + expect(view.eventWatermark).toBe('100'); + expect(view.tenantId).toBe(tenantId); + expect(view.legalEntityIds).toEqual([legalEntityId]); + const party = yield* view.tenant(readParty); + const counterparty = yield* view.forLegalEntity(legalEntityId, readCounterparty); + return { + counterparty, + party, + projectionVersion: view.projectionVersion, + }; + }), + ); + expect(result).toEqual({ + counterparty: 'counterparty', + party: 'party', + projectionVersion: '42', }); - } -); + expect(installedScopes).toEqual([undefined, undefined, legalEntityId, undefined]); + }); +}); -it.effect( - 'worker snapshot maps persistence failure to a sanitized unavailable error', - () => { - const snapshot = makeCoreSearchWorkerSnapshot({ - run: () => - Effect.fail( - new CoreSearchProjectionUnavailable({ - cause: new Error('private database details'), - code: 'core_search_projection_unavailable', - reason: 'Core Search worker snapshot is temporarily unavailable', - }) - ), - }); - return Effect.gen(function* mapPersistenceFailure() { - const failure = yield* Effect.flip( - snapshot.read(attestOutboxWorkerHandlerContext(context), () => - Effect.succeed('no') - ) - ); - expect( - Predicate.isTagged(failure, 'CoreSearchProjectionUnavailable') - ).toBe(true); - expect(failure.reason).not.toMatch(/private database/u); - }); - } -); +it.effect('worker snapshot rejects a Legal Entity outside its tenant enumeration and preserves owner failures', () => { + const installedScopes: (string | undefined)[] = []; + const backend: CoreSearchSnapshotBackend = { + run: (_context, readSnapshot) => + readSnapshot( + { + eventWatermark: '100', + legalEntityIds: [legalEntityId], + projectionVersion: '42', + tenantId, + }, + executor, + (scope) => { + installedScopes.push(scope); + return Effect.void; + }, + ), + }; + const snapshot = makeCoreSearchWorkerSnapshot(backend); + const verified = attestOutboxWorkerHandlerContext(context); + return Effect.gen(function* rejectInvalidAndPreserveOwnerFailure() { + const invalidScope = yield* Effect.flip( + snapshot.read(verified, (view) => + view.forLegalEntity('20000000-0000-4000-8000-000000000002', () => Effect.succeed('no')), + ), + ); + expect(Predicate.isTagged(invalidScope, 'CoreSearchProjectionInvalid')).toBe(true); + expect(installedScopes).toEqual([]); + const failure = yield* Effect.flip( + snapshot.read(verified, (view) => view.forLegalEntity(legalEntityId, () => Effect.fail('owner-unavailable'))), + ); + expect(failure).toBe('owner-unavailable'); + expect(installedScopes).toEqual([legalEntityId, undefined]); + }); +}); -it.effect( - 'snapshot generation retries serialization conflicts only and bounds repeated contention', - () => { - let attempts = 0; - return Effect.gen(function* retrySerializationFailures() { - const snapshot = yield* Effect.suspend(() => { - attempts += 1; - return attempts < 3 - ? Effect.fail( - new SnapshotRetryFailure({ - cause: { code: '40001' }, - message: 'wrapped serialization', - }) - ) - : Effect.succeed('fresh snapshot'); - }).pipe(retryCoreSearchSnapshot); - expect(snapshot).toBe('fresh snapshot'); - expect(attempts).toBe(3); +it.effect('worker snapshot maps persistence failure to a sanitized unavailable error', () => { + const snapshot = makeCoreSearchWorkerSnapshot({ + run: () => + Effect.fail( + new CoreSearchProjectionUnavailable({ + cause: new Error('private database details'), + code: 'core_search_projection_unavailable', + reason: 'Core Search worker snapshot is temporarily unavailable', + }), + ), + }); + return Effect.gen(function* mapPersistenceFailure() { + const failure = yield* Effect.flip( + snapshot.read(attestOutboxWorkerHandlerContext(context), () => Effect.succeed('no')), + ); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionUnavailable')).toBe(true); + expect(failure.reason).not.toMatch(/private database/u); + }); +}); - attempts = 0; - const contention = yield* Effect.flip( - Effect.suspend(() => { - attempts += 1; - return Effect.fail( - new SnapshotRetryFailure({ code: '40001', message: 'contention' }) - ); - }).pipe(retryCoreSearchSnapshot) - ); - expect(contention.message).toMatch(/contention/u); - expect(attempts).toBe(4); +it.effect('snapshot generation retries serialization conflicts only and bounds repeated contention', () => { + let attempts = 0; + return Effect.gen(function* retrySerializationFailures() { + const snapshot = yield* Effect.suspend(() => { + attempts += 1; + return attempts < 3 + ? Effect.fail( + new SnapshotRetryFailure({ + cause: { code: '40001' }, + message: 'wrapped serialization', + }), + ) + : Effect.succeed('fresh snapshot'); + }).pipe(retryCoreSearchSnapshot); + expect(snapshot).toBe('fresh snapshot'); + expect(attempts).toBe(3); - attempts = 0; - const nonSerialization = yield* Effect.flip( - Effect.suspend(() => { - attempts += 1; - return Effect.fail( - new SnapshotRetryFailure({ message: 'not serialization' }) - ); - }).pipe(retryCoreSearchSnapshot) - ); - expect(nonSerialization.message).toMatch(/not serialization/u); - expect(attempts).toBe(1); - }); - } -); + attempts = 0; + const contention = yield* Effect.flip( + Effect.suspend(() => { + attempts += 1; + return Effect.fail(new SnapshotRetryFailure({ code: '40001', message: 'contention' })); + }).pipe(retryCoreSearchSnapshot), + ); + expect(contention.message).toMatch(/contention/u); + expect(attempts).toBe(4); -it.effect( - 'snapshot revokes escaped scope capabilities when the owner callback finishes', - () => { - const reader = makeCoreSearchWorkerSnapshot({ - run: (_context, readSnapshot) => - readSnapshot( - { - eventWatermark: '3', - legalEntityIds: [legalEntityId], - projectionVersion: '1', - tenantId, - }, - executor, - () => Effect.void - ), - }); - return Effect.gen(function* rejectEscapedCapability() { - const escaped: CoreSearchWorkerSnapshotView = yield* reader.read( - attestOutboxWorkerHandlerContext(context), - Effect.succeed - ); - const failure = yield* Effect.flip( - escaped.tenant(() => Effect.succeed('stale')) - ); - expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe( - true - ); - }); - } -); + attempts = 0; + const nonSerialization = yield* Effect.flip( + Effect.suspend(() => { + attempts += 1; + return Effect.fail(new SnapshotRetryFailure({ message: 'not serialization' })); + }).pipe(retryCoreSearchSnapshot), + ); + expect(nonSerialization.message).toMatch(/not serialization/u); + expect(attempts).toBe(1); + }); +}); -it.effect( - 'nested scope rejection does not unlock the active owner read', - () => { - const reader = makeCoreSearchWorkerSnapshot({ - run: (_context, readSnapshot) => - readSnapshot( - { - eventWatermark: '3', - legalEntityIds: [legalEntityId], - projectionVersion: '1', - tenantId, - }, - executor, - () => Effect.void - ), - }); - return reader.read(attestOutboxWorkerHandlerContext(context), (snapshot) => - snapshot.tenant(() => - Effect.gen(function* nestedReads() { - const first = yield* Effect.flip( - snapshot.forLegalEntity(legalEntityId, readInvalid) - ); - const second = yield* Effect.flip(snapshot.tenant(readStillInvalid)); - expect(Predicate.isTagged(first, 'CoreSearchProjectionInvalid')).toBe( - true - ); - expect( - Predicate.isTagged(second, 'CoreSearchProjectionInvalid') - ).toBe(true); - }) - ) +it.effect('snapshot revokes escaped scope capabilities when the owner callback finishes', () => { + const reader = makeCoreSearchWorkerSnapshot({ + run: (_context, readSnapshot) => + readSnapshot( + { + eventWatermark: '3', + legalEntityIds: [legalEntityId], + projectionVersion: '1', + tenantId, + }, + executor, + () => Effect.void, + ), + }); + return Effect.gen(function* rejectEscapedCapability() { + const escaped: CoreSearchWorkerSnapshotView = yield* reader.read( + attestOutboxWorkerHandlerContext(context), + Effect.succeed, ); - } -); + const failure = yield* Effect.flip(escaped.tenant(() => Effect.succeed('stale'))); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + }); +}); + +it.effect('nested scope rejection does not unlock the active owner read', () => { + const reader = makeCoreSearchWorkerSnapshot({ + run: (_context, readSnapshot) => + readSnapshot( + { + eventWatermark: '3', + legalEntityIds: [legalEntityId], + projectionVersion: '1', + tenantId, + }, + executor, + () => Effect.void, + ), + }); + return reader.read(attestOutboxWorkerHandlerContext(context), (snapshot) => + snapshot.tenant(() => + Effect.gen(function* nestedReads() { + const first = yield* Effect.flip(snapshot.forLegalEntity(legalEntityId, readInvalid)); + const second = yield* Effect.flip(snapshot.tenant(readStillInvalid)); + expect(Predicate.isTagged(first, 'CoreSearchProjectionInvalid')).toBe(true); + expect(Predicate.isTagged(second, 'CoreSearchProjectionInvalid')).toBe(true); + }), + ), + ); +}); diff --git a/app/packages/core-runtime/tests/unit/service-public-surface.test.ts b/app/packages/core-runtime/tests/unit/service-public-surface.test.ts index bb95dda7f..a2252e303 100644 --- a/app/packages/core-runtime/tests/unit/service-public-surface.test.ts +++ b/app/packages/core-runtime/tests/unit/service-public-surface.test.ts @@ -23,9 +23,7 @@ type PublicServiceContract = | SupportRecoveryPrincipalContextResolverService | TenantModuleStateServiceContract; -const preservePublicServiceContract = ( - service: Service -): Service => service; +const preservePublicServiceContract = (service: Service): Service => service; it('exports the anti-slop-compliant Core service contracts', () => { expect(preservePublicServiceContract.length).toBe(1); diff --git a/app/packages/core-runtime/tests/unit/shell-contribution.test.ts b/app/packages/core-runtime/tests/unit/shell-contribution.test.ts index 49b334991..b67c1df61 100644 --- a/app/packages/core-runtime/tests/unit/shell-contribution.test.ts +++ b/app/packages/core-runtime/tests/unit/shell-contribution.test.ts @@ -13,9 +13,7 @@ const first = (values: readonly Value[]): Value => { } return value; }; -const entrypoint = ( - role: 'api' | 'page' | 'public_component' | 'report' | 'search' -) => ({ +const entrypoint = (role: 'api' | 'page' | 'public_component' | 'report' | 'search') => ({ access: 'read' as const, authorization: { kind: 'context_permission' as const, @@ -128,23 +126,17 @@ it('accepts safe dynamic page templates as plain serialized data', () => { const decoded = validateShellContributions(dynamic, references); expect(decoded.pages[0]?.routePath).toBe('/contacts/customers/:id/edit'); expect(structuredClone(decoded)).toEqual(decoded); - expect(encodeJson(decoded)).not.toMatch( - /handler|loader|sourcePath|remote|import/iu - ); + expect(encodeJson(decoded)).not.toMatch(/handler|loader|sourcePath|remote|import/iu); }); it('rejects extra keys, duplicates, cross-owner entrypoints, and missing references', () => { - expect(() => - validateShellContributions({ ...full(), route: '/private' }, references) - ).toThrow(); + expect(() => validateShellContributions({ ...full(), route: '/private' }, references)).toThrow(); const duplicate = full(); duplicate.publicComponents[0] = { ...first(duplicate.publicComponents), contributionKey: first(duplicate.pages).contributionKey, }; - expect(() => validateShellContributions(duplicate, references)).toThrow( - /duplicate/u - ); + expect(() => validateShellContributions(duplicate, references)).toThrow(/duplicate/u); const crossOwner = full(); crossOwner.pages[0] = { ...first(crossOwner.pages), @@ -153,14 +145,12 @@ it('rejects extra keys, duplicates, cross-owner entrypoints, and missing referen moduleKey: 'billing.core', }, }; - expect(() => validateShellContributions(crossOwner, references)).toThrow( - /owner/u - ); + expect(() => validateShellContributions(crossOwner, references)).toThrow(/owner/u); expect(() => validateShellContributions(full(), { ...references, componentKeys: new Set(), - }) + }), ).toThrow(); }); @@ -172,8 +162,8 @@ it('rejects incompatible entrypoint roles and arbitrary transport metadata', () ...baseline, search: [{ ...first(baseline.search), entrypoint: entrypoint('page') }], }, - references - ) + references, + ), ).toThrow(); expect(() => validateShellContributions( @@ -189,8 +179,8 @@ it('rejects incompatible entrypoint roles and arbitrary transport metadata', () }, ], }, - references - ) + references, + ), ).toThrow(); expect(() => validateShellContributions( @@ -206,8 +196,8 @@ it('rejects incompatible entrypoint roles and arbitrary transport metadata', () }, ], }, - references - ) + references, + ), ).toThrow(); expect(() => validateShellContributions( @@ -215,17 +205,15 @@ it('rejects incompatible entrypoint roles and arbitrary transport metadata', () ...baseline, pages: [{ ...first(baseline.pages), remote: 'private/remote' }], }, - references - ) + references, + ), ).toThrow(); const withUnsafeRoute = full(); withUnsafeRoute.pages[0] = { ...first(withUnsafeRoute.pages), routePath: '/modules/:module-id', }; - expect(() => - validateShellContributions(withUnsafeRoute, references) - ).toThrow(); + expect(() => validateShellContributions(withUnsafeRoute, references)).toThrow(); }); for (const routePath of [ diff --git a/app/packages/core-runtime/tests/unit/spicedb-client.test.ts b/app/packages/core-runtime/tests/unit/spicedb-client.test.ts index 0f889893f..1e37727b0 100644 --- a/app/packages/core-runtime/tests/unit/spicedb-client.test.ts +++ b/app/packages/core-runtime/tests/unit/spicedb-client.test.ts @@ -9,14 +9,14 @@ it('uses authenticated plaintext credentials for an explicitly insecure transpor spiceDbClientSecurity({ endpoint: 'localhost:50051', insecureLocal: true, - }) + }), ).toBe(v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS); expect( spiceDbClientSecurity({ deploymentEnvironment: 'stage', endpoint: 'spicedb:50051', insecureLocal: true, - }) + }), ).toBe(v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS); }); @@ -25,7 +25,7 @@ it('uses TLS credentials for a secure transport', () => { spiceDbClientSecurity({ endpoint: 'spicedb.internal.example:443', insecureLocal: false, - }) + }), ).toBe(v1.ClientSecurity.SECURE); }); @@ -39,8 +39,6 @@ it('rejects plaintext credentials for an arbitrary or non-stage endpoint', () => insecureLocal: true, }, ] as const) { - expect(() => spiceDbClientSecurity(configuration)).toThrow( - SpiceDbConfigError - ); + expect(() => spiceDbClientSecurity(configuration)).toThrow(SpiceDbConfigError); } }); diff --git a/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts b/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts index e37f6b3c3..a26f7b87d 100644 --- a/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts +++ b/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts @@ -12,9 +12,7 @@ const extractSchema = (source: string): string => source .slice( 'schema: |-\n'.length, - source.includes('\nrelationships: |-') - ? source.indexOf('\nrelationships: |-') - : source.length + source.includes('\nrelationships: |-') ? source.indexOf('\nrelationships: |-') : source.length, ) .trimEnd() .split('\n') @@ -26,7 +24,7 @@ it('accepts a distinct SpiceDB role and database on the administrative server', parseSpiceDbDatabaseBootstrapConfig({ DATABASE_ADMIN_URL: 'postgresql://db:admin@db:5432/db', SPICEDB_DATABASE_URL: 'postgresql://spicedb:p%40ssword@db:5432/spicedb', - }) + }), ).toEqual({ adminUrl: 'postgresql://db:admin@db:5432/db', database: 'spicedb', @@ -56,60 +54,44 @@ it('rejects unsafe SpiceDB database bootstrap targets', () => { }); it.layer(NodeFileSystem.layer)('SpiceDB bootstrap sources', (suite) => { - suite.effect( - 'keeps the stage bootstrap schema aligned without development relationships', - () => - Effect.gen(function* testScenario1() { - const fs = yield* FileSystem.FileSystem; - const development = yield* fs.readFileString( - fileURLToPath( - new URL('../../spicedb/bootstrap.yaml', import.meta.url) - ) - ); - const stage = yield* fs.readFileString( - fileURLToPath( - new URL('../../spicedb/stage-bootstrap.yaml', import.meta.url) - ) - ); - expect(extractSchema(development)).toBe(ONTOS_SPICEDB_SCHEMA); - expect(extractSchema(stage)).toBe(ONTOS_SPICEDB_SCHEMA); - expect(stage).not.toMatch(/relationships:|assertions:/u); - expect(development).toMatch(/#executor@tenant:test-tenant#member/u); - expect(development).toMatch(/#executor@principal:allowed-principal/u); - }) + suite.effect('keeps the stage bootstrap schema aligned without development relationships', () => + Effect.gen(function* testScenario1() { + const fs = yield* FileSystem.FileSystem; + const development = yield* fs.readFileString( + fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)), + ); + const stage = yield* fs.readFileString( + fileURLToPath(new URL('../../spicedb/stage-bootstrap.yaml', import.meta.url)), + ); + expect(extractSchema(development)).toBe(ONTOS_SPICEDB_SCHEMA); + expect(extractSchema(stage)).toBe(ONTOS_SPICEDB_SCHEMA); + expect(stage).not.toMatch(/relationships:|assertions:/u); + expect(development).toMatch(/#executor@tenant:test-tenant#member/u); + expect(development).toMatch(/#executor@principal:allowed-principal/u); + }), ); suite.effect('grants fresh development module access only to Contacts', () => Effect.gen(function* testScenario2() { const fs = yield* FileSystem.FileSystem; const development = yield* fs.readFileString( - fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)) + fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)), ); const tenantId = '50000000-0000-4000-8000-000000000001'; const legalEntityId = '55000000-0000-4000-8000-000000000001'; - const contactsObjectId = toModuleAccessObjectId( - tenantId, - legalEntityId, - 'contacts.core' - ); + const contactsObjectId = toModuleAccessObjectId(tenantId, legalEntityId, 'contacts.core'); + expect(contactsObjectId !== undefined && contactsObjectId.length > 0).toBe(true); expect( - contactsObjectId !== undefined && contactsObjectId.length > 0 - ).toBe(true); - expect( - development.match( - /^ {2}module_access:\S+#accessor@principal:60000000-0000-4000-8000-000000000001$/gmu - ) - ).toEqual([ - ` module_access:${contactsObjectId}#accessor@principal:60000000-0000-4000-8000-000000000001`, - ]); - }) + development.match(/^ {2}module_access:\S+#accessor@principal:60000000-0000-4000-8000-000000000001$/gmu), + ).toEqual([` module_access:${contactsObjectId}#accessor@principal:60000000-0000-4000-8000-000000000001`]); + }), ); suite.effect('declares the complete Party tenant permission vocabulary', () => Effect.gen(function* testScenario3() { const fs = yield* FileSystem.FileSystem; const development = yield* fs.readFileString( - fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)) + fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)), ); for (const permission of [ 'manage_party_identity', @@ -118,28 +100,20 @@ it.layer(NodeFileSystem.layer)('SpiceDB bootstrap sources', (suite) => { 'read_party_identity', 'review_party_identity', ]) { - expect(development).toMatch( - new RegExp(`permission ${permission} =`, 'u') - ); + expect(development).toMatch(new RegExp(`permission ${permission} =`, 'u')); } - }) + }), ); - suite.effect( - 'declares the Counterparty Legal Entity permission vocabulary', - () => - Effect.gen(function* testScenario4() { - const fs = yield* FileSystem.FileSystem; - const development = yield* fs.readFileString( - fileURLToPath( - new URL('../../spicedb/bootstrap.yaml', import.meta.url) - ) - ); - for (const permission of ['manage_counterparty', 'read_counterparty']) { - expect(development).toMatch( - new RegExp(`permission ${permission} =`, 'u') - ); - } - }) + suite.effect('declares the Counterparty Legal Entity permission vocabulary', () => + Effect.gen(function* testScenario4() { + const fs = yield* FileSystem.FileSystem; + const development = yield* fs.readFileString( + fileURLToPath(new URL('../../spicedb/bootstrap.yaml', import.meta.url)), + ); + for (const permission of ['manage_counterparty', 'read_counterparty']) { + expect(development).toMatch(new RegExp(`permission ${permission} =`, 'u')); + } + }), ); }); diff --git a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts index 9e3a06256..1d917af72 100644 --- a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts +++ b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts @@ -20,137 +20,117 @@ const resolverFor = (record: { load: () => Effect.succeedSome(record), }); -it.effect( - 'constructs one immutable trusted system context from a branded registration', - () => - Effect.gen(function* testScenario1() { - const registration = registerSystemWorkload({ - jobKey: 'inventory-reconcile', - }); - const context = yield* resolverFor({ +it.effect('constructs one immutable trusted system context from a branded registration', () => + Effect.gen(function* testScenario1() { + const registration = registerSystemWorkload({ + jobKey: 'inventory-reconcile', + }); + const context = yield* resolverFor({ + kind: 'system', + principalStatus: 'active', + tenantStatus: 'active', + }).resolve({ + principalId, + registration, + runReference: 'run-42', + tenantId, + }); + + expect(Object.isFrozen(registration)).toBe(true); + expect(Object.isFrozen(context)).toBe(true); + expect(context).toEqual({ + authContextRef: 'job:inventory-reconcile:run:run-42', + authMethod: 'system', + principalId, + tenantId, + }); + expect(yield* Schema.decodeEffect(TrustedPrincipalContextSchema)(context)).toEqual(context); + expect(yield* decodeTrustedPrincipalContext(context)).toEqual(context); + expect(yield* Effect.flip(decodeTrustedPrincipalContext({ ...context }))).toBeDefined(); + }), +); + +it.effect('rejects forged registrations, unsafe refs, wrong kinds, and inactive state', () => + Effect.gen(function* testScenario2() { + const registration = registerSystemWorkload({ + jobKey: 'inventory-reconcile', + }); + const forged = { ...registration }; + const invalid = yield* Effect.flip( + resolverFor({ kind: 'system', principalStatus: 'active', tenantStatus: 'active', + }).resolve({ + principalId, + registration: forged, + runReference: 'run-42', + tenantId, + }), + ); + const wrongKind = yield* Effect.flip( + resolverFor({ + kind: 'human', + principalStatus: 'active', + tenantStatus: 'active', }).resolve({ principalId, registration, runReference: 'run-42', tenantId, - }); - - expect(Object.isFrozen(registration)).toBe(true); - expect(Object.isFrozen(context)).toBe(true); - expect(context).toEqual({ - authContextRef: 'job:inventory-reconcile:run:run-42', - authMethod: 'system', + }), + ); + const inactive = yield* Effect.flip( + resolverFor({ + kind: 'system', + principalStatus: 'disabled', + tenantStatus: 'active', + }).resolve({ principalId, + registration, + runReference: 'run-42', tenantId, - }); - expect( - yield* Schema.decodeEffect(TrustedPrincipalContextSchema)(context) - ).toEqual(context); - expect(yield* decodeTrustedPrincipalContext(context)).toEqual(context); - expect( - yield* Effect.flip(decodeTrustedPrincipalContext({ ...context })) - ).toBeDefined(); - }) -); - -it.effect( - 'rejects forged registrations, unsafe refs, wrong kinds, and inactive state', - () => - Effect.gen(function* testScenario2() { - const registration = registerSystemWorkload({ - jobKey: 'inventory-reconcile', - }); - const forged = { ...registration }; - const invalid = yield* Effect.flip( - resolverFor({ - kind: 'system', - principalStatus: 'active', - tenantStatus: 'active', - }).resolve({ - principalId, - registration: forged, - runReference: 'run-42', - tenantId, - }) - ); - const wrongKind = yield* Effect.flip( - resolverFor({ - kind: 'human', - principalStatus: 'active', - tenantStatus: 'active', - }).resolve({ - principalId, - registration, - runReference: 'run-42', - tenantId, - }) - ); - const inactive = yield* Effect.flip( - resolverFor({ - kind: 'system', - principalStatus: 'disabled', - tenantStatus: 'active', - }).resolve({ - principalId, - registration, - runReference: 'run-42', - tenantId, - }) - ); + }), + ); - expect( - Predicate.isTagged(invalid, 'SystemPrincipalContextInvalidError') - ).toBe(true); - expect( - Predicate.isTagged(wrongKind, 'SystemPrincipalContextDeniedError') - ).toBe(true); - expect( - Predicate.isTagged(inactive, 'SystemPrincipalContextDeniedError') - ).toBe(true); - expect(() => registerSystemWorkload({ jobKey: 'unsafe:key' })).toThrow( - TypeError - ); - }) + expect(Predicate.isTagged(invalid, 'SystemPrincipalContextInvalidError')).toBe(true); + expect(Predicate.isTagged(wrongKind, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(Predicate.isTagged(inactive, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(() => registerSystemWorkload({ jobKey: 'unsafe:key' })).toThrow(TypeError); + }), ); -it.effect( - 'permits service principals only when the trusted registration opts in', - () => - Effect.gen(function* testScenario3() { - const denied = yield* Effect.flip( - resolverFor({ - kind: 'service', - principalStatus: 'active', - tenantStatus: 'active', - }).resolve({ - principalId, - registration: registerSystemWorkload({ jobKey: 'service-job' }), - runReference: 'run-1', - tenantId, - }) - ); - const allowed = yield* resolverFor({ +it.effect('permits service principals only when the trusted registration opts in', () => + Effect.gen(function* testScenario3() { + const denied = yield* Effect.flip( + resolverFor({ kind: 'service', principalStatus: 'active', tenantStatus: 'active', }).resolve({ principalId, - registration: registerSystemWorkload({ - allowServicePrincipal: true, - jobKey: 'service-job', - }), + registration: registerSystemWorkload({ jobKey: 'service-job' }), runReference: 'run-1', tenantId, - }); + }), + ); + const allowed = yield* resolverFor({ + kind: 'service', + principalStatus: 'active', + tenantStatus: 'active', + }).resolve({ + principalId, + registration: registerSystemWorkload({ + allowServicePrincipal: true, + jobKey: 'service-job', + }), + runReference: 'run-1', + tenantId, + }); - expect( - Predicate.isTagged(denied, 'SystemPrincipalContextDeniedError') - ).toBe(true); - expect(allowed.authMethod).toBe('system'); - }) + expect(Predicate.isTagged(denied, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(allowed.authMethod).toBe('system'); + }), ); it('enforces mode-specific trusted context cross-field invariants', () => { @@ -181,9 +161,7 @@ it('enforces mode-specific trusted context cross-field invariants', () => { }, ]; for (const context of valid) { - expect(() => - Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context) - ).not.toThrow(); + expect(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context)).not.toThrow(); } expect(() => Schema.decodeSync(TrustedPrincipalContextSchema)({ @@ -191,7 +169,7 @@ it('enforces mode-specific trusted context cross-field invariants', () => { authMethod: 'api_key', principalId, tenantId, - }) + }), ).toThrow(); expect(() => Schema.decodeSync(TrustedPrincipalContextSchema)({ @@ -201,6 +179,6 @@ it('enforces mode-specific trusted context cross-field invariants', () => { impersonatedByPrincipalId: principalId, principalId, tenantId, - }) + }), ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts index 8a0a2522c..e695d629f 100644 --- a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts +++ b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts @@ -27,7 +27,7 @@ import { makeInstalledCatalogFixture as catalog } from '../support/installed-cat const contract = ( moduleId: string, - supportedStates: OntosModuleDeploymentContract['manifest']['activation']['supportedStates'] + supportedStates: OntosModuleDeploymentContract['manifest']['activation']['supportedStates'], ): OntosModuleDeploymentContract => makeModuleContractFixture({ appId: 'unit-module', @@ -40,63 +40,37 @@ const contract = ( it.effect('uses one canonical tenant module state schema', () => Effect.gen(function* testScenario1() { - const decodedStates = yield* Effect.forEach( - (state: (typeof TENANT_MODULE_STATES)[number]) => - Schema.decodeEffect(TenantModuleStateSchema)(state) + const decodedStates = yield* Effect.forEach((state: (typeof TENANT_MODULE_STATES)[number]) => + Schema.decodeEffect(TenantModuleStateSchema)(state), )(TENANT_MODULE_STATES); expect(decodedStates).toEqual(TENANT_MODULE_STATES); - const failure = yield* Effect.flip( - Schema.decodeUnknownEffect(TenantModuleStateSchema)('enabled') - ); + const failure = yield* Effect.flip(Schema.decodeUnknownEffect(TenantModuleStateSchema)('enabled')); expect(Predicate.isTagged(failure, 'SchemaError')).toBe(true); - }) + }), ); -it.effect( - 'maps only trusted supported authentication methods to history sources', - () => - Effect.gen(function* testScenario2() { - expect(yield* resolveTenantModuleStateChangeSource('session')).toBe( - 'user' - ); - expect( - yield* resolveTenantModuleStateChangeSource('support_impersonation') - ).toBe('support'); - expect(yield* resolveTenantModuleStateChangeSource('system')).toBe( - 'system' - ); +it.effect('maps only trusted supported authentication methods to history sources', () => + Effect.gen(function* testScenario2() { + expect(yield* resolveTenantModuleStateChangeSource('session')).toBe('user'); + expect(yield* resolveTenantModuleStateChangeSource('support_impersonation')).toBe('support'); + expect(yield* resolveTenantModuleStateChangeSource('system')).toBe('system'); - const unsupported = yield* Effect.flip( - resolveTenantModuleStateChangeSource('api_key') - ); - expect( - Predicate.isTagged( - unsupported, - 'TenantModuleStateUnsupportedChangeSourceError' - ) - ).toBe(true); - expect(unsupported.code).toBe( - 'tenant_module_state_change_source_unsupported' - ); - }) + const unsupported = yield* Effect.flip(resolveTenantModuleStateChangeSource('api_key')); + expect(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedChangeSourceError')).toBe(true); + expect(unsupported.code).toBe('tenant_module_state_change_source_unsupported'); + }), ); -it.effect( - 'rejects a no-op transition without changing first-state semantics', - () => - Effect.gen(function* testScenario3() { - yield* rejectUnchangedTenantModuleState(null, 'active'); - yield* rejectUnchangedTenantModuleState('inactive', 'active'); +it.effect('rejects a no-op transition without changing first-state semantics', () => + Effect.gen(function* testScenario3() { + yield* rejectUnchangedTenantModuleState(null, 'active'); + yield* rejectUnchangedTenantModuleState('inactive', 'active'); - const unchanged = yield* Effect.flip( - rejectUnchangedTenantModuleState('active', 'active') - ); - expect( - Predicate.isTagged(unchanged, 'TenantModuleStateUnchangedError') - ).toBe(true); - expect(unchanged.code).toBe('tenant_module_state_unchanged'); - }) + const unchanged = yield* Effect.flip(rejectUnchangedTenantModuleState('active', 'active')); + expect(Predicate.isTagged(unchanged, 'TenantModuleStateUnchangedError')).toBe(true); + expect(unchanged.code).toBe('tenant_module_state_unchanged'); + }), ); it('keeps Core module-state errors stable and sanitized', () => { @@ -141,107 +115,68 @@ it('keeps Core module-state errors stable and sanitized', () => { for (const error of errors) { const serialized = JSON.stringify(error); - expect(serialized).not.toMatch( - /postgres|select |insert |tenant-[0-9]|principal-[0-9]/iu - ); + expect(serialized).not.toMatch(/postgres|select |insert |tenant-[0-9]|principal-[0-9]/iu); } }); -it.effect( - 'validates only installed membership and the target module supported states', - () => - Effect.gen(function* testScenario4() { - const other = contract('documents.center', ['inactive', 'active']); - const target = contract('property.registry', [ - 'inactive', - 'active', - 'read_only', - ]); - const installed = catalog(other, target); +it.effect('validates only installed membership and the target module supported states', () => + Effect.gen(function* testScenario4() { + const other = contract('documents.center', ['inactive', 'active']); + const target = contract('property.registry', ['inactive', 'active', 'read_only']); + const installed = catalog(other, target); - const unknown = yield* Effect.flip( - validateTenantModuleStateTransition( - installed, - 'unknown.module', - 'active' - ) - ); - expect( - Predicate.isTagged(unknown, 'TenantModuleStateUnknownModuleError') - ).toBe(true); - const unsupported = yield* Effect.flip( - validateTenantModuleStateTransition( - installed, - 'property.registry', - 'archived' - ) - ); - expect( - Predicate.isTagged( - unsupported, - 'TenantModuleStateUnsupportedStateError' - ) - ).toBe(true); - yield* validateTenantModuleStateTransition( - installed, - 'property.registry', - 'active' - ); - yield* validateTenantModuleStateTransition( - installed, - 'stale.module', - 'inactive' - ); - }) + const unknown = yield* Effect.flip(validateTenantModuleStateTransition(installed, 'unknown.module', 'active')); + expect(Predicate.isTagged(unknown, 'TenantModuleStateUnknownModuleError')).toBe(true); + const unsupported = yield* Effect.flip( + validateTenantModuleStateTransition(installed, 'property.registry', 'archived'), + ); + expect(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedStateError')).toBe(true); + yield* validateTenantModuleStateTransition(installed, 'property.registry', 'active'); + yield* validateTenantModuleStateTransition(installed, 'stale.module', 'inactive'); + }), ); -it.effect( - 'declares the generated Core Action contract and bounded business payload', - () => - Effect.gen(function* testScenario5() { - const { descriptor } = changeTenantModuleStateAction; - expect(descriptor.actionKey).toBe( - 'core.modules.change-tenant-module-state' - ); - expect(descriptor.owningModuleKey).toBe('core.modules'); - expect(descriptor.auditProfile).toBe('sensitive'); - expect(descriptor.idempotency).toBe('required'); - expect(descriptor.policies).toEqual([]); - expect(Object.isFrozen(descriptor)).toBe(true); - expect(JSON.stringify(descriptor.domainErrorSchema.ast)).not.toMatch( - /dependency/iu - ); +it.effect('declares the generated Core Action contract and bounded business payload', () => + Effect.gen(function* testScenario5() { + const { descriptor } = changeTenantModuleStateAction; + expect(descriptor.actionKey).toBe('core.modules.change-tenant-module-state'); + expect(descriptor.owningModuleKey).toBe('core.modules'); + expect(descriptor.auditProfile).toBe('sensitive'); + expect(descriptor.idempotency).toBe('required'); + expect(descriptor.policies).toEqual([]); + expect(Object.isFrozen(descriptor)).toBe(true); + expect(JSON.stringify(descriptor.domainErrorSchema.ast)).not.toMatch(/dependency/iu); - expect( - yield* Schema.decodeEffect(descriptor.payloadSchema)({ - expectedState: 'inactive', - moduleKey: 'testing.module', - newState: 'active', - reason: 'Tenant administrator enabled the module', - }) - ).toEqual({ + expect( + yield* Schema.decodeEffect(descriptor.payloadSchema)({ expectedState: 'inactive', moduleKey: 'testing.module', newState: 'active', reason: 'Tenant administrator enabled the module', - }); - expect( - yield* Effect.flip( - Schema.decodeEffect(descriptor.payloadSchema)({ - moduleKey: 'testing.module', - newState: 'active', - reason: 'x'.repeat(501), - }) - ) - ).toBeDefined(); - expect( - yield* Effect.flip( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ - moduleKey: 'testing.module', - newState: 'enabled', - tenantId: 'browser-supplied', - }) - ) - ).toBeDefined(); - }) + }), + ).toEqual({ + expectedState: 'inactive', + moduleKey: 'testing.module', + newState: 'active', + reason: 'Tenant administrator enabled the module', + }); + expect( + yield* Effect.flip( + Schema.decodeEffect(descriptor.payloadSchema)({ + moduleKey: 'testing.module', + newState: 'active', + reason: 'x'.repeat(501), + }), + ), + ).toBeDefined(); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + moduleKey: 'testing.module', + newState: 'enabled', + tenantId: 'browser-supplied', + }), + ), + ).toBeDefined(); + }), ); diff --git a/app/packages/gateway-principal-verifier/src/server.ts b/app/packages/gateway-principal-verifier/src/server.ts index 17d4bbed6..a04d53e2f 100644 --- a/app/packages/gateway-principal-verifier/src/server.ts +++ b/app/packages/gateway-principal-verifier/src/server.ts @@ -21,53 +21,30 @@ import { Redacted, Schema, } from 'effect'; -import { - createLocalJWKSet, - decodeProtectedHeader, - importJWK, - jwtVerify, -} from 'jose'; +import { createLocalJWKSet, decodeProtectedHeader, importJWK, jwtVerify } from 'jose'; import type { LocalJWKSet } from 'jose'; export const ACTION_PRINCIPAL_BEARER_CHALLENGE = 'Bearer' as const; const errorFields = { reason: Schema.String }; -export const ActionPrincipalMissingErrorSchema = Schema.TaggedStruct( - 'ActionPrincipalMissingError', - errorFields -); -export type ActionPrincipalMissingError = - typeof ActionPrincipalMissingErrorSchema.Type; -export const ActionPrincipalInvalidErrorSchema = Schema.TaggedStruct( - 'ActionPrincipalInvalidError', - errorFields -); -export type ActionPrincipalInvalidError = - typeof ActionPrincipalInvalidErrorSchema.Type; -export const ActionPrincipalExpiredErrorSchema = Schema.TaggedStruct( - 'ActionPrincipalExpiredError', - errorFields -); -export type ActionPrincipalExpiredError = - typeof ActionPrincipalExpiredErrorSchema.Type; -export const ActionPrincipalScopeErrorSchema = Schema.TaggedStruct( - 'ActionPrincipalScopeError', - errorFields -); -export type ActionPrincipalScopeError = - typeof ActionPrincipalScopeErrorSchema.Type; +export const ActionPrincipalMissingErrorSchema = Schema.TaggedStruct('ActionPrincipalMissingError', errorFields); +export type ActionPrincipalMissingError = typeof ActionPrincipalMissingErrorSchema.Type; +export const ActionPrincipalInvalidErrorSchema = Schema.TaggedStruct('ActionPrincipalInvalidError', errorFields); +export type ActionPrincipalInvalidError = typeof ActionPrincipalInvalidErrorSchema.Type; +export const ActionPrincipalExpiredErrorSchema = Schema.TaggedStruct('ActionPrincipalExpiredError', errorFields); +export type ActionPrincipalExpiredError = typeof ActionPrincipalExpiredErrorSchema.Type; +export const ActionPrincipalScopeErrorSchema = Schema.TaggedStruct('ActionPrincipalScopeError', errorFields); +export type ActionPrincipalScopeError = typeof ActionPrincipalScopeErrorSchema.Type; export const ActionPrincipalConfigurationErrorSchema = Schema.TaggedStruct( 'ActionPrincipalConfigurationError', - errorFields + errorFields, ); -export type ActionPrincipalConfigurationError = - typeof ActionPrincipalConfigurationErrorSchema.Type; +export type ActionPrincipalConfigurationError = typeof ActionPrincipalConfigurationErrorSchema.Type; export const ActionPrincipalUnavailableErrorSchema = Schema.TaggedStruct( 'ActionPrincipalUnavailableError', - errorFields + errorFields, ); -export type ActionPrincipalUnavailableError = - typeof ActionPrincipalUnavailableErrorSchema.Type; +export type ActionPrincipalUnavailableError = typeof ActionPrincipalUnavailableErrorSchema.Type; export type ActionPrincipalError = | ActionPrincipalMissingError @@ -86,10 +63,9 @@ export interface GatewayPrincipalVerificationWithRedemptionOptions extends Gatew readonly redemption: GatewayAssertionRedemption; } -type GatewayPrincipalVerificationEnvironmentOptions = - GatewayPrincipalVerificationOptions & { - readonly environment: GatewayPrincipalVerificationEnvironment; - }; +type GatewayPrincipalVerificationEnvironmentOptions = GatewayPrincipalVerificationOptions & { + readonly environment: GatewayPrincipalVerificationEnvironment; +}; type GatewayPrincipalVerificationEnvironmentWithRedemptionOptions = GatewayPrincipalVerificationWithRedemptionOptions & { readonly environment: GatewayPrincipalVerificationEnvironment; @@ -119,9 +95,7 @@ const PublicVerificationKeySchema = Schema.Struct({ alg: Schema.Literal('EdDSA'), crv: Schema.Literal('Ed25519'), d: Schema.optionalKey(Schema.Never), - key_ops: Schema.optionalKey( - Schema.Array(Schema.Literal('verify')).check(Schema.isMinLength(1)) - ), + key_ops: Schema.optionalKey(Schema.Array(Schema.Literal('verify')).check(Schema.isMinLength(1))), kid: Schema.String.check(Schema.isMinLength(1)), kty: Schema.Literal('OKP'), use: Schema.Literal('sig'), @@ -133,20 +107,16 @@ const PublicVerificationKeysSchema = Schema.Struct({ Schema.makeFilter((keys) => new Set(keys.map(({ kid }) => kid)).size === keys.length ? undefined - : 'Verification key identifiers must be unique' - ) + : 'Verification key identifiers must be unique', + ), ), }); const VerificationEnvironmentSchema = Schema.Struct({ ONTOS_GATEWAY_ISSUER: Schema.String.check( Schema.isPattern(/^https?:\/\//u), - Schema.makeFilter((value) => - URL.canParse(value) ? undefined : 'An absolute HTTP issuer is required' - ) - ), - ONTOS_GATEWAY_PUBLIC_JWKS: Schema.fromJsonString( - PublicVerificationKeysSchema + Schema.makeFilter((value) => (URL.canParse(value) ? undefined : 'An absolute HTTP issuer is required')), ), + ONTOS_GATEWAY_PUBLIC_JWKS: Schema.fromJsonString(PublicVerificationKeysSchema), }); const gatewayVerificationEnvironment = Config.all({ @@ -161,73 +131,61 @@ interface VerificationConfiguration { } interface GatewayPrincipalVerifierService { - readonly configuration: Effect.Effect< - VerificationConfiguration, - ActionPrincipalConfigurationError - >; + readonly configuration: Effect.Effect; } export class GatewayPrincipalVerifierConfiguration extends Context.Service< GatewayPrincipalVerifierConfiguration, GatewayPrincipalVerifierService ->()( - '@app/gateway-principal-verifier/server/GatewayPrincipalVerifierConfiguration' -) {} +>()('@app/gateway-principal-verifier/server/GatewayPrincipalVerifierConfiguration') {} const loadGatewayPrincipalVerificationConfiguration = ( - provider?: ConfigProvider.ConfigProvider -): Effect.Effect< - VerificationConfiguration, - ActionPrincipalConfigurationError -> => { + provider?: ConfigProvider.ConfigProvider, +): Effect.Effect => { const configuration = - provider === undefined - ? gatewayVerificationEnvironment - : gatewayVerificationEnvironment.parse(provider); + provider === undefined ? gatewayVerificationEnvironment : gatewayVerificationEnvironment.parse(provider); return configuration.pipe( Effect.flatMap(Schema.decodeUnknownEffect(VerificationEnvironmentSchema)), - Effect.flatMap( - ({ ONTOS_GATEWAY_ISSUER: issuer, ONTOS_GATEWAY_PUBLIC_JWKS: jwks }) => { - const keys = jwks.keys.map(({ alg, crv, kid, kty, use, x }) => ({ - alg, - crv, - kid, - kty, - use, - x, - })); - return Effect.tryPromise({ - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Configuration failures are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - catch: () => configurationError(), - // oxlint-disable-next-line typescript/promise-function-async -- Effect.tryPromise owns the Promise boundary; remove-when: the lint rule recognizes Effect.tryPromise callbacks. - try: () => Promise.all(keys.map((key) => importJWK(key, 'EdDSA'))), - }).pipe( - Effect.timeoutOrElse({ - duration: VERIFY_ASSERTION_TIMEOUT, - orElse: () => Effect.fail(configurationError()), - }), - Effect.map((): VerificationConfiguration => ({ - issuer, - keySet: createLocalJWKSet({ keys }), - })) - ); - } - ), + Effect.flatMap(({ ONTOS_GATEWAY_ISSUER: issuer, ONTOS_GATEWAY_PUBLIC_JWKS: jwks }) => { + const keys = jwks.keys.map(({ alg, crv, kid, kty, use, x }) => ({ + alg, + crv, + kid, + kty, + use, + x, + })); + return Effect.tryPromise({ + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Configuration failures are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + catch: () => configurationError(), + // oxlint-disable-next-line typescript/promise-function-async -- Effect.tryPromise owns the Promise boundary; remove-when: the lint rule recognizes Effect.tryPromise callbacks. + try: () => Promise.all(keys.map((key) => importJWK(key, 'EdDSA'))), + }).pipe( + Effect.timeoutOrElse({ + duration: VERIFY_ASSERTION_TIMEOUT, + orElse: () => Effect.fail(configurationError()), + }), + Effect.map((): VerificationConfiguration => ({ + issuer, + keySet: createLocalJWKSet({ keys }), + })), + ); + }), // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Configuration failures are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - Effect.mapError(() => configurationError()) + Effect.mapError(() => configurationError()), ); }; export const makeGatewayPrincipalVerifierLayer = ( - provider?: ConfigProvider.ConfigProvider + provider?: ConfigProvider.ConfigProvider, ): Layer.Layer => Layer.effect( GatewayPrincipalVerifierConfiguration, Effect.cached(loadGatewayPrincipalVerificationConfiguration(provider)).pipe( Effect.map((configuration): GatewayPrincipalVerifierService => ({ configuration, - })) - ) + })), + ), ); export const GatewayPrincipalVerifierLive = makeGatewayPrincipalVerifierLayer(); @@ -238,13 +196,9 @@ const VerificationFailureSchema = Schema.Struct({ name: Schema.optionalKey(Schema.String), }); type VerificationFailure = typeof VerificationFailureSchema.Type; -const decodeVerificationFailure = Schema.decodeUnknownOption( - VerificationFailureSchema -); +const decodeVerificationFailure = Schema.decodeUnknownOption(VerificationFailureSchema); -const classifyVerificationFailure = ( - error: VerificationFailure -): ActionPrincipalError => { +const classifyVerificationFailure = (error: VerificationFailure): ActionPrincipalError => { if (error.name === 'JWTExpired') { return ActionPrincipalExpiredErrorSchema.make({ reason: 'The Bearer assertion has expired', @@ -268,21 +222,18 @@ const classifyVerificationFailure = ( }; const readBearer = ( - authorization: Redacted.Redacted + authorization: Redacted.Redacted, ): Effect.Effect => { const authorizationValue = Redacted.value(authorization); if (authorizationValue === undefined) { return Effect.fail( ActionPrincipalMissingErrorSchema.make({ reason: 'A Bearer assertion is required', - }) + }), ); } - const token = /^Bearer (?[^\s]+)$/iu.exec(authorizationValue) - ?.groups?.['token']; - return token === undefined - ? Effect.fail(invalidError()) - : Effect.succeed(token); + const token = /^Bearer (?[^\s]+)$/iu.exec(authorizationValue)?.groups?.['token']; + return token === undefined ? Effect.fail(invalidError()) : Effect.succeed(token); }; interface VerifiedGatewayPrincipal { @@ -292,115 +243,92 @@ interface VerifiedGatewayPrincipal { readonly principal: TrustedPrincipalContext; } -const verifyAuthenticatedToken = Effect.fn( - 'GatewayPrincipalVerifier.verifyAuthenticatedToken' -)(function* verifyAuthenticatedTokenEffect( - expectedAudience: string, - token: string, - options: GatewayPrincipalVerificationOptions -): Effect.fn.Return< - VerifiedGatewayPrincipal, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration -> { - const audience = yield* Schema.decodeEffect(GatewayAudienceSchema)( - expectedAudience - ).pipe( - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Schema diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - Effect.mapError(() => configurationError()) - ); - const verifier = yield* GatewayPrincipalVerifierConfiguration; - const configuration = yield* verifier.configuration; - const now = yield* ( - options.currentTimeSeconds ?? - Clock.currentTimeMillis.pipe( - Effect.map((milliseconds) => Math.floor(milliseconds / 1000)) - ) - ); - if (!Number.isSafeInteger(now) || now < 0) { - return yield* Effect.fail(configurationError()); - } - const unverifiedHeader = yield* Effect.try({ - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Token parser diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - catch: () => invalidError(), - try: () => decodeProtectedHeader(token), - }); - yield* decodeGatewayContextProtectedHeader(unverifiedHeader).pipe( - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Header diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - Effect.mapError(() => invalidError()) - ); - const verified = yield* Effect.tryPromise({ - catch: (cause) => - Option.match(decodeVerificationFailure(cause), { - onNone: unavailableError, - onSome: classifyVerificationFailure, - }), - // oxlint-disable-next-line typescript/promise-function-async -- Effect.tryPromise owns the Promise boundary; remove-when: the lint rule recognizes Effect.tryPromise callbacks. - try: () => - jwtVerify(token, configuration.keySet, { - algorithms: ['EdDSA'], - audience, - clockTolerance: GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS, - currentDate: DateTime.toDateUtc(DateTime.makeUnsafe(now * 1000)), - issuer: configuration.issuer, +const verifyAuthenticatedToken = Effect.fn('GatewayPrincipalVerifier.verifyAuthenticatedToken')( + function* verifyAuthenticatedTokenEffect( + expectedAudience: string, + token: string, + options: GatewayPrincipalVerificationOptions, + ): Effect.fn.Return { + const audience = yield* Schema.decodeEffect(GatewayAudienceSchema)(expectedAudience).pipe( + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Schema diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + Effect.mapError(() => configurationError()), + ); + const verifier = yield* GatewayPrincipalVerifierConfiguration; + const configuration = yield* verifier.configuration; + const now = yield* ( + options.currentTimeSeconds ?? + Clock.currentTimeMillis.pipe(Effect.map((milliseconds) => Math.floor(milliseconds / 1000))) + ); + if (!Number.isSafeInteger(now) || now < 0) { + return yield* Effect.fail(configurationError()); + } + const unverifiedHeader = yield* Effect.try({ + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Token parser diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + catch: () => invalidError(), + try: () => decodeProtectedHeader(token), + }); + yield* decodeGatewayContextProtectedHeader(unverifiedHeader).pipe( + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Header diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + Effect.mapError(() => invalidError()), + ); + const verified = yield* Effect.tryPromise({ + catch: (cause) => + Option.match(decodeVerificationFailure(cause), { + onNone: unavailableError, + onSome: classifyVerificationFailure, + }), + // oxlint-disable-next-line typescript/promise-function-async -- Effect.tryPromise owns the Promise boundary; remove-when: the lint rule recognizes Effect.tryPromise callbacks. + try: () => + jwtVerify(token, configuration.keySet, { + algorithms: ['EdDSA'], + audience, + clockTolerance: GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS, + currentDate: DateTime.toDateUtc(DateTime.makeUnsafe(now * 1000)), + issuer: configuration.issuer, + }), + }).pipe( + Effect.timeoutOrElse({ + duration: VERIFY_ASSERTION_TIMEOUT, + orElse: () => Effect.fail(unavailableError()), }), - }).pipe( - Effect.timeoutOrElse({ - duration: VERIFY_ASSERTION_TIMEOUT, - orElse: () => Effect.fail(unavailableError()), - }) - ); - const claims = yield* decodeGatewayContextClaims(verified.payload).pipe( - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Claim diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - Effect.mapError(() => invalidError()) - ); - if ( - claims.ver !== GATEWAY_ASSERTION_VERSION || - claims.iat > now + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS - ) { - return yield* Effect.fail(invalidError()); - } - const principal = yield* Schema.decodeEffect(TrustedPrincipalContextSchema, { - onExcessProperty: 'error', - })(claims.principal).pipe( - // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Principal decode diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. - Effect.mapError(() => invalidError()) - ); - return { - expiresAtEpochSeconds: claims.exp, - issuer: claims.iss, - jti: claims.jti, - principal, - }; -}); + ); + const claims = yield* decodeGatewayContextClaims(verified.payload).pipe( + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Claim diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + Effect.mapError(() => invalidError()), + ); + if (claims.ver !== GATEWAY_ASSERTION_VERSION || claims.iat > now + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS) { + return yield* Effect.fail(invalidError()); + } + const principal = yield* Schema.decodeEffect(TrustedPrincipalContextSchema, { + onExcessProperty: 'error', + })(claims.principal).pipe( + // oxlint-disable-next-line effect-native/no-failure-discarding-error-callback -- Principal decode diagnostics are deliberately sanitized at the trust boundary; remove-when: the rule supports security-boundary sanitizers. + Effect.mapError(() => invalidError()), + ); + return { + expiresAtEpochSeconds: claims.exp, + issuer: claims.iss, + jti: claims.jti, + principal, + }; + }, +); -export const bindGatewayPrincipalVerifier = ( - expectedAudience: Audience -) => { +export const bindGatewayPrincipalVerifier = (expectedAudience: Audience) => { function verifyPrincipal( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationEnvironmentOptions + options: GatewayPrincipalVerificationEnvironmentOptions, ): Effect.Effect; function verifyPrincipal( authorization: Redacted.Redacted, - options?: GatewayPrincipalVerificationOptions - ): Effect.Effect< - VerifiedGatewayPrincipal, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - >; + options?: GatewayPrincipalVerificationOptions, + ): Effect.Effect; function verifyPrincipal( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationOptions = {} - ): Effect.Effect< - VerifiedGatewayPrincipal, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - > { + options: GatewayPrincipalVerificationOptions = {}, + ): Effect.Effect { const verification = readBearer(authorization).pipe( - Effect.flatMap((token) => - verifyAuthenticatedToken(expectedAudience, token, options) - ) + Effect.flatMap((token) => verifyAuthenticatedToken(expectedAudience, token, options)), ); return options.environment === undefined ? verification @@ -408,57 +336,39 @@ export const bindGatewayPrincipalVerifier = ( // oxlint-disable-next-line effect-native/no-effect-provide-in-library -- Compatibility input for generated owner bindings; remove-when: callers inject ConfigProvider at runtime roots. Effect.provideService(GatewayPrincipalVerifierConfiguration, { configuration: loadGatewayPrincipalVerificationConfiguration( - ConfigProvider.fromUnknown(options.environment) + ConfigProvider.fromUnknown(options.environment), ), - }) + }), ); } function verify( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationEnvironmentOptions + options: GatewayPrincipalVerificationEnvironmentOptions, ): Effect.Effect; function verify( authorization: Redacted.Redacted, - options?: GatewayPrincipalVerificationOptions - ): Effect.Effect< - TrustedPrincipalContext, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - >; + options?: GatewayPrincipalVerificationOptions, + ): Effect.Effect; function verify( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationOptions = {} - ): Effect.Effect< - TrustedPrincipalContext, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - > { - return verifyPrincipal(authorization, options).pipe( - Effect.map(({ principal }) => principal) - ); + options: GatewayPrincipalVerificationOptions = {}, + ): Effect.Effect { + return verifyPrincipal(authorization, options).pipe(Effect.map(({ principal }) => principal)); } function verifyAndRedeem( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationEnvironmentWithRedemptionOptions + options: GatewayPrincipalVerificationEnvironmentWithRedemptionOptions, ): Effect.Effect; function verifyAndRedeem( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationWithRedemptionOptions - ): Effect.Effect< - TrustedPrincipalContext, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - >; + options: GatewayPrincipalVerificationWithRedemptionOptions, + ): Effect.Effect; function verifyAndRedeem( authorization: Redacted.Redacted, - options: GatewayPrincipalVerificationWithRedemptionOptions - ): Effect.Effect< - TrustedPrincipalContext, - ActionPrincipalError, - GatewayPrincipalVerifierConfiguration - > { + options: GatewayPrincipalVerificationWithRedemptionOptions, + ): Effect.Effect { return verifyPrincipal(authorization, options).pipe( Effect.tap(({ expiresAtEpochSeconds, issuer, jti }) => options.redemption @@ -470,13 +380,12 @@ export const bindGatewayPrincipalVerifier = ( }) .pipe( Effect.catchTags({ - GatewayAssertionRedemptionUnavailableError: - mapRedemptionUnavailable, + GatewayAssertionRedemptionUnavailableError: mapRedemptionUnavailable, GatewayAssertionReplayError: mapRedemptionReplay, - }) - ) + }), + ), ), - Effect.map(({ principal }) => principal) + Effect.map(({ principal }) => principal), ); } diff --git a/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts b/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts index ddba36f8d..1a7f43290 100644 --- a/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts +++ b/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts @@ -1,7 +1,4 @@ -import { - GatewayAssertionRedemptionUnavailableError, - GatewayAssertionReplayError, -} from '@app/core-runtime'; +import { GatewayAssertionRedemptionUnavailableError, GatewayAssertionReplayError } from '@app/core-runtime'; import { Effect, Redacted, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; @@ -28,9 +25,7 @@ const principal = { const makeFixture = (audience: string, version = 1) => Effect.gen(function* createFixture() { - const { privateKey, publicKey } = yield* Effect.promise(() => - generateKeyPair('Ed25519') - ); + const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); const publicJwk = { ...(yield* Effect.promise(() => exportJWK(publicKey))), alg: 'EdDSA', @@ -50,14 +45,12 @@ const makeFixture = (audience: string, version = 1) => .setIssuedAt(1_700_000_000) .setExpirationTime(1_700_000_300) .setJti('60000000-0000-4000-8000-000000000001') - .sign(privateKey) + .sign(privateKey), ); return { environment: { ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: yield* Schema.encodeEffect( - Schema.fromJsonString(Schema.Unknown) - )({ + ONTOS_GATEWAY_PUBLIC_JWKS: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ keys: [publicJwk], }), }, @@ -70,242 +63,193 @@ const isConfigurationError = Schema.is(ActionPrincipalConfigurationErrorSchema); const isInvalidError = Schema.is(ActionPrincipalInvalidErrorSchema); const isScopeError = Schema.is(ActionPrincipalScopeErrorSchema); const isUnavailableError = Schema.is(ActionPrincipalUnavailableErrorSchema); -const failingKeySet = Object.assign( - () => Promise.reject(new Error('fixture verifier details must be discarded')), - { jwks: () => ({ keys: [] }) } -) satisfies LocalJWKSet; +const failingKeySet = Object.assign(() => Promise.reject(new Error('fixture verifier details must be discarded')), { + jwks: () => ({ keys: [] }), +}) satisfies LocalJWKSet; -it.effect( - 'an audience-bound verifier accepts only its exact topology app ID', - () => - Effect.gen(function* verifyAudienceBinding() { - const partyFixture = yield* makeFixture('party-registry'); - const billingFixture = yield* makeFixture('billing'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verify = ( - token: string, - environment: typeof partyFixture.environment - ) => - verifier.verify(Redacted.make(`Bearer ${token}`), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment, - }); +it.effect('an audience-bound verifier accepts only its exact topology app ID', () => + Effect.gen(function* verifyAudienceBinding() { + const partyFixture = yield* makeFixture('party-registry'); + const billingFixture = yield* makeFixture('billing'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verify = (token: string, environment: typeof partyFixture.environment) => + verifier.verify(Redacted.make(`Bearer ${token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment, + }); - expect( - yield* verify(partyFixture.token, partyFixture.environment) - ).toEqual(principal); - expect( - isScopeError( - yield* Effect.flip( - verify(billingFixture.token, billingFixture.environment) - ) - ) - ).toBe(true); - }) + expect(yield* verify(partyFixture.token, partyFixture.environment)).toEqual(principal); + expect(isScopeError(yield* Effect.flip(verify(billingFixture.token, billingFixture.environment)))).toBe(true); + }), ); -it.effect( - 'Bearer scheme matching is case insensitive without changing the signed token', - () => - Effect.gen(function* verifyBearerCaseVariants() { - const fixture = yield* makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - yield* Effect.forEach( - ['Bearer', 'bearer', 'BEARER', 'bEaReR'], - (scheme) => - Effect.gen(function* verifyBearerScheme() { - const verified = yield* verifier.verify( - Redacted.make(`${scheme} ${fixture.token}`), - { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: fixture.environment, - } - ); - expect(verified).toEqual(principal); - }), - { concurrency: 'unbounded' } - ); - }) +it.effect('Bearer scheme matching is case insensitive without changing the signed token', () => + Effect.gen(function* verifyBearerCaseVariants() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + yield* Effect.forEach( + ['Bearer', 'bearer', 'BEARER', 'bEaReR'], + (scheme) => + Effect.gen(function* verifyBearerScheme() { + const verified = yield* verifier.verify(Redacted.make(`${scheme} ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: fixture.environment, + }); + expect(verified).toEqual(principal); + }), + { concurrency: 'unbounded' }, + ); + }), ); -it.effect( - 'case insensitive Bearer matching still rejects malformed authorization headers', - () => - Effect.gen(function* rejectMalformedBearerHeaders() { - const fixture = yield* makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - yield* Effect.forEach( - [ - ` bearer ${fixture.token}`, - `bearer ${fixture.token}`, - `bearer\t${fixture.token}`, - `bearer ${fixture.token} `, - `bearer ${fixture.token} extra`, - 'bearer ', - `Basic ${fixture.token}`, - ], - (authorization) => - Effect.gen(function* rejectMalformedBearerHeader() { - const failure = yield* Effect.flip( - verifier.verify(Redacted.make(authorization), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: fixture.environment, - }) - ); - expect(isInvalidError(failure)).toBe(true); - }), - { concurrency: 'unbounded' } - ); - }) +it.effect('case insensitive Bearer matching still rejects malformed authorization headers', () => + Effect.gen(function* rejectMalformedBearerHeaders() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + yield* Effect.forEach( + [ + ` bearer ${fixture.token}`, + `bearer ${fixture.token}`, + `bearer\t${fixture.token}`, + `bearer ${fixture.token} `, + `bearer ${fixture.token} extra`, + 'bearer ', + `Basic ${fixture.token}`, + ], + (authorization) => + Effect.gen(function* rejectMalformedBearerHeader() { + const failure = yield* Effect.flip( + verifier.verify(Redacted.make(authorization), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: fixture.environment, + }), + ); + expect(isInvalidError(failure)).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), ); -it.effect( - 'empty and malformed audience bindings fail closed as configuration errors', - () => - Effect.gen(function* rejectMalformedBindings() { - const fixture = yield* makeFixture('party-registry'); - yield* Effect.forEach( - ['', 'Party Registry', 'party/registry'], - (audience) => - Effect.gen(function* checkMalformedBinding() { - const failure = yield* Effect.flip( - bindGatewayPrincipalVerifier(audience).verify( - Redacted.make(`Bearer ${fixture.token}`), - { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: fixture.environment, - } - ) - ); - expect(isConfigurationError(failure)).toBe(true); - }), - { concurrency: 'unbounded' } - ); - }) +it.effect('empty and malformed audience bindings fail closed as configuration errors', () => + Effect.gen(function* rejectMalformedBindings() { + const fixture = yield* makeFixture('party-registry'); + yield* Effect.forEach( + ['', 'Party Registry', 'party/registry'], + (audience) => + Effect.gen(function* checkMalformedBinding() { + const failure = yield* Effect.flip( + bindGatewayPrincipalVerifier(audience).verify(Redacted.make(`Bearer ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: fixture.environment, + }), + ); + expect(isConfigurationError(failure)).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), ); -it.effect( - 'redemption failures remain sanitized and distinguish replay from unavailability', - () => - Effect.gen(function* verifyRedemptionFailures() { - const fixture = yield* makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verify = ( - redemption: Parameters[1]['redemption'] - ) => - verifier.verifyAndRedeem(Redacted.make(`Bearer ${fixture.token}`), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: fixture.environment, - redemption, - }); +it.effect('redemption failures remain sanitized and distinguish replay from unavailability', () => + Effect.gen(function* verifyRedemptionFailures() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verify = (redemption: Parameters[1]['redemption']) => + verifier.verifyAndRedeem(Redacted.make(`Bearer ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: fixture.environment, + redemption, + }); - const replayFailure = yield* Effect.flip( - verify({ - consume: () => - Effect.fail( - new GatewayAssertionReplayError({ - reason: 'fixture replay details must be discarded', - }) - ), - }) - ); - expect(isInvalidError(replayFailure)).toBe(true); - expect( - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - replayFailure - ) - ).not.toMatch(/fixture|eyJ/u); - const unavailableFailure = yield* Effect.flip( - verify({ - consume: () => - Effect.fail( - new GatewayAssertionRedemptionUnavailableError({ - reason: 'fixture storage details must be discarded', - }) - ), - }) - ); - expect(isUnavailableError(unavailableFailure)).toBe(true); - expect( - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - unavailableFailure - ) - ).not.toMatch(/fixture|eyJ/u); - }) + const replayFailure = yield* Effect.flip( + verify({ + consume: () => + Effect.fail( + new GatewayAssertionReplayError({ + reason: 'fixture replay details must be discarded', + }), + ), + }), + ); + expect(isInvalidError(replayFailure)).toBe(true); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(replayFailure)).not.toMatch( + /fixture|eyJ/u, + ); + const unavailableFailure = yield* Effect.flip( + verify({ + consume: () => + Effect.fail( + new GatewayAssertionRedemptionUnavailableError({ + reason: 'fixture storage details must be discarded', + }), + ), + }), + ); + expect(isUnavailableError(unavailableFailure)).toBe(true); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(unavailableFailure)).not.toMatch( + /fixture|eyJ/u, + ); + }), ); -it.effect( - 'unsupported assertion versions and unexpected verifier failures fail closed', - () => - Effect.gen(function* rejectUnsupportedAndUnexpectedFailures() { - const unsupportedVersion = yield* makeFixture('party-registry', 2); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const versionFailure = yield* Effect.flip( - verifier.verify(Redacted.make(`Bearer ${unsupportedVersion.token}`), { +it.effect('unsupported assertion versions and unexpected verifier failures fail closed', () => + Effect.gen(function* rejectUnsupportedAndUnexpectedFailures() { + const unsupportedVersion = yield* makeFixture('party-registry', 2); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const versionFailure = yield* Effect.flip( + verifier.verify(Redacted.make(`Bearer ${unsupportedVersion.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: unsupportedVersion.environment, + }), + ); + expect(isInvalidError(versionFailure)).toBe(true); + + const fixture = yield* makeFixture('party-registry'); + const failure = yield* Effect.flip( + verifier + .verify(Redacted.make(`Bearer ${fixture.token}`), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: unsupportedVersion.environment, }) - ); - expect(isInvalidError(versionFailure)).toBe(true); - - const fixture = yield* makeFixture('party-registry'); - const failure = yield* Effect.flip( - verifier - .verify(Redacted.make(`Bearer ${fixture.token}`), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - }) - .pipe( - Effect.provideService(GatewayPrincipalVerifierConfiguration, { - configuration: Effect.succeed({ issuer, keySet: failingKeySet }), - }) - ) - ); - expect(isUnavailableError(failure)).toBe(true); - expect( - yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - failure - ) - ).not.toMatch(/fixture|eyJ/u); - }) + .pipe( + Effect.provideService(GatewayPrincipalVerifierConfiguration, { + configuration: Effect.succeed({ issuer, keySet: failingKeySet }), + }), + ), + ); + expect(isUnavailableError(failure)).toBe(true); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(failure)).not.toMatch(/fixture|eyJ/u); + }), ); -it.effect( - 'malformed Ed25519 public keys fail during configuration acquisition', - () => - Effect.gen(function* rejectMalformedPublicKeys() { - const fixture = yield* makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verifyWithKey = (key: JWK) => - Effect.gen(function* verifyPublicKey() { - const jwks = yield* Schema.encodeEffect( - Schema.fromJsonString(Schema.Unknown) - )({ - keys: [key], - }); - return yield* verifier.verify( - Redacted.make(`Bearer ${fixture.token}`), - { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: { - ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: jwks, - }, - } - ); +it.effect('malformed Ed25519 public keys fail during configuration acquisition', () => + Effect.gen(function* rejectMalformedPublicKeys() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verifyWithKey = (key: JWK) => + Effect.gen(function* verifyPublicKey() { + const jwks = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + keys: [key], }); + return yield* verifier.verify(Redacted.make(`Bearer ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: { + ONTOS_GATEWAY_ISSUER: issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: jwks, + }, + }); + }); - yield* Effect.forEach( - [ - { ...fixture.publicJwk, key_ops: [] }, - { ...fixture.publicJwk, x: '!!!' }, - ], - (key) => - Effect.gen(function* checkMalformedPublicKey() { - expect( - isConfigurationError(yield* Effect.flip(verifyWithKey(key))) - ).toBe(true); - }), - { concurrency: 'unbounded' } - ); - }) + yield* Effect.forEach( + [ + { ...fixture.publicJwk, key_ops: [] }, + { ...fixture.publicJwk, x: '!!!' }, + ], + (key) => + Effect.gen(function* checkMalformedPublicKey() { + expect(isConfigurationError(yield* Effect.flip(verifyWithKey(key)))).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), ); diff --git a/app/packages/shared-contracts/src/client-runtime.ts b/app/packages/shared-contracts/src/client-runtime.ts index a6292d5fd..a4c0ff0a0 100644 --- a/app/packages/shared-contracts/src/client-runtime.ts +++ b/app/packages/shared-contracts/src/client-runtime.ts @@ -1,37 +1,17 @@ -import { - Effect, - Schema, - makeEffectHttpApiClient, -} from '@modern-js/plugin-bff/effect-client'; -import type { - EffectHttpApiClientOptions, - HttpApi, - HttpApiGroup, -} from '@modern-js/plugin-bff/effect-client'; +import { Effect, Schema, makeEffectHttpApiClient } from '@modern-js/plugin-bff/effect-client'; +import type { EffectHttpApiClientOptions, HttpApi, HttpApiGroup } from '@modern-js/plugin-bff/effect-client'; import { Redacted } from 'effect'; -import { - Headers as HttpHeaders, - HttpClient, - HttpClientRequest, -} from 'effect/unstable/http'; +import { Headers as HttpHeaders, HttpClient, HttpClientRequest } from 'effect/unstable/http'; const EffectBffOperationContextSchema = Schema.Struct({ method: Schema.String, // eslint-disable-next-line effect-native/no-unbranded-identifier-schema -- The framework operation name is owner-supplied routing metadata, not an interchangeable Resource identifier. operationId: Schema.String, routePath: Schema.String, - source: Schema.Literals([ - 'client', - 'server', - 'generated-client', - 'effect-adapter', - 'data-platform', - 'unknown', - ]), + source: Schema.Literals(['client', 'server', 'generated-client', 'effect-adapter', 'data-platform', 'unknown']), }); -export type EffectBffOperationContext = - typeof EffectBffOperationContextSchema.Type; +export type EffectBffOperationContext = typeof EffectBffOperationContextSchema.Type; export interface EffectBffRequestContext { readonly locale?: string; @@ -54,14 +34,9 @@ export interface EffectBffClientConfig< readonly defaultApiPrefix: string | URL; } -const encodeOperationContext = Schema.encodeResult( - Schema.fromJsonString(EffectBffOperationContextSchema) -); +const encodeOperationContext = Schema.encodeResult(Schema.fromJsonString(EffectBffOperationContextSchema)); -export const makeEffectBffClient = < - ApiId extends string, - Groups extends HttpApiGroup.Constraint, ->({ +export const makeEffectBffClient = ({ api, baseUrl, defaultApiPrefix, @@ -77,39 +52,23 @@ export const makeEffectBffClient = < const transformedClient = client.pipe( HttpClient.mapRequest((request) => { let nextRequest = request; - if ( - operationContext !== undefined && - operationContextText !== null - ) { + if (operationContext !== undefined && operationContextText !== null) { nextRequest = HttpClientRequest.setHeader( nextRequest, 'x-modernjs-bff-operation-context', - operationContextText + operationContextText, ); const { operationId } = operationContext; - nextRequest = HttpClientRequest.setHeader( - nextRequest, - 'x-operation-id', - operationId - ); + nextRequest = HttpClientRequest.setHeader(nextRequest, 'x-operation-id', operationId); } - return HttpClientRequest.setHeaders( - nextRequest, - resolvedTransportHeaders - ); - }) + return HttpClientRequest.setHeaders(nextRequest, resolvedTransportHeaders); + }), ); // Effect injects fresh trace headers after request transforms. This boundary instead makes // the caller's request context authoritative so explicit traceparent values survive and // absent optional tracing metadata stays absent. return transformedClient.pipe( - HttpClient.transform((effect) => - Effect.provideService( - effect, - HttpClient.TracerPropagationEnabled, - false - ) - ) + HttpClient.transform((effect) => Effect.provideService(effect, HttpClient.TracerPropagationEnabled, false)), ); }, }; @@ -120,16 +79,11 @@ export const makeEffectBffClient = < return makeEffectHttpApiClient(api, clientOptions); }; const operationContextText: Effect.Effect = - operationContext === undefined - ? Effect.succeed(null) - : Effect.fromResult(encodeOperationContext(operationContext)); + operationContext === undefined ? Effect.succeed(null) : Effect.fromResult(encodeOperationContext(operationContext)); return operationContextText.pipe(Effect.flatMap(makeClient)); }; -interface GovernedEffectBffClientConfig< - ApiId extends string, - Groups extends HttpApiGroup.Constraint, -> { +interface GovernedEffectBffClientConfig { readonly api: HttpApi.HttpApi; readonly credential: Redacted.Redacted; readonly defaultApiPrefix: string | URL; @@ -151,17 +105,9 @@ const isGovernedBaseUrl = (value: string): boolean => { }; /** Fresh per-invocation transport; credentials remain redacted until HTTP header construction. */ -export const makeGovernedEffectBffClient = < - ApiId extends string, - Groups extends HttpApiGroup.Constraint, ->( - { - api, - credential, - defaultApiPrefix, - requestCorrelation, - }: GovernedEffectBffClientConfig, - options: Pick +export const makeGovernedEffectBffClient = ( + { api, credential, defaultApiPrefix, requestCorrelation }: GovernedEffectBffClientConfig, + options: Pick, ) => { const baseUrl = String(options.baseUrl ?? defaultApiPrefix); const clientConfig = { @@ -173,10 +119,8 @@ export const makeGovernedEffectBffClient = < 'x-correlation-id': requestCorrelation, }, }; - return Schema.decodeUnknownEffect(Schema.Literal(true))( - isGovernedBaseUrl(baseUrl) - ).pipe( + return Schema.decodeUnknownEffect(Schema.Literal(true))(isGovernedBaseUrl(baseUrl)).pipe( Effect.map(() => clientConfig), - Effect.flatMap(makeEffectBffClient) + Effect.flatMap(makeEffectBffClient), ); }; diff --git a/app/packages/shared-contracts/src/effect-bff-runtime.ts b/app/packages/shared-contracts/src/effect-bff-runtime.ts index 94aaf5902..1f71ac50a 100644 --- a/app/packages/shared-contracts/src/effect-bff-runtime.ts +++ b/app/packages/shared-contracts/src/effect-bff-runtime.ts @@ -1,16 +1,8 @@ /** Server-only assembly for the invariant tail of a strict Effect BFF runtime factory. */ /* oxlint-disable effect-native/no-dependency-parameters -- The approved BFF assembly seam intentionally accepts caller-composed Layers; expires: 2027-09-07. */ import { governedReadHttpStatus } from '@app/core-runtime/http/governed-read'; -import { - defineEffectBff, - HttpApiBuilder, - Layer, -} from '@modern-js/plugin-bff/effect-edge'; -import type { - EffectRuntimeRequirements, - HttpApi, - HttpApiGroup, -} from '@modern-js/plugin-bff/effect-edge'; +import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; +import type { EffectRuntimeRequirements, HttpApi, HttpApiGroup } from '@modern-js/plugin-bff/effect-edge'; export interface EffectBffRuntimeAssembly< ApiId extends string, @@ -19,11 +11,7 @@ export interface EffectBffRuntimeAssembly< TransportRequirements extends EffectRuntimeRequirements = never, > { readonly api: HttpApi.HttpApi; - readonly handlers: Layer.Layer< - HttpApiGroup.ToService, - never, - HandlerRequirements - >; + readonly handlers: Layer.Layer, never, HandlerRequirements>; readonly transport?: Layer.Layer; } @@ -36,15 +24,9 @@ export const assembleEffectBffRuntime = < api, handlers, transport, -}: EffectBffRuntimeAssembly< - ApiId, - Groups, - HandlerRequirements, - TransportRequirements ->) => { +}: EffectBffRuntimeAssembly) => { const apiLayer = HttpApiBuilder.layer(api).pipe(Layer.provide(handlers)); - const layer = - transport === undefined ? apiLayer : apiLayer.pipe(Layer.merge(transport)); + const layer = transport === undefined ? apiLayer : apiLayer.pipe(Layer.merge(transport)); return defineEffectBff({ api, layer }); }; @@ -79,9 +61,7 @@ export const makeGovernedReadProblems = < readonly policyConflict: GovernedProblemConstructor<409, PolicyConflict>; readonly policyIneligible: GovernedProblemConstructor<422, PolicyIneligible>; readonly unavailable: { - readonly make: ( - fields: GovernedProblemFields<503> & { readonly retryable: true } - ) => Unavailable; + readonly make: (fields: GovernedProblemFields<503> & { readonly retryable: true }) => Unavailable; }; }) => ({ authentication: () => diff --git a/app/packages/shared-contracts/src/effect-bff-runtime.type-test.ts b/app/packages/shared-contracts/src/effect-bff-runtime.type-test.ts index ca565c36c..d0e720f8f 100644 --- a/app/packages/shared-contracts/src/effect-bff-runtime.type-test.ts +++ b/app/packages/shared-contracts/src/effect-bff-runtime.type-test.ts @@ -16,21 +16,16 @@ import { Context, Data, Schema } from 'effect'; import { assembleEffectBffRuntime } from './effect-bff-runtime.ts'; -class FixtureDependency extends Context.Service< - FixtureDependency, - { readonly value: string } ->()('@app/shared-contracts/effect-bff-runtime.type-test/FixtureDependency') {} +class FixtureDependency extends Context.Service()( + '@app/shared-contracts/effect-bff-runtime.type-test/FixtureDependency', +) {} const FixtureStartupError = Data.TaggedError('FixtureStartupError')<{ readonly reason: string; }>; type IsExact = - (() => Value extends Left ? 1 : 2) extends < - Value, - >() => Value extends Right ? 1 : 2 - ? true - : false; + (() => Value extends Left ? 1 : 2) extends () => Value extends Right ? 1 : 2 ? true : false; type ExpectedRuntimeRequirements = | Exclude< @@ -46,16 +41,11 @@ const fixtureApi = HttpApi.make('RuntimeAssemblyTypeFixture').add( HttpApiGroup.make('fixture').add( HttpApiEndpoint.get('read', '/fixture', { success: Schema.Struct({ value: Schema.String }), - }) - ) + }), + ), ); -const fixtureHandlers = HttpApiBuilder.group( - fixtureApi, - 'fixture', - (handlers) => - handlers.handle('read', () => - FixtureDependency.pipe(Effect.map(({ value }) => ({ value }))) - ) +const fixtureHandlers = HttpApiBuilder.group(fixtureApi, 'fixture', (handlers) => + handlers.handle('read', () => FixtureDependency.pipe(Effect.map(({ value }) => ({ value })))), ).pipe(Layer.provide(Layer.succeed(FixtureDependency, { value: 'fixture' }))); const fixtureRuntime = assembleEffectBffRuntime({ @@ -63,12 +53,8 @@ const fixtureRuntime = assembleEffectBffRuntime({ handlers: fixtureHandlers, }); -const concreteRuntime: EffectBffDefinition & - EffectBffRuntime = fixtureRuntime; -const inferredApiIsExact: IsExact< - typeof fixtureRuntime.api, - typeof fixtureApi -> = true; +const concreteRuntime: EffectBffDefinition & EffectBffRuntime = fixtureRuntime; +const inferredApiIsExact: IsExact = true; const inferredRequirementsAreExact: IsExact< Layer.Services, ExpectedRuntimeRequirements @@ -78,11 +64,8 @@ void concreteRuntime; void inferredApiIsExact; void inferredRequirementsAreExact; -const failingFixtureHandlers = HttpApiBuilder.group( - fixtureApi, - 'fixture', - (handlers) => - handlers.handle('read', () => Effect.succeed({ value: 'unreachable' })) +const failingFixtureHandlers = HttpApiBuilder.group(fixtureApi, 'fixture', (handlers) => + handlers.handle('read', () => Effect.succeed({ value: 'unreachable' })), ).pipe( Layer.provide( Layer.effect( @@ -90,10 +73,10 @@ const failingFixtureHandlers = HttpApiBuilder.group( Effect.fail( new FixtureStartupError({ reason: 'must be resolved at the runtime root', - }) - ) - ) - ) + }), + ), + ), + ), ); assembleEffectBffRuntime({ @@ -106,8 +89,8 @@ const otherApi = HttpApi.make('OtherRuntimeAssemblyTypeFixture').add( HttpApiGroup.make('other').add( HttpApiEndpoint.get('readOther', '/other', { success: Schema.Struct({ value: Schema.String }), - }) - ) + }), + ), ); assembleEffectBffRuntime({ diff --git a/app/packages/shared-contracts/src/gateway-context.ts b/app/packages/shared-contracts/src/gateway-context.ts index be598d8e6..ebbd9ffd0 100644 --- a/app/packages/shared-contracts/src/gateway-context.ts +++ b/app/packages/shared-contracts/src/gateway-context.ts @@ -13,10 +13,7 @@ import { Context } from 'effect'; import { HttpClient, HttpClientRequest } from 'effect/unstable/http'; /* oxlint-disable anti-slop-effect/no-service-constructor-imports -- These pure helpers construct contract schemas, not Effect services. */ -import { - makeProblemDetailsSchema, - makeRetryableProblemDetailsSchema, -} from './problem-details.ts'; +import { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from './problem-details.ts'; /* oxlint-enable anti-slop-effect/no-service-constructor-imports */ export const GATEWAY_ASSERTION_VERSION = 1 as const; @@ -26,20 +23,14 @@ export const GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS = 30 as const; const nonEmptyString = Schema.String.check(Schema.isMinLength(1)); const uuid = Schema.String.check(Schema.isUUID()); const LegalEntityIdSchema = uuid.pipe(Schema.brand('LegalEntityId')); -const epochSeconds = Schema.Finite.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(0) -); +const epochSeconds = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)); export const GatewayAudienceSchema = nonEmptyString.check( Schema.makeFilter((value) => - /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(value) - ? undefined - : 'audience must be a stable topology app ID' - ) + /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(value) ? undefined : 'audience must be a stable topology app ID', + ), ); -export const GatewayTrustedPrincipalContextSchema = - TrustedPrincipalContextSchema; +export const GatewayTrustedPrincipalContextSchema = TrustedPrincipalContextSchema; export type GatewayTrustedPrincipalContext = TrustedPrincipalContext; @@ -49,9 +40,7 @@ export const GatewayContextProtectedHeaderSchema = Schema.Struct({ typ: Schema.Literal('JWT'), }); -export type GatewayContextProtectedHeader = Schema.Schema.Type< - typeof GatewayContextProtectedHeaderSchema ->; +export type GatewayContextProtectedHeader = Schema.Schema.Type; export const GatewayContextClaimsSchema = Schema.Struct({ aud: GatewayAudienceSchema, @@ -81,24 +70,18 @@ export const GatewayContextClaimsSchema = Schema.Struct({ }); } return issues; - }) + }), ); -export type GatewayContextClaims = Schema.Schema.Type< - typeof GatewayContextClaimsSchema ->; +export type GatewayContextClaims = Schema.Schema.Type; -export const decodeGatewayContextClaims = Schema.decodeUnknownEffect( - GatewayContextClaimsSchema, - { - onExcessProperty: 'error', - } -); +export const decodeGatewayContextClaims = Schema.decodeUnknownEffect(GatewayContextClaimsSchema, { + onExcessProperty: 'error', +}); -export const decodeGatewayContextProtectedHeader = Schema.decodeUnknownEffect( - GatewayContextProtectedHeaderSchema, - { onExcessProperty: 'error' } -); +export const decodeGatewayContextProtectedHeader = Schema.decodeUnknownEffect(GatewayContextProtectedHeaderSchema, { + onExcessProperty: 'error', +}); export const GatewayContextRequestSchema = Schema.Struct({ audience: GatewayAudienceSchema, @@ -110,55 +93,31 @@ export const GatewayContextResponseSchema = Schema.Struct({ expiresAt: epochSeconds, token: nonEmptyString, }); -export type GatewayContextResponse = Schema.Schema.Type< - typeof GatewayContextResponseSchema ->; - -export const GatewayAuthenticationRequiredProblemSchema = - makeProblemDetailsSchema('GatewayAuthenticationRequiredProblem', 401); +export type GatewayContextResponse = Schema.Schema.Type; -export const GatewayAudienceInvalidProblemSchema = makeProblemDetailsSchema( - 'GatewayAudienceInvalidProblem', - 400 +export const GatewayAuthenticationRequiredProblemSchema = makeProblemDetailsSchema( + 'GatewayAuthenticationRequiredProblem', + 401, ); -export const GatewayUnavailableProblemSchema = - makeRetryableProblemDetailsSchema('GatewayUnavailableProblem', 503); +export const GatewayAudienceInvalidProblemSchema = makeProblemDetailsSchema('GatewayAudienceInvalidProblem', 400); -export const GatewayInternalProblemSchema = makeProblemDetailsSchema( - 'GatewayInternalProblem', - 500 -); -const GatewayForbiddenProblemSchema = makeProblemDetailsSchema( - 'GatewayForbiddenProblem', - 403 -); -export const GatewayRateLimitedProblemSchema = makeProblemDetailsSchema( - 'GatewayRateLimitedProblem', - 429, - { - retryAfterSeconds: Schema.Finite, - } -); +export const GatewayUnavailableProblemSchema = makeRetryableProblemDetailsSchema('GatewayUnavailableProblem', 503); + +export const GatewayInternalProblemSchema = makeProblemDetailsSchema('GatewayInternalProblem', 500); +const GatewayForbiddenProblemSchema = makeProblemDetailsSchema('GatewayForbiddenProblem', 403); +export const GatewayRateLimitedProblemSchema = makeProblemDetailsSchema('GatewayRateLimitedProblem', 429, { + retryAfterSeconds: Schema.Finite, +}); export type GatewayAuthenticationRequiredProblem = Schema.Schema.Type< typeof GatewayAuthenticationRequiredProblemSchema >; -export type GatewayAudienceInvalidProblem = Schema.Schema.Type< - typeof GatewayAudienceInvalidProblemSchema ->; -export type GatewayUnavailableProblem = Schema.Schema.Type< - typeof GatewayUnavailableProblemSchema ->; -export type GatewayInternalProblem = Schema.Schema.Type< - typeof GatewayInternalProblemSchema ->; -type GatewayForbiddenProblem = Schema.Schema.Type< - typeof GatewayForbiddenProblemSchema ->; -type GatewayRateLimitedProblem = Schema.Schema.Type< - typeof GatewayRateLimitedProblemSchema ->; +export type GatewayAudienceInvalidProblem = Schema.Schema.Type; +export type GatewayUnavailableProblem = Schema.Schema.Type; +export type GatewayInternalProblem = Schema.Schema.Type; +type GatewayForbiddenProblem = Schema.Schema.Type; +type GatewayRateLimitedProblem = Schema.Schema.Type; export type GatewayContextProblem = | GatewayAuthenticationRequiredProblem @@ -184,36 +143,29 @@ export const GatewayContextApiGroup = HttpApiGroup.make('gatewayContext') ], payload: GatewayContextRequestSchema, success: GatewayContextResponseSchema, - }) + }), ) .add( - HttpApiEndpoint.post( - 'issueApiKeyGatewayContext', - '/auth/api-key/gateway-context', - { - error: [ - GatewayAuthenticationRequiredProblemSchema, - GatewayAudienceInvalidProblemSchema, - GatewayForbiddenProblemSchema, - GatewayRateLimitedProblemSchema, - GatewayUnavailableProblemSchema, - GatewayInternalProblemSchema, - ], - headers: ApiKeyGatewayHeadersSchema, - payload: GatewayContextRequestSchema, - success: GatewayContextResponseSchema, - } - ) + HttpApiEndpoint.post('issueApiKeyGatewayContext', '/auth/api-key/gateway-context', { + error: [ + GatewayAuthenticationRequiredProblemSchema, + GatewayAudienceInvalidProblemSchema, + GatewayForbiddenProblemSchema, + GatewayRateLimitedProblemSchema, + GatewayUnavailableProblemSchema, + GatewayInternalProblemSchema, + ], + headers: ApiKeyGatewayHeadersSchema, + payload: GatewayContextRequestSchema, + success: GatewayContextResponseSchema, + }), ); -export const GatewayContextApi = HttpApi.make('shellGatewayContextApi').add( - GatewayContextApiGroup -); +export const GatewayContextApi = HttpApi.make('shellGatewayContextApi').add(GatewayContextApiGroup); export const shellGatewayContextContract = { apiPrefix: '/shell-super-app-api', - issueApiKeyGatewayContextPath: - '/shell-super-app-api/auth/api-key/gateway-context', + issueApiKeyGatewayContextPath: '/shell-super-app-api/auth/api-key/gateway-context', issueGatewayContextPath: '/shell-super-app-api/auth/gateway-context', ownerId: 'shell-super-app', } as const; @@ -242,21 +194,13 @@ export interface GatewayContextClientOptions { readonly cookie?: string; } -export type GatewayContextClientError = - | GatewayContextProblem - | HttpClientError.HttpClientError - | Schema.SchemaError; +export type GatewayContextClientError = GatewayContextProblem | HttpClientError.HttpClientError | Schema.SchemaError; -export type GatewayContextClientEffect = Effect.Effect< - Success, - GatewayContextClientError ->; +export type GatewayContextClientEffect = Effect.Effect; -const GatewayContextRequestOptions = - Context.Reference( - 'GatewayContextRequestOptions', - { defaultValue: () => ({}) } - ); +const GatewayContextRequestOptions = Context.Reference('GatewayContextRequestOptions', { + defaultValue: () => ({}), +}); const gatewayContextClient = makeEffectHttpApiClient(GatewayContextApi, { transformClient: HttpClient.mapRequestEffect((request) => @@ -264,32 +208,26 @@ const gatewayContextClient = makeEffectHttpApiClient(GatewayContextApi, { Effect.map((options) => { let nextRequest = HttpClientRequest.prependUrl( request, - (options.baseUrl ?? shellGatewayContextContract.apiPrefix).toString() + (options.baseUrl ?? shellGatewayContextContract.apiPrefix).toString(), ); if (options.cookie !== undefined) { - nextRequest = HttpClientRequest.setHeader( - nextRequest, - 'cookie', - options.cookie - ); + nextRequest = HttpClientRequest.setHeader(nextRequest, 'cookie', options.cookie); } return nextRequest; - }) - ) + }), + ), ), }); export const issueGatewayContext = ( payload: GatewayContextRequest, - options: GatewayContextClientOptions = {} + options: GatewayContextClientOptions = {}, ): GatewayContextClientEffect => Schema.decodeEffect(GatewayContextRequestSchema)(payload).pipe( Effect.flatMap((decodedPayload) => gatewayContextClient.pipe( - Effect.flatMap((client) => - client.gatewayContext.issueGatewayContext({ payload: decodedPayload }) - ) - ) + Effect.flatMap((client) => client.gatewayContext.issueGatewayContext({ payload: decodedPayload })), + ), ), - Effect.provideService(GatewayContextRequestOptions, options) + Effect.provideService(GatewayContextRequestOptions, options), ); diff --git a/app/packages/shared-contracts/src/index.ts b/app/packages/shared-contracts/src/index.ts index 3f60c3189..2ed164f10 100644 --- a/app/packages/shared-contracts/src/index.ts +++ b/app/packages/shared-contracts/src/index.ts @@ -1,9 +1,6 @@ import { Schema } from 'effect'; -export { - makeProblemDetailsSchema, - makeRetryableProblemDetailsSchema, -} from './problem-details.ts'; +export { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from './problem-details.ts'; export type { ProblemDetailsStatus } from './problem-details.ts'; export { @@ -57,11 +54,7 @@ export type { MicroVerticalReadiness, } from './microvertical-api-baseline.ts'; export { makeOperationGateway } from './operation-gateway.ts'; -export type { - OperationGateway, - OperationGatewayAttempt, - OperationGatewayIssuer, -} from './operation-gateway.ts'; +export type { OperationGateway, OperationGatewayAttempt, OperationGatewayIssuer } from './operation-gateway.ts'; export const UltramodernPublicSitemapChangeFrequencySchema = Schema.Literals([ 'always', @@ -72,8 +65,7 @@ export const UltramodernPublicSitemapChangeFrequencySchema = Schema.Literals([ 'yearly', 'never', ]); -export type UltramodernPublicSitemapChangeFrequency = - typeof UltramodernPublicSitemapChangeFrequencySchema.Type; +export type UltramodernPublicSitemapChangeFrequency = typeof UltramodernPublicSitemapChangeFrequencySchema.Type; export interface UltramodernPublicSitemapEntry { changeFrequency?: UltramodernPublicSitemapChangeFrequency; @@ -83,9 +75,7 @@ export interface UltramodernPublicSitemapEntry { /** * Per-locale overrides when translated URLs use translated params. */ - localeParams?: Partial< - Record<'en' | 'cs', Record> - >; + localeParams?: Partial>>; /** * Params used to expand every localized route pattern, for example * { slug: 'platform-story' } for /talks/:slug. @@ -102,8 +92,7 @@ export const UltramodernPerformanceReadinessSignalIdSchema = Schema.Literals([ 'save-data-behavior', 'cloudflare-ssr-cache-hints', ]); -export type UltramodernPerformanceReadinessSignalId = - typeof UltramodernPerformanceReadinessSignalIdSchema.Type; +export type UltramodernPerformanceReadinessSignalId = typeof UltramodernPerformanceReadinessSignalIdSchema.Type; export interface UltramodernPerformanceReadinessDiagnosticsConfig { /** @@ -130,10 +119,8 @@ export const ultramodernWorkspaceContract = { ownership: 'topology/ownership.json', performanceReadiness: { defaultOn: true, - optOut: - 'scripts/ultramodern-performance-readiness.config.mjs#enabled=false', - report: - '.codex/reports/performance-readiness/ultramodern-performance-readiness.json', + optOut: 'scripts/ultramodern-performance-readiness.config.mjs#enabled=false', + report: '.codex/reports/performance-readiness/ultramodern-performance-readiness.json', signals: [ 'bfcache', 'core-web-vitals-rum', @@ -148,13 +135,10 @@ export const ultramodernWorkspaceContract = { } as const; export const UltramodernWorkspaceLocaleSchema = Schema.Literals(['en', 'cs']); -export type UltramodernWorkspaceLocale = - typeof UltramodernWorkspaceLocaleSchema.Type; +export type UltramodernWorkspaceLocale = typeof UltramodernWorkspaceLocaleSchema.Type; -export const UltramodernPerformanceReadinessSignalStatusSchema = - Schema.Literals(['pass', 'warn', 'fail']); -export type UltramodernPerformanceReadinessSignalStatus = - typeof UltramodernPerformanceReadinessSignalStatusSchema.Type; +export const UltramodernPerformanceReadinessSignalStatusSchema = Schema.Literals(['pass', 'warn', 'fail']); +export type UltramodernPerformanceReadinessSignalStatus = typeof UltramodernPerformanceReadinessSignalStatusSchema.Type; export const ultramodernWorkspaceEventNames = { navigate: 'ultramodern:navigate', @@ -166,51 +150,39 @@ export const ultramodernWorkspaceEventNames = { export type UltramodernWorkspaceEventName = (typeof ultramodernWorkspaceEventNames)[keyof typeof ultramodernWorkspaceEventNames]; -const UltramodernWorkspaceJsonValueSchema: Schema.Codec = - Schema.suspend(() => - Schema.Union([ - Schema.Null, - Schema.Finite, - Schema.Boolean, - Schema.String, - Schema.Array(UltramodernWorkspaceJsonValueSchema), - Schema.Record(Schema.String, UltramodernWorkspaceJsonValueSchema), - ]) - ); - -export const UltramodernWorkspaceJsonObjectSchema = Schema.Record( - Schema.String, - UltramodernWorkspaceJsonValueSchema +const UltramodernWorkspaceJsonValueSchema: Schema.Codec = Schema.suspend(() => + Schema.Union([ + Schema.Null, + Schema.Finite, + Schema.Boolean, + Schema.String, + Schema.Array(UltramodernWorkspaceJsonValueSchema), + Schema.Record(Schema.String, UltramodernWorkspaceJsonValueSchema), + ]), ); -export type UltramodernWorkspaceJsonObject = - typeof UltramodernWorkspaceJsonObjectSchema.Type; -const UltramodernWorkspaceNonEmptyStringSchema = Schema.String.check( - Schema.isPattern(/\S/u) -); -const UltramodernWorkspaceNonNegativeNumberSchema = Schema.Finite.check( - Schema.isGreaterThanOrEqualTo(0) +export const UltramodernWorkspaceJsonObjectSchema = Schema.Record(Schema.String, UltramodernWorkspaceJsonValueSchema); +export type UltramodernWorkspaceJsonObject = typeof UltramodernWorkspaceJsonObjectSchema.Type; + +const UltramodernWorkspaceNonEmptyStringSchema = Schema.String.check(Schema.isPattern(/\S/u)); +const UltramodernWorkspaceNonNegativeNumberSchema = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)); +const UltramodernWorkspaceAppIdSchema = UltramodernWorkspaceNonEmptyStringSchema.pipe( + Schema.brand('UltramodernWorkspaceAppId'), ); -const UltramodernWorkspaceAppIdSchema = - UltramodernWorkspaceNonEmptyStringSchema.pipe( - Schema.brand('UltramodernWorkspaceAppId') - ); export const UltramodernNavigatePayloadSchema = Schema.Struct({ replace: Schema.optional(Schema.Boolean), state: Schema.optional(UltramodernWorkspaceJsonObjectSchema), to: UltramodernWorkspaceNonEmptyStringSchema, }); -export type UltramodernNavigatePayload = - typeof UltramodernNavigatePayloadSchema.Type; +export type UltramodernNavigatePayload = typeof UltramodernNavigatePayloadSchema.Type; export const UltramodernRouteSettledPayloadSchema = Schema.Struct({ locale: Schema.optional(UltramodernWorkspaceLocaleSchema), pathname: UltramodernWorkspaceNonEmptyStringSchema, title: Schema.optional(UltramodernWorkspaceNonEmptyStringSchema), }); -export type UltramodernRouteSettledPayload = - typeof UltramodernRouteSettledPayloadSchema.Type; +export type UltramodernRouteSettledPayload = typeof UltramodernRouteSettledPayloadSchema.Type; export const UltramodernRemoteReadyPayloadSchema = Schema.Struct({ appId: UltramodernWorkspaceAppIdSchema, @@ -218,8 +190,7 @@ export const UltramodernRemoteReadyPayloadSchema = Schema.Struct({ surface: Schema.optional(UltramodernWorkspaceNonEmptyStringSchema), version: Schema.optional(UltramodernWorkspaceNonEmptyStringSchema), }); -export type UltramodernRemoteReadyPayload = - typeof UltramodernRemoteReadyPayloadSchema.Type; +export type UltramodernRemoteReadyPayload = typeof UltramodernRemoteReadyPayloadSchema.Type; export const UltramodernPerformanceSignalPayloadSchema = Schema.Struct({ detail: Schema.optional(UltramodernWorkspaceJsonObjectSchema), @@ -227,8 +198,7 @@ export const UltramodernPerformanceSignalPayloadSchema = Schema.Struct({ signalId: UltramodernPerformanceReadinessSignalIdSchema, status: UltramodernPerformanceReadinessSignalStatusSchema, }); -export type UltramodernPerformanceSignalPayload = - typeof UltramodernPerformanceSignalPayloadSchema.Type; +export type UltramodernPerformanceSignalPayload = typeof UltramodernPerformanceSignalPayloadSchema.Type; export interface UltramodernWorkspaceEventPayloadMap { 'ultramodern:navigate': UltramodernNavigatePayload; @@ -251,62 +221,46 @@ export class UltramodernWorkspaceEventValidationError { } export const isUltramodernNavigatePayload = ( - payload: Payload -): payload is Payload & UltramodernNavigatePayload => - Schema.is(UltramodernNavigatePayloadSchema)(payload); + payload: Payload, +): payload is Payload & UltramodernNavigatePayload => Schema.is(UltramodernNavigatePayloadSchema)(payload); export const isUltramodernRouteSettledPayload = ( - payload: Payload -): payload is Payload & UltramodernRouteSettledPayload => - Schema.is(UltramodernRouteSettledPayloadSchema)(payload); + payload: Payload, +): payload is Payload & UltramodernRouteSettledPayload => Schema.is(UltramodernRouteSettledPayloadSchema)(payload); export const isUltramodernRemoteReadyPayload = ( - payload: Payload -): payload is Payload & UltramodernRemoteReadyPayload => - Schema.is(UltramodernRemoteReadyPayloadSchema)(payload); + payload: Payload, +): payload is Payload & UltramodernRemoteReadyPayload => Schema.is(UltramodernRemoteReadyPayloadSchema)(payload); export const isUltramodernPerformanceSignalPayload = ( - payload: Payload + payload: Payload, ): payload is Payload & UltramodernPerformanceSignalPayload => Schema.is(UltramodernPerformanceSignalPayloadSchema)(payload); const ultramodernWorkspaceEventPayloadSchemas = { [ultramodernWorkspaceEventNames.navigate]: UltramodernNavigatePayloadSchema, - [ultramodernWorkspaceEventNames.performanceSignal]: - UltramodernPerformanceSignalPayloadSchema, - [ultramodernWorkspaceEventNames.remoteReady]: - UltramodernRemoteReadyPayloadSchema, - [ultramodernWorkspaceEventNames.routeSettled]: - UltramodernRouteSettledPayloadSchema, + [ultramodernWorkspaceEventNames.performanceSignal]: UltramodernPerformanceSignalPayloadSchema, + [ultramodernWorkspaceEventNames.remoteReady]: UltramodernRemoteReadyPayloadSchema, + [ultramodernWorkspaceEventNames.routeSettled]: UltramodernRouteSettledPayloadSchema, }; -const ultramodernWorkspaceCustomEventSchema = < - Name extends UltramodernWorkspaceEventName, ->( - eventName: Name -) => +const ultramodernWorkspaceCustomEventSchema = (eventName: Name) => Schema.Opaque>()( Schema.Struct({ detail: ultramodernWorkspaceEventPayloadSchemas[eventName], initCustomEvent: Schema.instanceOf(Function), - }) + }), ); -export const isUltramodernWorkspaceEventPayload = < - Name extends UltramodernWorkspaceEventName, - Payload, ->( +export const isUltramodernWorkspaceEventPayload = ( eventName: Name, - payload: Payload + payload: Payload, ): payload is Payload & UltramodernWorkspaceEventPayloadMap[Name] => Schema.is(ultramodernWorkspaceEventPayloadSchemas[eventName])(payload); -export const assertUltramodernWorkspaceEventPayload = < - Name extends UltramodernWorkspaceEventName, - Payload, ->( +export const assertUltramodernWorkspaceEventPayload = ( eventName: Name, - payload: Payload + payload: Payload, ): Payload & UltramodernWorkspaceEventPayloadMap[Name] => { if (!isUltramodernWorkspaceEventPayload(eventName, payload)) { throw new UltramodernWorkspaceEventValidationError(eventName, payload); @@ -315,11 +269,9 @@ export const assertUltramodernWorkspaceEventPayload = < return payload; }; -export const createUltramodernWorkspaceEvent = < - Name extends UltramodernWorkspaceEventName, ->( +export const createUltramodernWorkspaceEvent = ( eventName: Name, - payload: UltramodernWorkspaceEventPayloadMap[Name] + payload: UltramodernWorkspaceEventPayloadMap[Name], ): CustomEvent => new CustomEvent(eventName, { bubbles: true, @@ -327,23 +279,19 @@ export const createUltramodernWorkspaceEvent = < detail: assertUltramodernWorkspaceEventPayload(eventName, payload), }); -export const dispatchUltramodernWorkspaceEvent = < - Name extends UltramodernWorkspaceEventName, ->( +export const dispatchUltramodernWorkspaceEvent = ( target: EventTarget, eventName: Name, - payload: UltramodernWorkspaceEventPayloadMap[Name] + payload: UltramodernWorkspaceEventPayloadMap[Name], ) => target.dispatchEvent(createUltramodernWorkspaceEvent(eventName, payload)); -export const onUltramodernWorkspaceEvent = < - Name extends UltramodernWorkspaceEventName, ->( +export const onUltramodernWorkspaceEvent = ( target: EventTarget, eventName: Name, handler: ( payload: UltramodernWorkspaceEventPayloadMap[Name], - event: CustomEvent - ) => void + event: CustomEvent, + ) => void, ) => { const listener = (event: Event) => { if (!('detail' in event)) { @@ -364,94 +312,39 @@ export const onUltramodernWorkspaceEvent = < }; }; -export const dispatchUltramodernNavigate = ( - target: EventTarget, - payload: UltramodernNavigatePayload -) => - dispatchUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.navigate, - payload - ); +export const dispatchUltramodernNavigate = (target: EventTarget, payload: UltramodernNavigatePayload) => + dispatchUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.navigate, payload); -export const dispatchUltramodernRouteSettled = ( - target: EventTarget, - payload: UltramodernRouteSettledPayload -) => - dispatchUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.routeSettled, - payload - ); +export const dispatchUltramodernRouteSettled = (target: EventTarget, payload: UltramodernRouteSettledPayload) => + dispatchUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.routeSettled, payload); -export const dispatchUltramodernRemoteReady = ( - target: EventTarget, - payload: UltramodernRemoteReadyPayload -) => - dispatchUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.remoteReady, - payload - ); +export const dispatchUltramodernRemoteReady = (target: EventTarget, payload: UltramodernRemoteReadyPayload) => + dispatchUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.remoteReady, payload); export const dispatchUltramodernPerformanceSignal = ( target: EventTarget, - payload: UltramodernPerformanceSignalPayload -) => - dispatchUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.performanceSignal, - payload - ); + payload: UltramodernPerformanceSignalPayload, +) => dispatchUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.performanceSignal, payload); export const onUltramodernNavigate = ( target: EventTarget, - handler: ( - payload: UltramodernNavigatePayload, - event: CustomEvent - ) => void -) => - onUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.navigate, - handler - ); + handler: (payload: UltramodernNavigatePayload, event: CustomEvent) => void, +) => onUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.navigate, handler); export const onUltramodernRouteSettled = ( target: EventTarget, - handler: ( - payload: UltramodernRouteSettledPayload, - event: CustomEvent - ) => void -) => - onUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.routeSettled, - handler - ); + handler: (payload: UltramodernRouteSettledPayload, event: CustomEvent) => void, +) => onUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.routeSettled, handler); export const onUltramodernRemoteReady = ( target: EventTarget, - handler: ( - payload: UltramodernRemoteReadyPayload, - event: CustomEvent - ) => void -) => - onUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.remoteReady, - handler - ); + handler: (payload: UltramodernRemoteReadyPayload, event: CustomEvent) => void, +) => onUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.remoteReady, handler); export const onUltramodernPerformanceSignal = ( target: EventTarget, handler: ( payload: UltramodernPerformanceSignalPayload, - event: CustomEvent - ) => void -) => - onUltramodernWorkspaceEvent( - target, - ultramodernWorkspaceEventNames.performanceSignal, - handler - ); + event: CustomEvent, + ) => void, +) => onUltramodernWorkspaceEvent(target, ultramodernWorkspaceEventNames.performanceSignal, handler); diff --git a/app/packages/shared-contracts/src/microvertical-api-baseline.ts b/app/packages/shared-contracts/src/microvertical-api-baseline.ts index 1aa7e65fa..2f3ba629c 100644 --- a/app/packages/shared-contracts/src/microvertical-api-baseline.ts +++ b/app/packages/shared-contracts/src/microvertical-api-baseline.ts @@ -1,20 +1,17 @@ import { Schema } from 'effect'; -const MicroVerticalAppIdSchema = Schema.String.pipe( - Schema.brand('MicroVerticalAppId'), - Schema.decodeTo(Schema.String) -); +const MicroVerticalAppIdSchema = Schema.String.pipe(Schema.brand('MicroVerticalAppId'), Schema.decodeTo(Schema.String)); const MicroVerticalUnitIdSchema = Schema.String.pipe( Schema.brand('MicroVerticalUnitId'), - Schema.decodeTo(Schema.String) + Schema.decodeTo(Schema.String), ); const MicroVerticalOperationIdSchema = Schema.String.pipe( Schema.brand('MicroVerticalOperationId'), - Schema.decodeTo(Schema.String) + Schema.decodeTo(Schema.String), ); const MicroVerticalTraceIdSchema = Schema.String.pipe( Schema.brand('MicroVerticalTraceId'), - Schema.decodeTo(Schema.String) + Schema.decodeTo(Schema.String), ); export const MicroVerticalBuildMarkerSchema = Schema.Struct({ @@ -28,8 +25,7 @@ export const MicroVerticalBuildMarkerSchema = Schema.Struct({ unitId: MicroVerticalUnitIdSchema, version: Schema.String, }); -export type MicroVerticalBuildMarker = - typeof MicroVerticalBuildMarkerSchema.Type; +export type MicroVerticalBuildMarker = typeof MicroVerticalBuildMarkerSchema.Type; export const MicroVerticalReadinessSchema = Schema.Struct({ checks: Schema.Struct({ @@ -52,8 +48,7 @@ export const MicroVerticalOperationSourceSchema = Schema.Literals([ 'data-platform', 'unknown', ]); -export type MicroVerticalOperationSource = - typeof MicroVerticalOperationSourceSchema.Type; +export type MicroVerticalOperationSource = typeof MicroVerticalOperationSourceSchema.Type; export const MicroVerticalOperationContextSchema = Schema.Struct({ method: Schema.String, @@ -62,8 +57,7 @@ export const MicroVerticalOperationContextSchema = Schema.Struct({ source: MicroVerticalOperationSourceSchema, traceId: Schema.optionalKey(MicroVerticalTraceIdSchema), }); -export type MicroVerticalOperationContext = - typeof MicroVerticalOperationContextSchema.Type; +export type MicroVerticalOperationContext = typeof MicroVerticalOperationContextSchema.Type; export const createMicroVerticalOperationContext = < const Method extends string, @@ -86,14 +80,10 @@ export const createMicroVerticalOperationContext = < routePath: input.routePath, source: 'generated-client' as const, }; - return input.traceId === undefined - ? context - : { ...context, traceId: input.traceId }; + return input.traceId === undefined ? context : { ...context, traceId: input.traceId }; }; -export const microVerticalOperationAttributes = ( - operationContext: MicroVerticalOperationContext -) => { +export const microVerticalOperationAttributes = (operationContext: MicroVerticalOperationContext) => { const attributes = { 'modernjs.operation.id': operationContext.operationId, 'modernjs.operation.method': operationContext.method, diff --git a/app/packages/shared-contracts/src/operation-gateway.ts b/app/packages/shared-contracts/src/operation-gateway.ts index a6b1f250c..4756f11ec 100644 --- a/app/packages/shared-contracts/src/operation-gateway.ts +++ b/app/packages/shared-contracts/src/operation-gateway.ts @@ -9,53 +9,47 @@ import type { export type OperationGatewayIssuer = ( payload: { readonly audience: Audience }, - options?: GatewayContextClientOptions + options?: GatewayContextClientOptions, ) => Effect.Effect; export type OperationGatewayAttempt = ( - authorizationHeader: string + authorizationHeader: string, ) => Effect.Effect; // eslint-disable-next-line effect-native/require-context-service-for-service-interface -- This browser gateway is an audience-bound value, not an injectable application service. expires: 2027-03-31. export interface OperationGateway { readonly invoke: ( attempt: OperationGatewayAttempt, - options?: GatewayContextClientOptions + options?: GatewayContextClientOptions, ) => Effect.Effect; } const makeOperationGatewayWithIssuer = ( audience: Audience, - issuer: OperationGatewayIssuer + issuer: OperationGatewayIssuer, ): OperationGateway => ({ invoke: ( attempt: OperationGatewayAttempt, - options: GatewayContextClientOptions = {} + options: GatewayContextClientOptions = {}, ) => Effect.suspend(() => issuer({ audience }, options)).pipe( Effect.flatMap(({ token }) => { const authorization = Redacted.make(`Bearer ${token}`); return attempt(Redacted.value(authorization)); - }) + }), ), }); export function makeOperationGateway( - audience: Audience + audience: Audience, ): OperationGateway; -export function makeOperationGateway< - const Audience extends string, - IssuerFailure, ->( +export function makeOperationGateway( audience: Audience, - acquire: OperationGatewayIssuer + acquire: OperationGatewayIssuer, ): OperationGateway; -export function makeOperationGateway< - const Audience extends string, - IssuerFailure, ->( +export function makeOperationGateway( audience: Audience, - acquire?: OperationGatewayIssuer + acquire?: OperationGatewayIssuer, ): OperationGateway { return acquire === undefined ? makeOperationGatewayWithIssuer(audience, issueGatewayContext) diff --git a/app/packages/shared-contracts/src/operation-gateway.type-test.ts b/app/packages/shared-contracts/src/operation-gateway.type-test.ts index 7b6196cd7..71625bd97 100644 --- a/app/packages/shared-contracts/src/operation-gateway.type-test.ts +++ b/app/packages/shared-contracts/src/operation-gateway.type-test.ts @@ -5,11 +5,7 @@ import { makeOperationGateway } from './operation-gateway.ts'; import type { OperationGatewayIssuer } from './operation-gateway.ts'; type Equal = - (() => Value extends Left ? 1 : 2) extends < - Value, - >() => Value extends Right ? 1 : 2 - ? true - : false; + (() => Value extends Left ? 1 : 2) extends () => Value extends Right ? 1 : 2 ? true : false; type EffectChannels = Value extends Effect.Effect ? readonly [Success, Failure, Requirements] @@ -18,26 +14,16 @@ type EffectChannels = const inventoryAudience = 'inventory-stock' as const; const acquisitionFailure = { _tag: 'AcquisitionFailure' } as const; const attemptFailure = { _tag: 'AttemptFailure' } as const; -const inventoryGateway = makeOperationGateway( - inventoryAudience, - ({ audience }) => { - const exactAudience: typeof inventoryAudience = audience; - return Effect.fail(acquisitionFailure).pipe( - Effect.annotateLogs({ exactAudience }) - ); - } -); -const failedInvocation = inventoryGateway.invoke(() => - Effect.fail(attemptFailure) -); +const inventoryGateway = makeOperationGateway(inventoryAudience, ({ audience }) => { + const exactAudience: typeof inventoryAudience = audience; + return Effect.fail(acquisitionFailure).pipe(Effect.annotateLogs({ exactAudience })); +}); +const failedInvocation = inventoryGateway.invoke(() => Effect.fail(attemptFailure)); const successfulInvocation = makeOperationGateway(inventoryAudience, () => - Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }) + Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), ).invoke(() => Effect.succeed('completed' as const)); -type InventoryIssuer = OperationGatewayIssuer< - typeof inventoryAudience, - typeof acquisitionFailure ->; +type InventoryIssuer = OperationGatewayIssuer; type InventoryAudience = Parameters[0]['audience']; const audienceIsExact: Equal = true; diff --git a/app/packages/shared-contracts/src/problem-details.ts b/app/packages/shared-contracts/src/problem-details.ts index 5d2416f03..43f32ddd2 100644 --- a/app/packages/shared-contracts/src/problem-details.ts +++ b/app/packages/shared-contracts/src/problem-details.ts @@ -4,39 +4,24 @@ import { Match, Predicate, Schema, SchemaAST } from 'effect'; /* oxlint-disable anti-slop/require-safety-comment-for-type-assertion, typescript/no-unsafe-argument, typescript/no-unsafe-assignment, typescript/no-unsafe-call, typescript/no-unsafe-return, typescript/no-unsafe-type-assertion -- SAFETY: Schema ASTs are runtime-discriminated Effect objects; the clone boundary preserves the checked graph's exact generic schema type. remove-when: Effect exposes a typed AST clone API; expires: 2026-12-31. */ /** HTTP statuses currently used by OntOS public Problem Details contracts. */ -export type ProblemDetailsStatus = - | 400 - | 401 - | 403 - | 404 - | 409 - | 422 - | 428 - | 429 - | 500 - | 503 - | 504; +export type ProblemDetailsStatus = 400 | 401 | 403 | 404 | 409 | 422 | 428 | 429 | 500 | 503 | 504; const problemDetailsRepresentation = HttpApiSchema.asJson({ contentType: 'application/problem+json', }); -const hasJsonSafeNumberCheck = ( - checks: SchemaAST.Checks | undefined -): boolean => +const hasJsonSafeNumberCheck = (checks: SchemaAST.Checks | undefined): boolean => checks?.some((check) => Match.value(check).pipe( Match.tag( 'Filter', ({ annotations }) => annotations?.representation?.id === 'effect/schema/isFinite' || - annotations?.representation?.id === 'effect/schema/isInt' + annotations?.representation?.id === 'effect/schema/isInt', ), - Match.tag('FilterGroup', ({ checks: nestedChecks }) => - hasJsonSafeNumberCheck(nestedChecks) - ), - Match.exhaustive - ) + Match.tag('FilterGroup', ({ checks: nestedChecks }) => hasJsonSafeNumberCheck(nestedChecks)), + Match.exhaustive, + ), ) === true; const isUnsupportedExtensionPrimitive = (ast: SchemaAST.AST): boolean => @@ -66,27 +51,19 @@ const isConcreteExtensionValue = (ast: SchemaAST.AST): boolean => { return hasJsonSafeNumberCheck(ast.checks); } if (SchemaAST.isEnum(ast)) { - return ast.enums.every( - ([, value]) => Predicate.isString(value) || Number.isFinite(value) - ); + return ast.enums.every(([, value]) => Predicate.isString(value) || Number.isFinite(value)); } return true; }; -const visitExtensionChildren = ( - ast: SchemaAST.AST, - visit: (ast: SchemaAST.AST) => boolean -): boolean => { +const visitExtensionChildren = (ast: SchemaAST.AST, visit: (ast: SchemaAST.AST) => boolean): boolean => { if (SchemaAST.isArrays(ast)) { return ast.elements.every(visit) && ast.rest.every(visit); } if (SchemaAST.isObjects(ast)) { return ( ast.indexSignatures.length === 0 && - ast.propertySignatures.every( - ({ name, type }) => - Predicate.isString(name) && name !== '__proto__' && visit(type) - ) + ast.propertySignatures.every(({ name, type }) => Predicate.isString(name) && name !== '__proto__' && visit(type)) ); } if (SchemaAST.isUnion(ast)) { @@ -106,11 +83,7 @@ const isConcreteExtensionAst = (root: SchemaAST.AST): boolean => { } seen.add(ast); if (ast.encoding !== undefined && ast.encoding.length > 0) { - if ( - SchemaAST.isAny(ast) || - SchemaAST.isUnknown(ast) || - SchemaAST.isObjectKeyword(ast) - ) { + if (SchemaAST.isAny(ast) || SchemaAST.isUnknown(ast) || SchemaAST.isObjectKeyword(ast)) { return false; } return ast.encoding.every((link) => visit(link.to)); @@ -120,19 +93,13 @@ const isConcreteExtensionAst = (root: SchemaAST.AST): boolean => { return visit(root); }; -const cloneDescriptors = ( - value: Value, - clone: (value: Current) => Current -) => { +const cloneDescriptors = (value: Value, clone: (value: Current) => Current) => { const descriptors = Object.getOwnPropertyDescriptors(value); for (const [key, descriptor] of Object.entries(descriptors)) { if ('value' in descriptor) { const descriptorValue = descriptor.value; descriptor.value = - key === 'thunk' && - SchemaAST.isAST(value) && - SchemaAST.isSuspend(value) && - Predicate.isFunction(descriptorValue) + key === 'thunk' && SchemaAST.isAST(value) && SchemaAST.isSuspend(value) && Predicate.isFunction(descriptorValue) ? () => clone(descriptorValue()) : clone(descriptorValue); } @@ -167,9 +134,7 @@ const cloneAstGraph = (root: Value): Value => { } return copy as Current; } - const copy: object = Array.isArray(value) - ? [] - : Object.create(Object.getPrototypeOf(value)); + const copy: object = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(objectValue, copy); Object.defineProperties(copy, cloneDescriptors(value, clone)); return copy as Current; @@ -181,85 +146,46 @@ const stableExtensionField = (name: string, descriptor: PropertyDescriptor) => { const field = descriptor.value; if (!Schema.isSchema(field)) { // eslint-disable-next-line effect-native/no-native-error-construction -- This synchronous, browser-safe schema factory rejects a caller programming error before a contract can be published. - throw new TypeError( - `Problem Details extension field "${name}" must use a concrete schema.` - ); + throw new TypeError(`Problem Details extension field "${name}" must use a concrete schema.`); } const fieldDescriptors = Object.getOwnPropertyDescriptors(field); const astDescriptor = fieldDescriptors.ast; - if ( - astDescriptor === undefined || - !('value' in astDescriptor) || - !SchemaAST.isAST(astDescriptor.value) - ) { + if (astDescriptor === undefined || !('value' in astDescriptor) || !SchemaAST.isAST(astDescriptor.value)) { // eslint-disable-next-line effect-native/no-native-error-construction -- Schema AST accessors could change after validation; the captured AST must be the one consumed by TaggedStruct. - throw new TypeError( - `Problem Details extension field "${name}" must use a concrete schema.` - ); + throw new TypeError(`Problem Details extension field "${name}" must use a concrete schema.`); } const stableAst = cloneAstGraph(astDescriptor.value); if (!isConcreteExtensionAst(stableAst)) { // eslint-disable-next-line effect-native/no-native-error-construction -- This synchronous, browser-safe schema factory rejects a caller programming error before a contract can be published. - throw new TypeError( - `Problem Details extension field "${name}" must use a concrete schema.` - ); + throw new TypeError(`Problem Details extension field "${name}" must use a concrete schema.`); } astDescriptor.value = stableAst; - return Object.defineProperties( - Object.create(Object.getPrototypeOf(field)), - fieldDescriptors - ); + return Object.defineProperties(Object.create(Object.getPrototypeOf(field)), fieldDescriptors); }; -const reservedExtensionFields = new Set([ - '__proto__', - '_tag', - 'detail', - 'retryable', - 'status', - 'title', - 'type', -]); +const reservedExtensionFields = new Set(['__proto__', '_tag', 'detail', 'retryable', 'status', 'title', 'type']); -const concreteExtensionsSnapshot = < - const Extensions extends Schema.Struct.Fields, ->( - extensions: Extensions +const concreteExtensionsSnapshot = ( + extensions: Extensions, ): Extensions => { - if ( - Object.getPrototypeOf(extensions) !== Object.prototype && - Object.getPrototypeOf(extensions) !== null - ) { + if (Object.getPrototypeOf(extensions) !== Object.prototype && Object.getPrototypeOf(extensions) !== null) { // eslint-disable-next-line effect-native/no-native-error-construction -- This synchronous, browser-safe schema factory rejects a caller programming error before a contract can be published. throw new TypeError('Problem Details extensions must use a plain object.'); } - const snapshot = Object.defineProperties( - {}, - Object.getOwnPropertyDescriptors(extensions) - ) as Extensions; + const snapshot = Object.defineProperties({}, Object.getOwnPropertyDescriptors(extensions)) as Extensions; for (const name of Reflect.ownKeys(snapshot)) { if (!Predicate.isString(name)) { // eslint-disable-next-line effect-native/no-native-error-construction -- This synchronous, browser-safe schema factory rejects a caller programming error before a contract can be published. - throw new TypeError( - 'Problem Details extension field names must be strings.' - ); + throw new TypeError('Problem Details extension field names must be strings.'); } if (reservedExtensionFields.has(name)) { // eslint-disable-next-line effect-native/no-native-error-construction -- This synchronous, browser-safe schema factory rejects a caller programming error before a contract can be published. - throw new TypeError( - `Problem Details extension field "${name}" is reserved.` - ); + throw new TypeError(`Problem Details extension field "${name}" is reserved.`); } const descriptor = Object.getOwnPropertyDescriptor(snapshot, name); - if ( - descriptor === undefined || - descriptor.enumerable !== true || - !('value' in descriptor) - ) { + if (descriptor === undefined || descriptor.enumerable !== true || !('value' in descriptor)) { // eslint-disable-next-line effect-native/no-native-error-construction -- Accessors could change after validation; contract fields must be immutable schema data descriptors. - throw new TypeError( - `Problem Details extension field "${name}" must be an enumerable data property.` - ); + throw new TypeError(`Problem Details extension field "${name}" must be an enumerable data property.`); } const stableField = stableExtensionField(name, descriptor); Object.defineProperty(snapshot, name, { @@ -280,12 +206,9 @@ const makeAnnotatedProblemDetailsSchema = < tag: Tag, status: Status, marker: Marker, - extensions?: Extensions + extensions?: Extensions, ) => { - const concreteExtensions = - extensions === undefined - ? extensions - : concreteExtensionsSnapshot(extensions); + const concreteExtensions = extensions === undefined ? extensions : concreteExtensionsSnapshot(extensions); // eslint-disable-next-line prefer-object-spread -- Object.assign preserves the concrete generic marker and extension fields in the inferred schema type. const fields = Object.assign( { @@ -295,12 +218,9 @@ const makeAnnotatedProblemDetailsSchema = < type: Schema.String, }, marker, - concreteExtensions - ); - return Schema.TaggedStruct(tag, fields).pipe( - problemDetailsRepresentation, - HttpApiSchema.status(status) + concreteExtensions, ); + return Schema.TaggedStruct(tag, fields).pipe(problemDetailsRepresentation, HttpApiSchema.status(status)); }; /** @@ -315,7 +235,7 @@ export const makeProblemDetailsSchema = < >( tag: Tag, status: Status, - extensions?: Extensions + extensions?: Extensions, ) => makeAnnotatedProblemDetailsSchema(tag, status, {}, extensions); /** Builds a Problem Details schema with the deliberate literal `retryable: true` marker. */ @@ -327,11 +247,5 @@ export const makeRetryableProblemDetailsSchema = < >( tag: Tag, status: Status, - extensions?: Extensions -) => - makeAnnotatedProblemDetailsSchema( - tag, - status, - { retryable: Schema.Literal(true) }, - extensions - ); + extensions?: Extensions, +) => makeAnnotatedProblemDetailsSchema(tag, status, { retryable: Schema.Literal(true) }, extensions); diff --git a/app/packages/shared-contracts/src/ultramodern-build.ts b/app/packages/shared-contracts/src/ultramodern-build.ts index 4a7f9a9fd..344cfaba0 100644 --- a/app/packages/shared-contracts/src/ultramodern-build.ts +++ b/app/packages/shared-contracts/src/ultramodern-build.ts @@ -16,7 +16,7 @@ interface BuildArtifact { export const withUltramodernBuildIdentity = ( artifact: Artifact, buildMarker: string, - sourceRevision: string + sourceRevision: string, ) => { const identity = { build: buildMarker, buildMarker, sourceRevision }; return { diff --git a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts index 1ea656cec..f76e60b0d 100644 --- a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts @@ -11,50 +11,38 @@ import { Predicate, Struct } from 'effect'; import { expect, it } from 'effect-rstest'; import { FetchHttpClient } from 'effect/unstable/http'; -const RepresentativeConflictSchema = Schema.TaggedStruct( - 'RepresentativeConflict', - { - detail: Schema.String, - status: Schema.Literal(409), - title: Schema.String, - type: Schema.String, - } -).pipe( - HttpApiSchema.asJson({ contentType: 'application/problem+json' }), - HttpApiSchema.status(409) -); +const RepresentativeConflictSchema = Schema.TaggedStruct('RepresentativeConflict', { + detail: Schema.String, + status: Schema.Literal(409), + title: Schema.String, + type: Schema.String, +}).pipe(HttpApiSchema.asJson({ contentType: 'application/problem+json' }), HttpApiSchema.status(409)); const RepresentativeApi = HttpApi.make('RepresentativeApi').add( HttpApiGroup.make('representative').add( HttpApiEndpoint.get('read', '/read', { error: RepresentativeConflictSchema, success: Schema.Struct({ value: Schema.String }), - }) - ) + }), + ), ); const controlledTransportFailureFetch: typeof fetch = () => Promise.reject(new TypeError('controlled transport failure')); -const invalidResponseFetch: typeof fetch = () => - Promise.resolve(Response.json({ value: 358 })); +const invalidResponseFetch: typeof fetch = () => Promise.resolve(Response.json({ value: 358 })); const representativeClientEffect = makeEffectBffClient({ api: RepresentativeApi, defaultApiPrefix: '/representative-api', }); type RepresentativeClient = Effect.Success; -type RepresentativeReadEffect = ReturnType< - RepresentativeClient['representative']['read'] ->; +type RepresentativeReadEffect = ReturnType; type RepresentativeReadSuccess = Effect.Success; type RepresentativeReadError = Effect.Error; -const preserveRepresentativeReadType = ( - client: RepresentativeClient -): RepresentativeReadEffect => client.representative.read({}); -const preserveRepresentativeSuccessType = ( - success: RepresentativeReadSuccess -): Readonly<{ value: string }> => success; +const preserveRepresentativeReadType = (client: RepresentativeClient): RepresentativeReadEffect => + client.representative.read({}); +const preserveRepresentativeSuccessType = (success: RepresentativeReadSuccess): Readonly<{ value: string }> => success; const preserveRepresentativeErrorType = (error: RepresentativeReadError) => { if (Schema.is(RepresentativeConflictSchema)(error)) { return error.status satisfies 409; @@ -79,7 +67,7 @@ it.effect('constructs fresh typed clients lazily as Effect values', () => expect(first).not.toBe(second); expect(Effect.isEffect(first.representative.read({}))).toBe(true); - }) + }), ); it.effect('uses the owner-supplied API prefix by default', () => @@ -97,7 +85,7 @@ it.effect('uses the owner-supplied API prefix by default', () => } else { Object.defineProperty(globalThis, 'location', location); } - }) + }), ); Object.defineProperty(globalThis, 'location', { configurable: true, @@ -109,14 +97,12 @@ it.effect('uses the owner-supplied API prefix by default', () => defaultApiPrefix: '/representative-api', }).pipe( Effect.flatMap((client) => client.representative.read({})), - Effect.provideService(FetchHttpClient.Fetch, fakeFetch) + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), ); expect(result).toEqual({ value: 'default-prefix' }); - expect(requests.map(({ url }) => url)).toEqual([ - 'https://shell.example/representative-api/read', - ]); - }) + expect(requests.map(({ url }) => url)).toEqual(['https://shell.example/representative-api/read']); + }), ); it.effect('uses an explicit caller base URL instead of the owner prefix', () => @@ -133,163 +119,141 @@ it.effect('uses an explicit caller base URL instead of the owner prefix', () => defaultApiPrefix: '/representative-api', }).pipe( Effect.flatMap((client) => client.representative.read({})), - Effect.provideService(FetchHttpClient.Fetch, fakeFetch) + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), ); expect(result).toEqual({ value: 'override' }); - expect(requests.map(({ url }) => url)).toEqual([ - 'https://owner.example/custom-api/read', - ]); - }) + expect(requests.map(({ url }) => url)).toEqual(['https://owner.example/custom-api/read']); + }), ); -it.effect( - 'propagates supported request context and resolved transport headers', - () => - Effect.gen(function* testScenario4() { - const requests: Request[] = []; - const fakeFetch: typeof fetch = (input, init) => { - requests.push(new Request(input, init)); - return Promise.resolve(Response.json({ value: 'context' })); - }; - const operationContext = { - method: 'GET', - operationId: 'RepresentativeApi:/read', - routePath: '/read', - source: 'generated-client' as const, - }; - const traceparent = - '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; +it.effect('propagates supported request context and resolved transport headers', () => + Effect.gen(function* testScenario4() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'context' })); + }; + const operationContext = { + method: 'GET', + operationId: 'RepresentativeApi:/read', + routePath: '/read', + source: 'generated-client' as const, + }; + const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; - yield* makeEffectBffClient({ - api: RepresentativeApi, - baseUrl: 'https://owner.example/representative-api', - defaultApiPrefix: '/representative-api', - requestContext: { - locale: 'cs', - operationContext, - traceparent, - }, - transportHeaders: { - authorization: 'Bearer owner-resolved-assertion', - 'x-correlation-id': 'correlation-358', - }, - }).pipe( - Effect.flatMap((client) => client.representative.read({})), - Effect.provideService(FetchHttpClient.Fetch, fakeFetch) - ); + yield* makeEffectBffClient({ + api: RepresentativeApi, + baseUrl: 'https://owner.example/representative-api', + defaultApiPrefix: '/representative-api', + requestContext: { + locale: 'cs', + operationContext, + traceparent, + }, + transportHeaders: { + authorization: 'Bearer owner-resolved-assertion', + 'x-correlation-id': 'correlation-358', + }, + }).pipe( + Effect.flatMap((client) => client.representative.read({})), + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), + ); - const [request] = requests; - expect(request).toBeDefined(); - if (request === undefined) { - throw new Error('Expected captured request'); - } - expect(request.headers.get('accept-language')).toBe('cs'); - expect(request.headers.get('traceparent')).toBe(traceparent); - expect(request.headers.get('x-operation-id')).toBe( - operationContext.operationId - ); - expect( - yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( - request.headers.get('x-modernjs-bff-operation-context') ?? '' - ) - ).toEqual(operationContext); - expect(request.headers.get('authorization')).toBe( - 'Bearer owner-resolved-assertion' - ); - expect(request.headers.get('x-correlation-id')).toBe('correlation-358'); - }) + const [request] = requests; + expect(request).toBeDefined(); + if (request === undefined) { + throw new Error('Expected captured request'); + } + expect(request.headers.get('accept-language')).toBe('cs'); + expect(request.headers.get('traceparent')).toBe(traceparent); + expect(request.headers.get('x-operation-id')).toBe(operationContext.operationId); + expect( + yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + request.headers.get('x-modernjs-bff-operation-context') ?? '', + ), + ).toEqual(operationContext); + expect(request.headers.get('authorization')).toBe('Bearer owner-resolved-assertion'); + expect(request.headers.get('x-correlation-id')).toBe('correlation-358'); + }), ); -it.effect( - 'omits absent optional request context and transport header values', - () => - Effect.gen(function* testScenario5() { - const requests: Request[] = []; - const fakeFetch: typeof fetch = (input, init) => { - requests.push(new Request(input, init)); - return Promise.resolve(Response.json({ value: 'omitted' })); - }; +it.effect('omits absent optional request context and transport header values', () => + Effect.gen(function* testScenario5() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'omitted' })); + }; - yield* makeEffectBffClient({ - api: RepresentativeApi, - baseUrl: 'https://owner.example/representative-api', - defaultApiPrefix: '/representative-api', - requestContext: {}, - transportHeaders: [], - }).pipe( - Effect.flatMap((client) => client.representative.read({})), - Effect.provideService(FetchHttpClient.Fetch, fakeFetch) - ); + yield* makeEffectBffClient({ + api: RepresentativeApi, + baseUrl: 'https://owner.example/representative-api', + defaultApiPrefix: '/representative-api', + requestContext: {}, + transportHeaders: [], + }).pipe( + Effect.flatMap((client) => client.representative.read({})), + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), + ); - const [request] = requests; - expect(request).toBeDefined(); - if (request === undefined) { - throw new Error('Expected captured request'); - } - for (const header of [ - 'accept-language', - 'traceparent', - 'x-modernjs-bff-operation-context', - 'x-operation-id', - ]) { - expect(request.headers.has(header), header).toBe(false); - } - }) + const [request] = requests; + expect(request).toBeDefined(); + if (request === undefined) { + throw new Error('Expected captured request'); + } + for (const header of ['accept-language', 'traceparent', 'x-modernjs-bff-operation-context', 'x-operation-id']) { + expect(request.headers.has(header), header).toBe(false); + } + }), ); -it.effect( - 'keeps declared backend failures in the typed Effect error channel', - () => - Effect.gen(function* testScenario6() { - const problem = { - _tag: 'RepresentativeConflict' as const, - detail: 'The representative value changed.', - status: 409 as const, - title: 'Representative conflict', - type: 'urn:ontos:test:representative-conflict', - }; - const fakeFetch: typeof fetch = () => - Promise.resolve( - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, - status: 409, - }) - ); - - const outcome = yield* makeEffectBffClient({ - api: RepresentativeApi, - defaultApiPrefix: 'https://owner.example/representative-api', - }).pipe( - Effect.flatMap((client) => client.representative.read({})), - Effect.flip, - Effect.provideService(FetchHttpClient.Fetch, fakeFetch) +it.effect('keeps declared backend failures in the typed Effect error channel', () => + Effect.gen(function* testScenario6() { + const problem = { + _tag: 'RepresentativeConflict' as const, + detail: 'The representative value changed.', + status: 409 as const, + title: 'Representative conflict', + type: 'urn:ontos:test:representative-conflict', + }; + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: 409, + }), ); - expect(Schema.is(RepresentativeConflictSchema)(outcome)).toBe(true); - expect(Struct.omit(outcome, ['_tag'])).toEqual( - Struct.omit(problem, ['_tag']) - ); - }) + const outcome = yield* makeEffectBffClient({ + api: RepresentativeApi, + defaultApiPrefix: 'https://owner.example/representative-api', + }).pipe( + Effect.flatMap((client) => client.representative.read({})), + Effect.flip, + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), + ); + + expect(Schema.is(RepresentativeConflictSchema)(outcome)).toBe(true); + expect(Struct.omit(outcome, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + }), ); for (const [failureKind, transport, expectedTag] of [ ['transport', controlledTransportFailureFetch, 'HttpClientError'], ['response decoding', invalidResponseFetch, 'SchemaError'], ] as const) { - it.effect( - `keeps ${failureKind} failures in the typed Effect error channel`, - () => - Effect.gen(function* typedClientFailure() { - const outcome = yield* makeEffectBffClient({ - api: RepresentativeApi, - defaultApiPrefix: 'https://owner.example/representative-api', - }).pipe( - Effect.flatMap((client) => client.representative.read({})), - Effect.flip, - Effect.provideService(FetchHttpClient.Fetch, transport) - ); - expect(Predicate.isTagged(outcome, expectedTag)).toBe(true); - }) + it.effect(`keeps ${failureKind} failures in the typed Effect error channel`, () => + Effect.gen(function* typedClientFailure() { + const outcome = yield* makeEffectBffClient({ + api: RepresentativeApi, + defaultApiPrefix: 'https://owner.example/representative-api', + }).pipe( + Effect.flatMap((client) => client.representative.read({})), + Effect.flip, + Effect.provideService(FetchHttpClient.Fetch, transport), + ); + expect(Predicate.isTagged(outcome, expectedTag)).toBe(true); + }), ); } diff --git a/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts b/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts index 37d76169a..42a53b831 100644 --- a/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts @@ -7,47 +7,38 @@ import { HttpRouter, Layer, } from '@modern-js/plugin-bff/effect-edge'; -import type { - EffectBffDefinition, - EffectBffRuntime, -} from '@modern-js/plugin-bff/effect-edge'; +import type { EffectBffDefinition, EffectBffRuntime } from '@modern-js/plugin-bff/effect-edge'; import { Context, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { assembleEffectBffRuntime } from '../../src/effect-bff-runtime.ts'; class Greeting extends Context.Service()( - '@app/shared-contracts/tests/unit/effect-bff-runtime.test/Greeting' + '@app/shared-contracts/tests/unit/effect-bff-runtime.test/Greeting', ) {} const GreetingSchema = Schema.Struct({ greeting: Schema.String }); const api = HttpApi.make('AssemblyFixture').add( HttpApiGroup.make('fixture') .add(HttpApiEndpoint.get('greet', '/greet', { success: GreetingSchema })) - .add(HttpApiEndpoint.get('fail', '/fail', { success: GreetingSchema })) + .add(HttpApiEndpoint.get('fail', '/fail', { success: GreetingSchema })), ); const handlers = HttpApiBuilder.group(api, 'fixture', (group) => group - .handle('greet', () => - Greeting.pipe(Effect.map(({ value }) => ({ greeting: value }))) - ) - .handle('fail', () => Effect.die('fixture handler defect')) + .handle('greet', () => Greeting.pipe(Effect.map(({ value }) => ({ greeting: value })))) + .handle('fail', () => Effect.die('fixture handler defect')), ); const makeRuntime = (greeting: string) => assembleEffectBffRuntime({ api, - handlers: handlers.pipe( - Layer.provide(Layer.succeed(Greeting, { value: greeting })) - ), + handlers: handlers.pipe(Layer.provide(Layer.succeed(Greeting, { value: greeting }))), }); const makeCorsRuntime = (greeting: string) => assembleEffectBffRuntime({ api, - handlers: handlers.pipe( - Layer.provide(Layer.succeed(Greeting, { value: greeting })) - ), + handlers: handlers.pipe(Layer.provide(Layer.succeed(Greeting, { value: greeting }))), transport: HttpRouter.cors({ allowedHeaders: ['content-type'], allowedMethods: ['GET', 'OPTIONS'], @@ -58,86 +49,70 @@ const makeCorsRuntime = (greeting: string) => const failingStartupRuntime = assembleEffectBffRuntime({ api, - handlers: handlers.pipe( - Layer.provide( - Layer.effect(Greeting, Effect.die('fixture layer startup defect')) - ) - ), + handlers: handlers.pipe(Layer.provide(Layer.effect(Greeting, Effect.die('fixture layer startup defect')))), }); -const inferredRuntime: EffectBffDefinition & - EffectBffRuntime = makeRuntime('compile-time fixture'); +const inferredRuntime: EffectBffDefinition & EffectBffRuntime = + makeRuntime('compile-time fixture'); void inferredRuntime; -it.live( - 'assembles a concrete API with caller-provided handler dependencies', - () => - Effect.gen(function* assembleRuntimeEffect() { - const server = yield* Effect.acquireRelease( - Effect.sync(() => makeRuntime('substitute runtime').createHandler()), - (runtimeServer) => Effect.promise(() => runtimeServer.dispose()) - ); - const response = yield* Effect.promise(() => - server.handler(new Request('http://localhost/greet')) - ); - expect(response.status).toBe(200); - expect(yield* Effect.promise(() => response.json())).toEqual({ - greeting: 'substitute runtime', - }); - }) +it.live('assembles a concrete API with caller-provided handler dependencies', () => + Effect.gen(function* assembleRuntimeEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeRuntime('substitute runtime').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const response = yield* Effect.promise(() => server.handler(new Request('http://localhost/greet'))); + expect(response.status).toBe(200); + expect(yield* Effect.promise(() => response.json())).toEqual({ + greeting: 'substitute runtime', + }); + }), ); -it.live( - 'keeps an optional caller-owned CORS layer in the assembled runtime', - () => - Effect.gen(function* corsRuntimeEffect() { - const server = yield* Effect.acquireRelease( - Effect.sync(() => makeCorsRuntime('with cors').createHandler()), - (runtimeServer) => Effect.promise(() => runtimeServer.dispose()) - ); - const response = yield* Effect.promise(() => - server.handler( - new Request('http://localhost/greet', { - headers: { - 'access-control-request-method': 'GET', - origin: 'https://shell.example.test', - }, - method: 'OPTIONS', - }) - ) - ); - expect(response.status).toBe(204); - expect(response.headers.get('access-control-allow-origin')).toBe( - 'https://shell.example.test' - ); - expect(response.headers.get('access-control-max-age')).toBe('600'); - }) +it.live('keeps an optional caller-owned CORS layer in the assembled runtime', () => + Effect.gen(function* corsRuntimeEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeCorsRuntime('with cors').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const response = yield* Effect.promise(() => + server.handler( + new Request('http://localhost/greet', { + headers: { + 'access-control-request-method': 'GET', + origin: 'https://shell.example.test', + }, + method: 'OPTIONS', + }), + ), + ); + expect(response.status).toBe(204); + expect(response.headers.get('access-control-allow-origin')).toBe('https://shell.example.test'); + expect(response.headers.get('access-control-max-age')).toBe('600'); + }), ); -it.live( - 'keeps strict runtime defect handling at the generated HTTP boundary', - () => - Effect.gen(function* runtimeDefectEffect() { - const server = yield* Effect.acquireRelease( - Effect.sync(() => makeRuntime('unused').createHandler()), - (runtimeServer) => Effect.promise(() => runtimeServer.dispose()) - ); - const response = yield* Effect.promise(() => - server.handler(new Request('http://localhost/fail')) - ); - expect(response.status).toBe(500); - }) +it.live('keeps strict runtime defect handling at the generated HTTP boundary', () => + Effect.gen(function* runtimeDefectEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeRuntime('unused').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const response = yield* Effect.promise(() => server.handler(new Request('http://localhost/fail'))); + expect(response.status).toBe(500); + }), ); it.live('preserves caller-owned Layer startup defects', () => Effect.gen(function* startupDefectEffect() { const server = yield* Effect.acquireRelease( Effect.sync(() => failingStartupRuntime.createHandler()), - (runtimeServer) => Effect.promise(() => runtimeServer.dispose()) + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const error = yield* Effect.tryPromise(() => server.handler(new Request('http://localhost/greet'))).pipe( + Effect.flip, ); - const error = yield* Effect.tryPromise(() => - server.handler(new Request('http://localhost/greet')) - ).pipe(Effect.flip); expect(String(error.cause)).toMatch(/fixture layer startup defect/u); - }) + }), ); diff --git a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts index 26404760a..6fb6d86ad 100644 --- a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts +++ b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts @@ -16,7 +16,7 @@ import { } from '../../src/gateway-context.ts'; const endpointStatuses = ( - endpoint: (typeof GatewayContextApiGroup.endpoints)[keyof typeof GatewayContextApiGroup.endpoints] + endpoint: (typeof GatewayContextApiGroup.endpoints)[keyof typeof GatewayContextApiGroup.endpoints], ) => [...endpoint.error] .map((schema) => schema.ast.annotations?.['httpApiStatus']) @@ -28,9 +28,7 @@ const problemTag = (schema: Schema.Top) => { ? schema.ast.propertySignatures.find(({ name }) => name === '_tag')?.type : undefined; expect(tag !== undefined && SchemaAST.isLiteral(tag)).toBe(true); - return tag !== undefined && SchemaAST.isLiteral(tag) - ? tag.literal - : undefined; + return tag !== undefined && SchemaAST.isLiteral(tag) ? tag.literal : undefined; }; const principal = { @@ -60,7 +58,7 @@ it.effect('decodes the exact versioned public assertion contract', () => alg: 'EdDSA', kid: 'current-2026-08', typ: 'JWT', - }) + }), ).toEqual({ alg: 'EdDSA', kid: 'current-2026-08', @@ -69,91 +67,75 @@ it.effect('decodes the exact versioned public assertion contract', () => expect( yield* Schema.decodeEffect(GatewayContextRequestSchema)({ audience: 'inventory-stock', - }) + }), ).toEqual({ audience: 'inventory-stock' }); expect( yield* Schema.decodeEffect(GatewayContextResponseSchema)({ expiresAt: claims.exp, token: 'header.payload.signature', - }) + }), ).toEqual({ expiresAt: claims.exp, token: 'header.payload.signature' }); - }) + }), ); -it.effect( - 'rejects malformed audiences, invalid ordering, and subject mismatch', - () => - Effect.gen(function* testScenario2() { - expect( - yield* Effect.flip( - Schema.decodeEffect(GatewayContextRequestSchema)({ - audience: '', - }) - ) - ).toBeDefined(); - expect( - yield* Effect.flip( - decodeGatewayContextClaims({ ...claims, exp: claims.iat }) - ) - ).toBeDefined(); - expect( - yield* Effect.flip( - decodeGatewayContextClaims({ ...claims, exp: claims.iat + 301 }) - ) - ).toBeDefined(); - expect( - yield* Effect.flip( - decodeGatewayContextClaims({ - ...claims, - sub: '60000000-0000-4000-8000-000000000001', - }) - ) - ).toBeDefined(); - }) +it.effect('rejects malformed audiences, invalid ordering, and subject mismatch', () => + Effect.gen(function* testScenario2() { + expect( + yield* Effect.flip( + Schema.decodeEffect(GatewayContextRequestSchema)({ + audience: '', + }), + ), + ).toBeDefined(); + expect(yield* Effect.flip(decodeGatewayContextClaims({ ...claims, exp: claims.iat }))).toBeDefined(); + expect(yield* Effect.flip(decodeGatewayContextClaims({ ...claims, exp: claims.iat + 301 }))).toBeDefined(); + expect( + yield* Effect.flip( + decodeGatewayContextClaims({ + ...claims, + sub: '60000000-0000-4000-8000-000000000001', + }), + ), + ).toBeDefined(); + }), ); -it.effect( - 'rejects credential, display, authorization, Action, and business claim expansion', - () => - Effect.gen(function* testScenario3() { - const forbiddenFields = [ - 'email', - 'displayName', - 'credential', - 'rawApiKey', - 'providerKeyId', - 'keyId', - 'cookie', - 'sessionToken', - 'actionKey', - 'permission', - 'policyDecision', - 'businessPayload', - ] as const; +it.effect('rejects credential, display, authorization, Action, and business claim expansion', () => + Effect.gen(function* testScenario3() { + const forbiddenFields = [ + 'email', + 'displayName', + 'credential', + 'rawApiKey', + 'providerKeyId', + 'keyId', + 'cookie', + 'sessionToken', + 'actionKey', + 'permission', + 'policyDecision', + 'businessPayload', + ] as const; - for (const field of forbiddenFields) { - expect( - yield* Effect.flip( - decodeGatewayContextClaims({ ...claims, [field]: 'must-not-pass' }) - ), - field - ).toBeDefined(); - } + for (const field of forbiddenFields) { expect( - yield* Effect.flip( - decodeGatewayContextClaims({ - ...claims, - principal: { ...principal, email: 'must-not-pass@example.test' }, - }) - ) + yield* Effect.flip(decodeGatewayContextClaims({ ...claims, [field]: 'must-not-pass' })), + field, ).toBeDefined(); - }) + } + expect( + yield* Effect.flip( + decodeGatewayContextClaims({ + ...claims, + principal: { ...principal, email: 'must-not-pass@example.test' }, + }), + ), + ).toBeDefined(); + }), ); it('schemas publish only the required public field names', () => { - expect(GatewayTrustedPrincipalContextSchema).toBe( - TrustedPrincipalContextSchema - ); + expect(GatewayTrustedPrincipalContextSchema).toBe(TrustedPrincipalContextSchema); expect(Object.keys(GatewayContextClaimsSchema.fields).toSorted()).toEqual([ 'aud', 'exp', @@ -164,16 +146,14 @@ it('schemas publish only the required public field names', () => { 'sub', 'ver', ]); - expect( - Object.keys(GatewayContextProtectedHeaderSchema.fields).toSorted() - ).toEqual(['alg', 'kid', 'typ']); + expect(Object.keys(GatewayContextProtectedHeaderSchema.fields).toSorted()).toEqual(['alg', 'kid', 'typ']); }); it('publishes the exact API-key credential boundary and failure statuses', () => { expect(Object.keys(ApiKeyGatewayHeadersSchema.fields)).toEqual(['x-api-key']); - expect( - endpointStatuses(GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext) - ).toEqual([400, 401, 403, 429, 500, 503]); + expect(endpointStatuses(GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext)).toEqual([ + 400, 401, 403, 429, 500, 503, + ]); }); it('preserves migrated gateway Problem Details shapes and ordered endpoint membership', () => { @@ -193,35 +173,21 @@ it('preserves migrated gateway Problem Details shapes and ordered endpoint membe title: 'Gateway unavailable', type: 'https://ontos.dev/problems/gateway-unavailable', } as const; - const decodedRateLimited = Schema.decodeSync(GatewayRateLimitedProblemSchema)( - rateLimited - ); - expect(Schema.is(GatewayRateLimitedProblemSchema)(decodedRateLimited)).toBe( - true - ); - expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual( - Struct.omit(rateLimited, ['_tag']) - ); - const decodedUnavailable = Schema.decodeSync(GatewayUnavailableProblemSchema)( - unavailable - ); - expect(Schema.is(GatewayUnavailableProblemSchema)(decodedUnavailable)).toBe( - true - ); - expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual( - Struct.omit(unavailable, ['_tag']) - ); + const decodedRateLimited = Schema.decodeSync(GatewayRateLimitedProblemSchema)(rateLimited); + expect(Schema.is(GatewayRateLimitedProblemSchema)(decodedRateLimited)).toBe(true); + expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual(Struct.omit(rateLimited, ['_tag'])); + const decodedUnavailable = Schema.decodeSync(GatewayUnavailableProblemSchema)(unavailable); + expect(Schema.is(GatewayUnavailableProblemSchema)(decodedUnavailable)).toBe(true); + expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual(Struct.omit(unavailable, ['_tag'])); expect(() => Schema.decodeUnknownSync(GatewayRateLimitedProblemSchema, { onExcessProperty: 'error', })({ ...rateLimited, internalDiagnostic: 'must-not-pass', - }) + }), ).toThrow(); - const actual = [ - ...GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext.error, - ]; + const actual = [...GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext.error]; expect(actual.map(problemTag)).toEqual([ 'GatewayAuthenticationRequiredProblem', 'GatewayAudienceInvalidProblem', diff --git a/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts b/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts index 0865a3ed0..5d59a0b5a 100644 --- a/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts @@ -1,18 +1,11 @@ -import { - HttpApi, - HttpApiEndpoint, - HttpApiGroup, -} from '@modern-js/plugin-bff/effect-client'; +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from '@modern-js/plugin-bff/effect-client'; import { Effect, Redacted, Schema } from 'effect'; import { expect, it } from 'effect-rstest'; import { FetchHttpClient } from 'effect/unstable/http'; import { makeGovernedEffectBffClient } from '../../src/client-runtime.ts'; import { makeGovernedReadProblems } from '../../src/effect-bff-runtime.ts'; -import { - makeProblemDetailsSchema, - makeRetryableProblemDetailsSchema, -} from '../../src/problem-details.ts'; +import { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from '../../src/problem-details.ts'; const schemas = { authentication: makeProblemDetailsSchema('AuthenticationProblem', 401), @@ -49,7 +42,7 @@ it('shared problem factories preserve concrete schemas, statuses, retryability a 'policyConflict', 'policyIneligible', 'unavailable', - ]) + ]), )(key); const problem = problems[kind](); expect(Schema.is(schemas[kind])(problem)).toBe(true); @@ -66,7 +59,7 @@ it('shared problem factories preserve concrete schemas, statuses, retryability a status: 401, title: 'Authentication required', type: 'https://ontos.dev/problems/operation-authentication-required', - }) + }), ); expect(problems.internal()).toEqual( schemas.internal.make({ @@ -74,7 +67,7 @@ it('shared problem factories preserve concrete schemas, statuses, retryability a status: 500, title: 'Read failed', type: 'https://ontos.dev/problems/read-failed', - }) + }), ); }); @@ -83,14 +76,10 @@ const api = HttpApi.make('GovernedTransportTest').add( HttpApiEndpoint.get('execute', '/read', { error: schemas.unavailable, success: Schema.String, - }) - ) + }), + ), ); -const makeClient = ( - credential: string, - requestCorrelation: string, - baseUrl: string | URL -) => +const makeClient = (credential: string, requestCorrelation: string, baseUrl: string | URL) => makeGovernedEffectBffClient( { api, @@ -98,91 +87,65 @@ const makeClient = ( defaultApiPrefix: '/owner-api', requestCorrelation, }, - { baseUrl } + { baseUrl }, ); -it.effect( - 'shared transport is lazy and keeps each invocation credential, correlation and trusted URL', - () => - Effect.gen(function* checkTransport() { - const requests: Request[] = []; - const fetch: typeof globalThis.fetch = (input, init) => { - requests.push(new Request(input, init)); - return Promise.resolve(Response.json('ok')); - }; - const url = new URL('https://owner.example/custom'); - const first = makeClient('Bearer first', 'first-correlation', url); - url.protocol = 'ftp:'; - url.hostname = 'attacker.example'; - const second = makeClient( - 'Bearer second', - 'second-correlation', - 'https://owner.example/custom' +it.effect('shared transport is lazy and keeps each invocation credential, correlation and trusted URL', () => + Effect.gen(function* checkTransport() { + const requests: Request[] = []; + const fetch: typeof globalThis.fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json('ok')); + }; + const url = new URL('https://owner.example/custom'); + const first = makeClient('Bearer first', 'first-correlation', url); + url.protocol = 'ftp:'; + url.hostname = 'attacker.example'; + const second = makeClient('Bearer second', 'second-correlation', 'https://owner.example/custom'); + expect(requests.length).toBe(0); + for (const client of [first, second]) { + const result = yield* client.pipe( + Effect.flatMap((value) => value.read.execute({})), + Effect.provideService(FetchHttpClient.Fetch, fetch), ); - expect(requests.length).toBe(0); - for (const client of [first, second]) { - const result = yield* client.pipe( - Effect.flatMap((value) => value.read.execute({})), - Effect.provideService(FetchHttpClient.Fetch, fetch) - ); - expect(result).toBe('ok'); - } - expect( - requests.map((request) => [ - request.url, - request.headers.get('authorization'), - request.headers.get('x-correlation-id'), - ]) - ).toEqual([ - [ - 'https://owner.example/custom/read', - 'Bearer first', - 'first-correlation', - ], - [ - 'https://owner.example/custom/read', - 'Bearer second', - 'second-correlation', - ], - ]); - }) + expect(result).toBe('ok'); + } + expect( + requests.map((request) => [ + request.url, + request.headers.get('authorization'), + request.headers.get('x-correlation-id'), + ]), + ).toEqual([ + ['https://owner.example/custom/read', 'Bearer first', 'first-correlation'], + ['https://owner.example/custom/read', 'Bearer second', 'second-correlation'], + ]); + }), ); -it.effect( - 'shared transport retains the concrete retryable backend error union', - () => - Effect.gen(function* checkTypedFailure() { - const fetch: typeof globalThis.fetch = () => - Promise.resolve( - Response.json(problems.unavailable(), { - headers: { 'content-type': 'application/problem+json' }, - status: 503, - }) - ); - const result = yield* makeClient( - 'Bearer proof', - 'correlation', - 'https://owner.example/api' - ).pipe( - Effect.flatMap((client) => client.read.execute({})), - Effect.provideService(FetchHttpClient.Fetch, fetch), - Effect.flip - ); - expect(Schema.is(schemas.unavailable)(result)).toBe(true); - const problem = yield* Schema.decodeUnknownEffect(schemas.unavailable)( - result - ); - expect(yield* Schema.encodeEffect(schemas.unavailable)(problem)).toEqual( - yield* Schema.encodeEffect(schemas.unavailable)(problems.unavailable()) +it.effect('shared transport retains the concrete retryable backend error union', () => + Effect.gen(function* checkTypedFailure() { + const fetch: typeof globalThis.fetch = () => + Promise.resolve( + Response.json(problems.unavailable(), { + headers: { 'content-type': 'application/problem+json' }, + status: 503, + }), ); - }) + const result = yield* makeClient('Bearer proof', 'correlation', 'https://owner.example/api').pipe( + Effect.flatMap((client) => client.read.execute({})), + Effect.provideService(FetchHttpClient.Fetch, fetch), + Effect.flip, + ); + expect(Schema.is(schemas.unavailable)(result)).toBe(true); + const problem = yield* Schema.decodeUnknownEffect(schemas.unavailable)(result); + expect(yield* Schema.encodeEffect(schemas.unavailable)(problem)).toEqual( + yield* Schema.encodeEffect(schemas.unavailable)(problems.unavailable()), + ); + }), ); -for (const baseUrl of [ - 'data:text/plain,unsafe', - 'https://user:password@owner.example/api', - '//attacker.example/api', -]) { +for (const baseUrl of ['data:text/plain,unsafe', 'https://user:password@owner.example/api', '//attacker.example/api']) { it.effect(`shared transport rejects unsafe URL ${baseUrl} before fetch`, () => Effect.gen(function* checkUnsafeUrl() { let calls = 0; @@ -190,22 +153,16 @@ for (const baseUrl of [ calls += 1; return Promise.resolve(Response.json('unsafe')); }; - const result = yield* makeClient( - 'Bearer secret', - 'correlation', - baseUrl - ).pipe( + const result = yield* makeClient('Bearer secret', 'correlation', baseUrl).pipe( Effect.flatMap((client) => client.read.execute({})), Effect.provideService(FetchHttpClient.Fetch, fetch), - Effect.flip + Effect.flip, ); expect(Schema.isSchemaError(result)).toBe(true); if (Schema.isSchemaError(result)) { - expect(result.message).not.toMatch( - /password|Bearer secret|owner\.example/u - ); + expect(result.message).not.toMatch(/password|Bearer secret|owner\.example/u); } expect(calls).toBe(0); - }) + }), ); } diff --git a/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts b/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts index 2f41670bf..cd7a361d2 100644 --- a/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts +++ b/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts @@ -34,37 +34,25 @@ const marker = { version: generatedBuildMetadata.version, }; -it.effect( - 'marker and readiness schemas preserve the generated wire representation', - () => - Effect.gen(function* wireRepresentationEffect() { - const readiness = { - checks: { - api: 'ready' as const, - moduleFederation: 'ready' as const, - ssr: 'ready' as const, - translations: 'ready' as const, - }, - marker, - status: 'ready' as const, - versionSkew: 'none' as const, - }; +it.effect('marker and readiness schemas preserve the generated wire representation', () => + Effect.gen(function* wireRepresentationEffect() { + const readiness = { + checks: { + api: 'ready' as const, + moduleFederation: 'ready' as const, + ssr: 'ready' as const, + translations: 'ready' as const, + }, + marker, + status: 'ready' as const, + versionSkew: 'none' as const, + }; - expect( - yield* Schema.decodeEffect(MicroVerticalBuildMarkerSchema)( - generatedBuildMetadata - ) - ).toEqual(marker); - expect( - yield* Schema.encodeEffect(MicroVerticalBuildMarkerSchema)(marker) - ).toEqual(marker); - expect( - yield* Schema.decodeEffect(MicroVerticalReadinessSchema)(readiness) - ).toEqual(readiness); - expect( - yield* Schema.encodeEffect(MicroVerticalReadinessSchema)(readiness) - ).toEqual(readiness); - }) + expect(yield* Schema.decodeEffect(MicroVerticalBuildMarkerSchema)(generatedBuildMetadata)).toEqual(marker); + expect(yield* Schema.encodeEffect(MicroVerticalBuildMarkerSchema)(marker)).toEqual(marker); + expect(yield* Schema.decodeEffect(MicroVerticalReadinessSchema)(readiness)).toEqual(readiness); + expect(yield* Schema.encodeEffect(MicroVerticalReadinessSchema)(readiness)).toEqual(readiness); + }), ); it('constructs generated-client operation metadata with and without trace identity', () => { @@ -77,21 +65,19 @@ it('constructs generated-client operation metadata with and without trace identi routePath: '/inventory/readiness', tenantId: 'must-not-pass', }; - expect(createMicroVerticalOperationContext(inputWithSensitiveExtras)).toEqual( - { - method: 'GET', - operationId: 'InventoryApi:inventory:readiness', - routePath: '/inventory/readiness', - source: 'generated-client', - } - ); + expect(createMicroVerticalOperationContext(inputWithSensitiveExtras)).toEqual({ + method: 'GET', + operationId: 'InventoryApi:inventory:readiness', + routePath: '/inventory/readiness', + source: 'generated-client', + }); expect( createMicroVerticalOperationContext({ method: 'POST', operationId: 'InventoryApi:inventory:create', routePath: '/inventory', traceId: 'trace-123', - }) + }), ).toEqual({ method: 'POST', operationId: 'InventoryApi:inventory:create', @@ -130,8 +116,8 @@ it('projects only standard operation telemetry attributes', () => { method: 'GET', operationId: 'InventoryApi:inventory:list', routePath: '/inventory', - }) - ) + }), + ), ).toEqual({ 'modernjs.operation.id': 'InventoryApi:inventory:list', 'modernjs.operation.method': 'GET', diff --git a/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts b/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts index 77da72efe..94575705b 100644 --- a/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts +++ b/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts @@ -20,124 +20,102 @@ const options = { const expectType = (value: Expected): Expected => value; -it.effect( - 'preserves the literal audience and success and failure inference', - () => - Effect.gen(function* inferenceEffect() { - const gateway = makeOperationGateway( - audience, - ({ audience: receivedAudience }) => { - expectType(receivedAudience); - return Effect.fail(gatewayAcquisitionFailure); - } - ); - - yield* expectType< - Effect.Effect< - never, - typeof gatewayAcquisitionFailure | typeof operationAttemptFailure - > - >(gateway.invoke(() => Effect.fail(operationAttemptFailure))).pipe( - Effect.flip - ); - yield* expectType>( - makeOperationGateway(audience, () => - Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }) - ).invoke(() => Effect.succeed('completed' as const)) - ); - }) +it.effect('preserves the literal audience and success and failure inference', () => + Effect.gen(function* inferenceEffect() { + const gateway = makeOperationGateway(audience, ({ audience: receivedAudience }) => { + expectType(receivedAudience); + return Effect.fail(gatewayAcquisitionFailure); + }); + + yield* expectType>( + gateway.invoke(() => Effect.fail(operationAttemptFailure)), + ).pipe(Effect.flip); + yield* expectType>( + makeOperationGateway(audience, () => Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' })).invoke( + () => Effect.succeed('completed' as const), + ), + ); + }), ); -it.effect( - 'acquires one audience-scoped assertion and forwards options unchanged', - () => - Effect.gen(function* audienceAssertionEffect() { - const issuerCalls: { - readonly audience: typeof audience; - readonly options: typeof options; - }[] = []; - const authorizations: string[] = []; - const gateway = makeOperationGateway( - audience, - (payload, receivedOptions) => - Effect.sync(() => { - expect(receivedOptions).toBe(options); - issuerCalls.push({ audience: payload.audience, options }); - return { - expiresAt: 1_700_000_300, - token: 'header.payload.signature', - }; - }) - ); - - const result = yield* gateway.invoke((authorization) => { - authorizations.push(authorization); - return Effect.succeed('completed' as const); - }, options); +it.effect('acquires one audience-scoped assertion and forwards options unchanged', () => + Effect.gen(function* audienceAssertionEffect() { + const issuerCalls: { + readonly audience: typeof audience; + readonly options: typeof options; + }[] = []; + const authorizations: string[] = []; + const gateway = makeOperationGateway(audience, (payload, receivedOptions) => + Effect.sync(() => { + expect(receivedOptions).toBe(options); + issuerCalls.push({ audience: payload.audience, options }); + return { + expiresAt: 1_700_000_300, + token: 'header.payload.signature', + }; + }), + ); + + const result = yield* gateway.invoke((authorization) => { + authorizations.push(authorization); + return Effect.succeed('completed' as const); + }, options); - expect(result).toBe('completed'); - expect(issuerCalls).toEqual([{ audience, options }]); - expect(authorizations).toEqual(['Bearer header.payload.signature']); - }) + expect(result).toBe('completed'); + expect(issuerCalls).toEqual([{ audience, options }]); + expect(authorizations).toEqual(['Bearer header.payload.signature']); + }), ); -it.effect( - 'acquires a fresh assertion whenever an invocation Effect is executed', - () => - Effect.gen(function* freshAssertionEffect() { - let acquisitions = 0; - const gateway = makeOperationGateway(audience, () => - Effect.sync(() => { - acquisitions += 1; - return { expiresAt: 1_700_000_300, token: `attempt-${acquisitions}` }; - }) - ); - const authorizations: string[] = []; - const invocation = gateway.invoke((authorization) => - Effect.sync(() => { - authorizations.push(authorization); - }) - ); - - yield* invocation; - yield* invocation; - - expect(acquisitions).toBe(2); - expect(authorizations).toEqual(['Bearer attempt-1', 'Bearer attempt-2']); - }) +it.effect('acquires a fresh assertion whenever an invocation Effect is executed', () => + Effect.gen(function* freshAssertionEffect() { + let acquisitions = 0; + const gateway = makeOperationGateway(audience, () => + Effect.sync(() => { + acquisitions += 1; + return { expiresAt: 1_700_000_300, token: `attempt-${acquisitions}` }; + }), + ); + const authorizations: string[] = []; + const invocation = gateway.invoke((authorization) => + Effect.sync(() => { + authorizations.push(authorization); + }), + ); + + yield* invocation; + yield* invocation; + + expect(acquisitions).toBe(2); + expect(authorizations).toEqual(['Bearer attempt-1', 'Bearer attempt-2']); + }), ); -it.effect( - 'preserves issuer failure and does not construct the attempted Effect', - () => - Effect.gen(function* issuerFailureEffect() { - let attemptCalls = 0; - const gateway = makeOperationGateway(audience, () => - Effect.fail(gatewayAcquisitionFailure) - ); - - const received = yield* gateway - .invoke(() => { - attemptCalls += 1; - return Effect.succeed('must not run'); - }) - .pipe(Effect.flip); - - expect(received).toBe(gatewayAcquisitionFailure); - expect(attemptCalls).toBe(0); - }) +it.effect('preserves issuer failure and does not construct the attempted Effect', () => + Effect.gen(function* issuerFailureEffect() { + let attemptCalls = 0; + const gateway = makeOperationGateway(audience, () => Effect.fail(gatewayAcquisitionFailure)); + + const received = yield* gateway + .invoke(() => { + attemptCalls += 1; + return Effect.succeed('must not run'); + }) + .pipe(Effect.flip); + + expect(received).toBe(gatewayAcquisitionFailure); + expect(attemptCalls).toBe(0); + }), ); it.effect('preserves the attempted-operation failure without translation', () => Effect.gen(function* attemptFailureEffect() { const gateway = makeOperationGateway(audience, () => - Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }) + Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), ); - const received = yield* gateway - .invoke(() => Effect.fail(operationAttemptFailure)) - .pipe(Effect.flip); + const received = yield* gateway.invoke(() => Effect.fail(operationAttemptFailure)).pipe(Effect.flip); expect(received).toBe(operationAttemptFailure); - }) + }), ); diff --git a/app/packages/shared-contracts/tests/unit/problem-details.test.ts b/app/packages/shared-contracts/tests/unit/problem-details.test.ts index feb7c4a76..ff989710c 100644 --- a/app/packages/shared-contracts/tests/unit/problem-details.test.ts +++ b/app/packages/shared-contracts/tests/unit/problem-details.test.ts @@ -7,27 +7,14 @@ import { HttpRouter, HttpServer, } from '@modern-js/plugin-bff/effect-edge'; -import { - Context, - Effect, - Layer, - Predicate, - Schema, - SchemaAST, - Struct, -} from 'effect'; +import { Context, Effect, Layer, Predicate, Schema, SchemaAST, Struct } from 'effect'; import { expect, it } from 'effect-rstest'; import { FetchHttpClient } from 'effect/unstable/http'; -import { - makeProblemDetailsSchema, - makeRetryableProblemDetailsSchema, -} from '../../src/problem-details.ts'; +import { makeProblemDetailsSchema, makeRetryableProblemDetailsSchema } from '../../src/problem-details.ts'; import { ImportedUnconstrainedExtensionSchema } from '../fixtures/unconstrained-extension.ts'; -const statuses = [ - 400, 401, 403, 404, 409, 422, 428, 429, 500, 503, 504, -] as const; +const statuses = [400, 401, 403, 404, 409, 422, 428, 429, 500, 503, 504] as const; for (const status of statuses) { it(`couples the ${status} body, schema, and HttpApi status`, () => { @@ -45,14 +32,10 @@ for (const status of statuses) { const decodedProblem = Schema.decodeSync(schema)(problem); expect(Schema.is(schema)(decodedProblem)).toBe(true); - expect(Struct.omit(decodedProblem, ['_tag'])).toEqual( - Struct.omit(problem, ['_tag']) - ); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); const encodedProblem = Schema.encodeUnknownSync(schema)(problem); expect(Schema.is(Schema.toEncoded(schema))(encodedProblem)).toBe(true); - expect(Struct.omit(encodedProblem, ['_tag'])).toEqual( - Struct.omit(problem, ['_tag']) - ); + expect(Struct.omit(encodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); expect(schema.ast.annotations?.['httpApiStatus']).toBe(status); const encoding = schema.ast.annotations?.['~httpApiEncoding']; expect(Predicate.isTagged(encoding, 'Json')).toBe(true); @@ -62,26 +45,20 @@ for (const status of statuses) { expect(Struct.omit(encoding, ['_tag'])).toEqual({ contentType: 'application/problem+json', }); - expect(() => - Schema.decodeUnknownSync(schema)({ ...problem, status: 418 }) - ).toThrow(); + expect(() => Schema.decodeUnknownSync(schema)({ ...problem, status: 418 })).toThrow(); expect(() => Schema.decodeUnknownSync(schema, { onExcessProperty: 'error' })({ ...problem, internalDiagnostic: 'must-not-pass', - }) + }), ).toThrow(); }); } it('adds only the deliberate retryable literal marker', () => { - const schema = makeRetryableProblemDetailsSchema( - 'RetryableFixtureProblem', - 503, - { - retryAfterSeconds: Schema.Finite, - } - ); + const schema = makeRetryableProblemDetailsSchema('RetryableFixtureProblem', 503, { + retryAfterSeconds: Schema.Finite, + }); const problem = { _tag: 'RetryableFixtureProblem', detail: 'Try later.', @@ -94,23 +71,15 @@ it('adds only the deliberate retryable literal marker', () => { const decodedProblem = Schema.decodeSync(schema)(problem); expect(Schema.is(schema)(decodedProblem)).toBe(true); - expect(Struct.omit(decodedProblem, ['_tag'])).toEqual( - Struct.omit(problem, ['_tag']) - ); - expect(() => - Schema.decodeUnknownSync(schema)({ ...problem, retryable: false }) - ).toThrow(); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + expect(() => Schema.decodeUnknownSync(schema)({ ...problem, retryable: false })).toThrow(); }); it.live('drives real HttpApi responses and generated client decoding', () => Effect.gen(function* problemDetailsHttpScenario() { - const schema = makeRetryableProblemDetailsSchema( - 'FixtureGatewayTimeoutProblem', - 504, - { - operation: Schema.Literal('fixture-read'), - } - ); + const schema = makeRetryableProblemDetailsSchema('FixtureGatewayTimeoutProblem', 504, { + operation: Schema.Literal('fixture-read'), + }); const problem = schema.make({ detail: 'The fixture operation timed out.', operation: 'fixture-read', @@ -124,23 +93,20 @@ it.live('drives real HttpApi responses and generated client decoding', () => error: [schema], payload: Schema.Struct({}), success: Schema.Struct({ ok: Schema.Literal(true) }), - }) + }), ); const api = HttpApi.make('ProblemFixtureApi').add(group); const handlers = HttpApiBuilder.group(api, 'problemFixture', (builder) => - builder.handle('execute', () => Effect.fail(problem)) + builder.handle('execute', () => Effect.fail(problem)), ); const server = yield* Effect.acquireRelease( Effect.sync(() => HttpRouter.toWebHandler( - HttpApiBuilder.layer(api).pipe( - Layer.provide(handlers), - Layer.provide(HttpServer.layerServices) - ), - { disableLogger: true } - ) + HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices)), + { disableLogger: true }, + ), ), - (webHandler) => Effect.promise(() => webHandler.dispose()) + (webHandler) => Effect.promise(() => webHandler.dispose()), ); const request = new Request('https://fixture.ontos.test/problem-fixture', { @@ -148,13 +114,9 @@ it.live('drives real HttpApi responses and generated client decoding', () => headers: { 'content-type': 'application/json' }, method: 'POST', }); - const response = yield* Effect.promise(() => - server.handler(request, Context.empty()) - ); + const response = yield* Effect.promise(() => server.handler(request, Context.empty())); expect(response.status).toBe(problem.status); - expect(response.headers.get('content-type') ?? '').toMatch( - /^application\/problem\+json\b/u - ); + expect(response.headers.get('content-type') ?? '').toMatch(/^application\/problem\+json\b/u); expect(yield* Effect.promise(() => response.json())).toEqual(problem); const client = makeEffectHttpApiClient(api, { @@ -162,16 +124,14 @@ it.live('drives real HttpApi responses and generated client decoding', () => }); const clientError = yield* Effect.flip( client.pipe( - Effect.flatMap((generated) => - generated.problemFixture.execute({ payload: {} }) - ), + Effect.flatMap((generated) => generated.problemFixture.execute({ payload: {} })), Effect.provideService(FetchHttpClient.Fetch, (input, init) => - server.handler(new Request(input, init), Context.empty()) - ) - ) + server.handler(new Request(input, init), Context.empty()), + ), + ), ); expect(clientError).toEqual(problem); - }) + }), ); it('rejects reserved and unconstrained extension schemas at construction', () => { @@ -180,32 +140,32 @@ it('rejects reserved and unconstrained extension schemas at construction', () => expect(() => makeProblemDetailsSchema('ReservedFixtureProblem', 400, { status: Schema.Finite, - }) + }), ).toThrow(/reserved/u); expect(() => makeProblemDetailsSchema('PrototypeSyntaxFixtureProblem', 400, { __proto__: Schema.String, - }) + }), ).toThrow(/plain object/u); expect(() => makeProblemDetailsSchema('SymbolKeyFixtureProblem', 400, { [uniqueSymbol]: Schema.Unknown, - }) + }), ).toThrow(/names must be strings/u); expect(() => makeProblemDetailsSchema('PrototypeKeyFixtureProblem', 400, { ['__proto__']: Schema.String, - }) + }), ).toThrow(/reserved/u); expect(() => makeProblemDetailsSchema('UnknownFixtureProblem', 400, { unsafe: Schema.Unknown, - }) + }), ).toThrow(/concrete/u); expect(() => makeProblemDetailsSchema('AnyFixtureProblem', 400, { unsafe: Schema.Any, - }) + }), ).toThrow(/concrete/u); for (const unsafe of [ Schema.Array(Schema.Unknown), @@ -225,16 +185,12 @@ it('rejects reserved and unconstrained extension schemas at construction', () => Schema.Undefined, ImportedUnconstrainedExtensionSchema, ]) { - expect(() => - makeProblemDetailsSchema('NestedUnknownFixtureProblem', 400, { unsafe }) - ).toThrow(/concrete/u); + expect(() => makeProblemDetailsSchema('NestedUnknownFixtureProblem', 400, { unsafe })).toThrow(/concrete/u); } for (const literal of [undefined, Symbol('non-json-literal')]) { // @ts-expect-error JavaScript callers can provide unsupported literal values, so the runtime factory must still reject them. const unsafe = Schema.Literal(literal); - expect(() => - makeProblemDetailsSchema('NonJsonLiteralFixtureProblem', 400, { unsafe }) - ).toThrow(/concrete/u); + expect(() => makeProblemDetailsSchema('NonJsonLiteralFixtureProblem', 400, { unsafe })).toThrow(/concrete/u); } }); @@ -248,9 +204,9 @@ it('rejects accessor-backed extension fields before reading them', () => { }, }); - expect(() => - makeProblemDetailsSchema('AccessorFixtureProblem', 400, extensions) - ).toThrow(/enumerable data property/u); + expect(() => makeProblemDetailsSchema('AccessorFixtureProblem', 400, extensions)).toThrow( + /enumerable data property/u, + ); expect(reads).toBe(0); }); @@ -269,13 +225,9 @@ it('uses one descriptor snapshot for extension keys and schema ASTs', () => { ownKeyReads += 1; return ownKeyReads === 1 ? [] : ['status', Symbol('unsafe')]; }, - } - ); - const stableSchema = makeProblemDetailsSchema( - 'StableSnapshotProblem', - 400, - changingFields + }, ); + const stableSchema = makeProblemDetailsSchema('StableSnapshotProblem', 400, changingFields); expect(ownKeyReads).toBe(1); expect(() => Schema.decodeUnknownSync(stableSchema)({ @@ -284,7 +236,7 @@ it('uses one descriptor snapshot for extension keys and schema ASTs', () => { status: 418, title: 'Wrong status', type: 'urn:ontos:test:wrong-status', - }) + }), ).toThrow(); let astReads = 0; @@ -301,17 +253,13 @@ it('uses one descriptor snapshot for extension keys and schema ASTs', () => { expect(() => makeProblemDetailsSchema('StableAstProblem', 400, { diagnostics: changingSchema, - }) + }), ).toThrow(/concrete/u); const mutableExtension = Schema.Struct({ note: Schema.String }); - const immutableProblem = makeProblemDetailsSchema( - 'ImmutableAstProblem', - 400, - { - metadata: mutableExtension, - } - ); + const immutableProblem = makeProblemDetailsSchema('ImmutableAstProblem', 400, { + metadata: mutableExtension, + }); expect(SchemaAST.isObjects(mutableExtension.ast)).toBe(true); const [note] = mutableExtension.ast.propertySignatures; expect(note).toBeDefined(); @@ -327,7 +275,7 @@ it('uses one descriptor snapshot for extension keys and schema ASTs', () => { status: 400, title: 'Immutable AST', type: 'urn:ontos:test:immutable-ast', - }) + }), ).toThrow(); }); @@ -359,9 +307,7 @@ it('accepts concrete JSON literals and schemas with JSON-safe encodings', () => expect(decoded.occurredAt.toISOString()).toBe(encoded.occurredAt); const reencoded = Schema.encodeUnknownSync(schema)(decoded); expect(Schema.is(Schema.toEncoded(schema))(reencoded)).toBe(true); - expect(Struct.omit(reencoded, ['_tag'])).toEqual( - Struct.omit(encoded, ['_tag']) - ); + expect(Struct.omit(reencoded, ['_tag'])).toEqual(Struct.omit(encoded, ['_tag'])); }); const narrowSchema = makeProblemDetailsSchema('NarrowFixtureProblem', 409, { diff --git a/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts b/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts index 6978da2c8..42731cb15 100644 --- a/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts +++ b/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts @@ -17,11 +17,7 @@ it('injected build identity updates all surfaces without mutating generated meta ui: { ...deliveryUnit, surface: 'ui' }, }, } as const; - const result = withUltramodernBuildIdentity( - artifact, - 'injected-build', - 'source-revision' - ); + const result = withUltramodernBuildIdentity(artifact, 'injected-build', 'source-revision'); const expectedIdentity = { ...deliveryUnit, build: 'injected-build', diff --git a/app/packages/shared-contracts/tooling/modern-config.ts b/app/packages/shared-contracts/tooling/modern-config.ts index f03eac9e4..a328d3a2c 100644 --- a/app/packages/shared-contracts/tooling/modern-config.ts +++ b/app/packages/shared-contracts/tooling/modern-config.ts @@ -1,23 +1,17 @@ import { builtinModules } from 'node:module'; import path from 'node:path'; -const nodeBuiltinRequests = new Set( - builtinModules.flatMap((name) => [name, `node:${name}`]) -); +const nodeBuiltinRequests = new Set(builtinModules.flatMap((name) => [name, `node:${name}`])); interface ExternalRequest { dependencyType?: string; request?: string; } -type ExternalResult = [ - error?: Error | undefined, - result?: string | string[], - type?: 'module-import', -]; +type ExternalResult = [error?: Error | undefined, result?: string | string[], type?: 'module-import']; export const resolveCloudflareExternal = ( { dependencyType, request }: ExternalRequest, - includeNodeBuiltins = true + includeNodeBuiltins = true, ): ExternalResult => { if (request === undefined) { return []; @@ -26,12 +20,8 @@ export const resolveCloudflareExternal = ( if (request !== 'cloudflare:sockets' && !isNodeBuiltin) { return []; } - const specifier = - isNodeBuiltin && !request.startsWith('node:') ? `node:${request}` : request; - const nativeImport = - dependencyType?.startsWith('commonjs') === true - ? [specifier, 'default'] - : specifier; + const specifier = isNodeBuiltin && !request.startsWith('node:') ? `node:${request}` : request; + const nativeImport = dependencyType?.startsWith('commonjs') === true ? [specifier, 'default'] : specifier; return [undefined, nativeImport, 'module-import']; }; @@ -63,14 +53,7 @@ export const createCloudflareWorkerSecurity = () => ({ 'img-src': ["'self'", 'data:', 'blob:', 'https:', 'http:'], 'manifest-src': ["'self'", 'https:', 'http:'], 'object-src': ["'none'"], - 'script-src': [ - "'self'", - "'unsafe-inline'", - "'unsafe-eval'", - 'https:', - 'http:', - 'blob:', - ], + 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'", 'https:', 'http:', 'blob:'], 'style-src': ["'self'", "'unsafe-inline'", 'https:', 'http:'], 'worker-src': ["'self'", 'blob:'], }, @@ -81,8 +64,7 @@ export const createCloudflareWorkerSecurity = () => ({ enabled: true, headers: { contentTypeOptions: 'nosniff' as const, - permissionsPolicy: - 'camera=(), geolocation=(), microphone=(), payment=(), usb=()', + permissionsPolicy: 'camera=(), geolocation=(), microphone=(), payment=(), usb=()', referrerPolicy: 'strict-origin-when-cross-origin' as const, }, noindex: { @@ -98,16 +80,10 @@ interface ReplacementResource { } const retainWorkerLoader = (resource: ReplacementResource) => { - resource.request = resource.request.replace( - /(?[?&])retain=[^&]*/u, - '$retain=true' - ); + resource.request = resource.request.replace(/(?[?&])retain=[^&]*/u, '$retain=true'); }; -const markWorkerApiSource = ( - resource: ReplacementResource, - sourceDirectory: string -) => { +const markWorkerApiSource = (resource: ReplacementResource, sourceDirectory: string) => { const [requestPath] = resource.request.split('?', 1); if ( requestPath !== undefined && @@ -123,16 +99,13 @@ export const createWorkerSsrPlugins = ( DefinePlugin: new (definitions: Record) => DefinitionPlugin; NormalModuleReplacementPlugin: new ( pattern: RegExp, - replace: (resource: ReplacementResource) => void + replace: (resource: ReplacementResource) => void, ) => ReplacementPlugin; }, - sourceDirectory: string + sourceDirectory: string, ) => [ new rspack.DefinePlugin({ 'globalThis.FinalizationRegistry': 'undefined' }), - new rspack.NormalModuleReplacementPlugin( - /[?&]loaderId=/u, - retainWorkerLoader - ), + new rspack.NormalModuleReplacementPlugin(/[?&]loaderId=/u, retainWorkerLoader), new rspack.NormalModuleReplacementPlugin(/^\.\.?[/\\]/u, (resource) => { markWorkerApiSource(resource, sourceDirectory); }), diff --git a/app/quality-audit/import-clone-evidence.mts b/app/quality-audit/import-clone-evidence.mts index 41ccda7bd..8f66db87e 100644 --- a/app/quality-audit/import-clone-evidence.mts +++ b/app/quality-audit/import-clone-evidence.mts @@ -2,9 +2,7 @@ import { Effect, FileSystem, Path, Schema } from 'effect'; import { parseSync } from 'oxc-parser'; const CloneLocation = Schema.Struct({ - end: Schema.optional( - Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1)) - ), + end: Schema.optional(Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1))), name: Schema.String, start: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1)), }); @@ -14,9 +12,9 @@ const CloneReport = Schema.fromJsonString( Schema.Struct({ firstFile: CloneLocation, secondFile: CloneLocation, - }) + }), ), - }) + }), ); /** Static binding declarations do not duplicate implementation behavior. */ @@ -25,47 +23,39 @@ export const containsOnlyImportBindings = (source: string): boolean => { return ( parsed.errors.length === 0 && parsed.program.body.length > 1 && - parsed.program.body.every( - (statement) => - statement.type === 'ImportDeclaration' && - statement.specifiers.length > 0 - ) + parsed.program.body.every((statement) => statement.type === 'ImportDeclaration' && statement.specifiers.length > 0) ); }; -export const importCloneEvidence = Effect.fn( - 'QualityAudit.importCloneEvidence' -)(function* collectImportCloneEvidence(root: string, source: string) { +export const importCloneEvidence = Effect.fn('QualityAudit.importCloneEvidence')(function* collectImportCloneEvidence( + root: string, + source: string, +) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const report = yield* Schema.decodeUnknownEffect(CloneReport)(source); - const provesBindings = Effect.fn('QualityAudit.provesImportBindings')( - function* proveImportBindings(location: typeof CloneLocation.Type) { - const relative = path.relative(root, location.name); - if ( - location.end === undefined || - relative.startsWith('..') || - path.isAbsolute(relative) || - location.end < location.start - ) { - return false; - } - const text = yield* fs.readFileString(path.join(root, relative)); - const lines = text.split('\n'); - return ( - location.end <= lines.length && - containsOnlyImportBindings( - lines.slice(location.start - 1, location.end).join('\n') - ) - ); + const provesBindings = Effect.fn('QualityAudit.provesImportBindings')(function* proveImportBindings( + location: typeof CloneLocation.Type, + ) { + const relative = path.relative(root, location.name); + if ( + location.end === undefined || + relative.startsWith('..') || + path.isAbsolute(relative) || + location.end < location.start + ) { + return false; } - ); + const text = yield* fs.readFileString(path.join(root, relative)); + const lines = text.split('\n'); + return ( + location.end <= lines.length && + containsOnlyImportBindings(lines.slice(location.start - 1, location.end).join('\n')) + ); + }); const evidence = []; for (const pair of report.duplicates) { - if ( - (yield* provesBindings(pair.firstFile)) && - (yield* provesBindings(pair.secondFile)) - ) { + if ((yield* provesBindings(pair.firstFile)) && (yield* provesBindings(pair.secondFile))) { evidence.push(pair); } } diff --git a/app/quality-audit/knip-model.mts b/app/quality-audit/knip-model.mts index d3ab07769..2cc87f786 100644 --- a/app/quality-audit/knip-model.mts +++ b/app/quality-audit/knip-model.mts @@ -5,10 +5,7 @@ import type { PlatformError } from 'effect/PlatformError'; import { parseSync, Visitor } from 'oxc-parser'; import type { Node, ObjectExpression, Program } from 'oxc-parser'; -import { - buildKnipRuntimeEvidence, - workspaceDirectories, -} from './knip-runtime-model.mts'; +import { buildKnipRuntimeEvidence, workspaceDirectories } from './knip-runtime-model.mts'; const Strings = Schema.Array(Schema.String); const PluginSchema = Schema.Struct({ @@ -30,14 +27,8 @@ export const KnipConfigSchema = Schema.Struct({ ignore: Schema.optional(Strings), workspaces: Schema.optional(Schema.Record(Schema.String, WorkspaceSchema)), }); -const ExportLeaf = Schema.Union([ - Schema.String, - Schema.Record(Schema.String, Schema.String), -]); -const ExportsSchema = Schema.Union([ - Schema.String, - Schema.Record(Schema.String, ExportLeaf), -]); +const ExportLeaf = Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.String)]); +const ExportsSchema = Schema.Union([Schema.String, Schema.Record(Schema.String, ExportLeaf)]); const DependencyDeclarationSchema = Schema.Struct({ dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), @@ -53,29 +44,22 @@ const PackageSchema = Schema.Struct({ Schema.Struct({ manifest: Schema.optional(Schema.String), registration: Schema.optional(Schema.String), - }) + }), ), - }) - ), - 'zephyr:dependencies': Schema.optional( - Schema.Record(Schema.String, Schema.String) + }), ), + 'zephyr:dependencies': Schema.optional(Schema.Record(Schema.String, Schema.String)), }); -class KnipModelError extends Schema.TaggedError()( - 'KnipModelError', - { - reason: Schema.String, - } -) {} +class KnipModelError extends Schema.TaggedError()('KnipModelError', { + reason: Schema.String, +}) {} const unprovenResolver = { kind: 'resolver-unproven' } as const; export const KnipModelEvidenceSchema = Schema.Struct({ anchor: Schema.optional(Schema.String), - column: Schema.optional( - Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)) - ), + column: Schema.optional(Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))), kind: Schema.Literals([ 'entry', 'file', @@ -132,10 +116,7 @@ const propertyName = (node: Node): string | undefined => { const declarations = (program: Program): Map => { const result = new Map(); for (const statement of program.body) { - const declaration = - statement.type === 'ExportNamedDeclaration' - ? statement.declaration - : statement; + const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement; if (declaration?.type !== 'VariableDeclaration') { continue; } @@ -148,31 +129,20 @@ const declarations = (program: Program): Map => { return result; }; -const unwrap = ( - node: Node | undefined, - variables: ReadonlyMap, - depth = 0 -): Node | undefined => { +const unwrap = (node: Node | undefined, variables: ReadonlyMap, depth = 0): Node | undefined => { if (node === undefined || depth > 12) { return undefined; } if (node.type === 'Identifier') { return unwrap(variables.get(node.name), variables, depth + 1); } - if ( - node.type === 'TSAsExpression' || - node.type === 'TSSatisfiesExpression' || - node.type === 'TSNonNullExpression' - ) { + if (node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression' || node.type === 'TSNonNullExpression') { return unwrap(node.expression, variables, depth + 1); } return node; }; -const staticString = ( - node: Node | undefined, - variables: ReadonlyMap -): string | undefined => { +const staticString = (node: Node | undefined, variables: ReadonlyMap): string | undefined => { const value = unwrap(node, variables); if (value?.type === 'Literal' && isString(value.value)) { return value.value; @@ -185,7 +155,7 @@ const staticString = ( const objectExpression = ( node: Node | undefined, - variables: ReadonlyMap + variables: ReadonlyMap, ): ObjectExpression | undefined => { const value = unwrap(node, variables); if (value?.type === 'ObjectExpression') { @@ -197,35 +167,19 @@ const objectExpression = ( return undefined; }; -const exportedObject = ({ - program, - variables, -}: SourceFacts): ObjectExpression | undefined => { - const exported = program.body.find( - (node) => node.type === 'ExportDefaultDeclaration' - ); - return exported?.type === 'ExportDefaultDeclaration' - ? objectExpression(exported.declaration, variables) - : undefined; +const exportedObject = ({ program, variables }: SourceFacts): ObjectExpression | undefined => { + const exported = program.body.find((node) => node.type === 'ExportDefaultDeclaration'); + return exported?.type === 'ExportDefaultDeclaration' ? objectExpression(exported.declaration, variables) : undefined; }; -const objectValue = ( - object: ObjectExpression | undefined, - name: string -): Node | undefined => { +const objectValue = (object: ObjectExpression | undefined, name: string): Node | undefined => { const property = object?.properties.find( - (node) => - node.type === 'Property' && - !node.computed && - propertyName(node.key) === name + (node) => node.type === 'Property' && !node.computed && propertyName(node.key) === name, ); return property?.type === 'Property' ? property.value : undefined; }; -const rstestEnvironmentEvidence = ( - facts: SourceFacts, - workspace: string -): KnipModelEvidence[] => { +const rstestEnvironmentEvidence = (facts: SourceFacts, workspace: string): KnipModelEvidence[] => { if (!/rstest\.config\.[cm]?[jt]s$/u.test(facts.file)) { return []; } @@ -252,24 +206,20 @@ const rstestEnvironmentEvidence = ( 'dependency', target, environment?.start ?? 0, - 'Rstest testEnvironment consumer' + 'Rstest testEnvironment consumer', ), ]; }); }; -const exportLeaves = ( - value: typeof ExportsSchema.Type | undefined -): string[] => { +const exportLeaves = (value: typeof ExportsSchema.Type | undefined): string[] => { if (isString(value)) { return [value]; } if (value === undefined) { return []; } - return Object.values(value).flatMap((entry) => - isString(entry) ? [entry] : Object.values(entry) - ); + return Object.values(value).flatMap((entry) => (isString(entry) ? [entry] : Object.values(entry))); }; const importedUrlBase = (node: Node | undefined): boolean => @@ -278,61 +228,52 @@ const importedUrlBase = (node: Node | undefined): boolean => node.object.type === 'MetaProperty' && node.object.meta.name === 'import'; -const parseSource = Effect.fn('QualityAudit.parseKnipModelSource')( - function* parseModelSource(file: string, source: string) { - const result = yield* Effect.try({ - catch: () => - new KnipModelError({ - reason: `Unable to parse quality model source ${file}`, - }), - try: () => parseSync(file, source), +const parseSource = Effect.fn('QualityAudit.parseKnipModelSource')(function* parseModelSource( + file: string, + source: string, +) { + const result = yield* Effect.try({ + catch: () => + new KnipModelError({ + reason: `Unable to parse quality model source ${file}`, + }), + try: () => parseSync(file, source), + }); + if (result.errors.length > 0) { + return yield* new KnipModelError({ + reason: `Invalid quality model source ${file}: ${result.errors[0]?.message}`, }); - if (result.errors.length > 0) { - return yield* new KnipModelError({ - reason: `Invalid quality model source ${file}: ${result.errors[0]?.message}`, - }); - } - return { - file, - program: result.program, - source, - variables: declarations(result.program), - }; } -); + return { + file, + program: result.program, + source, + variables: declarations(result.program), + }; +}); -const sourceFiles = Effect.fn('QualityAudit.knipModelSourceFiles')( - function* readModelFiles( - root: string, - relative: string - ): Effect.fn.Return< - string[], - PlatformError, - FileSystem.FileSystem | Path.Path - > { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const files: string[] = []; - const names = yield* fs.readDirectory(path.join(root, relative)); - const children = names.flatMap((name) => { - const child = relative.length > 0 ? `${relative}/${name}` : name; - return name.startsWith('.') || - skippedDirectories.has(name) || - child === invalidFixtures - ? [] - : [child]; - }); - for (const child of children) { - const stat = yield* fs.stat(path.join(root, child)); - if (stat.type === 'Directory') { - files.push(...(yield* sourceFiles(root, child))); - } else if (sourceExtension.test(child)) { - files.push(child); - } +const sourceFiles = Effect.fn('QualityAudit.knipModelSourceFiles')(function* readModelFiles( + root: string, + relative: string, +): Effect.fn.Return { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const files: string[] = []; + const names = yield* fs.readDirectory(path.join(root, relative)); + const children = names.flatMap((name) => { + const child = relative.length > 0 ? `${relative}/${name}` : name; + return name.startsWith('.') || skippedDirectories.has(name) || child === invalidFixtures ? [] : [child]; + }); + for (const child of children) { + const stat = yield* fs.stat(path.join(root, child)); + if (stat.type === 'Directory') { + files.push(...(yield* sourceFiles(root, child))); + } else if (sourceExtension.test(child)) { + files.push(child); } - return files; } -); + return files; +}); /** * `zephyr:dependencies` maps a Module Federation remote alias to a versioned package @@ -353,7 +294,7 @@ const zephyrPackageName = (reference: string): string | undefined => { const manifestEvidence = ( manifest: typeof PackageSchema.Type, manifestFile: string, - workspace: string + workspace: string, ): readonly KnipModelEvidence[] => { const facts: KnipModelEvidence[] = []; for (const target of [ @@ -372,9 +313,7 @@ const manifestEvidence = ( }); } } - for (const [alias, reference] of Object.entries( - manifest['zephyr:dependencies'] ?? {} - )) { + for (const [alias, reference] of Object.entries(manifest['zephyr:dependencies'] ?? {})) { const target = zephyrPackageName(reference); if (target !== undefined) { facts.push({ @@ -396,7 +335,7 @@ const evidenceAt = ( kind: KnipModelEvidence['kind'], target: string, offset: number, - reason: string + reason: string, ): KnipModelEvidence => ({ column: offset - facts.source.lastIndexOf('\n', offset - 1), kind, @@ -411,7 +350,7 @@ const federationFieldEvidence = ( facts: SourceFacts, workspace: string, field: string, - entries: ObjectExpression | undefined + entries: ObjectExpression | undefined, ): KnipModelEvidence[] => { const result: KnipModelEvidence[] = []; for (const item of entries?.properties ?? []) { @@ -419,8 +358,7 @@ const federationFieldEvidence = ( continue; } const name = propertyName(item.key); - const target = - field === 'exposes' ? staticString(item.value, facts.variables) : name; + const target = field === 'exposes' ? staticString(item.value, facts.variables) : name; if (target !== undefined) { result.push( evidenceAt( @@ -429,40 +367,25 @@ const federationFieldEvidence = ( field === 'exposes' ? 'entry' : 'dependency', target, item.start, - `Module Federation ${field} consumer` - ) + `Module Federation ${field} consumer`, + ), ); } } return result; }; -const federationEvidence = ( - facts: SourceFacts, - workspace: string -): KnipModelEvidence[] => { - if ( - !/(?:module-federation|backend-federation)\.config\.[cm]?[jt]s$/u.test( - facts.file - ) - ) { +const federationEvidence = (facts: SourceFacts, workspace: string): KnipModelEvidence[] => { + if (!/(?:module-federation|backend-federation)\.config\.[cm]?[jt]s$/u.test(facts.file)) { return []; } const object = exportedObject(facts); return ['remotes', 'shared', 'exposes'].flatMap((field) => - federationFieldEvidence( - facts, - workspace, - field, - objectExpression(objectValue(object, field), facts.variables) - ) + federationFieldEvidence(facts, workspace, field, objectExpression(objectValue(object, field), facts.variables)), ); }; -const scopedVariables = ( - facts: SourceFacts, - offset: number -): ReadonlyMap => { +const scopedVariables = (facts: SourceFacts, offset: number): ReadonlyMap => { const variables = new Map(facts.variables); const recordBlock = (node: Extract) => { if (node.start <= offset && offset < node.end) { @@ -471,10 +394,7 @@ const scopedVariables = ( continue; } for (const declaration of statement.declarations) { - if ( - declaration.id.type === 'Identifier' && - declaration.init !== null - ) { + if (declaration.id.type === 'Identifier' && declaration.init !== null) { variables.set(declaration.id.name, declaration.init); } } @@ -487,10 +407,7 @@ const scopedVariables = ( const requiredSourceEvidence = (facts: SourceFacts): KnipModelEvidence[] => { const evidence: KnipModelEvidence[] = []; - const requiredPaths = unwrap( - facts.variables.get('requiredPaths'), - facts.variables - ); + const requiredPaths = unwrap(facts.variables.get('requiredPaths'), facts.variables); if (requiredPaths?.type === 'ArrayExpression') { for (const element of requiredPaths.elements) { const target = staticString(element ?? undefined, facts.variables); @@ -502,8 +419,8 @@ const requiredSourceEvidence = (facts: SourceFacts): KnipModelEvidence[] => { 'file', target, element?.start ?? 0, - 'Workspace validator requiredPaths checks this source file; named exports remain audited' - ) + 'Workspace validator requiredPaths checks this source file; named exports remain audited', + ), ); } } @@ -518,59 +435,31 @@ const isAppBuildTemplate = (argument: Node | undefined): boolean => argument.expressions[0].name === 'appPath' && argument.quasis[1]?.value.cooked === '/shared/ultramodern-build.ts'; -const configuredBuildDirectories = ( - facts: SourceFacts, - field: string -): string[] => { - const contract = objectExpression( - facts.variables.get('workspaceValidationContractDefinition'), - facts.variables - ); - const topology = objectExpression( - objectValue(contract, 'topology'), - facts.variables - ); - const compact = objectExpression( - objectValue(topology, 'compactConfig'), - facts.variables - ); - const collection = - field === 'apps' - ? objectValue(compact, 'apps') - : objectValue(contract, field); +const configuredBuildDirectories = (facts: SourceFacts, field: string): string[] => { + const contract = objectExpression(facts.variables.get('workspaceValidationContractDefinition'), facts.variables); + const topology = objectExpression(objectValue(contract, 'topology'), facts.variables); + const compact = objectExpression(objectValue(topology, 'compactConfig'), facts.variables); + const collection = field === 'apps' ? objectValue(compact, 'apps') : objectValue(contract, field); const array = unwrap(collection, facts.variables); if (array?.type !== 'ArrayExpression') { return []; } return array.elements.flatMap((element) => { const target = staticString( - objectValue( - objectExpression(element ?? undefined, facts.variables), - 'path' - ), - facts.variables + objectValue(objectExpression(element ?? undefined, facts.variables), 'path'), + facts.variables, ); return target === undefined ? [] : [target]; }); }; -const buildSourceScope = ( - source: Node | undefined, - variables: ReadonlyMap -): Node | undefined => { +const buildSourceScope = (source: Node | undefined, variables: ReadonlyMap): Node | undefined => { const reader = unwrap(source, variables); - if ( - reader?.type !== 'CallExpression' || - reader.callee.type !== 'Identifier' || - reader.callee.name !== 'readText' - ) { + if (reader?.type !== 'CallExpression' || reader.callee.type !== 'Identifier' || reader.callee.name !== 'readText') { return undefined; } const [argument] = reader.arguments; - if ( - argument?.type !== 'TemplateLiteral' || - argument.quasis[1]?.value.cooked !== '/shared/ultramodern-build.ts' - ) { + if (argument?.type !== 'TemplateLiteral' || argument.quasis[1]?.value.cooked !== '/shared/ultramodern-build.ts') { return undefined; } return argument.expressions[0]; @@ -579,7 +468,7 @@ const buildSourceScope = ( const buildSourceDirectories = ( source: Node | undefined, variables: ReadonlyMap, - facts: SourceFacts + facts: SourceFacts, ): string[] => { const expression = buildSourceScope(source, variables); if (expression?.type === 'Identifier' && expression.name === 'appPath') { @@ -598,53 +487,35 @@ const buildSourceDirectories = ( ['shell', 'additionalShells'], ]); const collection = collections.get(expression.object.name); - return collection === undefined - ? [] - : configuredBuildDirectories(facts, collection); + return collection === undefined ? [] : configuredBuildDirectories(facts, collection); }; const validatedBuildExport = ( node: Extract, - variables: ReadonlyMap + variables: ReadonlyMap, ): { name: string; source: Node } | undefined => { - if ( - node.callee.type === 'Identifier' && - node.callee.name === 'assertBuildFacadeExport' - ) { + if (node.callee.type === 'Identifier' && node.callee.name === 'assertBuildFacadeExport') { const name = staticString(node.arguments[1], variables); const [source] = node.arguments; - return name === undefined || source === undefined - ? undefined - : { name, source }; + return name === undefined || source === undefined ? undefined : { name, source }; } - if ( - node.callee.type !== 'MemberExpression' || - propertyName(node.callee.property) !== 'includes' - ) { + if (node.callee.type !== 'MemberExpression' || propertyName(node.callee.property) !== 'includes') { return undefined; } const expected = staticString(node.arguments[0], variables); - const { name } = - expected?.match(/^export const (?[A-Za-z_$][A-Za-z0-9_$]*)\b/u) - ?.groups ?? {}; + const { name } = expected?.match(/^export const (?[A-Za-z_$][A-Za-z0-9_$]*)\b/u)?.groups ?? {}; return name === undefined ? undefined : { name, source: node.callee.object }; }; const namedBuildEvidence = (facts: SourceFacts): KnipModelEvidence[] => { const evidence: KnipModelEvidence[] = []; - const recordBuildExport = ( - node: Extract - ) => { + const recordBuildExport = (node: Extract) => { const variables = scopedVariables(facts, node.start); const contract = validatedBuildExport(node, variables); if (contract === undefined) { return; } - for (const directory of buildSourceDirectories( - contract.source, - variables, - facts - )) { + for (const directory of buildSourceDirectories(contract.source, variables, facts)) { evidence.push( evidenceAt( facts, @@ -652,8 +523,8 @@ const namedBuildEvidence = (facts: SourceFacts): KnipModelEvidence[] => { 'export', `${directory}/shared/ultramodern-build.ts#${contract.name}`, node.start, - 'Workspace validator verifies this exact named build facade export' - ) + 'Workspace validator verifies this exact named build facade export', + ), ); } }; @@ -665,14 +536,9 @@ const validatorEvidence = (facts: SourceFacts): KnipModelEvidence[] => { if (facts.file !== 'scripts/validate-ultramodern-workspace.mts') { return []; } - const evidence = [ - ...requiredSourceEvidence(facts), - ...namedBuildEvidence(facts), - ]; + const evidence = [...requiredSourceEvidence(facts), ...namedBuildEvidence(facts)]; const appPaths = configuredBuildDirectories(facts, 'apps'); - const recordValidatorCall = ( - node: Extract - ) => { + const recordValidatorCall = (node: Extract) => { if (node.callee.type !== 'Identifier') { return; } @@ -688,8 +554,8 @@ const validatorEvidence = (facts: SourceFacts): KnipModelEvidence[] => { 'file', `${appPath}/shared/ultramodern-build.ts`, node.start, - 'Workspace validator reads build source for each declared topology.compactConfig.apps path' - ) + 'Workspace validator reads build source for each declared topology.compactConfig.apps path', + ), ); } }; @@ -708,7 +574,7 @@ const isJoinedSourceSpecifier = ( node: Node | undefined, directory: string, file: string, - variables: ReadonlyMap + variables: ReadonlyMap, ): boolean => node?.type === 'CallExpression' && node.callee.type === 'MemberExpression' && @@ -721,12 +587,9 @@ const sourceEvidence = ( facts: SourceFacts, workspace: string, directory: string, - path: Path.Path + path: Path.Path, ): KnipModelEvidence[] => { - const result = [ - ...federationEvidence(facts, workspace), - ...validatorEvidence(facts), - ]; + const result = [...federationEvidence(facts, workspace), ...validatorEvidence(facts)]; const manualCommand = `Usage: node ${facts.file} `; if ( facts.file === 'tools/oxlint/effect-native/tests/run-on-repo.mts' && @@ -739,22 +602,15 @@ const sourceEvidence = ( 'entry', facts.file, facts.source.indexOf(manualCommand), - 'Documented manual diagnostic CLI; operator entry, not proof of CI execution' - ) + 'Documented manual diagnostic CLI; operator entry, not proof of CI execution', + ), ); } const recordUrl = (node: Extract) => { - if ( - node.callee.type !== 'Identifier' || - node.callee.name !== 'URL' || - !importedUrlBase(node.arguments[1]) - ) { + if (node.callee.type !== 'Identifier' || node.callee.name !== 'URL' || !importedUrlBase(node.arguments[1])) { return; } - const target = staticString( - node.arguments[0], - scopedVariables(facts, node.start) - ); + const target = staticString(node.arguments[0], scopedVariables(facts, node.start)); if (target !== undefined && !target.includes(':')) { result.push( evidenceAt( @@ -763,47 +619,31 @@ const sourceEvidence = ( 'file', path.join(directory, target), node.start, - 'Static source URL consumed relative to import.meta.url' - ) + 'Static source URL consumed relative to import.meta.url', + ), ); } }; - const recordSubprocess = ( - node: Extract - ) => { + const recordSubprocess = (node: Extract) => { if (isChildProcessMake(node.callee)) { const [, args] = node.arguments; if (args?.type === 'ArrayExpression') { for (const argument of args.elements) { const target = staticString(argument ?? undefined, facts.variables); if (target !== undefined && sourceExtension.test(target)) { - result.push( - evidenceAt( - facts, - workspace, - 'file', - target, - node.start, - 'Node subprocess source argument' - ) - ); + result.push(evidenceAt(facts, workspace, 'file', target, node.start, 'Node subprocess source argument')); } } } } }; - const recordConfiguredFile = ( - node: Extract - ) => { + const recordConfiguredFile = (node: Extract) => { if ( facts.file === 'scripts/validate-ultramodern-workspace.mts' && node.callee.type === 'Identifier' && node.callee.name === 'readText' ) { - const target = staticString( - node.arguments[0], - scopedVariables(facts, node.start) - ); + const target = staticString(node.arguments[0], scopedVariables(facts, node.start)); if (target !== undefined) { result.push( evidenceAt( @@ -812,21 +652,16 @@ const sourceEvidence = ( 'file', target, node.start, - 'Workspace validator reads source text; exports are not marked used' - ) + 'Workspace validator reads source text; exports are not marked used', + ), ); } } }; - const recordLintConfig = ( - node: Extract - ) => { + const recordLintConfig = (node: Extract) => { const consumers = new Map([ ['tools/oxlint/effect-native/report.mts', 'report.config.ts'], - [ - 'tools/oxlint/effect-native/tests/repository-policy.test.mts', - 'repository-policy.config.ts', - ], + ['tools/oxlint/effect-native/tests/repository-policy.test.mts', 'repository-policy.config.ts'], ]); const config = consumers.get(facts.file); const [joined] = node.arguments; @@ -841,58 +676,37 @@ const sourceEvidence = ( } const target = `tools/oxlint/effect-native/${config}`; result.push( - evidenceAt( - facts, - workspace, - 'file', - target, - node.start, - 'Lint subprocess configuration' - ), + evidenceAt(facts, workspace, 'file', target, node.start, 'Lint subprocess configuration'), evidenceAt( facts, workspace, 'export', `${target}#default`, node.start, - 'Oxlint loader consumes the configuration default export' - ) + 'Oxlint loader consumes the configuration default export', + ), ); }; const recordLintPlugin = (node: ObjectExpression) => { const specifier = objectValue(node, 'specifier'); if ( - facts.file !== - 'tools/oxlint/effect-native/tests/shared-helpers.test.mts' || - staticString(objectValue(node, 'name'), facts.variables) !== - 'shared-helpers-probe' || - !isJoinedSourceSpecifier( - specifier, - 'testsDirectory', - 'shared-helpers-probe.ts', - facts.variables - ) + facts.file !== 'tools/oxlint/effect-native/tests/shared-helpers.test.mts' || + staticString(objectValue(node, 'name'), facts.variables) !== 'shared-helpers-probe' || + !isJoinedSourceSpecifier(specifier, 'testsDirectory', 'shared-helpers-probe.ts', facts.variables) ) { return; } const target = 'tools/oxlint/effect-native/tests/shared-helpers-probe.ts'; result.push( - evidenceAt( - facts, - workspace, - 'file', - target, - node.start, - 'Oxlint jsPlugins source specifier' - ), + evidenceAt(facts, workspace, 'file', target, node.start, 'Oxlint jsPlugins source specifier'), evidenceAt( facts, workspace, 'export', `${target}#default`, node.start, - 'Oxlint jsPlugins loader consumes only the plugin default export' - ) + 'Oxlint jsPlugins loader consumes only the plugin default export', + ), ); }; const recordCall = (node: Extract) => { @@ -909,21 +723,8 @@ const sourceEvidence = ( if (facts.file === 'scripts/quality-audit.mts') { const recordAuditStep = (node: ObjectExpression) => { const tool = staticString(objectValue(node, 'tool'), facts.variables); - if ( - tool !== undefined && - objectValue(node, 'args') !== undefined && - objectValue(node, 'name') !== undefined - ) { - result.push( - evidenceAt( - facts, - workspace, - 'dependency', - tool, - node.start, - 'Quality audit subprocess step' - ) - ); + if (tool !== undefined && objectValue(node, 'args') !== undefined && objectValue(node, 'name') !== undefined) { + result.push(evidenceAt(facts, workspace, 'dependency', tool, node.start, 'Quality audit subprocess step')); } }; new Visitor({ ObjectExpression: recordAuditStep }).visit(facts.program); @@ -933,28 +734,21 @@ const sourceEvidence = ( const drizzleFactories = (facts: SourceFacts): ReadonlySet => { const imports = facts.program.body.filter( - (node) => - node.type === 'ImportDeclaration' && - node.source.value === 'drizzle-orm/pg-core' + (node) => node.type === 'ImportDeclaration' && node.source.value === 'drizzle-orm/pg-core', ); return new Set( imports.flatMap((node) => node.type === 'ImportDeclaration' ? node.specifiers.flatMap((specifier) => specifier.type === 'ImportSpecifier' && - [ - 'pgSchema', - 'pgTable', - 'pgEnum', - 'pgSequence', - 'pgView', - 'pgMaterializedView', - ].includes(propertyName(specifier.imported) ?? '') + ['pgSchema', 'pgTable', 'pgEnum', 'pgSequence', 'pgView', 'pgMaterializedView'].includes( + propertyName(specifier.imported) ?? '', + ) ? [specifier.local.name] - : [] + : [], ) - : [] - ) + : [], + ), ); }; @@ -962,7 +756,7 @@ const isDrizzleDeclaration = ( node: Node | undefined, variables: ReadonlyMap, factories: ReadonlySet, - depth = 0 + depth = 0, ): boolean => { const expression = unwrap(node, variables); if (expression?.type !== 'CallExpression' || depth > 5) { @@ -971,32 +765,17 @@ const isDrizzleDeclaration = ( if (expression.callee.type === 'Identifier') { return factories.has(expression.callee.name); } - if ( - expression.callee.type === 'MemberExpression' && - propertyName(expression.callee.property) === 'table' - ) { - return isDrizzleDeclaration( - expression.callee.object, - variables, - factories, - depth + 1 - ); + if (expression.callee.type === 'MemberExpression' && propertyName(expression.callee.property) === 'table') { + return isDrizzleDeclaration(expression.callee.object, variables, factories, depth + 1); } return false; }; -const reflectedDrizzleExports = ( - facts: SourceFacts, - workspace: string, - configSource: string -): KnipModelEvidence[] => { +const reflectedDrizzleExports = (facts: SourceFacts, workspace: string, configSource: string): KnipModelEvidence[] => { const factories = drizzleFactories(facts); const result: KnipModelEvidence[] = []; for (const node of facts.program.body) { - if ( - node.type !== 'ExportNamedDeclaration' || - node.declaration?.type !== 'VariableDeclaration' - ) { + if (node.type !== 'ExportNamedDeclaration' || node.declaration?.type !== 'VariableDeclaration') { continue; } for (const declaration of node.declaration.declarations) { @@ -1012,8 +791,8 @@ const reflectedDrizzleExports = ( 'export', `${facts.file}#${declaration.id.name}`, declaration.start, - `Drizzle reflective schema consumer configured by ${configSource}` - ) + `Drizzle reflective schema consumer configured by ${configSource}`, + ), ); } } @@ -1023,9 +802,7 @@ const reflectedDrizzleExports = ( const resolver = createRequire(import.meta.url); const packageName = (specifier: string): string => - specifier.startsWith('@') - ? specifier.split('/').slice(0, 2).join('/') - : (specifier.split('/')[0] ?? specifier); + specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : (specifier.split('/')[0] ?? specifier); const isRequireResolve = (node: Node): boolean => node.type === 'CallExpression' && @@ -1044,7 +821,7 @@ const isPathJoin = (node: Node): boolean => const resolverAnchor = ( node: Extract, - variables: ReadonlyMap + variables: ReadonlyMap, ): Node | undefined => { const options = objectExpression(node.arguments[1], variables); const anchors = unwrap(objectValue(options, 'paths'), variables); @@ -1053,10 +830,7 @@ const resolverAnchor = ( : undefined; }; -const resolveInstalledDependency = ( - dependency: string, - anchor: string -): string | undefined => { +const resolveInstalledDependency = (dependency: string, anchor: string): string | undefined => { try { return resolver.resolve(dependency, { paths: [anchor] }); } catch { @@ -1067,7 +841,7 @@ const resolveInstalledDependency = ( const resolveStaticCall = ( value: Extract, variables: ReadonlyMap, - resolvePath: (node: Node | undefined) => string | undefined + resolvePath: (node: Node | undefined) => string | undefined, ): string | undefined => { const target = staticString(value.arguments[0], variables); const anchor = resolverAnchor(value, variables); @@ -1075,14 +849,10 @@ const resolveStaticCall = ( return undefined; } const resolvedAnchor = resolvePath(anchor); - return resolvedAnchor === undefined - ? undefined - : resolveInstalledDependency(target, resolvedAnchor); + return resolvedAnchor === undefined ? undefined : resolveInstalledDependency(target, resolvedAnchor); }; -const isImportMetaUrl = ( - node: Node | undefined -): node is Extract => +const isImportMetaUrl = (node: Node | undefined): node is Extract => node?.type === 'NewExpression' && node.callee.type === 'Identifier' && node.callee.name === 'URL' && @@ -1091,7 +861,7 @@ const isImportMetaUrl = ( const resolveJoinedPath = ( value: Extract, path: Path.Path, - resolvePath: (node: Node) => string | undefined + resolvePath: (node: Node) => string | undefined, ): string | undefined => { if (!isPathJoin(value.callee)) { return undefined; @@ -1108,7 +878,7 @@ const staticPath = ( variables: ReadonlyMap, file: string, path: Path.Path, - depth = 0 + depth = 0, ): string | undefined => { if (depth > 12) { return undefined; @@ -1120,33 +890,21 @@ const staticPath = ( } if (isImportMetaUrl(value)) { const target = staticString(value.arguments[0], variables); - return target === undefined - ? undefined - : path.resolve(path.dirname(file), target); + return target === undefined ? undefined : path.resolve(path.dirname(file), target); } if (value?.type !== 'CallExpression') { return undefined; } - if ( - value.callee.type === 'Identifier' && - value.callee.name === 'fileURLToPath' - ) { + if (value.callee.type === 'Identifier' && value.callee.name === 'fileURLToPath') { return staticPath(value.arguments[0], variables, file, path, depth + 1); } if (isRequireResolve(value)) { - return resolveStaticCall(value, variables, (argument) => - staticPath(argument, variables, file, path, depth + 1) - ); + return resolveStaticCall(value, variables, (argument) => staticPath(argument, variables, file, path, depth + 1)); } - return resolveJoinedPath(value, path, (argument) => - staticPath(argument, variables, file, path, depth + 1) - ); + return resolveJoinedPath(value, path, (argument) => staticPath(argument, variables, file, path, depth + 1)); }; -const hasNativeRequire = ( - facts: SourceFacts, - variables: ReadonlyMap -): boolean => { +const hasNativeRequire = (facts: SourceFacts, variables: ReadonlyMap): boolean => { const binding = variables.get('require'); if ( binding?.type !== 'CallExpression' || @@ -1163,8 +921,8 @@ const hasNativeRequire = ( (specifier) => specifier.type === 'ImportSpecifier' && propertyName(specifier.imported) === 'createRequire' && - specifier.local.name === 'createRequire' - ) + specifier.local.name === 'createRequire', + ), ); }; @@ -1172,7 +930,7 @@ const resolverEvidence = ( facts: SourceFacts, workspace: string, appRoot: string, - path: Path.Path + path: Path.Path, ): KnipModelEvidence[] => { const evidence: KnipModelEvidence[] = []; const recordResolver = (node: Extract) => { @@ -1184,25 +942,11 @@ const resolverEvidence = ( return; } const target = staticString(node.arguments[0], variables); - if ( - target === undefined || - target.startsWith('.') || - target.startsWith('node:') - ) { + if (target === undefined || target.startsWith('.') || target.startsWith('node:')) { return; } - const anchor = staticPath( - resolverAnchor(node, variables), - variables, - path.join(appRoot, facts.file), - path - ); - const resolved = staticPath( - node, - variables, - path.join(appRoot, facts.file), - path - ); + const anchor = staticPath(resolverAnchor(node, variables), variables, path.join(appRoot, facts.file), path); + const resolved = staticPath(node, variables, path.join(appRoot, facts.file), path); if (anchor === undefined || resolved === undefined) { return; } @@ -1213,7 +957,7 @@ const resolverEvidence = ( 'resolver', packageName(target), node.arguments[0]?.start ?? node.start, - `Explicit require.resolve paths anchor ${anchor}; resolves to ${resolved}` + `Explicit require.resolve paths anchor ${anchor}; resolves to ${resolved}`, ), anchor, resolved, @@ -1226,21 +970,15 @@ const resolverEvidence = ( const drizzleEvidence = ( factsByPath: ReadonlyMap, prefix: string, - workspace: string + workspace: string, ): KnipModelEvidence[] => { const evidence: KnipModelEvidence[] = []; for (const facts of factsByPath.values()) { if (!/drizzle(?:\.[^.]+)?\.config\.[cm]?[jt]s$/u.test(facts.file)) { continue; } - const schema = staticString( - objectValue(exportedObject(facts), 'schema'), - facts.variables - ); - const target = - schema === undefined - ? undefined - : factsByPath.get(`${prefix}${schema.replace(/^\.\//u, '')}`); + const schema = staticString(objectValue(exportedObject(facts), 'schema'), facts.variables); + const target = schema === undefined ? undefined : factsByPath.get(`${prefix}${schema.replace(/^\.\//u, '')}`); if (target !== undefined) { evidence.push(...reflectedDrizzleExports(target, workspace, facts.file)); } @@ -1248,221 +986,192 @@ const drizzleEvidence = ( return evidence; }; -const nearestPackage = Effect.fn('QualityAudit.nearestPackage')( - function* readNearestPackage(anchor: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - let directory = anchor; - if (!(yield* fs.exists(directory))) { - return null; - } - if ((yield* fs.stat(directory)).type !== 'Directory') { - directory = path.dirname(directory); - } - while (true) { - const manifest = path.join(directory, 'package.json'); - if (yield* fs.exists(manifest)) { - const declared = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(DependencyDeclarationSchema) - )(yield* fs.readFileString(manifest)); - if (declared.name !== undefined) { - return { declared, manifest }; - } - } - const parent = path.dirname(directory); - if (parent === directory) { - return null; +const nearestPackage = Effect.fn('QualityAudit.nearestPackage')(function* readNearestPackage(anchor: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + let directory = anchor; + if (!(yield* fs.exists(directory))) { + return null; + } + if ((yield* fs.stat(directory)).type !== 'Directory') { + directory = path.dirname(directory); + } + while (true) { + const manifest = path.join(directory, 'package.json'); + if (yield* fs.exists(manifest)) { + const declared = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(DependencyDeclarationSchema))( + yield* fs.readFileString(manifest), + ); + if (declared.name !== undefined) { + return { declared, manifest }; } - directory = parent; } + const parent = path.dirname(directory); + if (parent === directory) { + return null; + } + directory = parent; } -); +}); -const classifyProducerTarget = Effect.fn('QualityAudit.classifyProducerTarget')( - function* classifyProducer(fact: KnipModelEvidence, manifest: string) { - const fs = yield* FileSystem.FileSystem; - const resolved = resolveInstalledDependency(fact.target, manifest); - if (resolved === undefined || fact.resolved === undefined) { - return { - ...fact, - ...unprovenResolver, - producerManifest: manifest, - reason: `${fact.reason}; declaring producer could not resolve the exact target`, - }; - } - const [producerResolved, selectedResolved] = yield* Effect.all([ - fs.realPath(resolved), - fs.realPath(fact.resolved), - ]); - if (producerResolved !== selectedResolved) { - return { - ...fact, - ...unprovenResolver, - producerManifest: manifest, - producerResolved, - reason: `${fact.reason}; declaring producer ${manifest} resolves a different canonical target ${producerResolved}; selected target ${selectedResolved}`, - resolved: selectedResolved, - }; - } - return { ...fact, owningManifest: manifest }; +const classifyProducerTarget = Effect.fn('QualityAudit.classifyProducerTarget')(function* classifyProducer( + fact: KnipModelEvidence, + manifest: string, +) { + const fs = yield* FileSystem.FileSystem; + const resolved = resolveInstalledDependency(fact.target, manifest); + if (resolved === undefined || fact.resolved === undefined) { + return { + ...fact, + ...unprovenResolver, + producerManifest: manifest, + reason: `${fact.reason}; declaring producer could not resolve the exact target`, + }; + } + const [producerResolved, selectedResolved] = yield* Effect.all([fs.realPath(resolved), fs.realPath(fact.resolved)]); + if (producerResolved !== selectedResolved) { + return { + ...fact, + ...unprovenResolver, + producerManifest: manifest, + producerResolved, + reason: `${fact.reason}; declaring producer ${manifest} resolves a different canonical target ${producerResolved}; selected target ${selectedResolved}`, + resolved: selectedResolved, + }; } -); + return { ...fact, owningManifest: manifest }; +}); -const proveVendorDependency = Effect.fn('QualityAudit.proveVendorDependency')( - function* proveVendor( - fact: KnipModelEvidence, - owner: { - readonly declared: typeof DependencyDeclarationSchema.Type; - readonly manifest: string; - } - ) { - if (!owner.manifest.includes('/node_modules/')) { - return null; - } - let unproven: KnipModelEvidence | null = null; - for (const dependency of Object.keys(owner.declared.dependencies ?? {})) { - const resolved = resolveInstalledDependency(dependency, owner.manifest); - const producer = - resolved === undefined ? null : yield* nearestPackage(resolved); - if ( - producer !== null && - Object.hasOwn(producer.declared.dependencies ?? {}, fact.target) - ) { - const proof = yield* classifyProducerTarget( - { - ...fact, - reason: `${fact.reason}; dependency ownership ${owner.declared.name} -> ${producer.declared.name} -> ${fact.target}`, - }, - producer.manifest - ); - if (proof.kind === 'resolver') { - return proof; - } - unproven = proof; +const proveVendorDependency = Effect.fn('QualityAudit.proveVendorDependency')(function* proveVendor( + fact: KnipModelEvidence, + owner: { + readonly declared: typeof DependencyDeclarationSchema.Type; + readonly manifest: string; + }, +) { + if (!owner.manifest.includes('/node_modules/')) { + return null; + } + let unproven: KnipModelEvidence | null = null; + for (const dependency of Object.keys(owner.declared.dependencies ?? {})) { + const resolved = resolveInstalledDependency(dependency, owner.manifest); + const producer = resolved === undefined ? null : yield* nearestPackage(resolved); + if (producer !== null && Object.hasOwn(producer.declared.dependencies ?? {}, fact.target)) { + const proof = yield* classifyProducerTarget( + { + ...fact, + reason: `${fact.reason}; dependency ownership ${owner.declared.name} -> ${producer.declared.name} -> ${fact.target}`, + }, + producer.manifest, + ); + if (proof.kind === 'resolver') { + return proof; } + unproven = proof; } - return unproven; } -); + return unproven; +}); -const proveResolverOwnership = Effect.fn('QualityAudit.proveResolverOwnership')( - function* proveResolver(fact: KnipModelEvidence) { - if (fact.anchor === undefined) { - return null; - } - const owner = yield* nearestPackage(fact.anchor); - if (owner === null) { - return null; - } - const dependencies = { - ...owner.declared.dependencies, - ...owner.declared.devDependencies, - }; - if (Object.hasOwn(dependencies, fact.target)) { - return { ...fact, owningManifest: owner.manifest }; - } - const producerProof = yield* proveVendorDependency(fact, owner); - return ( - producerProof ?? { - ...fact, - ...unprovenResolver, - reason: `${fact.reason}; anchor package ${owner.manifest} does not declare ${fact.target}, and no producer selecting the same target was proven`, - } - ); +const proveResolverOwnership = Effect.fn('QualityAudit.proveResolverOwnership')(function* proveResolver( + fact: KnipModelEvidence, +) { + if (fact.anchor === undefined) { + return null; } -); + const owner = yield* nearestPackage(fact.anchor); + if (owner === null) { + return null; + } + const dependencies = { + ...owner.declared.dependencies, + ...owner.declared.devDependencies, + }; + if (Object.hasOwn(dependencies, fact.target)) { + return { ...fact, owningManifest: owner.manifest }; + } + const producerProof = yield* proveVendorDependency(fact, owner); + return ( + producerProof ?? { + ...fact, + ...unprovenResolver, + reason: `${fact.reason}; anchor package ${owner.manifest} does not declare ${fact.target}, and no producer selecting the same target was proven`, + } + ); +}); -const workspaceModel = Effect.fn('QualityAudit.knipWorkspaceModel')( - function* buildWorkspace( - appRoot: string, - workspace: string, - current: WorkspaceConfig, - files: readonly string[], - workspaces: readonly string[] - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const prefix = workspace === '.' ? '' : `${workspace}/`; - const manifestFile = `${prefix}package.json`; - const manifest = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(PackageSchema) - )(yield* fs.readFileString(path.join(appRoot, manifestFile))); - const evidence: KnipModelEvidence[] = []; - const fileSet = new Set(files); - const entries = new Set(current.entry); - const dependencies = new Set(current.ignoreDependencies); - const add = (fact: KnipModelEvidence) => { - if (fact.kind === 'entry' || fact.kind === 'file') { - const target = path - .relative(appRoot, path.resolve(appRoot, prefix, fact.target)) - .replaceAll('\\', '/'); - if (!fileSet.has(target)) { - return; - } - if (fact.kind === 'entry') { - entries.add(fact.target); - } - } else if (fact.kind === 'dependency' && workspace !== '.') { - dependencies.add(fact.target); +const workspaceModel = Effect.fn('QualityAudit.knipWorkspaceModel')(function* buildWorkspace( + appRoot: string, + workspace: string, + current: WorkspaceConfig, + files: readonly string[], + workspaces: readonly string[], +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const prefix = workspace === '.' ? '' : `${workspace}/`; + const manifestFile = `${prefix}package.json`; + const manifest = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PackageSchema))( + yield* fs.readFileString(path.join(appRoot, manifestFile)), + ); + const evidence: KnipModelEvidence[] = []; + const fileSet = new Set(files); + const entries = new Set(current.entry); + const dependencies = new Set(current.ignoreDependencies); + const add = (fact: KnipModelEvidence) => { + if (fact.kind === 'entry' || fact.kind === 'file') { + const target = path.relative(appRoot, path.resolve(appRoot, prefix, fact.target)).replaceAll('\\', '/'); + if (!fileSet.has(target)) { + return; } - evidence.push(fact); - }; - for (const fact of manifestEvidence(manifest, manifestFile, workspace)) { - add(fact); - } - const ownedFiles = files.filter( - (file) => - !file.endsWith('.css') && - (workspace === '.' - ? !workspaces.some( - (owner) => owner !== '.' && file.startsWith(`${owner}/`) - ) - : file.startsWith(prefix)) - ); - const factsByPath = new Map(); - for (const file of ownedFiles) { - const facts = yield* parseSource( - file, - yield* fs.readFileString(path.join(appRoot, file)) - ); - factsByPath.set(file, facts); - for (const candidate of resolverEvidence( - facts, - workspace, - appRoot, - path - )) { - const proof = yield* proveResolverOwnership(candidate); - if (proof !== null) { - evidence.push(proof); - } + if (fact.kind === 'entry') { + entries.add(fact.target); } - const directory = path.dirname( - path.relative( - path.resolve(appRoot, prefix), - path.resolve(appRoot, file) - ) - ); - for (const fact of sourceEvidence(facts, workspace, directory, path)) { - add(fact); + } else if (fact.kind === 'dependency' && workspace !== '.') { + dependencies.add(fact.target); + } + evidence.push(fact); + }; + for (const fact of manifestEvidence(manifest, manifestFile, workspace)) { + add(fact); + } + const ownedFiles = files.filter( + (file) => + !file.endsWith('.css') && + (workspace === '.' + ? !workspaces.some((owner) => owner !== '.' && file.startsWith(`${owner}/`)) + : file.startsWith(prefix)), + ); + const factsByPath = new Map(); + for (const file of ownedFiles) { + const facts = yield* parseSource(file, yield* fs.readFileString(path.join(appRoot, file))); + factsByPath.set(file, facts); + for (const candidate of resolverEvidence(facts, workspace, appRoot, path)) { + const proof = yield* proveResolverOwnership(candidate); + if (proof !== null) { + evidence.push(proof); } } - evidence.push(...drizzleEvidence(factsByPath, prefix, workspace)); - return { - config: { - ...current, - entry: [...entries], - ignoreDependencies: [...dependencies], - }, - evidence, - }; + const directory = path.dirname(path.relative(path.resolve(appRoot, prefix), path.resolve(appRoot, file))); + for (const fact of sourceEvidence(facts, workspace, directory, path)) { + add(fact); + } } -); + evidence.push(...drizzleEvidence(factsByPath, prefix, workspace)); + return { + config: { + ...current, + entry: [...entries], + ignoreDependencies: [...dependencies], + }, + evidence, + }; +}); const mergeRuntimeEvidence = ( workspaces: Record, evidence: KnipModelEvidence[], - runtime: readonly KnipModelEvidence[] + runtime: readonly KnipModelEvidence[], ): void => { for (const fact of runtime) { const current = workspaces[fact.workspace]; @@ -1472,9 +1181,7 @@ const mergeRuntimeEvidence = ( if (fact.kind === 'dependency' && fact.workspace !== '.') { workspaces[fact.workspace] = { ...current, - ignoreDependencies: [ - ...new Set([...(current.ignoreDependencies ?? []), fact.target]), - ], + ignoreDependencies: [...new Set([...(current.ignoreDependencies ?? []), fact.target])], }; } else if (fact.kind === 'entry') { workspaces[fact.workspace] = { @@ -1487,67 +1194,53 @@ const mergeRuntimeEvidence = ( }; /** Static adapters never evaluate application or configuration modules. */ -export const buildKnipModel = Effect.fn('QualityAudit.buildKnipModel')( - function* buildModel( - appRoot: string, - baseConfig: typeof KnipConfigSchema.Type, - consumerPath?: string - ) { - const path = yield* Path.Path; - const directories = yield* workspaceDirectories(appRoot); - const files = yield* sourceFiles(appRoot, ''); - const configured = baseConfig.workspaces ?? { '.': baseConfig }; - const workspaces: Record = {}; - const evidence: KnipModelEvidence[] = []; - for (const workspace of directories) { - const pattern = workspace === '.' ? '.' : `${workspace.split('/')[0]}/*`; - const inherited = configured[workspace] ?? configured[pattern]; - if (inherited === undefined) { - continue; - } - const result = yield* workspaceModel( - appRoot, - workspace, - inherited, - files, - directories - ); - workspaces[workspace] = result.config; - evidence.push(...result.evidence); - } - const runtime = yield* buildKnipRuntimeEvidence(appRoot); - mergeRuntimeEvidence(workspaces, evidence, runtime); - const reflected = evidence.filter( - (fact) => - fact.kind === 'export' || - fact.kind === 'file' || - (fact.kind === 'dependency' && - fact.workspace === '.' && - fact.reason !== 'Module Federation remotes consumer') - ); - const consumerSource = reflected - .map((fact, index) => { - if (fact.kind === 'dependency') { - return `import {} from ${JSON.stringify(fact.target)};`; - } - if (fact.kind === 'file') { - const owner = fact.workspace === '.' ? '' : fact.workspace; - return `import {} from ${JSON.stringify(path.resolve(appRoot, owner, fact.target))};`; - } - const [file, name] = fact.target.split('#'); - return `import { ${name} as consumed${index} } from ${JSON.stringify(path.resolve(appRoot, file ?? ''))};\nvoid consumed${index};`; - }) - .join('\n'); - if ( - consumerPath !== undefined && - reflected.length > 0 && - workspaces['.'] !== undefined - ) { - workspaces['.'] = { - ...workspaces['.'], - entry: [...(workspaces['.'].entry ?? []), consumerPath], - }; +export const buildKnipModel = Effect.fn('QualityAudit.buildKnipModel')(function* buildModel( + appRoot: string, + baseConfig: typeof KnipConfigSchema.Type, + consumerPath?: string, +) { + const path = yield* Path.Path; + const directories = yield* workspaceDirectories(appRoot); + const files = yield* sourceFiles(appRoot, ''); + const configured = baseConfig.workspaces ?? { '.': baseConfig }; + const workspaces: Record = {}; + const evidence: KnipModelEvidence[] = []; + for (const workspace of directories) { + const pattern = workspace === '.' ? '.' : `${workspace.split('/')[0]}/*`; + const inherited = configured[workspace] ?? configured[pattern]; + if (inherited === undefined) { + continue; } - return { config: { ...baseConfig, workspaces }, consumerSource, evidence }; + const result = yield* workspaceModel(appRoot, workspace, inherited, files, directories); + workspaces[workspace] = result.config; + evidence.push(...result.evidence); } -); + const runtime = yield* buildKnipRuntimeEvidence(appRoot); + mergeRuntimeEvidence(workspaces, evidence, runtime); + const reflected = evidence.filter( + (fact) => + fact.kind === 'export' || + fact.kind === 'file' || + (fact.kind === 'dependency' && fact.workspace === '.' && fact.reason !== 'Module Federation remotes consumer'), + ); + const consumerSource = reflected + .map((fact, index) => { + if (fact.kind === 'dependency') { + return `import {} from ${JSON.stringify(fact.target)};`; + } + if (fact.kind === 'file') { + const owner = fact.workspace === '.' ? '' : fact.workspace; + return `import {} from ${JSON.stringify(path.resolve(appRoot, owner, fact.target))};`; + } + const [file, name] = fact.target.split('#'); + return `import { ${name} as consumed${index} } from ${JSON.stringify(path.resolve(appRoot, file ?? ''))};\nvoid consumed${index};`; + }) + .join('\n'); + if (consumerPath !== undefined && reflected.length > 0 && workspaces['.'] !== undefined) { + workspaces['.'] = { + ...workspaces['.'], + entry: [...(workspaces['.'].entry ?? []), consumerPath], + }; + } + return { config: { ...baseConfig, workspaces }, consumerSource, evidence }; +}); diff --git a/app/quality-audit/knip-reporter.mts b/app/quality-audit/knip-reporter.mts index 40b8877da..6503ec847 100644 --- a/app/quality-audit/knip-reporter.mts +++ b/app/quality-audit/knip-reporter.mts @@ -3,22 +3,16 @@ import type { ReporterOptions } from 'knip'; // Knip's stock JSON reporter omits coverage. This second NDJSON record preserves // the analyzer's own counters without deriving success from its exit status. -export default function reportCoverage({ - configurationHints, - counters, - includedWorkspaceDirs, -}: ReporterOptions): void { +export default function reportCoverage({ configurationHints, counters, includedWorkspaceDirs }: ReporterOptions): void { const source = Result.getOrThrow( Schema.encodeResult(Schema.fromJsonString(Schema.Unknown))({ configurationHints, coverage: counters, findingCounts: Object.fromEntries( - Object.entries(counters).filter( - ([category]) => category !== 'processed' && category !== 'total' - ) + Object.entries(counters).filter(([category]) => category !== 'processed' && category !== 'total'), ), workspaces: includedWorkspaceDirs, - }) + }), ); process.stdout.write(`${source}\n`); } diff --git a/app/quality-audit/knip-runtime-model.mts b/app/quality-audit/knip-runtime-model.mts index 1602550be..8fa08b415 100644 --- a/app/quality-audit/knip-runtime-model.mts +++ b/app/quality-audit/knip-runtime-model.mts @@ -11,58 +11,45 @@ const EFFECT_PLUGIN = '@effect/language-service'; const Manifest = Schema.fromJsonString( Schema.Struct({ dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), - devDependencies: Schema.optional( - Schema.Record(Schema.String, Schema.String) - ), + devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), scripts: Schema.optional(Schema.Record(Schema.String, Schema.String)), - }) -); -const InstalledPackage = Schema.fromJsonString( - Schema.Struct({ name: Schema.String, version: Schema.String }) + }), ); +const InstalledPackage = Schema.fromJsonString(Schema.Struct({ name: Schema.String, version: Schema.String })); const documentsBuiltInPlugin = (readme: string): boolean => readme.includes('A wrapper around [TypeScript-Go]') && readme.includes('Adding the `@effect/tsgo` dependency to your project.') && - readme.includes( - 'Configuring your `tsconfig.json` to use the Effect Language Service plugin.' - ) && + readme.includes('Configuring your `tsconfig.json` to use the Effect Language Service plugin.') && readme.includes('"name": "@effect/language-service"'); -class InvalidTsconfig extends Schema.TaggedError()( - 'InvalidTsconfig', - { - file: Schema.String, - offset: Schema.Finite.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(0) - ), - } -) {} +class InvalidTsconfig extends Schema.TaggedError()('InvalidTsconfig', { + file: Schema.String, + offset: Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), +}) {} const Tsconfig = Schema.Struct({ compilerOptions: Schema.optional( Schema.Struct({ - plugins: Schema.optional( - Schema.Array(Schema.Struct({ name: Schema.String })) - ), + plugins: Schema.optional(Schema.Array(Schema.Struct({ name: Schema.String }))), types: Schema.optional(Schema.Array(Schema.String)), - }) + }), ), exclude: Schema.optional(Schema.Array(Schema.String)), files: Schema.optional(Schema.Array(Schema.String)), include: Schema.optional(Schema.Array(Schema.String)), }); -const parseTsconfig = Effect.fn('QualityAudit.parseTsconfig')( - function* parseTsconfigEffect(file: string, source: string) { - const errors: ParseError[] = []; - const parsed: unknown = parseJsonc(source, errors, { - allowTrailingComma: true, - }); - const [error] = errors; - if (error !== undefined) { - return yield* new InvalidTsconfig({ file, offset: error.offset }); - } - return yield* Schema.decodeUnknownEffect(Tsconfig)(parsed); +const parseTsconfig = Effect.fn('QualityAudit.parseTsconfig')(function* parseTsconfigEffect( + file: string, + source: string, +) { + const errors: ParseError[] = []; + const parsed: unknown = parseJsonc(source, errors, { + allowTrailingComma: true, + }); + const [error] = errors; + if (error !== undefined) { + return yield* new InvalidTsconfig({ file, offset: error.offset }); } -); + return yield* Schema.decodeUnknownEffect(Tsconfig)(parsed); +}); const at = ( source: string, text: string, @@ -70,7 +57,7 @@ const at = ( workspace: string, kind: KnipModelEvidence['kind'], target: string, - reason: string + reason: string, ): KnipModelEvidence => ({ column: offset - text.lastIndexOf('\n', offset - 1), kind, @@ -84,14 +71,9 @@ const packageName = (specifier: string): string | undefined => { if (/^(?:[./#]|[a-z]+:)/u.test(specifier)) { return undefined; } - return specifier.startsWith('@') - ? specifier.split('/').slice(0, 2).join('/') - : specifier.split('/')[0]; + return specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0]; }; -const uncomment = ( - file: string, - source: string | undefined -): string | undefined => { +const uncomment = (file: string, source: string | undefined): string | undefined => { if (source === undefined) { return undefined; } @@ -110,86 +92,63 @@ const uncomment = ( }; const invokedShell = (command: string): string | undefined => { - const { shell } = - /^(?:sh|bash)\s+(?:\.\/)?(?[\w./-]+\.sh)(?:\s|$)/u.exec(command) - ?.groups ?? {}; + const { shell } = /^(?:sh|bash)\s+(?:\.\/)?(?[\w./-]+\.sh)(?:\s|$)/u.exec(command)?.groups ?? {}; if (shell === undefined || shell.includes('..')) { return undefined; } return shell; }; -const cssDependencies = ( - cssFile: string, - css: string, - layoutFile: string, - workspace: string -): KnipModelEvidence[] => { +const cssDependencies = (cssFile: string, css: string, layoutFile: string, workspace: string): KnipModelEvidence[] => { const result: KnipModelEvidence[] = []; - const withoutComments = css.replaceAll(/\/\*[\s\S]*?\*\//gu, (comment) => - comment.replaceAll(/[^\n]/gu, ' ') - ); - for (const match of withoutComments.matchAll( - /@import\s+(?:url\(\s*)?["'](?[^"']+)["']/gu - )) { + const withoutComments = css.replaceAll(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replaceAll(/[^\n]/gu, ' ')); + for (const match of withoutComments.matchAll(/@import\s+(?:url\(\s*)?["'](?[^"']+)["']/gu)) { const { specifier = '' } = match.groups ?? {}; const target = packageName(specifier); if (target === undefined || target.length === 0) { continue; } result.push( - at( - cssFile, - css, - match.index, - workspace, - 'dependency', - target, - `CSS package import reached from ${layoutFile}` - ) + at(cssFile, css, match.index, workspace, 'dependency', target, `CSS package import reached from ${layoutFile}`), ); } return result; }; -export const workspaceDirectories = Effect.fn('QualityAudit.knipWorkspaces')( - function* readModelWorkspaces(appRoot: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaces = ['.']; - for (const directory of ['apps', 'verticals', 'packages']) { - const location = path.join(appRoot, directory); - if (!(yield* fs.exists(location))) { - continue; - } - for (const name of yield* fs.readDirectory(location)) { - if (yield* fs.exists(path.join(location, name, 'package.json'))) { - workspaces.push(`${directory}/${name}`); - } +export const workspaceDirectories = Effect.fn('QualityAudit.knipWorkspaces')(function* readModelWorkspaces( + appRoot: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workspaces = ['.']; + for (const directory of ['apps', 'verticals', 'packages']) { + const location = path.join(appRoot, directory); + if (!(yield* fs.exists(location))) { + continue; + } + for (const name of yield* fs.readDirectory(location)) { + if (yield* fs.exists(path.join(location, name, 'package.json'))) { + workspaces.push(`${directory}/${name}`); } } - return workspaces; } -); + return workspaces; +}); /** Model only source-backed runtime contracts; never execute a wrapper or vendor module. */ -export const buildKnipRuntimeEvidence = Effect.fn( - 'QualityAudit.buildKnipRuntimeEvidence' -)(function* buildRuntimeEvidence(appRoot: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const evidence: KnipModelEvidence[] = []; - const read = Effect.fn('QualityAudit.readRuntimeModelFile')( - function* readOptional(file: string) { +export const buildKnipRuntimeEvidence = Effect.fn('QualityAudit.buildKnipRuntimeEvidence')( + function* buildRuntimeEvidence(appRoot: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const evidence: KnipModelEvidence[] = []; + const read = Effect.fn('QualityAudit.readRuntimeModelFile')(function* readOptional(file: string) { return (yield* fs.exists(path.join(appRoot, file))) ? yield* fs.readFileString(path.join(appRoot, file)) : undefined; - } - ); - const shellEvidence = Effect.fn('QualityAudit.shellEvidence')( - function* shellEvidence( + }); + const shellEvidence = Effect.fn('QualityAudit.shellEvidence')(function* shellEvidence( command: string, prefix: string, - workspace: string + workspace: string, ) { const shell = invokedShell(command); if (shell === undefined) { @@ -201,21 +160,12 @@ export const buildKnipRuntimeEvidence = Effect.fn( return; } // This recognized wrapper explicitly changes from scripts/ to its package root. - if ( - !source.includes(`cd "\${script_directory}/.."`) || - !source.includes('dirname -- "$0"') - ) { + if (!source.includes(`cd "\${script_directory}/.."`) || !source.includes('dirname -- "$0"')) { return; } - for (const match of source.matchAll( - /^\s*node\s+(?[\w./-]+\.[cm]?[jt]s)(?:\s|$)/gmu - )) { + for (const match of source.matchAll(/^\s*node\s+(?[\w./-]+\.[cm]?[jt]s)(?:\s|$)/gmu)) { const { target } = match.groups ?? {}; - if ( - target !== undefined && - !target.includes('..') && - (yield* read(`${prefix}${target}`)) !== undefined - ) { + if (target !== undefined && !target.includes('..') && (yield* read(`${prefix}${target}`)) !== undefined) { evidence.push( at( file, @@ -224,29 +174,23 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'file', target, - 'Package script invokes shell wrapper; wrapper changes to package root and executes Node source' - ) + 'Package script invokes shell wrapper; wrapper changes to package root and executes Node source', + ), ); } } - } - ); - const testConsumerEvidence = Effect.fn('QualityAudit.testConsumerEvidence')( - function* testConsumerEvidence( + }); + const testConsumerEvidence = Effect.fn('QualityAudit.testConsumerEvidence')(function* testConsumerEvidence( manifestFile: string, manifestText: string, commands: readonly string[], prefix: string, - workspace: string + workspace: string, ) { // Rstest's default-config loader consumes default, not every named export. const config = `${prefix}rstest.config.ts`; if ( - commands.some( - (command) => - /^rstest(?:\s|$)/u.test(command) && - !/--config(?:\s|=)/u.test(command) - ) && + commands.some((command) => /^rstest(?:\s|$)/u.test(command) && !/--config(?:\s|=)/u.test(command)) && (yield* read(config)) !== undefined ) { evidence.push( @@ -257,7 +201,7 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'file', 'rstest.config.ts', - 'Package script invokes Rstest default configuration discovery' + 'Package script invokes Rstest default configuration discovery', ), at( manifestFile, @@ -266,8 +210,8 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'export', `${config}#default`, - 'Rstest default-config loader consumes the default export' - ) + 'Rstest default-config loader consumes the default export', + ), ); } const tsconfigFile = `${prefix}tsconfig.json`; @@ -286,9 +230,7 @@ export const buildKnipRuntimeEvidence = Effect.fn( ) { return; } - for (const name of yield* fs.readDirectory( - path.join(appRoot, prefix, 'src') - )) { + for (const name of yield* fs.readDirectory(path.join(appRoot, prefix, 'src'))) { if (name.endsWith('.type-test.ts')) { evidence.push( at( @@ -298,18 +240,16 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'file', `src/${name}`, - 'TypeScript include src compiles this type-test module; exports remain audited' - ) + 'TypeScript include src compiles this type-test module; exports remain audited', + ), ); } } - } - ); - const cssEvidence = Effect.fn('QualityAudit.cssEvidence')( - function* cssEvidence( + }); + const cssEvidence = Effect.fn('QualityAudit.cssEvidence')(function* cssEvidence( extension: string, prefix: string, - workspace: string + workspace: string, ) { const layoutFile = `${prefix}src/routes/layout.${extension}`; const layout = yield* read(layoutFile); @@ -331,22 +271,19 @@ export const buildKnipRuntimeEvidence = Effect.fn( const cssFile = path.join(prefix, 'src/routes', statement.source.value); const css = yield* read(cssFile); if (css !== undefined) { - evidence.push( - ...cssDependencies(cssFile, css, layoutFile, workspace) - ); + evidence.push(...cssDependencies(cssFile, css, layoutFile, workspace)); } } - } - ); - const federationEvidence = Effect.fn('QualityAudit.federationEvidence')( - function* federationEvidence(prefix: string, workspace: string) { + }); + const federationEvidence = Effect.fn('QualityAudit.federationEvidence')(function* federationEvidence( + prefix: string, + workspace: string, + ) { const federationFile = `${prefix}module-federation.config.ts`; const federation = uncomment(federationFile, yield* read(federationFile)); if ( federation?.includes("from '@modern-js/app-tools/config'") === true && - /resolveEffectTsgoCompiler\s*\(\s*\{\s*from:\s*import\.meta\.url\s*,?\s*\}\s*\)/u.test( - federation - ) + /resolveEffectTsgoCompiler\s*\(\s*\{\s*from:\s*import\.meta\.url\s*,?\s*\}\s*\)/u.test(federation) ) { const offset = federation.indexOf('resolveEffectTsgoCompiler'); evidence.push( @@ -357,15 +294,12 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'dependency', EFFECT_TSGO, - 'Framework DTS resolver resolves the Effect TSGo package from this configuration module' - ) + 'Framework DTS resolver resolves the Effect TSGo package from this configuration module', + ), ); const readmeFile = `${prefix}node_modules/@effect/tsgo/README.md`; const readme = yield* read(readmeFile); - if ( - readme?.includes('tries `typescript`, then `@typescript/native`') === - true - ) { + if (readme?.includes('tries `typescript`, then `@typescript/native`') === true) { evidence.push( at( federationFile, @@ -374,15 +308,13 @@ export const buildKnipRuntimeEvidence = Effect.fn( workspace, 'dependency', '@typescript/native', - `Effect TSGo native compiler fallback documented in ${readmeFile}` - ) + `Effect TSGo native compiler fallback documented in ${readmeFile}`, + ), ); } } - } - ); - const zeropsEvidence = Effect.fn('QualityAudit.zeropsEvidence')( - function* zeropsEvidence() { + }); + const zeropsEvidence = Effect.fn('QualityAudit.zeropsEvidence')(function* zeropsEvidence() { // Zerops buildCommands run from the repository root and explicitly cd into app. for (const file of ['zerops.yaml', 'zerops.yml']) { const source = yield* read(file); @@ -390,14 +322,10 @@ export const buildKnipRuntimeEvidence = Effect.fn( continue; } for (const match of source.matchAll( - /^\s*-\s+cd app && (?:[A-Z_]+=\S+\s+)*node\s+(?[\w./-]+\.[cm]?[jt]s)(?:\s|$)/gmu + /^\s*-\s+cd app && (?:[A-Z_]+=\S+\s+)*node\s+(?[\w./-]+\.[cm]?[jt]s)(?:\s|$)/gmu, )) { const { target } = match.groups ?? {}; - if ( - target !== undefined && - !target.includes('..') && - (yield* read(target)) !== undefined - ) { + if (target !== undefined && !target.includes('..') && (yield* read(target)) !== undefined) { evidence.push( at( file, @@ -406,57 +334,35 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'file', target, - 'Zerops build command changes to app and invokes this Node source' - ) + 'Zerops build command changes to app and invokes this Node source', + ), ); } } } - } - ); - const tsgoDocumentation = Effect.fn('QualityAudit.tsgoDocumentation')( - function* tsgoDocumentation() { + }); + const tsgoDocumentation = Effect.fn('QualityAudit.tsgoDocumentation')(function* tsgoDocumentation() { const readme = yield* read('node_modules/@effect/tsgo/README.md'); - const installedText = yield* read( - 'node_modules/@effect/tsgo/package.json' - ); + const installedText = yield* read('node_modules/@effect/tsgo/package.json'); const rootText = yield* read('package.json'); - if ( - readme === undefined || - installedText === undefined || - rootText === undefined - ) { + if (readme === undefined || installedText === undefined || rootText === undefined) { return false; } - const installed = - yield* Schema.decodeUnknownEffect(InstalledPackage)(installedText); + const installed = yield* Schema.decodeUnknownEffect(InstalledPackage)(installedText); const root = yield* Schema.decodeUnknownEffect(Manifest)(rootText); - const pinned = - root.devDependencies?.[EFFECT_TSGO] ?? root.dependencies?.[EFFECT_TSGO]; - return ( - installed.name === EFFECT_TSGO && - pinned === installed.version && - documentsBuiltInPlugin(readme) - ); - } - ); - const tsgoEvidence = Effect.fn('QualityAudit.tsgoEvidence')( - function* tsgoEvidence() { + const pinned = root.devDependencies?.[EFFECT_TSGO] ?? root.dependencies?.[EFFECT_TSGO]; + return installed.name === EFFECT_TSGO && pinned === installed.version && documentsBuiltInPlugin(readme); + }); + const tsgoEvidence = Effect.fn('QualityAudit.tsgoEvidence')(function* tsgoEvidence() { const typecheckFile = 'scripts/ultramodern-typecheck.mts'; const typecheck = yield* read(typecheckFile); const vendorTypecheck = yield* read( - 'node_modules/@modern-js/ultramodern-create/templates/workspace-scripts/ultramodern-typecheck.mjs' + 'node_modules/@modern-js/ultramodern-create/templates/workspace-scripts/ultramodern-typecheck.mjs', ); const usesTsgo = - hasUltramodernDispatch( - typecheck, - 'typecheck', - yield* read('scripts/shared/ultramodern-command.mts') - ) && + hasUltramodernDispatch(typecheck, 'typecheck', yield* read('scripts/shared/ultramodern-command.mts')) && vendorTypecheck?.includes('resolveEffectTsgoCompiler({') === true && - vendorTypecheck.includes( - "from: pathToFileURL(join(workspaceRoot, 'package.json'))" - ); + vendorTypecheck.includes("from: pathToFileURL(join(workspaceRoot, 'package.json'))"); if (usesTsgo && typecheck !== undefined) { evidence.push( at( @@ -466,24 +372,18 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'dependency', EFFECT_TSGO, - 'Invoked framework typecheck resolves Effect TSGo from workspaceRoot/package.json' - ) + 'Invoked framework typecheck resolves Effect TSGo from workspaceRoot/package.json', + ), ); const configFile = 'tsconfig.base.json'; const configText = yield* read(configFile); if (configText !== undefined && (yield* tsgoDocumentation())) { const config = yield* parseTsconfig(configFile, configText); if ( - config.compilerOptions?.plugins?.some( - (plugin) => plugin.name === EFFECT_PLUGIN - ) === true && - config.compilerOptions.types?.some( - (name) => packageName(name) === EFFECT_PLUGIN - ) !== true + config.compilerOptions?.plugins?.some((plugin) => plugin.name === EFFECT_PLUGIN) === true && + config.compilerOptions.types?.some((name) => packageName(name) === EFFECT_PLUGIN) !== true ) { - const match = /"name"\s*:\s*"@effect\/language-service"/u.exec( - configText - ); + const match = /"name"\s*:\s*"@effect\/language-service"/u.exec(configText); if (match !== null) { evidence.push({ ...at( @@ -493,7 +393,7 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'compiler-option', EFFECT_PLUGIN, - 'Effect TSGo built-in plugin configuration namespace' + 'Effect TSGo built-in plugin configuration namespace', ), anchor: typecheckFile, resolved: 'node_modules/@effect/tsgo/README.md', @@ -502,10 +402,8 @@ export const buildKnipRuntimeEvidence = Effect.fn( } } } - } - ); - const readinessEvidence = Effect.fn('QualityAudit.readinessEvidence')( - function* readinessEvidence() { + }); + const readinessEvidence = Effect.fn('QualityAudit.readinessEvidence')(function* readinessEvidence() { const readinessFile = 'scripts/ultramodern-performance-readiness.mts'; const readiness = yield* read(readinessFile); const vendorFile = @@ -515,10 +413,9 @@ export const buildKnipRuntimeEvidence = Effect.fn( hasUltramodernDispatch( readiness, 'performance-readiness', - yield* read('scripts/shared/ultramodern-command.mts') + yield* read('scripts/shared/ultramodern-command.mts'), ) && - vendor?.includes('pathToFileURL(path.join(root, configPath)).href') === - true && + vendor?.includes('pathToFileURL(path.join(root, configPath)).href') === true && vendor.includes('import(moduleUrl)') ) { const match = /const configPath = '(?[^']+)'/u.exec(vendor); @@ -526,11 +423,7 @@ export const buildKnipRuntimeEvidence = Effect.fn( return; } const [, target] = match; - if ( - target !== undefined && - !target.includes('..') && - (yield* read(target)) !== undefined - ) { + if (target !== undefined && !target.includes('..') && (yield* read(target)) !== undefined) { evidence.push( at( vendorFile, @@ -539,8 +432,8 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'file', target, - `Invoked by ${readinessFile}; installed framework imports this exact configPath` - ) + `Invoked by ${readinessFile}; installed framework imports this exact configPath`, + ), ); if (vendor.includes('module.default ?? {}')) { evidence.push( @@ -551,29 +444,22 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'export', `${target}#default`, - 'Installed framework loader reads the imported configuration default export' - ) + 'Installed framework loader reads the imported configuration default export', + ), ); } } } - } - ); - const lefthookEvidence = Effect.fn('QualityAudit.lefthookEvidence')( - function* lefthookEvidence() { + }); + const lefthookEvidence = Effect.fn('QualityAudit.lefthookEvidence')(function* lefthookEvidence() { const file = 'lefthook.yml'; const source = yield* read(file); if (source === undefined) { return; } - for (const match of source.matchAll( - /^(?:pre-commit|pre-push):\r?\n(?(?:^[ \t].*(?:\r?\n|$))*)/gmu - )) { + for (const match of source.matchAll(/^(?:pre-commit|pre-push):\r?\n(?(?:^[ \t].*(?:\r?\n|$))*)/gmu)) { const { body = '' } = match.groups ?? {}; - if ( - /^\s+commands:\s*$/mu.test(body) && - /^\s+run:\s+\S.+$/mu.test(body) - ) { + if (/^\s+commands:\s*$/mu.test(body) && /^\s+run:\s+\S.+$/mu.test(body)) { evidence.push( at( file, @@ -582,41 +468,35 @@ export const buildKnipRuntimeEvidence = Effect.fn( '.', 'dependency', 'lefthook', - 'Configured optional Lefthook tool; this configuration does not establish hook activation or enforcement' - ) + 'Configured optional Lefthook tool; this configuration does not establish hook activation or enforcement', + ), ); } } + }); + const workspaces = yield* workspaceDirectories(appRoot); + for (const workspace of workspaces) { + const prefix = workspace === '.' ? '' : `${workspace}/`; + const manifestFile = `${prefix}package.json`; + const manifestText = yield* read(manifestFile); + if (manifestText === undefined) { + continue; + } + const manifest = yield* Schema.decodeUnknownEffect(Manifest)(manifestText); + const commands = Object.values(manifest.scripts ?? {}); + yield* testConsumerEvidence(manifestFile, manifestText, commands, prefix, workspace); + for (const command of commands) { + yield* shellEvidence(command, prefix, workspace); + } + for (const extension of ['tsx', 'ts', 'jsx', 'js']) { + yield* cssEvidence(extension, prefix, workspace); + } + yield* federationEvidence(prefix, workspace); } - ); - const workspaces = yield* workspaceDirectories(appRoot); - for (const workspace of workspaces) { - const prefix = workspace === '.' ? '' : `${workspace}/`; - const manifestFile = `${prefix}package.json`; - const manifestText = yield* read(manifestFile); - if (manifestText === undefined) { - continue; - } - const manifest = yield* Schema.decodeUnknownEffect(Manifest)(manifestText); - const commands = Object.values(manifest.scripts ?? {}); - yield* testConsumerEvidence( - manifestFile, - manifestText, - commands, - prefix, - workspace - ); - for (const command of commands) { - yield* shellEvidence(command, prefix, workspace); - } - for (const extension of ['tsx', 'ts', 'jsx', 'js']) { - yield* cssEvidence(extension, prefix, workspace); - } - yield* federationEvidence(prefix, workspace); - } - yield* zeropsEvidence(); - yield* tsgoEvidence(); - yield* readinessEvidence(); - yield* lefthookEvidence(); - return evidence; -}); + yield* zeropsEvidence(); + yield* tsgoEvidence(); + yield* readinessEvidence(); + yield* lefthookEvidence(); + return evidence; + }, +); diff --git a/app/scripts/assert-mf-types.mts b/app/scripts/assert-mf-types.mts index bc35157b0..07ae28abe 100644 --- a/app/scripts/assert-mf-types.mts +++ b/app/scripts/assert-mf-types.mts @@ -2,10 +2,7 @@ import { NodeServices } from '@effect/platform-node'; import { Effect } from 'effect'; -import { - runUltramodernScript, - ultramodernExitCode, -} from './shared/ultramodern-command.mts'; +import { runUltramodernScript, ultramodernExitCode } from './shared/ultramodern-command.mts'; import { ultramodernCommandFailure } from './ultramodern-command-failure.mts'; const exit = await Effect.runPromiseExit( @@ -15,6 +12,6 @@ const exit = await Effect.runPromiseExit( failure: ultramodernCommandFailure, moduleUrl: import.meta.url, nodeExecutable: process.execPath, - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped) + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); process.exitCode = ultramodernExitCode(exit); diff --git a/app/scripts/audit-database-trust-boundaries.mts b/app/scripts/audit-database-trust-boundaries.mts index 2d686e2cc..db04c7a62 100644 --- a/app/scripts/audit-database-trust-boundaries.mts +++ b/app/scripts/audit-database-trust-boundaries.mts @@ -2,15 +2,7 @@ import { pathToFileURL } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { - Config, - Console, - Effect, - Exit, - FileSystem, - Path, - Schema, -} from 'effect'; +import { Config, Console, Effect, Exit, FileSystem, Path, Schema } from 'effect'; import { Client } from 'pg'; import { loadDatabaseConnectionPair } from '../packages/core-runtime/src/db/config.ts'; @@ -53,10 +45,9 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< Effect.mapError( () => new DatabaseTrustBoundaryAuditError({ - reason: - 'Administrative and runtime database configuration is unavailable', - }) - ) + reason: 'Administrative and runtime database configuration is unavailable', + }), + ), ); const admin = new Client({ connectionString: connections.admin.connectionString, @@ -84,12 +75,11 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< (error) => new DatabaseTrustBoundaryAuditError({ reason: - Schema.is(DatabaseTargetMismatchError)(error) || - Schema.is(DatabaseSessionIdentityError)(error) + Schema.is(DatabaseTargetMismatchError)(error) || Schema.is(DatabaseSessionIdentityError)(error) ? error.message : 'Database trust-boundary evidence could not be collected', - }) - ) + }), + ), ); return buildDatabaseTrustBoundaryReport(snapshot); }).pipe( @@ -99,82 +89,58 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< ...(runtimeConnected ? [runtime.end()] : []), ...(adminConnected ? [admin.end()] : []), ]); - }) - ) + }), + ), ); }); -const DatabaseTrustBoundaryReportJsonSchema = Schema.fromJsonString( - Schema.Unknown, - { space: 2 } -); +const DatabaseTrustBoundaryReportJsonSchema = Schema.fromJsonString(Schema.Unknown, { space: 2 }); -const writeDatabaseTrustBoundaryReport = Effect.gen( - function* writeDatabaseTrustBoundaryReportEffect() { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const defaultWorkspaceRoot = path.resolve(import.meta.dirname, '..'); - const workspaceRoot = yield* Config.string( - 'ULTRAMODERN_WORKSPACE_ROOT' - ).pipe( - Config.withDefault(defaultWorkspaceRoot), - Effect.mapError( - () => - new DatabaseTrustBoundaryAuditError({ - reason: genericAuditFailureMessage, - }) - ) - ); - const output = path.join( - workspaceRoot, - '.codex/reports/database/database-trust-boundary.json' - ); - const report = yield* auditDatabaseTrustBoundaries(); - const reportJson = yield* Schema.encodeEffect( - DatabaseTrustBoundaryReportJsonSchema - )(report).pipe( - Effect.mapError( - () => - new DatabaseTrustBoundaryAuditError({ - reason: genericAuditFailureMessage, - }) - ) - ); - yield* fileSystem - .makeDirectory(path.dirname(output), { recursive: true }) - .pipe( - Effect.mapError( - () => - new DatabaseTrustBoundaryAuditError({ - reason: genericAuditFailureMessage, - }) - ) - ); - yield* fileSystem.writeFileString(output, `${reportJson}\n`).pipe( - Effect.mapError( - () => - new DatabaseTrustBoundaryAuditError({ - reason: genericAuditFailureMessage, - }) - ) - ); - yield* Console.log( - `Database trust-boundary evidence written with ${report.findings.length} finding(s).` - ); - } -).pipe( - Effect.tapCause((cause) => - Console.error(getDatabaseTrustBoundaryFailureMessage(cause)) - ) -); +const writeDatabaseTrustBoundaryReport = Effect.gen(function* writeDatabaseTrustBoundaryReportEffect() { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const defaultWorkspaceRoot = path.resolve(import.meta.dirname, '..'); + const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( + Config.withDefault(defaultWorkspaceRoot), + Effect.mapError( + () => + new DatabaseTrustBoundaryAuditError({ + reason: genericAuditFailureMessage, + }), + ), + ); + const output = path.join(workspaceRoot, '.codex/reports/database/database-trust-boundary.json'); + const report = yield* auditDatabaseTrustBoundaries(); + const reportJson = yield* Schema.encodeEffect(DatabaseTrustBoundaryReportJsonSchema)(report).pipe( + Effect.mapError( + () => + new DatabaseTrustBoundaryAuditError({ + reason: genericAuditFailureMessage, + }), + ), + ); + yield* fileSystem.makeDirectory(path.dirname(output), { recursive: true }).pipe( + Effect.mapError( + () => + new DatabaseTrustBoundaryAuditError({ + reason: genericAuditFailureMessage, + }), + ), + ); + yield* fileSystem.writeFileString(output, `${reportJson}\n`).pipe( + Effect.mapError( + () => + new DatabaseTrustBoundaryAuditError({ + reason: genericAuditFailureMessage, + }), + ), + ); + yield* Console.log(`Database trust-boundary evidence written with ${report.findings.length} finding(s).`); +}).pipe(Effect.tapCause((cause) => Console.error(getDatabaseTrustBoundaryFailureMessage(cause)))); -const isMain = - process.argv[1] !== undefined && - import.meta.url === pathToFileURL(process.argv[1]).href; +const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href; if (isMain) { - const exit = await Effect.runPromiseExit( - writeDatabaseTrustBoundaryReport.pipe(Effect.provide(NodeServices.layer)) - ); + const exit = await Effect.runPromiseExit(writeDatabaseTrustBoundaryReport.pipe(Effect.provide(NodeServices.layer))); process.exitCode = Exit.match(exit, { onFailure: () => 1, onSuccess: () => 0, diff --git a/app/scripts/authorization/protected-entrypoint-inventory.mts b/app/scripts/authorization/protected-entrypoint-inventory.mts index ab7c7e5ed..1f003c50b 100644 --- a/app/scripts/authorization/protected-entrypoint-inventory.mts +++ b/app/scripts/authorization/protected-entrypoint-inventory.mts @@ -4,9 +4,7 @@ import { Array as EffectArray, Order, Result, Schema } from 'effect'; export const PROTECTED_ENTRYPOINT_INVENTORY_SCHEMA_VERSION = 1 as const; -const PermissionSchema = Schema.String.check( - Schema.isPattern(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u) -); +const PermissionSchema = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u)); const InventoryAuthorizationSchema = Schema.Union([ Schema.Struct({ kind: Schema.Literal('public') }), @@ -28,26 +26,15 @@ const InventoryAuthorizationSchema = Schema.Union([ export type InventoryAuthorization = typeof InventoryAuthorizationSchema.Type; -const ProtectedEntrypointSurfaceSchema = Schema.Literals([ - 'action', - 'capability_issuance', - 'route', - 'worker', -]); +const ProtectedEntrypointSurfaceSchema = Schema.Literals(['action', 'capability_issuance', 'route', 'worker']); type ProtectedEntrypointSurface = typeof ProtectedEntrypointSurfaceSchema.Type; -const StableIdentifierSchema = Schema.String.check( - Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u) -); +const StableIdentifierSchema = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u)); -const EntrypointKeySchema = StableIdentifierSchema.pipe( - Schema.brand('EntrypointKey') -); +const EntrypointKeySchema = StableIdentifierSchema.pipe(Schema.brand('EntrypointKey')); -const SourceRevisionSchema = Schema.String.check( - Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u) -); +const SourceRevisionSchema = Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u)); const ProtectedEntrypointInventoryEntrySchema = Schema.Struct({ authorization: InventoryAuthorizationSchema, @@ -64,87 +51,61 @@ const ProtectedEntrypointInventorySchema = Schema.Struct({ sourceRevision: SourceRevisionSchema, }); -export type ProtectedEntrypointInventoryEntry = - typeof ProtectedEntrypointInventoryEntrySchema.Encoded; +export type ProtectedEntrypointInventoryEntry = typeof ProtectedEntrypointInventoryEntrySchema.Encoded; -export type ProtectedEntrypointInventory = - typeof ProtectedEntrypointInventorySchema.Encoded; +export type ProtectedEntrypointInventory = typeof ProtectedEntrypointInventorySchema.Encoded; -const encodeJsonResult = Schema.encodeResult( - Schema.fromJsonString(Schema.Unknown) -); +const encodeJsonResult = Schema.encodeResult(Schema.fromJsonString(Schema.Unknown)); -const encodePrettyJsonResult = Schema.encodeResult( - Schema.fromJsonString(Schema.Unknown, { space: 2 }) -); +const encodePrettyJsonResult = Schema.encodeResult(Schema.fromJsonString(Schema.Unknown, { space: 2 })); class ProtectedEntrypointInventoryError extends Schema.TaggedError()( 'ProtectedEntrypointInventoryError', - { message: Schema.String } + { message: Schema.String }, ) {} const invalidInventory = (message: string): ProtectedEntrypointInventoryError => new ProtectedEntrypointInventoryError({ message }); -const toTypeError = (error: ProtectedEntrypointInventoryError): TypeError => - new TypeError(error.message); +const toTypeError = (error: ProtectedEntrypointInventoryError): TypeError => new TypeError(error.message); // Compatibility boundary for the established synchronous, TypeError-throwing public API. -const getOrThrowTypeError = ( - result: Result.Result -): A => Result.getOrThrowWith(result, toTypeError); +const getOrThrowTypeError = (result: Result.Result): A => + Result.getOrThrowWith(result, toTypeError); const encodingFailure = (): ProtectedEntrypointInventoryError => invalidInventory('protected entrypoint inventory encoding failed'); -const stableValue = ( - value: string, - field: string -): Result.Result => +const stableValue = (value: string, field: string): Result.Result => Schema.is(StableIdentifierSchema)(value) ? Result.succeed(value) - : Result.fail( - invalidInventory(`${field} must be a stable, non-sensitive identifier`) - ); + : Result.fail(invalidInventory(`${field} must be a stable, non-sensitive identifier`)); const normalizeAuthorization = ( - raw: InventoryAuthorization + raw: InventoryAuthorization, ): Result.Result => Schema.decodeUnknownResult(InventoryAuthorizationSchema, { onExcessProperty: 'error', })(raw).pipe( Result.mapError(() => - invalidInventory( - 'inventory authorization classification is invalid or contains excess data' - ) - ) + invalidInventory('inventory authorization classification is invalid or contains excess data'), + ), ); const normalizeSurface = ( - surface: ProtectedEntrypointSurface -): Result.Result< - ProtectedEntrypointSurface, - ProtectedEntrypointInventoryError -> => + surface: ProtectedEntrypointSurface, +): Result.Result => Schema.decodeUnknownResult(ProtectedEntrypointSurfaceSchema)(surface).pipe( - Result.mapError(() => - invalidInventory(`unsupported inventory surface: ${surface}`) - ) + Result.mapError(() => invalidInventory(`unsupported inventory surface: ${surface}`)), ); const normalizeEntry = ( - entry: ProtectedEntrypointInventoryEntry -): Result.Result< - ProtectedEntrypointInventoryEntry, - ProtectedEntrypointInventoryError -> => + entry: ProtectedEntrypointInventoryEntry, +): Result.Result => Result.gen(function* normalizeEntryResult() { const authorization = yield* normalizeAuthorization(entry.authorization); const deployment = yield* stableValue(entry.deployment, 'deployment'); - const entrypointKey = yield* stableValue( - entry.entrypointKey, - 'entrypointKey' - ); + const entrypointKey = yield* stableValue(entry.entrypointKey, 'entrypointKey'); const owner = yield* stableValue(entry.owner, 'owner'); const surface = yield* normalizeSurface(entry.surface); return { authorization, deployment, entrypointKey, owner, surface }; @@ -152,13 +113,10 @@ const normalizeEntry = ( const compareInventoryEntries = ( left: ProtectedEntrypointInventoryEntry, - right: ProtectedEntrypointInventoryEntry + right: ProtectedEntrypointInventoryEntry, ): -1 | 0 | 1 => { const surfaceOrder = left.surface.localeCompare(right.surface); - const order = - surfaceOrder === 0 - ? left.entrypointKey.localeCompare(right.entrypointKey) - : surfaceOrder; + const order = surfaceOrder === 0 ? left.entrypointKey.localeCompare(right.entrypointKey) : surfaceOrder; if (order < 0) { return -1; } @@ -171,21 +129,14 @@ const compareInventoryEntries = ( const InventoryEntryOrder = Order.make(compareInventoryEntries); const normalizeProtectedEntrypointInventoryResult = ( - entries: readonly ProtectedEntrypointInventoryEntry[] -): Result.Result< - readonly ProtectedEntrypointInventoryEntry[], - ProtectedEntrypointInventoryError -> => + entries: readonly ProtectedEntrypointInventoryEntry[], +): Result.Result => Result.gen(function* normalizeInventoryResult() { const normalized = yield* Result.all(entries.map(normalizeEntry)); const seen = new Set(); for (const entry of normalized) { if (seen.has(entry.entrypointKey)) { - return yield* Result.fail( - invalidInventory( - `duplicate protected entrypoint: ${entry.entrypointKey}` - ) - ); + return yield* Result.fail(invalidInventory(`duplicate protected entrypoint: ${entry.entrypointKey}`)); } seen.add(entry.entrypointKey); } @@ -193,47 +144,34 @@ const normalizeProtectedEntrypointInventoryResult = ( }); export const normalizeProtectedEntrypointInventory = ( - entries: readonly ProtectedEntrypointInventoryEntry[] + entries: readonly ProtectedEntrypointInventoryEntry[], ): readonly ProtectedEntrypointInventoryEntry[] => getOrThrowTypeError(normalizeProtectedEntrypointInventoryResult(entries)); -export const hashProtectedEntrypointInventory = ( - entries: readonly ProtectedEntrypointInventoryEntry[] -): string => { - const encodedEntries = getOrThrowTypeError( - encodeJsonResult(entries).pipe(Result.mapError(encodingFailure)) - ); +export const hashProtectedEntrypointInventory = (entries: readonly ProtectedEntrypointInventoryEntry[]): string => { + const encodedEntries = getOrThrowTypeError(encodeJsonResult(entries).pipe(Result.mapError(encodingFailure))); const source = `${encodedEntries}\n`; return bytesToHex(sha256(utf8ToBytes(source))); }; export const makeProtectedEntrypointInventory = ( sourceRevision: string, - entries: readonly ProtectedEntrypointInventoryEntry[] + entries: readonly ProtectedEntrypointInventoryEntry[], ): ProtectedEntrypointInventory => getOrThrowTypeError( Result.gen(function* makeInventoryResult() { if (!Schema.is(SourceRevisionSchema)(sourceRevision)) { - return yield* Result.fail( - invalidInventory( - 'sourceRevision must be a stable revision identifier' - ) - ); + return yield* Result.fail(invalidInventory('sourceRevision must be a stable revision identifier')); } - const normalized = - yield* normalizeProtectedEntrypointInventoryResult(entries); + const normalized = yield* normalizeProtectedEntrypointInventoryResult(entries); return { entries: normalized, inventoryHash: hashProtectedEntrypointInventory(normalized), schemaVersion: PROTECTED_ENTRYPOINT_INVENTORY_SCHEMA_VERSION, sourceRevision, }; - }) + }), ); -export const serializeProtectedEntrypointInventory = ( - inventory: ProtectedEntrypointInventory -): string => - `${getOrThrowTypeError( - encodePrettyJsonResult(inventory).pipe(Result.mapError(encodingFailure)) - )}\n`; +export const serializeProtectedEntrypointInventory = (inventory: ProtectedEntrypointInventory): string => + `${getOrThrowTypeError(encodePrettyJsonResult(inventory).pipe(Result.mapError(encodingFailure)))}\n`; diff --git a/app/scripts/authorization/rollout-contract.mts b/app/scripts/authorization/rollout-contract.mts index 8657e6d4c..54d517b1f 100644 --- a/app/scripts/authorization/rollout-contract.mts +++ b/app/scripts/authorization/rollout-contract.mts @@ -15,22 +15,14 @@ const AuthorizationRolloutContractSchema = Schema.Struct({ schemaVersion: Schema.Literal(AUTHORIZATION_ROLLOUT_SCHEMA_VERSION), }); -const BaselineSourceRevisionSchema = Schema.String.check( - Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u) -); +const BaselineSourceRevisionSchema = Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u)); const DecisionReferenceSchema = Schema.String.check( - Schema.isPattern( - /^(?:https:\/\/github\.com\/TechsioCZ\/ontos\/issues\/\d+|ADR-\d{4})$/u - ) + Schema.isPattern(/^(?:https:\/\/github\.com\/TechsioCZ\/ontos\/issues\/\d+|ADR-\d{4})$/u), ); -type DecodedAuthorizationRolloutContract = Schema.Schema.Type< - typeof AuthorizationRolloutContractSchema ->; +type DecodedAuthorizationRolloutContract = Schema.Schema.Type; -export type AuthorizationRolloutContract = Schema.Codec.Encoded< - typeof AuthorizationRolloutContractSchema ->; +export type AuthorizationRolloutContract = Schema.Codec.Encoded; type AuthorizationRolloutContractDocument = | boolean | null @@ -49,7 +41,7 @@ export class AuthorizationRolloutContractError extends Schema.TaggedError @@ -59,95 +51,63 @@ const malformedContract = (): AuthorizationRolloutContractError => invalidContract('authorization rollout contract is malformed'); const encodeContract = ( - contract: DecodedAuthorizationRolloutContract -): Result.Result< - AuthorizationRolloutContract, - AuthorizationRolloutContractError -> => - Schema.encodeUnknownResult(AuthorizationRolloutContractSchema)(contract).pipe( - Result.mapError(malformedContract) - ); + contract: DecodedAuthorizationRolloutContract, +): Result.Result => + Schema.encodeUnknownResult(AuthorizationRolloutContractSchema)(contract).pipe(Result.mapError(malformedContract)); const validateContractActivity = ( contract: DecodedAuthorizationRolloutContract, - context: RolloutValidationContext + context: RolloutValidationContext, ): Result.Result => { const activatedAtEpochMs = DateTime.toEpochMillis(contract.activatedAt); const expiresAtEpochMs = DateTime.toEpochMillis(contract.expiresAt); return activatedAtEpochMs >= expiresAtEpochMs || context.nowEpochMs < activatedAtEpochMs || (contract.mode === 'report_only' && context.nowEpochMs >= expiresAtEpochMs) - ? Result.fail( - invalidContract('authorization rollout contract is inactive or expired') - ) + ? Result.fail(invalidContract('authorization rollout contract is inactive or expired')) : Result.succeed(true); }; const validateInventoryBinding = ( contract: DecodedAuthorizationRolloutContract, - context: RolloutValidationContext + context: RolloutValidationContext, ): Result.Result => contract.baselineInventoryHash !== context.inventoryHash || !Schema.is(BaselineSourceRevisionSchema)(contract.baselineSourceRevision) - ? Result.fail( - invalidContract( - 'authorization rollout contract does not match the classified inventory' - ) - ) + ? Result.fail(invalidContract('authorization rollout contract does not match the classified inventory')) : Result.succeed(true); const validateDecisionReference = ( - contract: DecodedAuthorizationRolloutContract + contract: DecodedAuthorizationRolloutContract, ): Result.Result => Schema.is(DecisionReferenceSchema)(contract.decisionReference) ? Result.succeed(true) - : Result.fail( - invalidContract( - 'authorization rollout contract requires an auditable decision reference' - ) - ); + : Result.fail(invalidContract('authorization rollout contract requires an auditable decision reference')); const validateCompatibilityEntrypoints = ( contract: DecodedAuthorizationRolloutContract, - context: RolloutValidationContext + context: RolloutValidationContext, ): Result.Result => { - const entries = sortArray(StringOrder)( - dedupeArray(contract.compatibilityEligibleEntrypoints) - ); + const entries = sortArray(StringOrder)(dedupeArray(contract.compatibilityEligibleEntrypoints)); if (entries.length !== contract.compatibilityEligibleEntrypoints.length) { - return Result.fail( - invalidContract( - 'authorization rollout compatibility baseline contains duplicates' - ) - ); + return Result.fail(invalidContract('authorization rollout compatibility baseline contains duplicates')); } const { entrypointKeys } = context; - if ( - entrypointKeys !== undefined && - entries.some((entrypoint) => !entrypointKeys.has(entrypoint)) - ) { - return Result.fail( - invalidContract( - 'authorization rollout compatibility baseline contains an unknown entrypoint' - ) - ); + if (entrypointKeys !== undefined && entries.some((entrypoint) => !entrypointKeys.has(entrypoint))) { + return Result.fail(invalidContract('authorization rollout compatibility baseline contains an unknown entrypoint')); } return Result.succeed(entries); }; const validateDecodedContract = ( contract: DecodedAuthorizationRolloutContract, - context: RolloutValidationContext -): Result.Result< - AuthorizationRolloutContract, - AuthorizationRolloutContractError -> => + context: RolloutValidationContext, +): Result.Result => Result.gen(function* validateDecodedAuthorizationRolloutContract() { yield* validateContractActivity(contract, context); yield* validateInventoryBinding(contract, context); yield* validateDecisionReference(contract); - const compatibilityEligibleEntrypoints = - yield* validateCompatibilityEntrypoints(contract, context); + const compatibilityEligibleEntrypoints = yield* validateCompatibilityEntrypoints(contract, context); return yield* encodeContract({ ...contract, compatibilityEligibleEntrypoints, @@ -155,21 +115,14 @@ const validateDecodedContract = ( }); const decodeContract = ( - raw: AuthorizationRolloutContractDocument -): Result.Result< - DecodedAuthorizationRolloutContract, - AuthorizationRolloutContractError -> => + raw: AuthorizationRolloutContractDocument, +): Result.Result => Schema.decodeUnknownResult(AuthorizationRolloutContractSchema, { onExcessProperty: 'error', })(raw).pipe(Result.mapError(malformedContract)); export const validateAuthorizationRolloutContract = ( raw: AuthorizationRolloutContractDocument, - context: RolloutValidationContext + context: RolloutValidationContext, ): AuthorizationRolloutContract => - Result.getOrThrow( - Result.flatMap(decodeContract(raw), (contract) => - validateDecodedContract(contract, context) - ) - ); + Result.getOrThrow(Result.flatMap(decodeContract(raw), (contract) => validateDecodedContract(contract, context))); diff --git a/app/scripts/bootstrap-agent-skills.mts b/app/scripts/bootstrap-agent-skills.mts index 8ce617667..fe52f45fa 100644 --- a/app/scripts/bootstrap-agent-skills.mts +++ b/app/scripts/bootstrap-agent-skills.mts @@ -1,28 +1,15 @@ #!/usr/bin/env node import { NodeServices } from '@effect/platform-node'; -import { - Config, - ConfigProvider, - Console, - Effect, - Exit, - Option, - Path, - Predicate, - Schema, - Stdio, -} from 'effect'; +import { Config, ConfigProvider, Console, Effect, Exit, Option, Path, Predicate, Schema, Stdio } from 'effect'; import { ChildProcessSpawner } from 'effect/unstable/process'; import { ultramodernLaunch } from './shared/ultramodern-launch.mts'; -class AgentSkillsBootstrapError extends Schema.TaggedError()( - 'AgentSkillsBootstrapError', - { reason: Schema.String } -) {} +class AgentSkillsBootstrapError extends Schema.TaggedError()('AgentSkillsBootstrapError', { + reason: Schema.String, +}) {} -const failure = (reason: string): AgentSkillsBootstrapError => - new AgentSkillsBootstrapError({ reason }); +const failure = (reason: string): AgentSkillsBootstrapError => new AgentSkillsBootstrapError({ reason }); const program = Effect.gen(function* bootstrapAgentSkills() { const path = yield* Path.Path; @@ -30,12 +17,12 @@ const program = Effect.gen(function* bootstrapAgentSkills() { const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( Config.withDefault(path.resolve(import.meta.dirname, '..')), - Effect.mapError(() => failure('ULTRAMODERN_WORKSPACE_ROOT is invalid')) + Effect.mapError(() => failure('ULTRAMODERN_WORKSPACE_ROOT is invalid')), ); const createBin = yield* Config.string('ULTRAMODERN_CREATE_BIN').pipe( Config.option, Effect.map(Option.filter((value) => value.length > 0)), - Effect.mapError(() => failure('ULTRAMODERN_CREATE_BIN is invalid')) + Effect.mapError(() => failure('ULTRAMODERN_CREATE_BIN is invalid')), ); const forwardedArgs = yield* stdio.args; const checkOnly = forwardedArgs.includes('--check'); @@ -43,12 +30,7 @@ const program = Effect.gen(function* bootstrapAgentSkills() { ? ['skills', 'check', ...forwardedArgs.filter((arg) => arg !== '--check')] : ['skills', 'install', ...forwardedArgs]; const ultramodernArgs = ['ultramodern', ...skillArgs]; - const launch = ultramodernLaunch( - createBin, - ultramodernArgs, - workspaceRoot, - path.sep - ); + const launch = ultramodernLaunch(createBin, ultramodernArgs, workspaceRoot, path.sep); return yield* processSpawner.exitCode(launch.command).pipe( Effect.matchEffect({ onFailure: (error) => { @@ -63,25 +45,22 @@ const program = Effect.gen(function* bootstrapAgentSkills() { failure( `Failed to launch ${launch.target} for UltraModern command "${ultramodernArgs .slice(1) - .join(' ')}": ${causeMessage}` - ) + .join(' ')}": ${causeMessage}`, + ), ); }, onSuccess: (status) => Effect.succeed(Number(status)), - }) + }), ); }); const exit = await Effect.runPromiseExit( program.pipe( Effect.tapError((error) => Console.error(error.reason)), - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromEnv({ preserveEmptyStrings: true }) - ), + Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ preserveEmptyStrings: true })), Effect.provide(NodeServices.layer), - Effect.scoped - ) + Effect.scoped, + ), ); process.exitCode = Exit.match(exit, { onFailure: () => 1, diff --git a/app/scripts/boundary-source-structure.mts b/app/scripts/boundary-source-structure.mts index c4ea29148..d58604c53 100644 --- a/app/scripts/boundary-source-structure.mts +++ b/app/scripts/boundary-source-structure.mts @@ -16,11 +16,7 @@ const delimiterOpenings = new Map([ export class DelimiterDepth { private readonly depths = new Map(); - update( - character: string | undefined, - previous?: string, - angles = false - ): void { + update(character: string | undefined, previous?: string, angles = false): void { if (character === undefined) { return; } @@ -52,7 +48,7 @@ export const topLevelSeparators = ( separators: string, start = 0, end = structure.length, - angles = false + angles = false, ): readonly number[] => { const depth = new DelimiterDepth(); const positions: number[] = []; @@ -69,7 +65,7 @@ export const matchingDelimiter = ( structure: string, start: number, opening: string, - closing: string + closing: string, ): number | undefined => { let depth = 0; for (let index = start; index < structure.length; index += 1) { @@ -90,7 +86,7 @@ export const separatedSource = ( source: string, separators: readonly number[], start = 0, - end = source.length + end = source.length, ): readonly string[] => { const entries: string[] = []; let entryStart = start; diff --git a/app/scripts/check-authorization-readiness.mts b/app/scripts/check-authorization-readiness.mts index 99c90ea13..4abe540c1 100644 --- a/app/scripts/check-authorization-readiness.mts +++ b/app/scripts/check-authorization-readiness.mts @@ -2,19 +2,7 @@ /// import { createHash } from 'node:crypto'; -import { - Clock, - Config, - Console, - DateTime, - Duration, - Effect, - FileSystem, - Option, - Path, - Result, - Schema, -} from 'effect'; +import { Clock, Config, Console, DateTime, Duration, Effect, FileSystem, Option, Path, Result, Schema } from 'effect'; import { Argument, Command } from 'effect/unstable/cli'; import type { ProtectedEntrypointInventory } from './authorization/protected-entrypoint-inventory.mts'; @@ -23,13 +11,8 @@ import { validateAuthorizationRolloutContract } from './authorization/rollout-co import type { AuthorizationImpactReport } from './report-fail-closed-authorization-impact.mts'; import { loadCoreNodeServices } from './shared/core-node-services.mts'; -export const AuthorizationEnvironmentSchema = Schema.Literals([ - 'development', - 'production', - 'stage', -]); -export type AuthorizationEnvironment = - typeof AuthorizationEnvironmentSchema.Type; +export const AuthorizationEnvironmentSchema = Schema.Literals(['development', 'production', 'stage']); +export type AuthorizationEnvironment = typeof AuthorizationEnvironmentSchema.Type; export const CredentialSchema = Schema.Literals(['api_key', 'session']); export type Credential = typeof CredentialSchema.Type; @@ -116,8 +99,7 @@ const AuthorizationReadinessEvidenceSchema = Schema.Struct({ workerOwnershipVersion: Schema.String, }); -export type AuthorizationReadinessEvidence = - typeof AuthorizationReadinessEvidenceSchema.Encoded; +export type AuthorizationReadinessEvidence = typeof AuthorizationReadinessEvidenceSchema.Encoded; type AuthorizationHashEvidence = | AuthorizationImpactReport @@ -126,17 +108,14 @@ type AuthorizationHashEvidence = export class AuthorizationReadinessError extends Schema.TaggedError()( 'AuthorizationReadinessError', - { reason: Schema.String } + { reason: Schema.String }, ) { override get message(): string { return this.reason; } } -const sameList = ( - left: readonly string[], - right: readonly string[] -): boolean => { +const sameList = (left: readonly string[], right: readonly string[]): boolean => { const leftValues = new Set(left); const rightValues = new Set(right); if (leftValues.size !== rightValues.size) { @@ -149,33 +128,19 @@ const sameList = ( } return true; }; -const sha256 = (value: string): string => - createHash('sha256').update(value).digest('hex'); +const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex'); const AuthorizationEvidenceJsonSchema = Schema.fromJsonString(Schema.Unknown); -const FormattedAuthorizationEvidenceJsonSchema = Schema.fromJsonString( - Schema.Unknown, - { - space: 2, - } -); -const encodeAuthorizationEvidence = ( - value: AuthorizationHashEvidence -): string => - Result.getOrThrow( - Schema.encodeResult(AuthorizationEvidenceJsonSchema)(value) - ); -const encodeFormattedAuthorizationEvidence = ( - value: AuthorizationReadinessEvidence -): string => - Result.getOrThrow( - Schema.encodeResult(FormattedAuthorizationEvidenceJsonSchema)(value) - ); -export const hashAuthorizationEvidence = ( - value: AuthorizationHashEvidence -): string => sha256(encodeAuthorizationEvidence(value)); +const FormattedAuthorizationEvidenceJsonSchema = Schema.fromJsonString(Schema.Unknown, { + space: 2, +}); +const encodeAuthorizationEvidence = (value: AuthorizationHashEvidence): string => + Result.getOrThrow(Schema.encodeResult(AuthorizationEvidenceJsonSchema)(value)); +const encodeFormattedAuthorizationEvidence = (value: AuthorizationReadinessEvidence): string => + Result.getOrThrow(Schema.encodeResult(FormattedAuthorizationEvidenceJsonSchema)(value)); +export const hashAuthorizationEvidence = (value: AuthorizationHashEvidence): string => + sha256(encodeAuthorizationEvidence(value)); const validHash = (value: string): boolean => /^[a-f0-9]{64}$/u.test(value); -const validRevision = (value: string): boolean => - /^[a-zA-Z0-9._-]{1,100}$/u.test(value); +const validRevision = (value: string): boolean => /^[a-zA-Z0-9._-]{1,100}$/u.test(value); const fail = (message: string): never => Option.getOrThrowWith( @@ -183,31 +148,23 @@ const fail = (message: string): never => () => new AuthorizationReadinessError({ reason: `authorization readiness failed: ${message}`, - }) + }), ); const requiredEntrypoints = (inventory: ProtectedEntrypointInventory) => { const actions = inventory.entries - .filter( - ({ authorization, surface }) => - surface === 'action' && authorization.kind === 'action_execution' - ) + .filter(({ authorization, surface }) => surface === 'action' && authorization.kind === 'action_execution') .map(({ entrypointKey }) => entrypointKey); const contextPermissions = inventory.entries .filter(({ authorization }) => authorization.kind === 'context_permission') .map(({ entrypointKey }) => entrypointKey); const workers = inventory.entries - .filter( - ({ authorization, surface }) => - surface === 'worker' && authorization.kind === 'owner_local_background' - ) + .filter(({ authorization, surface }) => surface === 'worker' && authorization.kind === 'owner_local_background') .map(({ entrypointKey }) => entrypointKey); const activeModules = inventory.entries .filter( ({ authorization, owner, surface }) => - owner !== 'shell-super-app' && - surface !== 'capability_issuance' && - authorization.kind !== 'public' + owner !== 'shell-super-app' && surface !== 'capability_issuance' && authorization.kind !== 'public', ) .map(({ entrypointKey }) => entrypointKey); return { actions, activeModules, contextPermissions, workers }; @@ -218,10 +175,7 @@ const validateFixedContext = (input: AuthorizationReadinessInput): void => { if (context.schemaVersion !== 1 || context.approvalStatus !== 'approved') { fail('the fixed deployment context is absent or unapproved'); } - if ( - context.environment !== observation.environment || - context.environment !== negativeSmoke.environment - ) { + if (context.environment !== observation.environment || context.environment !== negativeSmoke.environment) { fail('evidence does not match the fixed deployment environment'); } if ( @@ -232,9 +186,7 @@ const validateFixedContext = (input: AuthorizationReadinessInput): void => { } }; -const validateEvidenceFreshness = ( - input: AuthorizationReadinessInput -): void => { +const validateEvidenceFreshness = (input: AuthorizationReadinessInput): void => { const { impact, inventory, negativeSmoke, observation } = input; if ( impact.schemaVersion !== 1 || @@ -246,9 +198,7 @@ const validateEvidenceFreshness = ( negativeSmoke.inventoryHash !== inventory.inventoryHash || negativeSmoke.sourceRevision !== inventory.sourceRevision ) { - fail( - 'inventory, impact, observation, or smoke evidence is stale or unresolved' - ); + fail('inventory, impact, observation, or smoke evidence is stale or unresolved'); } }; @@ -268,9 +218,7 @@ const validateEvidenceIdentity = (input: AuthorizationReadinessInput): void => { fail('build or evidence identity is malformed'); } validateAuthorizationRolloutContract(input.rollout, { - entrypointKeys: new Set( - inventory.entries.map(({ entrypointKey }) => entrypointKey) - ), + entrypointKeys: new Set(inventory.entries.map(({ entrypointKey }) => entrypointKey)), inventoryHash: inventory.inventoryHash, nowEpochMs: input.nowEpochMs, }); @@ -283,25 +231,16 @@ const timestampMillis = (value: string): number => onSome: DateTime.toEpochMillis, }); -const validateObservationWindow = ( - input: AuthorizationReadinessInput -): void => { - const observationStarted = timestampMillis( - input.impact.observation.startedAt - ); +const validateObservationWindow = (input: AuthorizationReadinessInput): void => { + const observationStarted = timestampMillis(input.impact.observation.startedAt); const observationEnded = timestampMillis(input.impact.observation.endedAt); const rolloutStarted = timestampMillis(input.rollout.activatedAt); const rolloutEnded = timestampMillis(input.rollout.expiresAt); if ( - ![observationStarted, observationEnded, rolloutStarted, rolloutEnded].every( - Number.isFinite - ) || + ![observationStarted, observationEnded, rolloutStarted, rolloutEnded].every(Number.isFinite) || observationStarted < rolloutStarted || observationEnded > rolloutEnded || - observationEnded - observationStarted < - Duration.toMillis( - Duration.seconds(input.context.minimumObservationSeconds) - ) + observationEnded - observationStarted < Duration.toMillis(Duration.seconds(input.context.minimumObservationSeconds)) ) { fail('compatibility observation is outside the approved bounds'); } @@ -321,32 +260,20 @@ const validateObservedVersions = (input: AuthorizationReadinessInput): void => { } }; -const validateEntrypointCoverage = ( - input: AuthorizationReadinessInput -): void => { +const validateEntrypointCoverage = (input: AuthorizationReadinessInput): void => { const required = requiredEntrypoints(input.inventory); const { observation } = input; if ( !sameList(observation.verifiedActionEntrypoints, required.actions) || - !sameList( - observation.verifiedContextPermissionEntrypoints, - required.contextPermissions - ) || + !sameList(observation.verifiedContextPermissionEntrypoints, required.contextPermissions) || !sameList(observation.verifiedWorkerEntrypoints, required.workers) || - !sameList( - observation.verifiedActiveModuleEntrypoints, - required.activeModules - ) + !sameList(observation.verifiedActiveModuleEntrypoints, required.activeModules) ) { - fail( - 'required relationships, route permissions, module state, or worker ownership are incomplete' - ); + fail('required relationships, route permissions, module state, or worker ownership are incomplete'); } }; -const validateGatewayAndSmokeEvidence = ( - input: AuthorizationReadinessInput -): void => { +const validateGatewayAndSmokeEvidence = (input: AuthorizationReadinessInput): void => { const { context, negativeSmoke, observation } = input; let issuer: URL; try { @@ -354,29 +281,21 @@ const validateGatewayAndSmokeEvidence = ( } catch { return fail('gateway issuer configuration is malformed'); } - if ( - issuer.protocol !== 'https:' || - !sameList(observation.gatewayAudiences, context.gatewayAudiences) - ) { + if (issuer.protocol !== 'https:' || !sameList(observation.gatewayAudiences, context.gatewayAudiences)) { fail('gateway issuer or audience topology is incorrect'); } const requiredSmoke = context.negativeSmokeScenarios.flatMap((scenario) => - (['api_key', 'session'] as const).map( - (credential) => `${credential}:${scenario}:denied` - ) + (['api_key', 'session'] as const).map((credential) => `${credential}:${scenario}:denied`), ); const observedSmoke = negativeSmoke.scenarios.map( - ({ credential, outcome, scenario }) => - `${credential}:${scenario}:${outcome}` + ({ credential, outcome, scenario }) => `${credential}:${scenario}:${outcome}`, ); if (!sameList(observedSmoke, requiredSmoke)) { fail('negative authorization smoke evidence is incomplete'); } }; -export const checkAuthorizationReadiness = ( - input: AuthorizationReadinessInput -): AuthorizationReadinessEvidence => { +export const checkAuthorizationReadiness = (input: AuthorizationReadinessInput): AuthorizationReadinessEvidence => { validateFixedContext(input); validateEvidenceIdentity(input); validateObservationWindow(input); @@ -404,12 +323,7 @@ export const checkAuthorizationReadiness = ( }; const EntrypointKeySchema = Schema.String.pipe(Schema.brand('EntrypointKey')); -const ProtectedEntrypointSurfaceSchema = Schema.Literals([ - 'action', - 'capability_issuance', - 'route', - 'worker', -]); +const ProtectedEntrypointSurfaceSchema = Schema.Literals(['action', 'capability_issuance', 'route', 'worker']); const InventoryAuthorizationSchema = Schema.Union([ Schema.Struct({ kind: Schema.Literal('public') }), @@ -437,7 +351,7 @@ const ProtectedEntrypointInventorySchema = Schema.Struct({ entrypointKey: EntrypointKeySchema, owner: Schema.String, surface: ProtectedEntrypointSurfaceSchema, - }) + }), ), inventoryHash: Schema.String, schemaVersion: Schema.Literal(1), @@ -468,7 +382,7 @@ const AuthorizationImpactReportSchema = Schema.Struct({ 'public', ]), surface: ProtectedEntrypointSurfaceSchema, - }) + }), ), inventoryHash: Schema.String, observation: Schema.Struct({ @@ -523,7 +437,7 @@ const AuthorizationNegativeSmokeEvidenceSchema = Schema.Struct({ credential: CredentialSchema, outcome: Schema.Literal('denied'), scenario: Schema.String, - }) + }), ), schemaVersion: Schema.Literal(1), sourceRevision: Schema.String, @@ -544,34 +458,23 @@ const readJson = (schema: S, file: string) => Effect.gen(function* readJsonEffect() { const fileSystem = yield* FileSystem.FileSystem; const source = yield* fileSystem.readFileString(file); - const decoded = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(schema), - { - onExcessProperty: 'error', - } - )(source); + const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema), { + onExcessProperty: 'error', + })(source); return yield* Schema.encodeEffect(schema)(decoded); }).pipe( Effect.mapError( () => new AuthorizationReadinessError({ reason: `authorization evidence is invalid: ${file}`, - }) - ) + }), + ), ); -const insideWorkspace = ( - pathService: Path.Path, - root: string, - relativeFile: string -): string => { +const insideWorkspace = (pathService: Path.Path, root: string, relativeFile: string): string => { const target = pathService.resolve(root, relativeFile); const relative = pathService.relative(root, target); - if ( - relative === '' || - relative.startsWith(`..${pathService.sep}`) || - pathService.isAbsolute(relative) - ) { + if (relative === '' || relative.startsWith(`..${pathService.sep}`) || pathService.isAbsolute(relative)) { fail('fixed context references a path outside the workspace'); } return target; @@ -580,95 +483,53 @@ const insideWorkspace = ( const authorizationReadinessCommand = Command.make( 'authorization-readiness', { - environment: Argument.choice('environment', [ - 'development', - 'production', - 'stage', - ]), + environment: Argument.choice('environment', ['development', 'production', 'stage']), }, ({ environment }) => Effect.gen(function* authorizationReadinessProgram() { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; - const defaultRoot = yield* pathService.fromFileUrl( - new URL('..', import.meta.url) - ); - const root = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( - Config.withDefault(defaultRoot) - ); - const reportDirectory = pathService.join( - root, - '.codex/reports/authorization' - ); - const contextPath = pathService.join( - root, - 'topology/authorization-contexts', - `${environment}.json` - ); - const context = yield* readJson( - FixedAuthorizationContextSchema, - contextPath - ).pipe( + const defaultRoot = yield* pathService.fromFileUrl(new URL('..', import.meta.url)); + const root = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe(Config.withDefault(defaultRoot)); + const reportDirectory = pathService.join(root, '.codex/reports/authorization'); + const contextPath = pathService.join(root, 'topology/authorization-contexts', `${environment}.json`); + const context = yield* readJson(FixedAuthorizationContextSchema, contextPath).pipe( Effect.mapError( () => new AuthorizationReadinessError({ reason: `no approved fixed ${environment} deployment context exists`, - }) - ) + }), + ), ); - if ( - context.environment !== environment || - context.approvalStatus !== 'approved' - ) { + if (context.environment !== environment || context.approvalStatus !== 'approved') { yield* new AuthorizationReadinessError({ reason: `no approved fixed ${environment} deployment context exists`, }); } - const [inventory, impact, observation, negativeSmoke, rollout] = - yield* Effect.all( - [ - readJson( - ProtectedEntrypointInventorySchema, - pathService.join(reportDirectory, 'protected-entrypoints.json') - ), - readJson( - AuthorizationImpactReportSchema, - pathService.join(reportDirectory, 'fail-closed-impact.json') - ), - readJson( - AuthorizationReadinessObservationSchema, - pathService.join( - reportDirectory, - `fixed-context-observation.${environment}.json` - ) - ), - readJson( - AuthorizationNegativeSmokeEvidenceSchema, - pathService.join( - reportDirectory, - `negative-smoke.${environment}.json` - ) - ), - readJson( - AuthorizationRolloutContractSchema, - pathService.join(root, 'topology/authorization-rollout.json') - ), - ], - { concurrency: 'unbounded' } - ); - const [contextSource, spiceDbSchemaSource, replayMigrationSource] = - yield* Effect.all( - [ - fileSystem.readFileString(contextPath), - fileSystem.readFileString( - insideWorkspace(pathService, root, context.spiceDbSchemaPath) - ), - fileSystem.readFileString( - insideWorkspace(pathService, root, context.replayMigrationPath) - ), - ], - { concurrency: 'unbounded' } - ); + const [inventory, impact, observation, negativeSmoke, rollout] = yield* Effect.all( + [ + readJson(ProtectedEntrypointInventorySchema, pathService.join(reportDirectory, 'protected-entrypoints.json')), + readJson(AuthorizationImpactReportSchema, pathService.join(reportDirectory, 'fail-closed-impact.json')), + readJson( + AuthorizationReadinessObservationSchema, + pathService.join(reportDirectory, `fixed-context-observation.${environment}.json`), + ), + readJson( + AuthorizationNegativeSmokeEvidenceSchema, + pathService.join(reportDirectory, `negative-smoke.${environment}.json`), + ), + readJson(AuthorizationRolloutContractSchema, pathService.join(root, 'topology/authorization-rollout.json')), + ], + { concurrency: 'unbounded' }, + ); + const [contextSource, spiceDbSchemaSource, replayMigrationSource] = yield* Effect.all( + [ + fileSystem.readFileString(contextPath), + fileSystem.readFileString(insideWorkspace(pathService, root, context.spiceDbSchemaPath)), + fileSystem.readFileString(insideWorkspace(pathService, root, context.replayMigrationPath)), + ], + { concurrency: 'unbounded' }, + ); const nowEpochMs = yield* Clock.currentTimeMillis; const evidence = yield* Effect.try({ catch: (error) => @@ -695,24 +556,16 @@ const authorizationReadinessCommand = Command.make( }); const outputPath = pathService.join(reportDirectory, 'readiness.json'); yield* fileSystem.makeDirectory(reportDirectory, { recursive: true }); - yield* fileSystem.writeFileString( - outputPath, - `${encodeFormattedAuthorizationEvidence(evidence)}\n` - ); - yield* Console.log( - `${outputPath} ${hashAuthorizationEvidence(evidence)}` - ); - }) + yield* fileSystem.writeFileString(outputPath, `${encodeFormattedAuthorizationEvidence(evidence)}\n`); + yield* Console.log(`${outputPath} ${hashAuthorizationEvidence(evidence)}`); + }), ); const [, invokedModule] = process.argv; -const isMain = - invokedModule !== undefined && import.meta.url.endsWith(invokedModule); +const isMain = invokedModule !== undefined && import.meta.url.endsWith(invokedModule); if (isMain) { const NodeServices = loadCoreNodeServices(); await Effect.runPromise( - Command.run(authorizationReadinessCommand, { version: '1.0.0' }).pipe( - Effect.provide(NodeServices.layer) - ) + Command.run(authorizationReadinessCommand, { version: '1.0.0' }).pipe(Effect.provide(NodeServices.layer)), ); } diff --git a/app/scripts/check-database-access-boundaries.mts b/app/scripts/check-database-access-boundaries.mts index c05147d4a..177498899 100644 --- a/app/scripts/check-database-access-boundaries.mts +++ b/app/scripts/check-database-access-boundaries.mts @@ -1,15 +1,5 @@ import { NodeServices } from '@effect/platform-node'; -import { - Array as EffectArray, - Console, - Effect, - Exit, - FileSystem, - ManagedRuntime, - Order, - Path, - Schema, -} from 'effect'; +import { Array as EffectArray, Console, Effect, Exit, FileSystem, ManagedRuntime, Order, Path, Schema } from 'effect'; import type { PlatformError } from 'effect/PlatformError'; export interface DatabaseAccessViolation { @@ -19,22 +9,10 @@ export interface DatabaseAccessViolation { } const sourceExtensions = new Set(['.ts', '.tsx', '.mts']); -const ignoredDirectories = new Set([ - 'dist', - 'node_modules', - 'repos', - '.output', - '.codex', -]); +const ignoredDirectories = new Set(['dist', 'node_modules', 'repos', '.output', '.codex']); const coreRuntimeSourcePrefix = 'packages/core-runtime/src/'; -const collect = ( - root: string -): Effect.Effect< - readonly string[], - PlatformError, - FileSystem.FileSystem | Path.Path -> => +const collect = (root: string): Effect.Effect => Effect.gen(function* collectSourceFiles() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -50,73 +28,50 @@ const collect = ( if (info.type === 'Directory') { return collect(candidate); } - return Effect.succeed( - sourceExtensions.has(path.extname(entry)) ? [candidate] : [] - ); - }) + return Effect.succeed(sourceExtensions.has(path.extname(entry)) ? [candidate] : []); + }), ); }), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); return EffectArray.sort(discovered.flat(), Order.String); }); const isGovernedAdapter = (relative: string, source: string): boolean => !/(?:^|\/)(?:tests?|__tests__)\//u.test(relative) && - ((!relative.startsWith(coreRuntimeSourcePrefix) && - /(?:^|\/)src\/(?:actions|reads)\//u.test(relative)) || + ((!relative.startsWith(coreRuntimeSourcePrefix) && /(?:^|\/)src\/(?:actions|reads)\//u.test(relative)) || /(?:^|\/)verticals\/[^/]+\/api\//u.test(relative) || (/(?:^|\/)apps\/[^/]+\/api\//u.test(relative) && !/(?:^|\/)api\/(?:[^/]+\/)*(?:db|repositories?)\//u.test(relative) && !/(?:service|runtime-infrastructure)\.ts$/u.test(relative)) || (!relative.startsWith('packages/core-runtime/') && !relative.startsWith('scripts/') && - /@generated by OntOS Codesmith (?:Action v|Governed Contribution)/u.test( - source - ))); + /@generated by OntOS Codesmith (?:Action v|Governed Contribution)/u.test(source))); const isOwnerOperationSource = (relative: string, source: string): boolean => !/(?:^|\/)(?:tests?|__tests__)\//u.test(relative) && - ((!relative.startsWith(coreRuntimeSourcePrefix) && - /(?:^|\/)src\/(?:actions|reads)\//u.test(relative)) || + ((!relative.startsWith(coreRuntimeSourcePrefix) && /(?:^|\/)src\/(?:actions|reads)\//u.test(relative)) || (!relative.startsWith('packages/core-runtime/') && !relative.startsWith('scripts/') && - /@generated by OntOS Codesmith (?:Action v|Governed Contribution)/u.test( - source - ))); + /@generated by OntOS Codesmith (?:Action v|Governed Contribution)/u.test(source))); const importPrefix = String.raw`(?:from\s+|import\s*\(\s*|import\s+)`; const forbiddenImportPattern = `${importPrefix}['"](?:drizzle-orm(?:\\/[^'"]*)?|pg|@app\\/core-runtime\\/db(?:\\/[^'"]*)?|[^'"]*\\/(?:db\\/(?:client|schema|types|scoped-transaction)|repositories?\\/|schema)(?:\\.[^'"]*)?)['"]`; const forbiddenImport = new RegExp(forbiddenImportPattern, 'u'); const multilineForbiddenImport = new RegExp(forbiddenImportPattern, 'gu'); -const importedSpecifier = new RegExp( - `${importPrefix}['"](?[^'"]+)['"]`, - 'gu' -); +const importedSpecifier = new RegExp(`${importPrefix}['"](?[^'"]+)['"]`, 'gu'); const isTestSource = (relative: string): boolean => - /(?:^|\/)(?:tests?|__tests__)\//u.test(relative) || - relative.startsWith('packages/core-runtime/src/testing/'); + /(?:^|\/)(?:tests?|__tests__)\//u.test(relative) || relative.startsWith('packages/core-runtime/src/testing/'); const hiddenCapability = /\b(?:CoreDatabase|CoreDatabaseExecutor|CoreTransaction|ScopedTransactionExecutor|ActionTransactionExecutor|coreDatabaseSchema|actionInvocations|CORE_TABLES)\b/u; -const hiddenCoreSchema = - /\b(?:coreDatabaseSchema|actionInvocations|CORE_TABLES)\b/u; -const isVerticalOwnerSource = (relative: string): boolean => - /(?:^|\/)verticals\/[^/]+\/src\//u.test(relative); -const importsCoreDatabaseSchema = new RegExp( - `${importPrefix}['"]@app\\/core-runtime\\/db\\/schema['"]`, - 'u' -); -const importSpecifier = new RegExp( - `${importPrefix}['"](?[^'"]+)['"]`, - 'u' -); +const hiddenCoreSchema = /\b(?:coreDatabaseSchema|actionInvocations|CORE_TABLES)\b/u; +const isVerticalOwnerSource = (relative: string): boolean => /(?:^|\/)verticals\/[^/]+\/src\//u.test(relative); +const importsCoreDatabaseSchema = new RegExp(`${importPrefix}['"]@app\\/core-runtime\\/db\\/schema['"]`, 'u'); +const importSpecifier = new RegExp(`${importPrefix}['"](?[^'"]+)['"]`, 'u'); const globalDatabaseImplementationImport = /(?:import\s+(?!type\b)[^;]*?\s+from\s+|import\s*\(\s*|import\s+|export\s+(?!type\b)[^;]*?\s+from\s+)['"](?:pg|drizzle-orm\/node-postgres|@app\/core-runtime\/db\/client|[^'"]*\/db\/client(?:\.[^'"]*)?)['"]/u; -const candidatesFor = ( - path: Path.Path, - unresolved: string -): readonly string[] => { +const candidatesFor = (path: Path.Path, unresolved: string): readonly string[] => { const extension = path.extname(unresolved); if (extension.length === 0) { return [ @@ -139,18 +94,14 @@ const resolveLocalSource = ( path: Path.Path, root: string, importer: string, - specifier: string + specifier: string, ): string | undefined => { if (specifier.startsWith('.')) { - return candidatesFor( - path, - path.resolve(path.dirname(importer), specifier) - ).find((candidate) => sourceFiles.has(candidate)); + return candidatesFor(path, path.resolve(path.dirname(importer), specifier)).find((candidate) => + sourceFiles.has(candidate), + ); } - const packageGroups = - /^@app\/(?[^/]+)(?:\/(?.+))?$/u.exec( - specifier - )?.groups; + const packageGroups = /^@app\/(?[^/]+)(?:\/(?.+))?$/u.exec(specifier)?.groups; const packageName = packageGroups?.packageName; if (packageName === undefined) { return undefined; @@ -174,8 +125,7 @@ const resolveLocalSource = ( }; const containsGlobalDatabaseCapability = (source: string): boolean => - globalDatabaseImplementationImport.test(source) || - hiddenCapability.test(source); + globalDatabaseImplementationImport.test(source) || hiddenCapability.test(source); const importsGlobalDatabaseCapability = ( file: string, @@ -183,7 +133,7 @@ const importsGlobalDatabaseCapability = ( sourceFiles: ReadonlySet, path: Path.Path, root: string, - visiting: ReadonlySet = new Set() + visiting: ReadonlySet = new Set(), ): boolean => { if (visiting.has(file)) { return false; @@ -209,23 +159,10 @@ const importsGlobalDatabaseCapability = ( if (specifier === undefined) { continue; } - const dependency = resolveLocalSource( - sourceFiles, - path, - root, - file, - specifier - ); + const dependency = resolveLocalSource(sourceFiles, path, root, file, specifier); if ( dependency !== undefined && - importsGlobalDatabaseCapability( - dependency, - sources, - sourceFiles, - path, - root, - nextVisiting - ) + importsGlobalDatabaseCapability(dependency, sources, sourceFiles, path, root, nextVisiting) ) { return true; } @@ -238,11 +175,9 @@ const crossOwnerPrivateImport = ( root: string, file: string, relative: string, - line: string + line: string, ): boolean => { - const sourceOwner = /(?:^|\/)verticals\/(?[^/]+)\/src\//u.exec( - relative - )?.groups?.owner; + const sourceOwner = /(?:^|\/)verticals\/(?[^/]+)\/src\//u.exec(relative)?.groups?.owner; const specifier = importSpecifier.exec(line)?.groups?.specifier; if (sourceOwner === undefined || specifier === undefined) { return false; @@ -253,9 +188,7 @@ const crossOwnerPrivateImport = ( .split(path.sep) .join('/') : specifier.replace(/^@app\//u, ''); - const targetOwner = /(?:^|\/)verticals\/(?[^/]+)\//u.exec( - targetRelative - )?.groups?.owner; + const targetOwner = /(?:^|\/)verticals\/(?[^/]+)\//u.exec(targetRelative)?.groups?.owner; return ( targetOwner !== undefined && targetOwner !== sourceOwner && @@ -275,8 +208,7 @@ interface SourceCheckContext { readonly sources: ReadonlyMap; } -const sourceLine = (source: string, index: number): number => - source.slice(0, index).split('\n').length; +const sourceLine = (source: string, index: number): number => source.slice(0, index).split('\n').length; const recordProductionTestingImports = (context: SourceCheckContext): void => { if (isTestSource(context.relative)) { @@ -287,24 +219,14 @@ const recordProductionTestingImports = (context: SourceCheckContext): void => { if (specifier === undefined) { continue; } - const dependency = resolveLocalSource( - context.sourceFiles, - context.path, - context.root, - context.file, - specifier - ); + const dependency = resolveLocalSource(context.sourceFiles, context.path, context.root, context.file, specifier); const dependencyRelative = dependency === undefined ? undefined - : context.path - .relative(context.root, dependency) - .split(context.path.sep) - .join('/'); + : context.path.relative(context.root, dependency).split(context.path.sep).join('/'); if ( specifier.startsWith('@app/core-runtime/testing/') || - dependencyRelative?.startsWith('packages/core-runtime/src/testing/') === - true + dependencyRelative?.startsWith('packages/core-runtime/src/testing/') === true ) { context.record({ file: context.relative, @@ -315,9 +237,7 @@ const recordProductionTestingImports = (context: SourceCheckContext): void => { } }; -const recordTransitiveDatabaseCapabilities = ( - context: SourceCheckContext -): void => { +const recordTransitiveDatabaseCapabilities = (context: SourceCheckContext): void => { if (!isOwnerOperationSource(context.relative, context.source)) { return; } @@ -326,69 +246,39 @@ const recordTransitiveDatabaseCapabilities = ( if (specifier === undefined) { continue; } - const dependency = resolveLocalSource( - context.sourceFiles, - context.path, - context.root, - context.file, - specifier - ); + const dependency = resolveLocalSource(context.sourceFiles, context.path, context.root, context.file, specifier); if ( dependency !== undefined && - importsGlobalDatabaseCapability( - dependency, - context.sources, - context.sourceFiles, - context.path, - context.root - ) + importsGlobalDatabaseCapability(dependency, context.sources, context.sourceFiles, context.path, context.root) ) { context.record({ file: context.relative, line: sourceLine(context.source, match.index), - reason: - 'transitive global database capability in governed handler requirements', + reason: 'transitive global database capability in governed handler requirements', }); } } }; -const isDirectDatabaseImport = ( - context: SourceCheckContext, - source: string -): boolean => +const isDirectDatabaseImport = (context: SourceCheckContext, source: string): boolean => context.governedAdapter || - (isVerticalOwnerSource(context.relative) && - importsCoreDatabaseSchema.test(source)) || - crossOwnerPrivateImport( - context.path, - context.root, - context.file, - context.relative, - source - ); + (isVerticalOwnerSource(context.relative) && importsCoreDatabaseSchema.test(source)) || + crossOwnerPrivateImport(context.path, context.root, context.file, context.relative, source); const recordMultilineDatabaseImports = (context: SourceCheckContext): void => { for (const match of context.source.matchAll(multilineForbiddenImport)) { - if ( - !match[0].includes('\n') || - !isDirectDatabaseImport(context, match[0]) - ) { + if (!match[0].includes('\n') || !isDirectDatabaseImport(context, match[0])) { continue; } context.record({ file: context.relative, line: sourceLine(context.source, match.index), - reason: - 'direct database/private repository import in governed handler or transport adapter', + reason: 'direct database/private repository import in governed handler or transport adapter', }); } }; -const exposesHiddenCapability = ( - context: SourceCheckContext, - line: string -): boolean => { +const exposesHiddenCapability = (context: SourceCheckContext, line: string): boolean => { const hasCapability = context.governedAdapter ? hiddenCapability.test(line) : isVerticalOwnerSource(context.relative) && hiddenCoreSchema.test(line); @@ -399,37 +289,25 @@ const recordLineDatabaseViolations = (context: SourceCheckContext): void => { for (const [index, line] of context.source.split('\n').entries()) { const directImport = (context.governedAdapter && forbiddenImport.test(line)) || - (isVerticalOwnerSource(context.relative) && - importsCoreDatabaseSchema.test(line)) || - crossOwnerPrivateImport( - context.path, - context.root, - context.file, - context.relative, - line - ); + (isVerticalOwnerSource(context.relative) && importsCoreDatabaseSchema.test(line)) || + crossOwnerPrivateImport(context.path, context.root, context.file, context.relative, line); if (directImport) { context.record({ file: context.relative, line: index + 1, - reason: - 'direct database/private repository import in governed handler or transport adapter', + reason: 'direct database/private repository import in governed handler or transport adapter', }); } else if (exposesHiddenCapability(context, line)) { context.record({ file: context.relative, line: index + 1, - reason: - 'database capability exposed through governed handler requirements', + reason: 'database capability exposed through governed handler requirements', }); } } }; -const compareViolations = ( - left: DatabaseAccessViolation, - right: DatabaseAccessViolation -): -1 | 0 | 1 => { +const compareViolations = (left: DatabaseAccessViolation, right: DatabaseAccessViolation): -1 | 0 | 1 => { const fileOrder = left.file.localeCompare(right.file); if (fileOrder < 0) { return -1; @@ -461,11 +339,9 @@ export const checkDatabaseAccessBoundaries = (root: string) => const files = yield* collect(root); const sourcePairs = yield* Effect.all( files.map((file) => - fileSystem - .readFileString(file, 'utf-8') - .pipe(Effect.map((source) => [file, source] as const)) + fileSystem.readFileString(file, 'utf-8').pipe(Effect.map((source) => [file, source] as const)), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const sources = new Map(sourcePairs); const sourceFiles = new Set(files); @@ -501,27 +377,21 @@ export const checkDatabaseAccessBoundaries = (root: string) => class DatabaseAccessBoundaryCheckFailed extends Schema.TaggedError()( 'DatabaseAccessBoundaryCheckFailed', - { violationCount: Schema.Number } + { violationCount: Schema.Number }, ) {} const main = Effect.gen(function* databaseAccessBoundaryMain() { const path = yield* Path.Path; - const violations = yield* checkDatabaseAccessBoundaries( - path.resolve(process.cwd()) - ); + const violations = yield* checkDatabaseAccessBoundaries(path.resolve(process.cwd())); if (violations.length > 0) { yield* Effect.all( - violations.map((violation) => - Console.error( - `${violation.file}:${violation.line}: ${violation.reason}` - ) - ), - { concurrency: 1, discard: true } + violations.map((violation) => Console.error(`${violation.file}:${violation.line}: ${violation.reason}`)), + { concurrency: 1, discard: true }, ); return yield* Effect.fail( new DatabaseAccessBoundaryCheckFailed({ violationCount: violations.length, - }) + }), ); } return yield* Console.log('Database access boundaries verified'); diff --git a/app/scripts/check-module-entrypoint-boundaries.mts b/app/scripts/check-module-entrypoint-boundaries.mts index dd00d1d81..2cc270296 100644 --- a/app/scripts/check-module-entrypoint-boundaries.mts +++ b/app/scripts/check-module-entrypoint-boundaries.mts @@ -1,10 +1,6 @@ #!/usr/bin/env node import { NodeRuntime, NodeServices } from '@effect/platform-node'; -import { - LanguageVariant, - SyntaxKind, - createScanner, -} from '@typescript/native/unstable/ast'; +import { LanguageVariant, SyntaxKind, createScanner } from '@typescript/native/unstable/ast'; import { Config, Console, @@ -56,31 +52,18 @@ import { } from './generated-module-api-boundary.mts'; import { maskNonCode } from './scaffolding/shared.mts'; -const SOURCE_EXTENSIONS = new Set([ - '.js', - '.jsx', - '.mjs', - '.mts', - '.ts', - '.tsx', -]); +const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']); const ACTION_EXTENSION = '.action.ts'; const ACTION_HEADER = '// @generated by OntOS Codesmith Action v1'; const WORKER_HEADER = '// @generated by OntOS Codesmith Outbox Worker v1'; -const APPROVED_REMOTE_LOADER = - 'apps/shell-super-app/src/routes/module-entrypoint-loader.ts'; -const SourceRevisionSchema = Schema.String.check( - Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u) -); +const APPROVED_REMOTE_LOADER = 'apps/shell-super-app/src/routes/module-entrypoint-loader.ts'; +const SourceRevisionSchema = Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,100}$/u)); const sortStrings = (values: readonly string[]): readonly string[] => { const sorted: string[] = []; for (const value of values) { let insertionIndex = 0; - while ( - insertionIndex < sorted.length && - (sorted[insertionIndex]?.localeCompare(value) ?? 0) <= 0 - ) { + while (insertionIndex < sorted.length && (sorted[insertionIndex]?.localeCompare(value) ?? 0) <= 0) { insertionIndex += 1; } sorted.splice(insertionIndex, 0, value); @@ -88,18 +71,13 @@ const sortStrings = (values: readonly string[]): readonly string[] => { return sorted; }; -const isGeneratedInfrastructureReadinessApi = ( - file: string, - source: string -): boolean => { +const isGeneratedInfrastructureReadinessApi = (file: string, source: string): boolean => { const stem = /(?[^/]+)\/[^/]+\/[^/]+$/u.exec(file)?.groups?.stem; if (stem === undefined) { return false; } const endpoints = [ - ...source.matchAll( - /HttpApiEndpoint\.(?get|post)\(\s*'(?[^']+)'\s*,\s*'(?[^']+)'/gu - ), + ...source.matchAll(/HttpApiEndpoint\.(?get|post)\(\s*'(?[^']+)'\s*,\s*'(?[^']+)'/gu), ].map((match) => { const { groups } = match; return `${groups?.method}:${groups?.name}:${groups?.path}`; @@ -113,13 +91,7 @@ const isGeneratedInfrastructureReadinessApi = ( const SKIPPED_DIRECTORIES = new Set(['.output', 'node_modules']); -const walk = ( - directory: string -): Effect.Effect< - readonly string[], - PlatformError, - FileSystem.FileSystem | Path.Path -> => +const walk = (directory: string): Effect.Effect => Effect.gen(function* walkEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -134,9 +106,7 @@ const walk = ( } else { const entryPath = path.join(directory, entry); const info = yield* fileSystem.stat(entryPath); - discovered.push( - info.type === 'Directory' ? yield* walk(entryPath) : [entryPath] - ); + discovered.push(info.type === 'Directory' ? yield* walk(entryPath) : [entryPath]); } } return sortStrings(discovered.flat()); @@ -147,18 +117,16 @@ const relative = (path: Path.Path, root: string, file: string): string => class ModuleEntrypointBoundaryError extends Schema.TaggedError()( 'ModuleEntrypointBoundaryError', - { message: Schema.String } + { message: Schema.String }, ) {} const fail = (file: string, message: string) => - Effect.fail( - new ModuleEntrypointBoundaryError({ message: `${file}: ${message}` }) - ); + Effect.fail(new ModuleEntrypointBoundaryError({ message: `${file}: ${message}` })); const requireDefined = ( value: Value | undefined, file: string, - message: string + message: string, ): Effect.Effect => value === undefined ? fail(file, message) : Effect.succeed(value); @@ -181,24 +149,14 @@ interface ObjectProperties { readonly values: ReadonlyMap; } -const tokenKindAt = ( - tokens: readonly SourceToken[], - index: number -): SyntaxKind | undefined => tokens[index]?.kind; +const tokenKindAt = (tokens: readonly SourceToken[], index: number): SyntaxKind | undefined => tokens[index]?.kind; -const isPropertyValue = ( - tokens: readonly SourceToken[], - index: number, - valueKind: SyntaxKind -): boolean => +const isPropertyValue = (tokens: readonly SourceToken[], index: number, valueKind: SyntaxKind): boolean => tokenKindAt(tokens, index) === SyntaxKind.Identifier && tokenKindAt(tokens, index + 1) === SyntaxKind.ColonToken && tokenKindAt(tokens, index + 2) === valueKind; -const readObjectStringProperties = ( - tokens: readonly SourceToken[], - openBraceIndex: number -): ObjectProperties => { +const readObjectStringProperties = (tokens: readonly SourceToken[], openBraceIndex: number): ObjectProperties => { const values = new Map(); let depth = 0; for (let cursor = openBraceIndex; cursor < tokens.length; cursor += 1) { @@ -213,72 +171,49 @@ const readObjectStringProperties = ( if (depth === 0) { return { closeBraceIndex: cursor, values }; } - } else if ( - depth === 1 && - isPropertyValue(tokens, cursor, SyntaxKind.StringLiteral) - ) { + } else if (depth === 1 && isPropertyValue(tokens, cursor, SyntaxKind.StringLiteral)) { values.set(current.value, tokens[cursor + 2]?.value ?? ''); } } return { closeBraceIndex: tokens.length, values }; }; -const readContextPermission = ( - properties: ReadonlyMap -): InventoryAuthorization | undefined => { +const readContextPermission = (properties: ReadonlyMap): InventoryAuthorization | undefined => { const permission = properties.get('permission'); - return properties.size === 2 && permission !== undefined - ? { kind: 'context_permission', permission } - : undefined; + return properties.size === 2 && permission !== undefined ? { kind: 'context_permission', permission } : undefined; }; -const readActionExecution = ( - properties: ReadonlyMap -): InventoryAuthorization | undefined => { +const readActionExecution = (properties: ReadonlyMap): InventoryAuthorization | undefined => { const provisioning = properties.get('provisioning'); - return properties.size === 2 && - (provisioning === 'explicit' || - provisioning === 'tenant_membership_default') + return properties.size === 2 && (provisioning === 'explicit' || provisioning === 'tenant_membership_default') ? { kind: 'action_execution', provisioning } : undefined; }; -const readCapabilityIssuance = ( - properties: ReadonlyMap -): InventoryAuthorization | undefined => { +const readCapabilityIssuance = (properties: ReadonlyMap): InventoryAuthorization | undefined => { const credential = properties.get('credential'); - return properties.size === 2 && - (credential === 'api_key' || credential === 'session') + return properties.size === 2 && (credential === 'api_key' || credential === 'session') ? { credential, kind: 'capability_issuance' } : undefined; }; const readAuthorization = ( tokens: readonly SourceToken[], - openBraceIndex: number + openBraceIndex: number, ): InventoryAuthorization | undefined => { const properties = readObjectStringProperties(tokens, openBraceIndex).values; return Match.value(properties.get('kind')).pipe( - Match.when('public', (kind) => - properties.size === 1 ? { kind } : undefined - ), - Match.when('authenticated_principal', (kind) => - properties.size === 1 ? { kind } : undefined - ), - Match.when('owner_local_background', (kind) => - properties.size === 1 ? { kind } : undefined - ), + Match.when('public', (kind) => (properties.size === 1 ? { kind } : undefined)), + Match.when('authenticated_principal', (kind) => (properties.size === 1 ? { kind } : undefined)), + Match.when('owner_local_background', (kind) => (properties.size === 1 ? { kind } : undefined)), Match.when('context_permission', () => readContextPermission(properties)), Match.when('action_execution', () => readActionExecution(properties)), Match.when('capability_issuance', () => readCapabilityIssuance(properties)), - Match.orElse(EffectFunction.constUndefined) + Match.orElse(EffectFunction.constUndefined), ); }; -const rescanTemplateClose = ( - scanner: ReturnType, - depths: number[] -): SyntaxKind => { +const rescanTemplateClose = (scanner: ReturnType, depths: number[]): SyntaxKind => { const index = depths.length - 1; const depth = depths[index] ?? 0; if (depth !== 0) { @@ -295,7 +230,7 @@ const rescanTemplateClose = ( const updateTemplateToken = ( scanner: ReturnType, kind: SyntaxKind, - depths: number[] + depths: number[], ): SyntaxKind => { if (kind === SyntaxKind.TemplateHead) { depths.push(0); @@ -316,41 +251,32 @@ const tokenize = (source: string): readonly SourceToken[] => { const templateExpressionBraceDepths: number[] = []; let scannedKind = scanner.scan(); while (scannedKind !== SyntaxKind.EndOfFile) { - const kind = updateTemplateToken( - scanner, - scannedKind, - templateExpressionBraceDepths - ); + const kind = updateTemplateToken(scanner, scannedKind, templateExpressionBraceDepths); tokens.push({ kind, value: scanner.getTokenValue() }); scannedKind = scanner.scan(); } return tokens; }; -const entrypointScope = ( - token: SourceToken -): ParsedEntrypoint['scope'] | undefined => { +const entrypointScope = (token: SourceToken): ParsedEntrypoint['scope'] | undefined => { if (token.kind !== SyntaxKind.Identifier) { return undefined; } return Match.value(token.value).pipe( Match.when('defineSystemModuleEntrypoint', () => 'System' as const), Match.when('defineTenantModuleEntrypoint', () => 'Tenant' as const), - Match.orElse(EffectFunction.constUndefined) + Match.orElse(EffectFunction.constUndefined), ); }; const readEntrypointAuthorization = ( tokens: readonly SourceToken[], start: number, - end: number + end: number, ): InventoryAuthorization | undefined => { let authorization: InventoryAuthorization | undefined; for (let cursor = start; cursor < end; cursor += 1) { - if ( - tokens[cursor]?.value === 'authorization' && - isPropertyValue(tokens, cursor, SyntaxKind.OpenBraceToken) - ) { + if (tokens[cursor]?.value === 'authorization' && isPropertyValue(tokens, cursor, SyntaxKind.OpenBraceToken)) { authorization = readAuthorization(tokens, cursor + 2); } } @@ -370,11 +296,7 @@ const readEntrypoints = (source: string): readonly ParsedEntrypoint[] => { const properties = readObjectStringProperties(tokens, index + 2); entrypoints.push({ access: properties.values.get('access'), - authorization: readEntrypointAuthorization( - tokens, - index + 3, - properties.closeBraceIndex - ), + authorization: readEntrypointAuthorization(tokens, index + 3, properties.closeBraceIndex), entrypointKey: properties.values.get('entrypointKey'), moduleKey: properties.values.get('moduleKey'), role: properties.values.get('role'), @@ -385,10 +307,7 @@ const readEntrypoints = (source: string): readonly ParsedEntrypoint[] => { return entrypoints; }; -const readStringProperties = ( - source: string, - propertyName: string -): ReadonlySet => { +const readStringProperties = (source: string, propertyName: string): ReadonlySet => { const tokens = tokenize(source); const values = new Set(); for (const [index, token] of tokens.entries()) { @@ -413,29 +332,19 @@ const callsIdentifier = (source: string, identifier: string): boolean => { (token, index) => token.kind === SyntaxKind.Identifier && token.value === identifier && - tokens[index + 1]?.kind === SyntaxKind.OpenParenToken + tokens[index + 1]?.kind === SyntaxKind.OpenParenToken, ); }; -const containsIdentifier = ( - source: string, - identifiers: ReadonlySet -): boolean => - tokenize(source).some( - (token) => - token.kind === SyntaxKind.Identifier && identifiers.has(token.value) - ); +const containsIdentifier = (source: string, identifiers: ReadonlySet): boolean => + tokenize(source).some((token) => token.kind === SyntaxKind.Identifier && identifiers.has(token.value)); -const isModuleSpecifierPosition = ( - tokens: readonly SourceToken[], - index: number -): boolean => { +const isModuleSpecifierPosition = (tokens: readonly SourceToken[], index: number): boolean => { const previous = tokenKindAt(tokens, index - 1); return ( previous === SyntaxKind.FromKeyword || previous === SyntaxKind.ImportKeyword || - (previous === SyntaxKind.OpenParenToken && - tokenKindAt(tokens, index - 2) === SyntaxKind.ImportKeyword) + (previous === SyntaxKind.OpenParenToken && tokenKindAt(tokens, index - 2) === SyntaxKind.ImportKeyword) ); }; @@ -453,10 +362,7 @@ const readImportedModuleSpecifiers = (source: string): readonly string[] => { return specifiers; }; -const authorizationEquals = ( - left: InventoryAuthorization | undefined, - right: InventoryAuthorization -): boolean => { +const authorizationEquals = (left: InventoryAuthorization | undefined, right: InventoryAuthorization): boolean => { if (left?.kind !== right.kind) { return false; } @@ -472,25 +378,19 @@ const authorizationEquals = ( return true; }; -const sourceOrEmpty = ( - sourceMap: ReadonlyMap, - file: string -): string => sourceMap.get(file) ?? ''; +const sourceOrEmpty = (sourceMap: ReadonlyMap, file: string): string => sourceMap.get(file) ?? ''; const readActionEntrypoints = ( sourceMap: ReadonlyMap, file: string, - source: string + source: string, ): readonly ParsedEntrypoint[] => { const inline = readEntrypoints(source); const action = /^verticals\/party-registry\/src\/actions\/(?(?:archive|unarchive)-(?:organization|person)-engagement)\.action\.ts$/u.exec( - file + file, )?.groups?.action; - if ( - action === undefined && - !containsIdentifier(source, new Set(['engagementLifecycleRegistration'])) - ) { + if (action === undefined && !containsIdentifier(source, new Set(['engagementLifecycleRegistration']))) { return inline; } if ( @@ -498,11 +398,8 @@ const readActionEntrypoints = ( inline.length !== 0 || !hasEngagementLifecycleRegistrationContract( source, - sourceOrEmpty( - sourceMap, - 'verticals/party-registry/src/actions/engagement-lifecycle-registration.ts' - ), - action + sourceOrEmpty(sourceMap, 'verticals/party-registry/src/actions/engagement-lifecycle-registration.ts'), + action, ) ) { return []; @@ -527,7 +424,7 @@ const requireExactEntrypoint = ( source: string, expected: Required, remediation: string, - entrypoints = readEntrypoints(source) + entrypoints = readEntrypoints(source), ): Effect.Effect => Effect.gen(function* requireExactEntrypointEffect() { const entrypoint = yield* requireDefined(entrypoints[0], file, remediation); @@ -545,37 +442,24 @@ const requireExactEntrypoint = ( return entrypoint; }); -const requireGeneratedActionEntrypoint = ( - sourceMap: ReadonlyMap, - file: string, - source: string -) => +const requireGeneratedActionEntrypoint = (sourceMap: ReadonlyMap, file: string, source: string) => Effect.gen(function* requireGeneratedActionEntrypointEffect() { if (!hasGeneratedSourceHeader(source, `${ACTION_HEADER}\n`)) { - yield* fail( - file, - 'Actions must be created and maintained with scaffold:action' - ); + yield* fail(file, 'Actions must be created and maintained with scaffold:action'); } - const owner = /^\/\/ @ontos-action-owner (?.+)$/mu.exec(source) - ?.groups?.owner; - const action = /^\/\/ @ontos-action-slug (?.+)$/mu.exec(source) - ?.groups?.action; + const owner = /^\/\/ @ontos-action-owner (?.+)$/mu.exec(source)?.groups?.owner; + const action = /^\/\/ @ontos-action-slug (?.+)$/mu.exec(source)?.groups?.action; const descriptorMessage = 'regenerate this Action with scaffold:action so it has its governed action/write entrypoint'; const definedOwner = yield* requireDefined(owner, file, descriptorMessage); - const definedAction = yield* requireDefined( - action, - file, - descriptorMessage - ); + const definedAction = yield* requireDefined(action, file, descriptorMessage); const entrypoints = readActionEntrypoints(sourceMap, file, source); const rawAuthorization = entrypoints[0]?.authorization; const authorization = yield* rawAuthorization?.kind === 'action_execution' ? Effect.succeed(rawAuthorization) : fail( file, - 'regenerate this Action with scaffold:action so it declares action_execution authorization and provisioning intent' + 'regenerate this Action with scaffold:action so it declares action_execution authorization and provisioning intent', ); yield* requireExactEntrypoint( file, @@ -589,32 +473,21 @@ const requireGeneratedActionEntrypoint = ( scope: definedOwner.startsWith('core.') ? 'System' : 'Tenant', }, descriptorMessage, - entrypoints + entrypoints, ); }); const requireGeneratedWorkerEntrypoint = (file: string, source: string) => Effect.gen(function* requireGeneratedWorkerEntrypointEffect() { if (!hasGeneratedSourceHeader(source, `${WORKER_HEADER}\n`)) { - yield* fail( - file, - 'Outbox Workers must be created and maintained with scaffold:outbox-worker' - ); + yield* fail(file, 'Outbox Workers must be created and maintained with scaffold:outbox-worker'); } - const owner = /^\/\/ @ontos-outbox-worker-owner (?.+)$/mu.exec( - source - )?.groups?.owner; - const workerKey = /^\/\/ @ontos-outbox-worker-key (?.+)$/mu.exec( - source - )?.groups?.workerKey; + const owner = /^\/\/ @ontos-outbox-worker-owner (?.+)$/mu.exec(source)?.groups?.owner; + const workerKey = /^\/\/ @ontos-outbox-worker-key (?.+)$/mu.exec(source)?.groups?.workerKey; const descriptorMessage = 'regenerate this Worker with scaffold:outbox-worker so it has its governed worker/background entrypoint'; const definedOwner = yield* requireDefined(owner, file, descriptorMessage); - const definedWorkerKey = yield* requireDefined( - workerKey, - file, - descriptorMessage - ); + const definedWorkerKey = yield* requireDefined(workerKey, file, descriptorMessage); yield* requireExactEntrypoint( file, source, @@ -626,7 +499,7 @@ const requireGeneratedWorkerEntrypoint = (file: string, source: string) => role: 'worker', scope: 'Tenant', }, - descriptorMessage + descriptorMessage, ); }); @@ -634,7 +507,7 @@ const requireRouteEntrypoint = ( file: string, source: string, moduleOwner: string, - expectedScope: 'system' | 'tenant' + expectedScope: 'system' | 'tenant', ): Effect.Effect => Effect.gen(function* requireRouteEntrypointEffect() { const entrypoints = readEntrypoints(source); @@ -659,22 +532,16 @@ const requireRouteEntrypoint = ( const TopologyMetadataSchema = Schema.Struct({ topology: Schema.optionalKey( Schema.Struct({ - apps: Schema.optionalKey( - Schema.Array(Schema.Struct({ id: Schema.String, path: Schema.String })) - ), - }) + apps: Schema.optionalKey(Schema.Array(Schema.Struct({ id: Schema.String, path: Schema.String }))), + }), ), }); -const decodeTopologyMetadata = Schema.decodeUnknownEffect( - Schema.fromJsonString(TopologyMetadataSchema) -); +const decodeTopologyMetadata = Schema.decodeUnknownEffect(Schema.fromJsonString(TopologyMetadataSchema)); const VerticalPackageJsonSchema = Schema.Struct({ exports: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), }); -const decodeVerticalPackageJson = Schema.decodeUnknownEffect( - Schema.fromJsonString(VerticalPackageJsonSchema) -); +const decodeVerticalPackageJson = Schema.decodeUnknownEffect(Schema.fromJsonString(VerticalPackageJsonSchema)); const readTopologyOwners = (root: string) => Effect.gen(function* readTopologyOwnersEffect() { @@ -683,32 +550,23 @@ const readTopologyOwners = (root: string) => const metadata = yield* fileSystem .readFileString(path.join(root, '.modernjs/ultramodern.json'), 'utf-8') .pipe(Effect.flatMap(decodeTopologyMetadata)); - return new Map( - (metadata.topology?.apps ?? []).map((app) => [ - app.path.replaceAll('\\', '/'), - app.id, - ]) - ); + return new Map((metadata.topology?.apps ?? []).map((app) => [app.path.replaceAll('\\', '/'), app.id])); }); const readSourceRevision = (root: string) => Effect.gen(function* readSourceRevisionEffect() { - const configured = yield* Config.option( - Config.string('ULTRAMODERN_SOURCE_REVISION') - ); + const configured = yield* Config.option(Config.string('ULTRAMODERN_SOURCE_REVISION')); if (Option.isSome(configured)) { return configured.value; } const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - return yield* spawner - .string(ChildProcess.make('git', ['rev-parse', 'HEAD'], { cwd: root })) - .pipe( - Effect.map((revision) => { - const trimmed = revision.trim(); - return trimmed.length === 0 ? 'working-tree' : trimmed; - }), - Effect.catch(() => Effect.succeed('working-tree')) - ); + return yield* spawner.string(ChildProcess.make('git', ['rev-parse', 'HEAD'], { cwd: root })).pipe( + Effect.map((revision) => { + const trimmed = revision.trim(); + return trimmed.length === 0 ? 'working-tree' : trimmed; + }), + Effect.catch(() => Effect.succeed('working-tree')), + ); }); interface RouteEntrypointRecord { @@ -724,9 +582,7 @@ interface BoundaryCheckState { readonly sourceMap: ReadonlyMap; } -const governedSourceHeader = ( - category: string | undefined -): string | undefined => { +const governedSourceHeader = (category: string | undefined): string | undefined => { if (category === 'components') { return '// @generated by OntOS Codesmith public-component v1\n'; } @@ -739,77 +595,48 @@ const governedSourceHeader = ( return undefined; }; -const inventorySurface = ( - role: string -): ProtectedEntrypointInventoryEntry['surface'] => { +const inventorySurface = (role: string): ProtectedEntrypointInventoryEntry['surface'] => { if (role === 'action') { return 'action'; } return role === 'worker' ? 'worker' : 'route'; }; -const validateRouteSource = ( - state: BoundaryCheckState, - file: string, - source: string -) => +const validateRouteSource = (state: BoundaryCheckState, file: string, source: string) => Effect.gen(function* validateRouteSourceEffect() { if (!file.endsWith('/route.meta.ts')) { return; } - const ownerEntry = [...state.owners.entries()].find(([appPath]) => - file.startsWith(`${appPath}/`) - ); - const [, owner] = yield* requireDefined( - ownerEntry, - file, - 'route owner is absent from the generated topology' - ); + const ownerEntry = [...state.owners.entries()].find(([appPath]) => file.startsWith(`${appPath}/`)); + const [, owner] = yield* requireDefined(ownerEntry, file, 'route owner is absent from the generated topology'); const ownerAppIds = readStringProperties(source, 'ownerAppId'); if (ownerAppIds.size !== 1 || !ownerAppIds.has(owner)) { - yield* fail( - file, - 'route ownerAppId must match the generated topology deployment identity' - ); + yield* fail(file, 'route ownerAppId must match the generated topology deployment identity'); } const expectedScope = owner.startsWith('shell-') ? 'system' : 'tenant'; const moduleIds = readStringProperties(source, 'moduleId'); const moduleOwner = expectedScope === 'tenant' ? [...moduleIds][0] : owner; - if ( - moduleOwner === undefined || - (expectedScope === 'tenant' && moduleIds.size !== 1) - ) { - yield* fail( - file, - 'MicroVertical route metadata must declare exactly one manifest moduleId' - ); + if (moduleOwner === undefined || (expectedScope === 'tenant' && moduleIds.size !== 1)) { + yield* fail(file, 'MicroVertical route metadata must declare exactly one manifest moduleId'); } state.routeEntrypoints.push({ deployment: owner, - entrypointKey: yield* requireRouteEntrypoint( - file, - source, - moduleOwner, - expectedScope - ), + entrypointKey: yield* requireRouteEntrypoint(file, source, moduleOwner, expectedScope), file, }); }); const validateGovernedSource = (file: string, source: string) => Effect.gen(function* validateGovernedSourceEffect() { - const category = /\/src\/(?components|search|reports)\//u.exec( - `/${file}` - )?.groups?.category; + const category = /\/src\/(?components|search|reports)\//u.exec(`/${file}`)?.groups?.category; const expectedHeader = governedSourceHeader(category); const invalidGovernedSource = /\/src\/public-components\//u.test(`/${file}`) || - (expectedHeader !== undefined && - !hasGeneratedSourceHeader(source, expectedHeader)); + (expectedHeader !== undefined && !hasGeneratedSourceHeader(source, expectedHeader)); if (invalidGovernedSource) { yield* fail( file, - 'public components, search, and reports require an approved Codesmith generator and reserved runtime registration first' + 'public components, search, and reports require an approved Codesmith generator and reserved runtime registration first', ); } }); @@ -822,16 +649,13 @@ interface GeneratedProviderLocation { readonly vertical: string; } -const generatedProviderLocation = ( - file: string, - source: string -): GeneratedProviderLocation | undefined => { +const generatedProviderLocation = (file: string, source: string): GeneratedProviderLocation | undefined => { if ( !['report', SEARCH_PROVIDER_KIND].some((kind) => hasGeneratedSourceHeader( source, - `// @generated by OntOS Codesmith Governed Contribution v1\n// @ontos-contribution-kind ${kind}\n` - ) + `// @generated by OntOS Codesmith Governed Contribution v1\n// @ontos-contribution-kind ${kind}\n`, + ), ) ) { return undefined; @@ -856,19 +680,16 @@ const generatedProviderLocation = ( }; const providerModuleId = (manifest: string): string => - /@ontos-module-id (?[a-z0-9]+(?:\.[a-z0-9]+)*)/u.exec(manifest) - ?.groups?.moduleId ?? ''; + /@ontos-module-id (?[a-z0-9]+(?:\.[a-z0-9]+)*)/u.exec(manifest)?.groups?.moduleId ?? ''; const validateGeneratedProviderClient = ( sourceMap: ReadonlyMap, file: string, deploymentAppId: string, - discoveredProvider?: GeneratedProviderLocation + discoveredProvider?: GeneratedProviderLocation, ) => Effect.gen(function* validateGeneratedProviderClientEffect() { - const provider = - discoveredProvider ?? - generatedProviderLocation(file, sourceOrEmpty(sourceMap, file)); + const provider = discoveredProvider ?? generatedProviderLocation(file, sourceOrEmpty(sourceMap, file)); if (provider === undefined) { return; } @@ -896,43 +717,18 @@ const validateGeneratedProviderClient = ( const providerSource = sourceOrEmpty(sourceMap, providerPath); const serverPath = `${vertical}/api/${name}-${kind}-server.ts`; const serverSource = sourceOrEmpty(sourceMap, serverPath); - const manifest = sourceOrEmpty( - sourceMap, - `${vertical}/vertical.manifest.ts` - ); - const registration = sourceOrEmpty( - sourceMap, - `${vertical}/vertical.registration.ts` - ); - const gateway = sourceOrEmpty( - sourceMap, - `${vertical}/src/api/action-gateway.ts` - ); + const manifest = sourceOrEmpty(sourceMap, `${vertical}/vertical.manifest.ts`); + const registration = sourceOrEmpty(sourceMap, `${vertical}/vertical.registration.ts`); + const gateway = sourceOrEmpty(sourceMap, `${vertical}/src/api/action-gateway.ts`); const moduleId = providerModuleId(manifest); const type = toPascalCase(name); const camel = `${type.slice(0, 1).toLowerCase()}${type.slice(1)}`; const ownerApiValue = `${type}${kindDetails.apiSuffix}Api`; - const endpointGroup = - generatedApiGroup(contractSource, ownerApiValue) ?? ''; - const expectedGroups = new Set([ - kindDetails.endpointGroup, - `${camel}${kindDetails.apiSuffix}`, - ]); + const endpointGroup = generatedApiGroup(contractSource, ownerApiValue) ?? ''; + const expectedGroups = new Set([kindDetails.endpointGroup, `${camel}${kindDetails.apiSuffix}`]); const providerChecks = { - apiContract: hasGeneratedProviderApiContract( - contractSource, - ownerApiValue, - moduleId, - name, - kind - ), - authorization: hasMatchingGeneratedProviderAuthorization( - providerSource, - manifest, - moduleId, - name, - kind - ), + apiContract: hasGeneratedProviderApiContract(contractSource, ownerApiValue, moduleId, name, kind), + authorization: hasMatchingGeneratedProviderAuthorization(providerSource, manifest, moduleId, name, kind), clientContract: hasGeneratedGovernedClientContract(clientSource, { authorizedOperation: `load${type}ClientWithAuthorization`, defaultApiPrefix: `/${deploymentAppId}-api`, @@ -947,22 +743,14 @@ const validateGeneratedProviderClient = ( contractHeader: hasGeneratedSourceHeader(contractSource, generatedHeader), endpointGroup: expectedGroups.has(endpointGroup), manifest: hasGeneratedProviderManifest(manifest, moduleId, name, kind), - operationGateway: hasGeneratedOperationGatewayContract( - gateway, - deploymentAppId - ), + operationGateway: hasGeneratedOperationGatewayContract(gateway, deploymentAppId), providerHeader: hasGeneratedSourceHeader(providerSource, generatedHeader), - readContract: hasGeneratedProviderReadContract( - providerSource, - moduleId, - name, - kind - ), + readContract: hasGeneratedProviderReadContract(providerSource, moduleId, name, kind), registration: hasGeneratedProviderRegistration(registration, name, kind), serverContract: hasGeneratedGovernedServerContract( serverSource, `${camel}ReadApiLive`, - sourceOrEmpty(sourceMap, `${vertical}/shared/api.ts`) + sourceOrEmpty(sourceMap, `${vertical}/shared/api.ts`), ), serverHeader: hasGeneratedSourceHeader(serverSource, generatedHeader), }; @@ -972,7 +760,7 @@ const validateGeneratedProviderClient = ( if (failedChecks.length > 0) { yield* fail( file, - `generated search and report clients require the shared client runtime, owner-local contract, operation gateway, authorization, and correlation metadata (failed: ${failedChecks.join(', ')})` + `generated search and report clients require the shared client runtime, owner-local contract, operation gateway, authorization, and correlation metadata (failed: ${failedChecks.join(', ')})`, ); } }); @@ -980,61 +768,34 @@ const validateGeneratedProviderClient = ( const validatePublishedProviderIdentities = ( state: BoundaryCheckState, file: string, - verticalPath: string | undefined + verticalPath: string | undefined, ) => Effect.gen(function* validatePublishedProviderIdentitiesEffect() { if ( verticalPath === undefined || - (file !== `${verticalPath}/vertical.manifest.ts` && - file !== `${verticalPath}/vertical.registration.ts`) + (file !== `${verticalPath}/vertical.manifest.ts` && file !== `${verticalPath}/vertical.registration.ts`) ) { return; } - const manifest = sourceOrEmpty( - state.sourceMap, - `${verticalPath}/vertical.manifest.ts` - ); - const registration = sourceOrEmpty( - state.sourceMap, - `${verticalPath}/vertical.registration.ts` - ); + const manifest = sourceOrEmpty(state.sourceMap, `${verticalPath}/vertical.manifest.ts`); + const registration = sourceOrEmpty(state.sourceMap, `${verticalPath}/vertical.registration.ts`); const moduleId = providerModuleId(manifest); - const deploymentAppId = - state.owners.get(verticalPath) ?? file.split('/')[1] ?? ''; - if ( - !hasExactGeneratedProviderIdentityTopology( - manifest, - registration, - moduleId - ) - ) { + const deploymentAppId = state.owners.get(verticalPath) ?? file.split('/')[1] ?? ''; + if (!hasExactGeneratedProviderIdentityTopology(manifest, registration, moduleId)) { yield* fail( file, - 'generated search and report clients require the shared client runtime and exact manifest and registration identity topology' + 'generated search and report clients require the shared client runtime and exact manifest and registration identity topology', ); } - for (const provider of generatedProviderIdentities( - manifest, - registration, - moduleId - )) { - yield* validateGeneratedProviderClient( - state.sourceMap, - file, - deploymentAppId, - { - ...provider, - vertical: verticalPath, - } - ); + for (const provider of generatedProviderIdentities(manifest, registration, moduleId)) { + yield* validateGeneratedProviderClient(state.sourceMap, file, deploymentAppId, { + ...provider, + vertical: verticalPath, + }); } }); -const validateVerticalApiSource = ( - state: BoundaryCheckState, - file: string, - source: string -) => +const validateVerticalApiSource = (state: BoundaryCheckState, file: string, source: string) => Effect.gen(function* validateVerticalApiSourceEffect() { const { sourceMap } = state; const diagnostics: string[] = []; @@ -1046,25 +807,22 @@ const validateVerticalApiSource = ( sourceMap, file, state.owners.get(file.slice(0, -'/shared/api.ts'.length)), - diagnostics + diagnostics, ) ) { yield* fail( file, - `module APIs require an approved Codesmith generator, structured api registration, trusted context, and server ModuleEntrypointGateway integration first (failed: ${diagnostics.join('; ')})` + `module APIs require an approved Codesmith generator, structured api registration, trusted context, and server ModuleEntrypointGateway integration first (failed: ${diagnostics.join('; ')})`, ); } if ( /\/shared\/apis\/[^/]+\.ts$/u.test(`/${file}`) && - !hasGeneratedSourceHeader( - source, - '// @generated by OntOS Codesmith module-api v1\n' - ) && + !hasGeneratedSourceHeader(source, '// @generated by OntOS Codesmith module-api v1\n') && !['report', SEARCH_PROVIDER_KIND].some((kind) => hasGeneratedSourceHeader( source, - `// @generated by OntOS Codesmith Governed Contribution v1\n// @ontos-contribution-kind ${kind}\n` - ) + `// @generated by OntOS Codesmith Governed Contribution v1\n// @ontos-contribution-kind ${kind}\n`, + ), ) ) { yield* fail(file, 'module APIs must be created with scaffold:module-api'); @@ -1075,7 +833,7 @@ const validateVerticalSource = ( state: BoundaryCheckState, file: string, source: string, - importedModuleSpecifiers: readonly string[] + importedModuleSpecifiers: readonly string[], ) => Effect.gen(function* validateVerticalSourceEffect() { const { sourceMap } = state; @@ -1087,23 +845,17 @@ const validateVerticalSource = ( yield* validateGeneratedProviderClient( sourceMap, file, - (verticalPath === undefined - ? undefined - : state.owners.get(verticalPath)) ?? - file.split('/')[1] ?? - '' + (verticalPath === undefined ? undefined : state.owners.get(verticalPath)) ?? file.split('/')[1] ?? '', ); yield* validateVerticalApiSource(state, file, source); yield* validateGovernedSource(file, source); const privateImport = importedModuleSpecifiers.some((specifier) => - /(?:verticals\/|@app\/).*\/(?:src|vertical\.registration|workers|search|reports|db)(?:\/|$)/u.test( - specifier - ) + /(?:verticals\/|@app\/).*\/(?:src|vertical\.registration|workers|search|reports|db)(?:\/|$)/u.test(specifier), ); if (privateImport) { yield* fail( file, - 'cross-vertical private imports are forbidden; use a public descriptor/client and the Shell/Core gateway' + 'cross-vertical private imports are forbidden; use a public descriptor/client and the Shell/Core gateway', ); } }); @@ -1121,7 +873,7 @@ const validatePrivateHandlerAccess = (file: string, source: string) => if (unauthorizedActionHandler || unauthorizedWorkerHandler) { yield* fail( file, - 'private handler accessors may only be called by their Core runtime after the module-state gate' + 'private handler accessors may only be called by their Core runtime after the module-state gate', ); } }); @@ -1130,17 +882,11 @@ const validatePackageExports = (file: string, source: string) => Effect.gen(function* validatePackageExportsEffect() { if (file.startsWith('verticals/') && file.endsWith('package.json')) { const packageJson = yield* decodeVerticalPackageJson(source); - const privateExport = Object.values(packageJson.exports ?? {}).some( - (target) => - /(?:vertical\.registration|\/src\/(?:handlers|workers|routes|search|reports|db))/u.test( - target - ) + const privateExport = Object.values(packageJson.exports ?? {}).some((target) => + /(?:vertical\.registration|\/src\/(?:handlers|workers|routes|search|reports|db))/u.test(target), ); if (privateExport) { - yield* fail( - file, - 'package exports must not publish private entrypoint implementations or registrations' - ); + yield* fail(file, 'package exports must not publish private entrypoint implementations or registrations'); } } }); @@ -1153,13 +899,10 @@ const validateRegistrationSlots = (file: string, source: string) => 'generated-search-registrations', 'generated-report-registrations', ]) { - if ( - !source.includes(`<${marker}>`) || - !source.includes(``) - ) { + if (!source.includes(`<${marker}>`) || !source.includes(``)) { yield* fail( file, - `Vertical Runtime Registration is missing reserved ${marker} slots; extend Codesmith first` + `Vertical Runtime Registration is missing reserved ${marker} slots; extend Codesmith first`, ); } } @@ -1179,36 +922,19 @@ const validateCoreExports = (file: string, source: string) => if (containsIdentifier(source, forbiddenGateExports)) { yield* fail( file, - 'Core may export only module-entrypoint descriptors, service/gateway contracts and live layers, and typed gate failures' + 'Core may export only module-entrypoint descriptors, service/gateway contracts and live layers, and typed gate failures', ); } } }); -const validateGeneralSource = ( - file: string, - source: string, - importedModuleSpecifiers: readonly string[] -) => +const validateGeneralSource = (file: string, source: string, importedModuleSpecifiers: readonly string[]) => Effect.gen(function* validateGeneralSourceEffect() { - if ( - callsIdentifier(source, 'loadRemote') && - file !== APPROVED_REMOTE_LOADER - ) { - yield* fail( - file, - 'raw loadRemote(...) is forbidden outside the approved Shell module-entrypoint loader' - ); + if (callsIdentifier(source, 'loadRemote') && file !== APPROVED_REMOTE_LOADER) { + yield* fail(file, 'raw loadRemote(...) is forbidden outside the approved Shell module-entrypoint loader'); } - if ( - importedModuleSpecifiers.some((specifier) => - /\/(?:remote|exposes)\//u.test(specifier) - ) - ) { - yield* fail( - file, - 'eager remote implementation imports are forbidden; pass a lazy thunk to the gateway' - ); + if (importedModuleSpecifiers.some((specifier) => /\/(?:remote|exposes)\//u.test(specifier))) { + yield* fail(file, 'eager remote implementation imports are forbidden; pass a lazy thunk to the gateway'); } yield* validatePrivateHandlerAccess(file, source); yield* validatePackageExports(file, source); @@ -1224,45 +950,22 @@ const isInventoryDescriptorSource = (file: string): boolean => file === 'apps/shell-super-app/api/modules/shell-governed-reads.ts' || file === 'packages/core-runtime/src/auth/principal-administration-reads.ts'; -const appendInventoryEntries = ( - state: BoundaryCheckState, - file: string, - source: string -) => +const appendInventoryEntries = (state: BoundaryCheckState, file: string, source: string) => Effect.gen(function* appendInventoryEntriesEffect() { if (!isInventoryDescriptorSource(file)) { return; } const deployment = - [...state.owners.entries()].find(([appPath]) => - file.startsWith(`${appPath}/`) - )?.[1] ?? 'shell-super-app'; + [...state.owners.entries()].find(([appPath]) => file.startsWith(`${appPath}/`))?.[1] ?? 'shell-super-app'; const entrypoints = file.endsWith(ACTION_EXTENSION) ? readActionEntrypoints(state.sourceMap, file, source) : readEntrypoints(source); for (const entrypoint of entrypoints) { - const descriptorMessage = - 'every runtime entrypoint must declare exactly one valid authorization'; - const authorization = yield* requireDefined( - entrypoint.authorization, - file, - descriptorMessage - ); - const entrypointKey = yield* requireDefined( - entrypoint.entrypointKey, - file, - descriptorMessage - ); - const owner = yield* requireDefined( - entrypoint.moduleKey, - file, - descriptorMessage - ); - const role = yield* requireDefined( - entrypoint.role, - file, - descriptorMessage - ); + const descriptorMessage = 'every runtime entrypoint must declare exactly one valid authorization'; + const authorization = yield* requireDefined(entrypoint.authorization, file, descriptorMessage); + const entrypointKey = yield* requireDefined(entrypoint.entrypointKey, file, descriptorMessage); + const owner = yield* requireDefined(entrypoint.moduleKey, file, descriptorMessage); + const role = yield* requireDefined(entrypoint.role, file, descriptorMessage); state.inventoryEntries.push({ authorization, deployment, @@ -1273,11 +976,7 @@ const appendInventoryEntries = ( } }); -const validateProductionSource = ( - state: BoundaryCheckState, - file: string, - source: string -) => +const validateProductionSource = (state: BoundaryCheckState, file: string, source: string) => Effect.gen(function* validateProductionSourceEffect() { if (file.endsWith(ACTION_EXTENSION)) { yield* requireGeneratedActionEntrypoint(state.sourceMap, file, source); @@ -1287,12 +986,7 @@ const validateProductionSource = ( } yield* validateRouteSource(state, file, source); const importedModuleSpecifiers = readImportedModuleSpecifiers(source); - yield* validateVerticalSource( - state, - file, - source, - importedModuleSpecifiers - ); + yield* validateVerticalSource(state, file, source, importedModuleSpecifiers); yield* validateGeneralSource(file, source, importedModuleSpecifiers); yield* appendInventoryEntries(state, file, source); }); @@ -1301,13 +995,9 @@ const collectRouteSourceKeys = (state: BoundaryCheckState) => Effect.gen(function* collectRouteSourceKeysEffect() { const routeSourceKeysByDeployment = new Map>(); for (const route of state.routeEntrypoints) { - const sourceKeys = - routeSourceKeysByDeployment.get(route.deployment) ?? new Set(); + const sourceKeys = routeSourceKeysByDeployment.get(route.deployment) ?? new Set(); if (sourceKeys.has(route.entrypointKey)) { - yield* fail( - route.file, - `route entrypoint ${route.entrypointKey} is duplicated` - ); + yield* fail(route.file, `route entrypoint ${route.entrypointKey} is duplicated`); } sourceKeys.add(route.entrypointKey); routeSourceKeysByDeployment.set(route.deployment, sourceKeys); @@ -1315,73 +1005,48 @@ const collectRouteSourceKeys = (state: BoundaryCheckState) => return routeSourceKeysByDeployment; }); -const validateManifestKeys = ( - state: BoundaryCheckState, - normalizedFile: string, - sourceKeys: ReadonlySet -) => +const validateManifestKeys = (state: BoundaryCheckState, normalizedFile: string, sourceKeys: ReadonlySet) => Effect.gen(function* validateManifestKeysEffect() { const manifestSource = sourceOrEmpty(state.sourceMap, normalizedFile); const manifestKeys = readStringProperties(manifestSource, 'entrypointKey'); - const missing = [...sourceKeys].filter( - (entrypointKey) => !manifestKeys.has(entrypointKey) - ); - const stale = [...manifestKeys].filter( - (entrypointKey) => !sourceKeys.has(entrypointKey) - ); + const missing = [...sourceKeys].filter((entrypointKey) => !manifestKeys.has(entrypointKey)); + const stale = [...manifestKeys].filter((entrypointKey) => !sourceKeys.has(entrypointKey)); if (missing.length > 0 || stale.length > 0) { - const missingLabel = - missing.length === 0 ? 'none' : sortStrings(missing).join(', '); - const staleLabel = - stale.length === 0 ? 'none' : sortStrings(stale).join(', '); + const missingLabel = missing.length === 0 ? 'none' : sortStrings(missing).join(', '); + const staleLabel = stale.length === 0 ? 'none' : sortStrings(stale).join(', '); yield* fail( normalizedFile, - `generated route manifest is stale (missing: ${missingLabel}; orphaned: ${staleLabel}); rerun the route generator` + `generated route manifest is stale (missing: ${missingLabel}; orphaned: ${staleLabel}); rerun the route generator`, ); } }); -const validateRouteManifests = ( - path: Path.Path, - files: readonly string[], - root: string, - state: BoundaryCheckState -) => +const validateRouteManifests = (path: Path.Path, files: readonly string[], root: string, state: BoundaryCheckState) => Effect.gen(function* validateRouteManifestsEffect() { const routeSourceKeysByDeployment = yield* collectRouteSourceKeys(state); - const routeManifests = files.filter((file) => - file.endsWith('/ultramodern-route-metadata.ts') - ); + const routeManifests = files.filter((file) => file.endsWith('/ultramodern-route-metadata.ts')); const seenManifestDeployments = new Set(); for (const manifestFile of routeManifests) { const normalizedFile = relative(path, root, manifestFile); - const ownerEntry = [...state.owners.entries()].find(([appPath]) => - normalizedFile.startsWith(`${appPath}/`) - ); + const ownerEntry = [...state.owners.entries()].find(([appPath]) => normalizedFile.startsWith(`${appPath}/`)); const [, deployment] = yield* requireDefined( ownerEntry, normalizedFile, - 'generated route manifest owner is absent from topology' + 'generated route manifest owner is absent from topology', ); if (seenManifestDeployments.has(deployment)) { - yield* fail( - normalizedFile, - `deployment ${deployment} has multiple generated route manifests` - ); + yield* fail(normalizedFile, `deployment ${deployment} has multiple generated route manifests`); } seenManifestDeployments.add(deployment); yield* validateManifestKeys( state, normalizedFile, - routeSourceKeysByDeployment.get(deployment) ?? new Set() + routeSourceKeysByDeployment.get(deployment) ?? new Set(), ); } for (const deployment of routeSourceKeysByDeployment.keys()) { if (!seenManifestDeployments.has(deployment)) { - yield* fail( - 'generated route manifests', - `deployment ${deployment} is missing its route manifest` - ); + yield* fail('generated route manifests', `deployment ${deployment} is missing its route manifest`); } } }); @@ -1389,9 +1054,7 @@ const validateRouteManifests = ( const validateIssuerCredentials = () => Effect.gen(function* validateIssuerCredentialsEffect() { const issuerCredentials = new Set( - gatewayContextAuthorizationEntrypoints.map( - ({ authorization }) => authorization.credential - ) + gatewayContextAuthorizationEntrypoints.map(({ authorization }) => authorization.credential), ); if ( gatewayContextAuthorizationEntrypoints.length !== 2 || @@ -1400,49 +1063,35 @@ const validateIssuerCredentials = () => ) { yield* fail( 'packages/shared-contracts/src/gateway-context.ts', - 'gateway authorization contract must classify exactly the session and API-key issuers' + 'gateway authorization contract must classify exactly the session and API-key issuers', ); } }); const hasIssuerHandler = (source: string, name: string): boolean => - source.includes(`.handle('${name}'`) || - containsIdentifier(source, new Set([name])); + source.includes(`.handle('${name}'`) || containsIdentifier(source, new Set([name])); -const validateGatewayRuntime = ( - shellApiContract: string, - shellApiRuntime: string -) => +const validateGatewayRuntime = (shellApiContract: string, shellApiRuntime: string) => Effect.gen(function* validateGatewayRuntimeEffect() { - const missingApiKeyHandler = !hasIssuerHandler( - shellApiRuntime, - 'issueApiKeyGatewayContext' - ); - const missingSessionHandler = !hasIssuerHandler( - shellApiRuntime, - 'issueGatewayContext' - ); + const missingApiKeyHandler = !hasIssuerHandler(shellApiRuntime, 'issueApiKeyGatewayContext'); + const missingSessionHandler = !hasIssuerHandler(shellApiRuntime, 'issueGatewayContext'); if ( !shellApiContract.includes('.add(GatewayContextApiGroup)') || - shellGatewayContextContract.issueGatewayContextPath !== - gatewayContextAuthorizationEntrypoints[0]?.path || - shellGatewayContextContract.issueApiKeyGatewayContextPath !== - gatewayContextAuthorizationEntrypoints[1]?.path || + shellGatewayContextContract.issueGatewayContextPath !== gatewayContextAuthorizationEntrypoints[0]?.path || + shellGatewayContextContract.issueApiKeyGatewayContextPath !== gatewayContextAuthorizationEntrypoints[1]?.path || missingApiKeyHandler || missingSessionHandler ) { yield* fail( 'apps/shell-super-app/api/index.ts', - 'both classified capability issuers must be mounted by the Shell API runtime' + 'both classified capability issuers must be mounted by the Shell API runtime', ); } }); const gatewayDeclaration = (source: string, name: string): string => { const structure = maskNonCode(source); - const declaration = new RegExp(`export\\s+const\\s+${name}\\s*=`, 'u').exec( - structure - ); + const declaration = new RegExp(`export\\s+const\\s+${name}\\s*=`, 'u').exec(structure); if (declaration === null) { return ''; } @@ -1454,45 +1103,26 @@ const gatewayDeclaration = (source: string, name: string): string => { const hasMountedIssuerPath = ( source: string, gatewaySource: string, - issuer: (typeof gatewayContextAuthorizationEntrypoints)[number] + issuer: (typeof gatewayContextAuthorizationEntrypoints)[number], ): boolean => { - const name = - issuer.authorization.credential === 'session' - ? 'issueGatewayContext' - : 'issueApiKeyGatewayContext'; - const endpointPath = issuer.path.slice( - shellGatewayContextContract.apiPrefix.length - ); + const name = issuer.authorization.credential === 'session' ? 'issueGatewayContext' : 'issueApiKeyGatewayContext'; + const endpointPath = issuer.path.slice(shellGatewayContextContract.apiPrefix.length); const group = gatewayDeclaration(gatewaySource, 'GatewayContextApiGroup'); const shellApi = gatewayDeclaration(source, 'ShellAuthenticationApi'); return ( - maskNonCode(source, true).includes( - "import { GatewayContextApiGroup } from '@app/shared-contracts'" - ) && + maskNonCode(source, true).includes("import { GatewayContextApiGroup } from '@app/shared-contracts'") && shellApi.startsWith('HttpApi.make(') && /\.add\(\s*GatewayContextApiGroup\s*\)/u.test(shellApi) && group.startsWith("HttpApiGroup.make('gatewayContext')") && - new RegExp( - `\\.add\\(\\s*HttpApiEndpoint\\.post\\(\\s*'${name}'\\s*,\\s*'${endpointPath}'\\s*,`, - 'u' - ).test(group) + new RegExp(`\\.add\\(\\s*HttpApiEndpoint\\.post\\(\\s*'${name}'\\s*,\\s*'${endpointPath}'\\s*,`, 'u').test(group) ); }; const validateGatewayContract = (state: BoundaryCheckState) => Effect.gen(function* validateGatewayContractEffect() { - const gatewayContract = sourceOrEmpty( - state.sourceMap, - 'packages/shared-contracts/src/gateway-context.ts' - ); - const shellApiContract = sourceOrEmpty( - state.sourceMap, - 'apps/shell-super-app/shared/api.ts' - ); - const shellApiRuntime = sourceOrEmpty( - state.sourceMap, - 'apps/shell-super-app/api/index.ts' - ); + const gatewayContract = sourceOrEmpty(state.sourceMap, 'packages/shared-contracts/src/gateway-context.ts'); + const shellApiContract = sourceOrEmpty(state.sourceMap, 'apps/shell-super-app/shared/api.ts'); + const shellApiRuntime = sourceOrEmpty(state.sourceMap, 'apps/shell-super-app/api/index.ts'); yield* validateIssuerCredentials(); for (const issuer of gatewayContextAuthorizationEntrypoints) { if ( @@ -1501,7 +1131,7 @@ const validateGatewayContract = (state: BoundaryCheckState) => ) { yield* fail( 'apps/shell-super-app/shared/api.ts', - `capability issuer ${issuer.path} is missing from the mounted gateway contract` + `capability issuer ${issuer.path} is missing from the mounted gateway contract`, ); } state.inventoryEntries.push(issuer); @@ -1532,11 +1162,8 @@ const checkModuleEntrypointBoundariesEffect = (root: string) => sourceMap, }; for (const [file, source] of sourcePairs) { - const supportedFile = - SOURCE_EXTENSIONS.has(path.extname(file)) || - file.endsWith('package.json'); - const productionFile = - !file.includes('/tests/') && !file.includes('/fixtures/'); + const supportedFile = SOURCE_EXTENSIONS.has(path.extname(file)) || file.endsWith('package.json'); + const productionFile = !file.includes('/tests/') && !file.includes('/fixtures/'); if (supportedFile && productionFile) { yield* validateProductionSource(state, file, source); } @@ -1548,14 +1175,11 @@ const checkModuleEntrypointBoundariesEffect = (root: string) => Effect.mapError( () => new ModuleEntrypointBoundaryError({ - message: - 'protected entrypoint inventory: sourceRevision must be a stable revision identifier', - }) - ) - ); - const entries = normalizeProtectedEntrypointInventory( - state.inventoryEntries + message: 'protected entrypoint inventory: sourceRevision must be a stable revision identifier', + }), + ), ); + const entries = normalizeProtectedEntrypointInventory(state.inventoryEntries); const inventory = { entries, inventoryHash: hashProtectedEntrypointInventory(entries), @@ -1566,27 +1190,24 @@ const checkModuleEntrypointBoundariesEffect = (root: string) => yield* fileSystem.makeDirectory(reportDirectory, { recursive: true }); yield* fileSystem.writeFileString( path.join(reportDirectory, 'protected-entrypoints.json'), - serializeProtectedEntrypointInventory(inventory) + serializeProtectedEntrypointInventory(inventory), ); }); -export const checkModuleEntrypointBoundaries = - checkModuleEntrypointBoundariesEffect; +export const checkModuleEntrypointBoundaries = checkModuleEntrypointBoundariesEffect; const [, invokedPath] = process.argv; if (invokedPath !== undefined && invokedPath === import.meta.filename) { const main = Effect.gen(function* moduleEntrypointBoundaryMain() { const path = yield* Path.Path; const root = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( - Config.withDefault(path.resolve(import.meta.dirname, '..')) + Config.withDefault(path.resolve(import.meta.dirname, '..')), ); yield* checkModuleEntrypointBoundariesEffect(root); yield* Console.log('Module entrypoint boundaries are valid.'); }); - const runnable = Effect.scoped( - Layer.build( - Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer)) - ) - ).pipe(Effect.asVoid); + const runnable = Effect.scoped(Layer.build(Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer)))).pipe( + Effect.asVoid, + ); NodeRuntime.runMain(runnable); } diff --git a/app/scripts/check-ontos-module-contracts.mts b/app/scripts/check-ontos-module-contracts.mts index efa790f97..6c493043a 100644 --- a/app/scripts/check-ontos-module-contracts.mts +++ b/app/scripts/check-ontos-module-contracts.mts @@ -67,7 +67,7 @@ const TopologySchema = Schema.Struct({ Schema.Struct({ id: OntosDeploymentAppIdSchema, path: Schema.String, - }) + }), ), }); @@ -88,28 +88,16 @@ const ModulePackageSchema = Schema.Struct({ export class OntosModuleContractCheckError extends Schema.TaggedError()( 'OntosModuleContractCheckError', - { reason: Schema.String } + { reason: Schema.String }, ) {} -const failure = (reason: string): OntosModuleContractCheckError => - new OntosModuleContractCheckError({ reason }); +const failure = (reason: string): OntosModuleContractCheckError => new OntosModuleContractCheckError({ reason }); -const sourceExtensions = new Set([ - '.cjs', - '.cts', - '.js', - '.jsx', - '.mjs', - '.mts', - '.ts', - '.tsx', -]); +const sourceExtensions = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']); type TopologyVertical = (typeof TopologySchema.Type.verticals)[number]; -const walkSourceFiles = ( - directory: string -): Effect.Effect => +const walkSourceFiles = (directory: string): Effect.Effect => Effect.gen(function* walkSourceDirectory() { const fileSystem = yield* FileSystem.FileSystem; if (!(yield* fileSystem.exists(directory))) { @@ -131,54 +119,38 @@ const walkSourceFiles = ( return [target]; } return []; - }) - ) + }), + ), ); return files.flat(); }); -const checkSharedSourceFile = ( - fileSystem: FileSystem.FileSystem, - filePath: string -) => +const checkSharedSourceFile = (fileSystem: FileSystem.FileSystem, filePath: string) => Effect.gen(function* checkSharedSource() { const content = yield* fileSystem.readFileString(filePath); if ( /(?:from\s+|import\s*\(|require\s*\()\s*['"][^'"]*(?:vertical\.manifest|vertical\.registration)(?:\.ts)?['"]/u.test( - content + content, ) ) { - yield* failure( - `${filePath} imports a private deployment manifest or registration` - ); + yield* failure(`${filePath} imports a private deployment manifest or registration`); } if ( /(?:from\s+|import\s*\(|require\s*\()\s*['"][^'"]*(?:verticals\/|@app\/(?!core-runtime(?:\/|['"])))[^'"]*\/(?:src|routes|providers|handlers|repositories|db|vertical\.registration)(?:\/|['"])/u.test( - content + content, ) ) { yield* failure(`${filePath} imports production vertical private source`); } }); -const checkOwnerSourceFile = ( - fileSystem: FileSystem.FileSystem, - ownerRoot: string, - filePath: string -) => +const checkOwnerSourceFile = (fileSystem: FileSystem.FileSystem, ownerRoot: string, filePath: string) => Effect.gen(function* checkOwnerSource() { const content = yield* fileSystem.readFileString(filePath); - const imports = [ - ...content.matchAll( - /(?:from\s+|import\s*\(|require\s*\()\s*['"](?[^'"]+)['"]/gu - ), - ]; + const imports = [...content.matchAll(/(?:from\s+|import\s*\(|require\s*\()\s*['"](?[^'"]+)['"]/gu)]; const hasPrivateOwnerImportViolation = imports.some((match) => { const specifier = match.groups?.specifier; - if ( - specifier === undefined || - !/vertical\.(?:manifest|registration)(?:\.ts)?$/u.test(specifier) - ) { + if (specifier === undefined || !/vertical\.(?:manifest|registration)(?:\.ts)?$/u.test(specifier)) { return false; } if (!specifier.startsWith('.')) { @@ -188,30 +160,18 @@ const checkOwnerSourceFile = ( return !resolved.startsWith(`${ownerRoot}${path.sep}`); }); if (hasPrivateOwnerImportViolation) { - yield* failure( - `${filePath} imports another deployment's private owner file` - ); + yield* failure(`${filePath} imports another deployment's private owner file`); } }); -const checkOwnerDirectory = ( - fileSystem: FileSystem.FileSystem, - ownerRoot: string -) => +const checkOwnerDirectory = (fileSystem: FileSystem.FileSystem, ownerRoot: string) => walkSourceFiles(ownerRoot).pipe( Effect.flatMap((ownerFiles) => - Effect.all( - ownerFiles.map((filePath) => - checkOwnerSourceFile(fileSystem, ownerRoot, filePath) - ) - ) - ) + Effect.all(ownerFiles.map((filePath) => checkOwnerSourceFile(fileSystem, ownerRoot, filePath))), + ), ); -const assertNoPrivateDeploymentImports = ( - workspaceRoot: string, - verticals: readonly TopologyVertical[] -) => +const assertNoPrivateDeploymentImports = (workspaceRoot: string, verticals: readonly TopologyVertical[]) => Effect.gen(function* checkPrivateDeploymentImports() { const fileSystem = yield* FileSystem.FileSystem; const sharedRoots = [ @@ -219,117 +179,78 @@ const assertNoPrivateDeploymentImports = ( path.join(workspaceRoot, 'packages/core-runtime'), ]; const sharedFiles = yield* Effect.all(sharedRoots.map(walkSourceFiles)); - yield* Effect.all( - sharedFiles - .flat() - .map((filePath) => checkSharedSourceFile(fileSystem, filePath)) - ); + yield* Effect.all(sharedFiles.flat().map((filePath) => checkSharedSourceFile(fileSystem, filePath))); yield* Effect.all( verticals.map((vertical) => { const ownerRoot = path.join(workspaceRoot, vertical.path); return checkOwnerDirectory(fileSystem, ownerRoot); - }) + }), ); }); const occursExactlyOnce = (content: string, marker: string): boolean => - content.includes(marker) && - content.indexOf(marker) === content.lastIndexOf(marker); + content.includes(marker) && content.indexOf(marker) === content.lastIndexOf(marker); const validateOwner = (filePath: string, markers: readonly string[]) => Effect.gen(function* validateGeneratedOwner() { const fileSystem = yield* FileSystem.FileSystem; const content = yield* fileSystem.readFileString(filePath); if (!content.startsWith(`${MODULE_CONTRACT_GENERATOR_HEADER}\n`)) { - return yield* failure( - `${filePath} is not a generated module-contract owner` - ); + return yield* failure(`${filePath} is not a generated module-contract owner`); } - const invalidMarker = markers.find( - (marker) => !occursExactlyOnce(content, marker) - ); + const invalidMarker = markers.find((marker) => !occursExactlyOnce(content, marker)); if (invalidMarker !== undefined) { - return yield* failure( - `${filePath} must contain exactly one ${invalidMarker}` - ); + return yield* failure(`${filePath} must contain exactly one ${invalidMarker}`); } - const moduleId = /^\/\/ @ontos-module-id (?[^\s]+)$/mu.exec( - content - )?.groups?.moduleId; + const moduleId = /^\/\/ @ontos-module-id (?[^\s]+)$/mu.exec(content)?.groups?.moduleId; if (moduleId === undefined) { - return yield* failure( - `${filePath} is missing its generated module ID marker` - ); + return yield* failure(`${filePath} is missing its generated module ID marker`); } return moduleId; }); -const validateEmittedContract = ( - verticalDirectory: string, - target: string, - expected: OntosModuleDeploymentContract -) => +const validateEmittedContract = (verticalDirectory: string, target: string, expected: OntosModuleDeploymentContract) => Effect.gen(function* validateGeneratedContract() { const fileSystem = yield* FileSystem.FileSystem; const publicDirectory = path.join(verticalDirectory, target, 'public'); if (!(yield* fileSystem.exists(publicDirectory))) { return; } - const contractPath = path.join( - publicDirectory, - ONTOS_MODULE_CONTRACT_PATH.slice(1) - ); + const contractPath = path.join(publicDirectory, ONTOS_MODULE_CONTRACT_PATH.slice(1)); if (!(yield* fileSystem.exists(contractPath))) { - yield* failure( - `${target} output is missing ${ONTOS_MODULE_CONTRACT_PATH}` - ); + yield* failure(`${target} output is missing ${ONTOS_MODULE_CONTRACT_PATH}`); } const content = yield* fileSystem.readFile(contractPath); if (content.byteLength > ONTOS_MODULE_CONTRACT_MAX_BYTES) { yield* failure(`${contractPath} exceeds the 1 MiB contract limit`); } const serialized = new TextDecoder().decode(content); - const contract = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(OntosModuleDeploymentContractSchema), - { onExcessProperty: 'error' } - )(serialized); + const contract = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OntosModuleDeploymentContractSchema), { + onExcessProperty: 'error', + })(serialized); if ( contract.deployment.appId !== expected.deployment.appId || contract.manifest.module.id !== expected.manifest.module.id ) { - yield* failure( - `${contractPath} identity does not match its generated owner metadata` - ); + yield* failure(`${contractPath} identity does not match its generated owner metadata`); } if (!Equal.equals(contract, expected)) { - yield* failure( - `${contractPath} is stale relative to its authored module contract` - ); + yield* failure(`${contractPath} is stale relative to its authored module contract`); } - if ( - /sourcePath|importPath|exportPath|registrationPath|handlerPath|migrationPath/u.test( - serialized - ) - ) { - yield* failure( - `${contractPath} contains forbidden private path metadata` - ); + if (/sourcePath|importPath|exportPath|registrationPath|handlerPath|migrationPath/u.test(serialized)) { + yield* failure(`${contractPath} contains forbidden private path metadata`); } - const validateResponseHeaders = Effect.gen( - function* validateResponseHeadersEffect() { - const headersPath = path.join(publicDirectory, '_headers'); - const headers = yield* fileSystem.readFileString(headersPath); - if ( - !headers.includes('Cache-Control: no-cache') || - !headers.includes('Content-Type: application/json') || - !/^ {2}ETag: "[a-f0-9]{64}"$/mu.test(headers) - ) { - yield* failure( - `${headersPath} is missing the immutable module-contract response headers` - ); - } + const validateResponseHeaders = Effect.gen(function* validateResponseHeadersEffect() { + const headersPath = path.join(publicDirectory, '_headers'); + const headers = yield* fileSystem.readFileString(headersPath); + if ( + !headers.includes('Cache-Control: no-cache') || + !headers.includes('Content-Type: application/json') || + !/^ {2}ETag: "[a-f0-9]{64}"$/mu.test(headers) + ) { + yield* failure(`${headersPath} is missing the immutable module-contract response headers`); } - ); + }); yield* validateResponseHeaders; }); @@ -377,28 +298,18 @@ const registrationMarkers = [ MODULE_REGISTRATION_WORKER_SLOT_END, ] as const; -const checkVertical = ( - workspaceRoot: string, - vertical: TopologyVertical, - contractUrl: string -) => +const checkVertical = (workspaceRoot: string, vertical: TopologyVertical, contractUrl: string) => Effect.gen(function* checkVerticalContract() { const fileSystem = yield* FileSystem.FileSystem; const appId = vertical.id; const relativePath = vertical.path; const verticalDirectory = path.join(workspaceRoot, relativePath); - const packageSource = yield* fileSystem.readFileString( - path.join(verticalDirectory, 'package.json') - ); - const packageJson = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(ModulePackageSchema), - { onExcessProperty: 'preserve' } - )(packageSource); + const packageSource = yield* fileSystem.readFileString(path.join(verticalDirectory, 'package.json')); + const packageJson = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ModulePackageSchema), { + onExcessProperty: 'preserve', + })(packageSource); const manifestPath = path.join(verticalDirectory, 'vertical.manifest.ts'); - const registrationPath = path.join( - verticalDirectory, - 'vertical.registration.ts' - ); + const registrationPath = path.join(verticalDirectory, 'vertical.registration.ts'); const [manifestModuleId, registrationModuleId] = yield* Effect.all([ validateOwner(manifestPath, manifestMarkers), validateOwner(registrationPath, registrationMarkers), @@ -407,46 +318,26 @@ const checkVertical = ( packageJson.modernjs.appId !== appId || packageJson.modernjs.ontosModule.moduleId !== manifestModuleId || registrationModuleId !== manifestModuleId || - packageJson.modernjs.ontosModule.schemaVersion !== - ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION + packageJson.modernjs.ontosModule.schemaVersion !== ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION ) { - return yield* failure( - `${appId} package, manifest, registration, and topology identities disagree` - ); + return yield* failure(`${appId} package, manifest, registration, and topology identities disagree`); } const verticalName = path.basename(relativePath); if ( - !packageJson.scripts.build?.includes( - `--vertical ${verticalName} --target dist` - ) || - !packageJson.scripts['cloudflare:build']?.includes( - `--vertical ${verticalName} --target cloudflare-dist` - ) + !packageJson.scripts.build?.includes(`--vertical ${verticalName} --target dist`) || + !packageJson.scripts['cloudflare:build']?.includes(`--vertical ${verticalName} --target cloudflare-dist`) ) { - return yield* failure( - `${appId} build scripts do not emit both module-contract deployment targets` - ); + return yield* failure(`${appId} build scripts do not emit both module-contract deployment targets`); } if (!contractUrl.endsWith(ONTOS_MODULE_CONTRACT_PATH)) { - return yield* failure( - `${appId} development module-contract URL is invalid` - ); + return yield* failure(`${appId} development module-contract URL is invalid`); } const derived = yield* deriveOntosModuleDeploymentContract({ vertical: verticalName, workspaceRoot, - }).pipe( - Effect.mapError(() => - failure(`${appId} authored module contract could not be derived`) - ) - ); - if ( - derived.deployment.appId !== appId || - derived.manifest.module.id !== manifestModuleId - ) { - return yield* failure( - `${appId} authored module contract disagrees with generated owner metadata` - ); + }).pipe(Effect.mapError(() => failure(`${appId} authored module contract could not be derived`))); + if (derived.deployment.appId !== appId || derived.manifest.module.id !== manifestModuleId) { + return yield* failure(`${appId} authored module contract disagrees with generated owner metadata`); } yield* Effect.all([ validateEmittedContract(verticalDirectory, 'dist', derived), @@ -462,49 +353,33 @@ const checkOntosModuleContractsEffect = (workspaceRoot: string) => Effect.gen(function* checkWorkspaceContracts() { const fileSystem = yield* FileSystem.FileSystem; const topologySource = yield* fileSystem.readFileString( - path.join(workspaceRoot, 'topology/reference-topology.json') + path.join(workspaceRoot, 'topology/reference-topology.json'), ); - const topology = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(TopologySchema), - { - onExcessProperty: 'preserve', - } - )(topologySource); + const topology = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(TopologySchema), { + onExcessProperty: 'preserve', + })(topologySource); yield* assertNoPrivateDeploymentImports(workspaceRoot, topology.verticals); const overlaySource = yield* fileSystem.readFileString( - path.join(workspaceRoot, 'topology/local-overlays/development.json') + path.join(workspaceRoot, 'topology/local-overlays/development.json'), ); - const overlay = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(DevelopmentOverlaySchema), - { onExcessProperty: 'preserve' } - )(overlaySource); + const overlay = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(DevelopmentOverlaySchema), { + onExcessProperty: 'preserve', + })(overlaySource); const appIds = topology.verticals.map((vertical) => vertical.id); const allowlistKeys = Object.keys(overlay.ontosModuleManifests); const keysMatch = allowlistKeys.length === appIds.length && - appIds.every((appId) => - Object.hasOwn(overlay.ontosModuleManifests, appId) - ); + appIds.every((appId) => Object.hasOwn(overlay.ontosModuleManifests, appId)); if (!keysMatch) { - yield* failure( - 'development ontosModuleManifests keys must exactly match topology verticals' - ); + yield* failure('development ontosModuleManifests keys must exactly match topology verticals'); } const contracts = yield* Effect.all( topology.verticals.map((vertical) => - checkVertical( - workspaceRoot, - vertical, - overlay.ontosModuleManifests[vertical.id] - ) - ) - ); - const moduleIds = contracts.map( - (entry) => entry.contract.manifest.module.id - ); - const duplicateModuleId = moduleIds.find( - (moduleId, index) => moduleIds.indexOf(moduleId) !== index + checkVertical(workspaceRoot, vertical, overlay.ontosModuleManifests[vertical.id]), + ), ); + const moduleIds = contracts.map((entry) => entry.contract.manifest.module.id); + const duplicateModuleId = moduleIds.find((moduleId, index) => moduleIds.indexOf(moduleId) !== index); if (duplicateModuleId !== undefined) { yield* failure(`duplicate OntOS module ID ${duplicateModuleId}`); } @@ -514,14 +389,9 @@ const checkOntosModuleContractsEffect = (workspaceRoot: string) => export const checkOntosModuleContracts = (workspaceRoot = process.cwd()) => checkOntosModuleContractsEffect(workspaceRoot); -if ( - process.argv[1] !== undefined && - import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href -) { +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { const programLayer = Layer.effectDiscard( - checkOntosModuleContracts().pipe( - Effect.tap(() => Effect.logInfo('OntOS module contracts validated')) - ) + checkOntosModuleContracts().pipe(Effect.tap(() => Effect.logInfo('OntOS module contracts validated'))), ).pipe(Layer.provide(NodeServices.layer)); NodeRuntime.runMain(Effect.scoped(Layer.build(programLayer))); } diff --git a/app/scripts/check-ultramodern-api-boundaries.mts b/app/scripts/check-ultramodern-api-boundaries.mts index 0a2d7474f..6c2d59301 100644 --- a/app/scripts/check-ultramodern-api-boundaries.mts +++ b/app/scripts/check-ultramodern-api-boundaries.mts @@ -1,14 +1,6 @@ #!/usr/bin/env node import { NodeFileSystem, NodePath, NodeRuntime } from '@effect/platform-node'; -import { - Config, - Console, - Effect, - FileSystem, - Layer, - Path, - Schema, -} from 'effect'; +import { Config, Console, Effect, FileSystem, Layer, Path, Schema } from 'effect'; import type { PlatformError } from 'effect/PlatformError'; import { hasCompleteGeneratedModuleApiSeam } from './generated-governed-http-boundary.mts'; @@ -23,10 +15,9 @@ import { unconstrainedHttpApiContractSchemaViolation, } from './ultramodern-api-boundary-rules.mts'; -class ApiBoundaryCheckFailed extends Schema.TaggedError()( - 'ApiBoundaryCheckFailed', - { failureCount: Schema.Int } -) {} +class ApiBoundaryCheckFailed extends Schema.TaggedError()('ApiBoundaryCheckFailed', { + failureCount: Schema.Int, +}) {} const PackageJsonSchema = Schema.Struct({ exports: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), @@ -44,35 +35,25 @@ const TopologySchema = Schema.Struct({ Schema.Struct({ prefix: Schema.optionalKey(Schema.String), strictEffectApproach: Schema.optionalKey(Schema.Boolean), - }) + }), ), effect: Schema.optionalKey(Schema.Json), - readiness: Schema.optionalKey( - Schema.Struct({ endpoint: Schema.optionalKey(Schema.String) }) - ), + readiness: Schema.optionalKey(Schema.Struct({ endpoint: Schema.optionalKey(Schema.String) })), runtime: Schema.optionalKey(Schema.String), serverEntry: Schema.optionalKey(Schema.String), - }) + }), ), id: Schema.String, path: Schema.optionalKey(Schema.String), - }) - ) + }), + ), ), }); -const decodePackageJson = Schema.decodeUnknownEffect( - Schema.fromJsonString(PackageJsonSchema) -); -const decodeTopology = Schema.decodeUnknownEffect( - Schema.fromJsonString(TopologySchema) -); +const decodePackageJson = Schema.decodeUnknownEffect(Schema.fromJsonString(PackageJsonSchema)); +const decodeTopology = Schema.decodeUnknownEffect(Schema.fromJsonString(TopologySchema)); const isFalsyJson = (value: Schema.Json | undefined): boolean => - value === undefined || - value === null || - value === false || - value === 0 || - value === ''; + value === undefined || value === null || value === false || value === 0 || value === ''; const ignoredDirectories = new Set([ '.git', @@ -93,7 +74,7 @@ interface WorkspaceAccess { const listWorkspaceFiles = ( { fileSystem, path, workspaceRoot }: WorkspaceAccess, - startDirectory: string + startDirectory: string, ): Effect.Effect => Effect.gen(function* listWorkspaceFilesEffect() { const absoluteStart = path.join(workspaceRoot, startDirectory); @@ -111,10 +92,7 @@ const listWorkspaceFiles = ( if (info.type === 'Directory') { yield* visit(absoluteEntry); } else if (info.type === 'File') { - const normalized = path - .relative(workspaceRoot, absoluteEntry) - .split(path.sep) - .join('/'); + const normalized = path.relative(workspaceRoot, absoluteEntry).split(path.sep).join('/'); files.push(normalized); } } @@ -128,33 +106,23 @@ const listWorkspaceFiles = ( const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( - Config.withDefault(path.resolve()) - ); + const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe(Config.withDefault(path.resolve())); const failures: string[] = []; const sourceByFile = new Map(); - const exists = (relativePath: string) => - fileSystem.exists(path.join(workspaceRoot, relativePath)); + const exists = (relativePath: string) => fileSystem.exists(path.join(workspaceRoot, relativePath)); - const readText = (relativePath: string) => - fileSystem.readFileString(path.join(workspaceRoot, relativePath), 'utf-8'); + const readText = (relativePath: string) => fileSystem.readFileString(path.join(workspaceRoot, relativePath), 'utf-8'); const topology = (yield* exists('topology/reference-topology.json')) - ? yield* readText('topology/reference-topology.json').pipe( - Effect.flatMap(decodeTopology) - ) + ? yield* readText('topology/reference-topology.json').pipe(Effect.flatMap(decodeTopology)) : { verticals: [] }; const verticalApiStem = (verticalPath: string): string => - configuredMicroVerticalApiStem(verticalPath, topology.verticals ?? []) ?? - path.basename(verticalPath); + configuredMicroVerticalApiStem(verticalPath, topology.verticals ?? []) ?? path.basename(verticalPath); const topologyVertical = (verticalPath: string) => - (topology.verticals ?? []).find( - (vertical) => - (vertical.path ?? `verticals/${vertical.id}`) === verticalPath - ); + (topology.verticals ?? []).find((vertical) => (vertical.path ?? `verticals/${vertical.id}`) === verticalPath); const fail = (message: string): void => { failures.push(message); @@ -166,8 +134,7 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { } }; - const listFiles = (startDirectory: string) => - listWorkspaceFiles({ fileSystem, path, workspaceRoot }, startDirectory); + const listFiles = (startDirectory: string) => listWorkspaceFiles({ fileSystem, path, workspaceRoot }, startDirectory); const listDirectories = (startDirectory: string) => Effect.gen(function* listDirectoriesEffect() { @@ -195,38 +162,20 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { } }); - const assertContains = ( - relativePath: string, - content: string, - pattern: RegExp, - message: string - ): void => { + const assertContains = (relativePath: string, content: string, pattern: RegExp, message: string): void => { assert(pattern.test(content), `${relativePath}: ${message}`); }; - const assertNotContains = ( - relativePath: string, - content: string, - pattern: RegExp, - message: string - ): void => { + const assertNotContains = (relativePath: string, content: string, pattern: RegExp, message: string): void => { assert(!pattern.test(content), `${relativePath}: ${message}`); }; - const isGeneratedInfrastructureReadinessApi = ( - verticalPath: string, - content: string - ): boolean => { + const isGeneratedInfrastructureReadinessApi = (verticalPath: string, content: string): boolean => { const stem = verticalApiStem(verticalPath); const apiPrefix = topologyVertical(verticalPath)?.api?.bff?.prefix; - const contractStem = stem.replaceAll( - /-(?[a-z0-9])/gu, - (_match, letter: string) => letter.toUpperCase() - ); + const contractStem = stem.replaceAll(/-(?[a-z0-9])/gu, (_match, letter: string) => letter.toUpperCase()); const endpoints = [ - ...content.matchAll( - /HttpApiEndpoint\.(?get|post)\(\s*'(?[^']+)'\s*,\s*'(?[^']+)'/gu - ), + ...content.matchAll(/HttpApiEndpoint\.(?get|post)\(\s*'(?[^']+)'\s*,\s*'(?[^']+)'/gu), ].map((match) => { const method = match.groups?.method ?? ''; const name = match.groups?.name ?? ''; @@ -244,21 +193,13 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { }; const assertPrivateOwnerImports = (file: string, content: string): void => { - const imports = content.matchAll( - /(?:from\s+|import\s*\(|require\s*\()\s*['"](?[^'"]+)['"]/gu - ); + const imports = content.matchAll(/(?:from\s+|import\s*\(|require\s*\()\s*['"](?[^'"]+)['"]/gu); for (const match of imports) { const specifier = match.groups?.specifier; if (specifier !== undefined) { - const violation = privateOwnerImportViolation( - workspaceRoot, - file, - specifier - ); + const violation = privateOwnerImportViolation(workspaceRoot, file, specifier); if (violation !== undefined) { - fail( - `${file}: ${violation}. Discover other deployments as allowlisted data.` - ); + fail(`${file}: ${violation}. Discover other deployments as allowlisted data.`); } } } @@ -282,97 +223,93 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { ]) { yield* assertNoPath( forbiddenPath, - `${forbiddenPath} is forbidden in UltraModern strictEffectApproach workspaces; use api/index.ts, shared/api.ts and src/api/* instead.` + `${forbiddenPath} is forbidden in UltraModern strictEffectApproach workspaces; use api/index.ts, shared/api.ts and src/api/* instead.`, ); } - const inspectGeneratedSources = Effect.gen( - function* inspectGeneratedSourcesEffect() { - const generatedFiles = [ - ...(yield* listFiles('apps')), - ...(yield* listFiles('verticals')), - ...(yield* listFiles('packages')), - ]; - const textFiles = generatedFiles.filter((file) => - /\.(?:[cm]?[jt]sx?|json|md|mjs|mts|cts)$/u.test(file) - ); + const inspectGeneratedSources = Effect.gen(function* inspectGeneratedSourcesEffect() { + const generatedFiles = [ + ...(yield* listFiles('apps')), + ...(yield* listFiles('verticals')), + ...(yield* listFiles('packages')), + ]; + const textFiles = generatedFiles.filter((file) => /\.(?:[cm]?[jt]sx?|json|md|mjs|mts|cts)$/u.test(file)); + + for (const file of textFiles) { + sourceByFile.set(file, yield* readText(file)); + } - for (const file of textFiles) { - sourceByFile.set(file, yield* readText(file)); + for (const [file, content] of sourceByFile) { + assertPrivateOwnerImports(file, content); + const unconstrainedContractSchema = file.includes('/tests/') + ? undefined + : unconstrainedHttpApiContractSchemaViolation(content, { + file, + sources: sourceByFile, + }); + if (unconstrainedContractSchema !== undefined) { + fail(`${file}: ${unconstrainedContractSchema}.`); } + assertNotContains( + file, + content, + /(?:from\s+|import\s*\(|require\s*\()\s*['"]@app\/[a-z0-9-]+\/(?:src|workers|worker-host)\//u, + 'cross-MicroVertical imports must use generated API clients, Module Federation, or schema-only Outbox exports rather than private source paths.', + ); - for (const [file, content] of sourceByFile) { - assertPrivateOwnerImports(file, content); - const unconstrainedContractSchema = file.includes('/tests/') - ? undefined - : unconstrainedHttpApiContractSchemaViolation(content, { - file, - sources: sourceByFile, - }); - if (unconstrainedContractSchema !== undefined) { - fail(`${file}: ${unconstrainedContractSchema}.`); - } + if (/\/api\//u.test(file)) { assertNotContains( file, content, - /(?:from\s+|import\s*\(|require\s*\()\s*['"]@app\/[a-z0-9-]+\/(?:src|workers|worker-host)\//u, - 'cross-MicroVertical imports must use generated API clients, Module Federation, or schema-only Outbox exports rather than private source paths.' + /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u, + 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.', ); - - if (/\/api\//u.test(file)) { - assertNotContains( - file, - content, - /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u, - 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.' - ); - assertNotContains( - file, - content, - /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u, - 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.' - ); - assertNotContains( - file, - content, - /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u, - 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.' - ); - assertNotContains( - file, - content, - /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u, - 'API modules must not define unbranded handler factories; use defineEffectBff(...).' - ); - assertNotContains( - file, - content, - /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u, - 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.' - ); - } - assertNotContains( file, content, - /@modern-js\/plugin-bff\/hono-server/u, - 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.' + /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u, + 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.', ); assertNotContains( file, content, - /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, - 'Generated UltraModern API apps must use the Effect runtime.' + /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u, + 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.', ); assertNotContains( file, content, - /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, - 'Generated UltraModern API apps must keep strictEffectApproach enabled.' + /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u, + 'API modules must not define unbranded handler factories; use defineEffectBff(...).', + ); + assertNotContains( + file, + content, + /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u, + 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.', ); } + + assertNotContains( + file, + content, + /@modern-js\/plugin-bff\/hono-server/u, + 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.', + ); + assertNotContains( + file, + content, + /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, + 'Generated UltraModern API apps must use the Effect runtime.', + ); + assertNotContains( + file, + content, + /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, + 'Generated UltraModern API apps must keep strictEffectApproach enabled.', + ); } - ); + }); yield* inspectGeneratedSources; const topologyResolverFor = (importer: string) => { @@ -381,9 +318,7 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { // oxlint-disable-next-line unicorn/no-useless-undefined -- The resolver's explicit miss value preserves its module-or-undefined contract. return undefined; } - const unresolved = path.normalize( - path.join(path.dirname(importer), specifier) - ); + const unresolved = path.normalize(path.join(path.dirname(importer), specifier)); const candidates = /\.[cm]?[jt]sx?$/u.test(unresolved) ? [unresolved] : [`${unresolved}.ts`, `${unresolved}.mts`, `${unresolved}/index.ts`]; @@ -403,43 +338,26 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { } } const shellClient = 'apps/shell-super-app/src/api/vertical-clients.ts'; - if ( - (yield* exists('apps/shell-super-app')) && - verticalDirectories.length > 0 - ) { - assert( - yield* exists(shellClient), - `${shellClient} must aggregate vertical API clients.` - ); + if ((yield* exists('apps/shell-super-app')) && verticalDirectories.length > 0) { + assert(yield* exists(shellClient), `${shellClient} must aggregate vertical API clients.`); } const assertApiRuntime = (apiEntry: string) => Effect.gen(function* assertApiRuntimeEffect() { if (yield* exists(apiEntry)) { const entry = yield* readText(apiEntry); - const usesRpcRuntime = usesStrictRpcRuntimeTopology( - entry, - topologyResolverFor(apiEntry) - ); - const runtimeTopologyViolation = strictEffectRuntimeTopologyViolation( - entry, - topologyResolverFor(apiEntry) - ); + const usesRpcRuntime = usesStrictRpcRuntimeTopology(entry, topologyResolverFor(apiEntry)); + const runtimeTopologyViolation = strictEffectRuntimeTopologyViolation(entry, topologyResolverFor(apiEntry)); if (runtimeTopologyViolation !== undefined) { fail(`${apiEntry}: ${runtimeTopologyViolation}.`); } - assertContains( - apiEntry, - entry, - /\bLayer\b/u, - 'must compose dependencies with Effect Layer.' - ); + assertContains(apiEntry, entry, /\bLayer\b/u, 'must compose dependencies with Effect Layer.'); if (!usesRpcRuntime) { assertContains( apiEntry, entry, /from ['"]\.\.\/shared\/api\.ts['"]/u, - 'must import the contract from ../shared/api.ts.' + 'must import the contract from ../shared/api.ts.', ); } } @@ -457,25 +375,18 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { } else if (apiPrefix === undefined || apiPrefix.length === 0) { fail(`${sharedApi}: topology must declare api.bff.prefix.`); } else { - const baselineViolation = microVerticalApiBaselineViolation( - apiStem, - path.join(workspaceRoot, sharedApi), - { - additionalPaths: - apiStem === 'checkout' - ? { checkoutCartPath: `${basePath}/cart` } - : {}, - apiPrefix, - basePath, - effectClientPackage: '@modern-js/plugin-bff/effect-client', - ownerId: vertical.id, - readinessPath: `${basePath}/readiness`, - sharedContractsPackage: '@app/shared-contracts', - } - ); + const baselineViolation = microVerticalApiBaselineViolation(apiStem, path.join(workspaceRoot, sharedApi), { + additionalPaths: apiStem === 'checkout' ? { checkoutCartPath: `${basePath}/cart` } : {}, + apiPrefix, + basePath, + effectClientPackage: '@modern-js/plugin-bff/effect-client', + ownerId: vertical.id, + readinessPath: `${basePath}/readiness`, + sharedContractsPackage: '@app/shared-contracts', + }); assert( baselineViolation === undefined, - `${sharedApi}: ${baselineViolation ?? 'invalid MicroVertical API baseline'}.` + `${sharedApi}: ${baselineViolation ?? 'invalid MicroVertical API baseline'}.`, ); } }; @@ -485,30 +396,10 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { const sharedApi = `${appPath}/shared/api.ts`; if (yield* exists(sharedApi)) { const contract = yield* readText(sharedApi); - assertContains( - sharedApi, - contract, - /\bHttpApi\.make\b/u, - 'must declare the HttpApi contract.' - ); - assertContains( - sharedApi, - contract, - /\bHttpApiGroup\.make\b/u, - 'must declare HttpApi groups.' - ); - assertContains( - sharedApi, - contract, - /\bHttpApiEndpoint\./u, - 'must declare endpoints through HttpApiEndpoint.' - ); - assertContains( - sharedApi, - contract, - /\bSchema\./u, - 'must use Schema for request, response and error shapes.' - ); + assertContains(sharedApi, contract, /\bHttpApi\.make\b/u, 'must declare the HttpApi contract.'); + assertContains(sharedApi, contract, /\bHttpApiGroup\.make\b/u, 'must declare HttpApi groups.'); + assertContains(sharedApi, contract, /\bHttpApiEndpoint\./u, 'must declare endpoints through HttpApiEndpoint.'); + assertContains(sharedApi, contract, /\bSchema\./u, 'must use Schema for request, response and error shapes.'); if (appPath.startsWith('verticals/')) { assertVerticalBaseline(appPath, sharedApi); } @@ -529,13 +420,8 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { assert(yield* exists(srcApiDirectory), `${srcApiDirectory} is required.`); if (yield* exists(srcApiDirectory)) { - const clientFiles = (yield* listFiles(srcApiDirectory)).filter((file) => - file.endsWith('-client.ts') - ); - assert( - clientFiles.length > 0, - `${srcApiDirectory} must contain a generated API client.` - ); + const clientFiles = (yield* listFiles(srcApiDirectory)).filter((file) => file.endsWith('-client.ts')); + assert(clientFiles.length > 0, `${srcApiDirectory} must contain a generated API client.`); } yield* assertApiRuntime(apiEntry); @@ -545,35 +431,35 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { backendEffectExpose, backendExpose, /backendFederationContract/u, - 'must export backendFederationContract metadata.' + 'must export backendFederationContract metadata.', ); assertContains( backendEffectExpose, backendExpose, /role:\s*['"]microvertical-server['"]/u, - 'must describe the MicroVertical server role.' + 'must describe the MicroVertical server role.', ); assertContains( backendEffectExpose, backendExpose, /strictEffectApproach:\s*true/u, - 'must preserve strict Effect backend execution.' + 'must preserve strict Effect backend execution.', ); assertContains( backendEffectExpose, backendExpose, /contractVersion:\s*['"]microvertical-server-effect-v1['"]/u, - 'must preserve the MicroVertical server contract version.' + 'must preserve the MicroVertical server contract version.', ); assertContains( backendEffectExpose, backendExpose, /export\s*\{\s*default\s*,\s*default\s+as\s+runtime\s*\}\s+from\s+['"]\.\/index\.ts['"]/u, - 'must re-export the generated Effect BFF runtime as both default and runtime.' + 'must re-export the generated Effect BFF runtime as both default and runtime.', ); assert( !/\b(?request|handler)\s*:\s*async\s*\(/u.test(backendExpose), - `${backendEffectExpose}: must not expose raw request handlers.` + `${backendEffectExpose}: must not expose raw request handlers.`, ); } @@ -584,65 +470,52 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { modernConfig, config, /runtimeFramework:\s*['"]effect['"]/u, - 'must use bff.runtimeFramework: effect.' + 'must use bff.runtimeFramework: effect.', ); assertContains( modernConfig, config, /entry:\s*['"]\.\/api\/index['"]/u, - 'must point bff.effect.entry at ./api/index.' + 'must point bff.effect.entry at ./api/index.', ); assertContains( modernConfig, config, /strictEffectApproach:\s*true/u, - 'must enable strictEffectApproach explicitly.' + 'must enable strictEffectApproach explicitly.', ); } - const validateApiPackage = Effect.gen( - function* validateApiPackageEffect() { - if (yield* exists(packageJsonPath)) { - const packageJson = yield* readText(packageJsonPath).pipe( - Effect.flatMap(decodePackageJson) + const validateApiPackage = Effect.gen(function* validateApiPackageEffect() { + if (yield* exists(packageJsonPath)) { + const packageJson = yield* readText(packageJsonPath).pipe(Effect.flatMap(decodePackageJson)); + const isPrivateVerticalInfrastructureApi = + appPath.startsWith('verticals/') && + (yield* exists(sharedApi)) && + isGeneratedInfrastructureReadinessApi(appPath, yield* readText(sharedApi)); + if (isPrivateVerticalInfrastructureApi) { + assert( + packageJson.exports?.['./api'] === undefined && packageJson.exports?.['./api/client'] === undefined, + `${packageJsonPath}: infrastructure-only vertical APIs must remain private deployment surfaces.`, + ); + } else { + assert( + packageJson.exports?.['./api'] === './shared/api.ts', + `${packageJsonPath}: package must export ./api from shared/api.ts.`, + ); + assert( + packageJson.exports?.['./api/client']?.startsWith('./src/api/') ?? false, + `${packageJsonPath}: package must export ./api/client from src/api/*.`, ); - const isPrivateVerticalInfrastructureApi = - appPath.startsWith('verticals/') && - (yield* exists(sharedApi)) && - isGeneratedInfrastructureReadinessApi( - appPath, - yield* readText(sharedApi) - ); - if (isPrivateVerticalInfrastructureApi) { - assert( - packageJson.exports?.['./api'] === undefined && - packageJson.exports?.['./api/client'] === undefined, - `${packageJsonPath}: infrastructure-only vertical APIs must remain private deployment surfaces.` - ); - } else { - assert( - packageJson.exports?.['./api'] === './shared/api.ts', - `${packageJsonPath}: package must export ./api from shared/api.ts.` - ); - assert( - packageJson.exports?.['./api/client']?.startsWith( - './src/api/' - ) ?? false, - `${packageJsonPath}: package must export ./api/client from src/api/*.` - ); - } } } - ); + }); yield* validateApiPackage; }); const inspectApiSurfaces = Effect.gen(function* inspectApiSurfacesEffect() { for (const appPath of appDirectories) { - if ( - (yield* exists(`${appPath}/api/index.ts`)) || - (yield* exists(`${appPath}/shared/api.ts`)) - ) { + if ((yield* exists(`${appPath}/api/index.ts`)) || (yield* exists(`${appPath}/shared/api.ts`))) { yield* assertApiSurface(appPath); } } @@ -650,78 +523,67 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { for (const verticalPath of verticalDirectories) { yield* assertApiSurface(verticalPath); const sharedApi = `${verticalPath}/shared/api.ts`; - const sharedApiContent = (yield* exists(sharedApi)) - ? yield* readText(sharedApi) - : ''; + const sharedApiContent = (yield* exists(sharedApi)) ? yield* readText(sharedApi) : ''; const verticalSources = new Map(); for (const file of yield* listFiles(verticalPath)) { verticalSources.set(file, yield* readText(file)); } if ( /\bHttpApiEndpoint\./u.test(sharedApiContent) && - !isGeneratedInfrastructureReadinessApi( - verticalPath, - sharedApiContent - ) && + !isGeneratedInfrastructureReadinessApi(verticalPath, sharedApiContent) && !hasCompleteGeneratedModuleApiSeam(verticalSources, sharedApi) ) { fail( - `${sharedApi}: module APIs require an approved Codesmith generator, structured api registration, verified trusted tenant context, and the server ModuleEntrypointGateway before an endpoint may be introduced.` + `${sharedApi}: module APIs require an approved Codesmith generator, structured api registration, verified trusted tenant context, and the server ModuleEntrypointGateway before an endpoint may be introduced.`, ); } } }); yield* inspectApiSurfaces; - const inspectWorkspaceContracts = Effect.gen( - function* inspectWorkspaceContractsEffect() { - if (yield* exists('apps/shell-super-app/package.json')) { - const shellPackageJson = yield* readText( - 'apps/shell-super-app/package.json' - ).pipe(Effect.flatMap(decodePackageJson)); - assert( - shellPackageJson.exports?.['./api/clients'] === - './src/api/vertical-clients.ts', - 'apps/shell-super-app/package.json must export ./api/clients.' - ); - } + const inspectWorkspaceContracts = Effect.gen(function* inspectWorkspaceContractsEffect() { + if (yield* exists('apps/shell-super-app/package.json')) { + const shellPackageJson = yield* readText('apps/shell-super-app/package.json').pipe( + Effect.flatMap(decodePackageJson), + ); + assert( + shellPackageJson.exports?.['./api/clients'] === './src/api/vertical-clients.ts', + 'apps/shell-super-app/package.json must export ./api/clients.', + ); + } - if (yield* exists('package.json')) { - const rootPackageJson = yield* readText('package.json').pipe( - Effect.flatMap(decodePackageJson) - ); - assert( - rootPackageJson.scripts?.['api:check'] === - 'node ./scripts/check-ultramodern-api-boundaries.mts', - 'Root package.json must expose api:check.' - ); - assert( - rootPackageJson.scripts?.check?.includes('pnpm api:check') ?? false, - 'Root check script must include pnpm api:check.' - ); - } + if (yield* exists('package.json')) { + const rootPackageJson = yield* readText('package.json').pipe(Effect.flatMap(decodePackageJson)); + assert( + rootPackageJson.scripts?.['api:check'] === 'node ./scripts/check-ultramodern-api-boundaries.mts', + 'Root package.json must expose api:check.', + ); + assert( + rootPackageJson.scripts?.check?.includes('pnpm api:check') ?? false, + 'Root check script must include pnpm api:check.', + ); + } - const validateTopologyContracts = Effect.sync(() => { - for (const vertical of topology.verticals ?? []) { - if (vertical.api?.runtime === 'effect') { - assert( - vertical.api.bff?.strictEffectApproach === true, - `${vertical.id} topology must mark strictEffectApproach as true.` - ); - assert( - vertical.api.serverEntry?.endsWith('/api/index.ts') ?? false, - `${vertical.id} topology must use api/index.ts as the server entry.` - ); - } + const validateTopologyContracts = Effect.sync(() => { + for (const vertical of topology.verticals ?? []) { + if (vertical.api?.runtime === 'effect') { + assert( + vertical.api.bff?.strictEffectApproach === true, + `${vertical.id} topology must mark strictEffectApproach as true.`, + ); assert( - isFalsyJson(vertical.api?.effect), - `${vertical.id} topology must describe the API directly, not under api.effect.` + vertical.api.serverEntry?.endsWith('/api/index.ts') ?? false, + `${vertical.id} topology must use api/index.ts as the server entry.`, ); } - }); - yield* validateTopologyContracts; - } - ); + assert( + isFalsyJson(vertical.api?.effect), + `${vertical.id} topology must describe the API directly, not under api.effect.`, + ); + } + }); + yield* validateTopologyContracts; + }); yield* inspectWorkspaceContracts; if (failures.length > 0) { @@ -737,15 +599,10 @@ const checkApiBoundaries = Effect.gen(function* checkApiBoundariesEffect() { }); const failureCount = await Effect.runPromise( - checkApiBoundaries.pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)) - ) + checkApiBoundaries.pipe(Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))), ); if (failureCount > 0) { - NodeRuntime.runMain( - Effect.fail(new ApiBoundaryCheckFailed({ failureCount })), - { - disableErrorReporting: true, - } - ); + NodeRuntime.runMain(Effect.fail(new ApiBoundaryCheckFailed({ failureCount })), { + disableErrorReporting: true, + }); } diff --git a/app/scripts/database-trust-audit/collect-snapshot.mts b/app/scripts/database-trust-audit/collect-snapshot.mts index a2d7ef6a3..e78478fd6 100644 --- a/app/scripts/database-trust-audit/collect-snapshot.mts +++ b/app/scripts/database-trust-audit/collect-snapshot.mts @@ -82,12 +82,7 @@ interface TablePrivilegeRow { readonly delete: boolean; readonly insert: boolean; readonly insertable: boolean; - readonly kind: - | 'foreign-table' - | 'materialized-view' - | 'partitioned-table' - | 'table' - | 'view'; + readonly kind: 'foreign-table' | 'materialized-view' | 'partitioned-table' | 'table' | 'view'; readonly maintain: boolean; readonly owner: string; readonly owner_bypass_rls: boolean; @@ -114,13 +109,7 @@ interface ParameterPrivilegeRow { } interface TypePrivilegeRow { - readonly kind: - | 'base' - | 'composite' - | 'domain' - | 'enum' - | 'multirange' - | 'range'; + readonly kind: 'base' | 'composite' | 'domain' | 'enum' | 'multirange' | 'range'; readonly owner: string; readonly schema: string; readonly type: string; @@ -164,24 +153,20 @@ class DatabaseTrustBoundarySnapshotError extends Schema.TaggedError; -type DatabaseTargetMismatchFailure = InstanceType< - typeof DatabaseTargetMismatchError ->; +type DatabaseSessionIdentityFailure = InstanceType; +type DatabaseTargetMismatchFailure = InstanceType; const query = ( client: ClientBase, statement: string, - values: unknown[] = [] + values: unknown[] = [], ): Effect.Effect, DatabaseTrustBoundarySnapshotError> => Effect.tryPromise({ catch: () => @@ -192,31 +177,22 @@ const query = ( try: async () => await client.query(statement, values), }); -export const hasTrustedContextValue = (value: string | null): boolean => - value !== null && value.length > 0; +export const hasTrustedContextValue = (value: string | null): boolean => value !== null && value.length > 0; const probeSettingEffect = Effect.fn('probeSetting')(function* probeSetting( client: ClientBase, setting: 'ontos.legal_entity_id' | 'ontos.tenant_id', - value: string + value: string, ) { yield* query(client, 'begin'); const settable = yield* Effect.gen(function* probeTrustedContextSetting() { yield* query(client, 'select set_config($1, $2, true)', [setting, value]); - const current = yield* query( - client, - 'select current_setting($1, true) as value', - [setting] - ); + const current = yield* query(client, 'select current_setting($1, true) as value', [setting]); const [currentRow] = current.rows; return currentRow?.value === value; }).pipe(Effect.catch(() => Effect.succeed(false))); yield* query(client, 'rollback'); - const after = yield* query( - client, - 'select current_setting($1, true) as value', - [setting] - ); + const after = yield* query(client, 'select current_setting($1, true) as value', [setting]); const [afterRow] = after.rows; return { retainedAfterRollback: hasTrustedContextValue(afterRow?.value ?? null), @@ -224,92 +200,83 @@ const probeSettingEffect = Effect.fn('probeSetting')(function* probeSetting( }; }); -export const collectSnapshot = Effect.fn('collectSnapshot')( - function* collectSnapshotEffect( - admin: Client, - runtime: Client - ): Effect.fn.Return< - DatabaseTrustBoundarySnapshot, - | DatabaseSessionIdentityFailure - | DatabaseTargetMismatchFailure - | DatabaseTrustBoundarySnapshotError - > { - const targetQuery = `select +export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSnapshotEffect( + admin: Client, + runtime: Client, +): Effect.fn.Return< + DatabaseTrustBoundarySnapshot, + DatabaseSessionIdentityFailure | DatabaseTargetMismatchFailure | DatabaseTrustBoundarySnapshotError +> { + const targetQuery = `select current_user::text as current_role, current_database() as database, session_user::text as session_role, inet_server_addr()::text as server_address, inet_server_port() as server_port`; - const [administrativeTarget, runtimeTarget] = yield* Effect.all( - [ - query(admin, targetQuery), - query(runtime, targetQuery), - ], - { concurrency: 'unbounded' } - ); - const [administrativeTargetRow] = administrativeTarget.rows; - const [runtimeTargetRow] = runtimeTarget.rows; - if ( - administrativeTargetRow === undefined || - runtimeTargetRow === undefined - ) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'database_target_identity_unavailable', - reason: 'database target identity is unavailable', - }); - } - const administrativeEndpoint = getEffectiveDatabaseEndpoint(admin); - const runtimeEndpoint = getEffectiveDatabaseEndpoint(runtime); - yield* Effect.try({ - catch: (cause) => - Schema.is(DatabaseTargetMismatchError)(cause) - ? cause - : new DatabaseTrustBoundarySnapshotError({ - code: 'database_target_identity_unavailable', - reason: 'database target identity is unavailable', - }), - try: () => - assertSameDatabaseTarget( - { - ...administrativeEndpoint, - database: administrativeTargetRow.database, - serverAddress: administrativeTargetRow.server_address, - serverPort: administrativeTargetRow.server_port, - }, - { - ...runtimeEndpoint, - database: runtimeTargetRow.database, - serverAddress: runtimeTargetRow.server_address, - serverPort: runtimeTargetRow.server_port, - } - ), - }); - yield* Effect.try({ - catch: (cause) => - Schema.is(DatabaseSessionIdentityError)(cause) - ? cause - : new DatabaseTrustBoundarySnapshotError({ - code: 'database_session_identity_unavailable', - reason: 'database session identity is unavailable', - }), - try: () => - assertDatabaseSessionIdentities( - { - currentRole: administrativeTargetRow.current_role, - sessionRole: administrativeTargetRow.session_role, - }, - { - currentRole: runtimeTargetRow.current_role, - sessionRole: runtimeTargetRow.session_role, - } - ), + const [administrativeTarget, runtimeTarget] = yield* Effect.all( + [query(admin, targetQuery), query(runtime, targetQuery)], + { concurrency: 'unbounded' }, + ); + const [administrativeTargetRow] = administrativeTarget.rows; + const [runtimeTargetRow] = runtimeTarget.rows; + if (administrativeTargetRow === undefined || runtimeTargetRow === undefined) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'database_target_identity_unavailable', + reason: 'database target identity is unavailable', }); - const administrativeRole = administrativeTargetRow.session_role; - const runtimeRole = runtimeTargetRow.session_role; + } + const administrativeEndpoint = getEffectiveDatabaseEndpoint(admin); + const runtimeEndpoint = getEffectiveDatabaseEndpoint(runtime); + yield* Effect.try({ + catch: (cause) => + Schema.is(DatabaseTargetMismatchError)(cause) + ? cause + : new DatabaseTrustBoundarySnapshotError({ + code: 'database_target_identity_unavailable', + reason: 'database target identity is unavailable', + }), + try: () => + assertSameDatabaseTarget( + { + ...administrativeEndpoint, + database: administrativeTargetRow.database, + serverAddress: administrativeTargetRow.server_address, + serverPort: administrativeTargetRow.server_port, + }, + { + ...runtimeEndpoint, + database: runtimeTargetRow.database, + serverAddress: runtimeTargetRow.server_address, + serverPort: runtimeTargetRow.server_port, + }, + ), + }); + yield* Effect.try({ + catch: (cause) => + Schema.is(DatabaseSessionIdentityError)(cause) + ? cause + : new DatabaseTrustBoundarySnapshotError({ + code: 'database_session_identity_unavailable', + reason: 'database session identity is unavailable', + }), + try: () => + assertDatabaseSessionIdentities( + { + currentRole: administrativeTargetRow.current_role, + sessionRole: administrativeTargetRow.session_role, + }, + { + currentRole: runtimeTargetRow.current_role, + sessionRole: runtimeTargetRow.session_role, + }, + ), + }); + const administrativeRole = administrativeTargetRow.session_role; + const runtimeRole = runtimeTargetRow.session_role; - const role = yield* query( - admin, - `select + const role = yield* query( + admin, + `select rolbypassrls as bypass_rls, rolcreatedb as can_create_databases, rolcreaterole as can_create_roles, @@ -320,19 +287,19 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( rolsuper as superuser from pg_catalog.pg_roles where rolname = $1`, - [runtimeRole] - ); - const [roleRow] = role.rows; - if (roleRow === undefined) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'runtime_role_absent', - reason: 'runtime role is absent', - }); - } + [runtimeRole], + ); + const [roleRow] = role.rows; + if (roleRow === undefined) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'runtime_role_absent', + reason: 'runtime role is absent', + }); + } - const memberships = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + const memberships = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -487,28 +454,28 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( or candidate.oid in (select role_oid from reachable_roles) ) order by candidate.rolname`, - [runtimeRole] - ); - const database = yield* query( - admin, - `select + [runtimeRole], + ); + const database = yield* query( + admin, + `select current_database() as database, has_database_privilege($1, current_database(), 'CONNECT') as connect, has_database_privilege($1, current_database(), 'CREATE') as create, has_database_privilege($1, current_database(), 'TEMPORARY') as temporary`, - [runtimeRole] - ); - const [databaseRow] = database.rows; - if (databaseRow === undefined) { - return yield* new DatabaseTrustBoundarySnapshotError({ - code: 'database_privilege_unavailable', - reason: 'database privilege row is absent', - }); - } + [runtimeRole], + ); + const [databaseRow] = database.rows; + if (databaseRow === undefined) { + return yield* new DatabaseTrustBoundarySnapshotError({ + code: 'database_privilege_unavailable', + reason: 'database privilege row is absent', + }); + } - const schemas = yield* query( - admin, - `select + const schemas = yield* query( + admin, + `select namespace.nspname as schema, owner.rolname as owner, has_schema_privilege($1, namespace.oid, 'USAGE') as usage, @@ -518,12 +485,12 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( where namespace.nspname !~ '^pg_' and namespace.nspname <> 'information_schema' order by namespace.nspname`, - [runtimeRole] - ); - const schemaNames = schemas.rows.map(({ schema }) => schema); - const routines = yield* query( - admin, - `select + [runtimeRole], + ); + const schemaNames = schemas.rows.map(({ schema }) => schema); + const routines = yield* query( + admin, + `select namespace.nspname as schema, routine.proname as routine, pg_get_function_identity_arguments(routine.oid) as identity_arguments, @@ -544,11 +511,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( join pg_catalog.pg_roles as owner on owner.oid = routine.proowner where namespace.nspname = any($2::text[]) order by namespace.nspname, routine.proname, routine.oid`, - [runtimeRole, schemaNames] - ); - const tables = yield* query( - admin, - `with recursive view_dependencies(view_oid, referenced_oid, effective_owner_oid) as ( + [runtimeRole, schemaNames], + ); + const tables = yield* query( + admin, + `with recursive view_dependencies(view_oid, referenced_oid, effective_owner_oid) as ( select rewrite.ev_class, dependency.refobjid, @@ -707,11 +674,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( where relation.relkind in ('r', 'p', 'v', 'm', 'f') and namespace.nspname = any($2::text[]) order by namespace.nspname, relation.relname`, - [runtimeRole, schemaNames, administrativeRole] - ); - const types = yield* query( - admin, - `select + [runtimeRole, schemaNames, administrativeRole], + ); + const types = yield* query( + admin, + `select namespace.nspname as schema, audited_type.typname as type, case audited_type.typtype @@ -741,11 +708,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( ) and namespace.nspname = any($1::text[]) order by namespace.nspname, audited_type.typname`, - [schemaNames] - ); - const sequences = yield* query( - admin, - `select + [schemaNames], + ); + const sequences = yield* query( + admin, + `select namespace.nspname as schema, relation.relname as sequence, owner.rolname as owner, @@ -766,11 +733,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( join pg_catalog.pg_roles as owner on owner.oid = relation.relowner where relation.relkind = 'S' and namespace.nspname = any($2::text[]) order by namespace.nspname, relation.relname`, - [runtimeRole, schemaNames] - ); - const parameterPrivileges = yield* query( - admin, - `select + [runtimeRole, schemaNames], + ); + const parameterPrivileges = yield* query( + admin, + `select parameter.parname as parameter, has_parameter_privilege($1, parameter.parname, 'ALTER SYSTEM') as alter_system, has_parameter_privilege($1, parameter.parname, 'SET') as set @@ -778,11 +745,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( where has_parameter_privilege($1, parameter.parname, 'ALTER SYSTEM') or has_parameter_privilege($1, parameter.parname, 'SET') order by parameter.parname`, - [runtimeRole] - ); - const grantOptions = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + [runtimeRole], + ); + const grantOptions = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -967,11 +934,11 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( ) ) as authority order by target.role_name, authority.grant_option`, - [runtimeRole, schemaNames] - ); - const defaultPrivileges = yield* query( - admin, - `with recursive reachable_roles(role_oid) as ( + [runtimeRole, schemaNames], + ); + const defaultPrivileges = yield* query( + admin, + `with recursive reachable_roles(role_oid) as ( select candidate.oid from pg_catalog.pg_roles as candidate where candidate.rolname = $1 @@ -1095,133 +1062,127 @@ export const collectSnapshot = Effect.fn('collectSnapshot')( privilege, source, grantable`, - [runtimeRole, schemaNames, administrativeRole] - ); - const tenant = yield* probeSettingEffect( - runtime, - 'ontos.tenant_id', - '00000000-0000-4000-8000-000000000001' - ); - const legalEntity = yield* probeSettingEffect( - runtime, - 'ontos.legal_entity_id', - '00000000-0000-4000-8000-000000000002' - ); + [runtimeRole, schemaNames, administrativeRole], + ); + const tenant = yield* probeSettingEffect(runtime, 'ontos.tenant_id', '00000000-0000-4000-8000-000000000001'); + const legalEntity = yield* probeSettingEffect( + runtime, + 'ontos.legal_entity_id', + '00000000-0000-4000-8000-000000000002', + ); - return { - administrativeRole, - database: databaseRow.database, - databasePrivileges: { - connect: databaseRow.connect, - create: databaseRow.create, - temporary: databaseRow.temporary, + return { + administrativeRole, + database: databaseRow.database, + databasePrivileges: { + connect: databaseRow.connect, + create: databaseRow.create, + temporary: databaseRow.temporary, + }, + defaultPrivileges: defaultPrivileges.rows.map((privilege) => ({ + grantable: privilege.grantable, + grantee: privilege.grantee, + objectType: privilege.object_type, + owner: privilege.owner, + privilege: privilege.privilege, + schema: privilege.schema, + source: privilege.source, + })), + grantOptions: grantOptions.rows.map(({ grant_option }) => grant_option), + memberships: memberships.rows.map((membership) => ({ + attributes: { + bypassRls: membership.bypass_rls, + canCreateDatabases: membership.can_create_databases, + canCreateRoles: membership.can_create_roles, + canLogin: membership.can_login, + inherit: membership.inherit, + replication: membership.replication, + superuser: membership.superuser, }, - defaultPrivileges: defaultPrivileges.rows.map((privilege) => ({ - grantable: privilege.grantable, - grantee: privilege.grantee, - objectType: privilege.object_type, - owner: privilege.owner, - privilege: privilege.privilege, - schema: privilege.schema, - source: privilege.source, - })), - grantOptions: grantOptions.rows.map(({ grant_option }) => grant_option), - memberships: memberships.rows.map((membership) => ({ - attributes: { - bypassRls: membership.bypass_rls, - canCreateDatabases: membership.can_create_databases, - canCreateRoles: membership.can_create_roles, - canLogin: membership.can_login, - inherit: membership.inherit, - replication: membership.replication, - superuser: membership.superuser, - }, - canAdministerRole: membership.can_administer_role, - canInheritRole: membership.can_inherit_role, - canSetRole: membership.can_set_role, - createSchemas: membership.create_schemas, - databaseCreate: membership.database_create, - ownedRelations: membership.owned_relations, - ownedRoutines: membership.owned_routines, - ownedSchemas: membership.owned_schemas, - ownedTypes: membership.owned_types, - parameterPrivileges: membership.parameter_privileges, - predefinedRole: membership.predefined_role, - relationPrivilegeSchemas: membership.relation_privilege_schemas, - role: membership.role, - securityDefinerRoutines: membership.security_definer_routines, - })), - parameterPrivileges: parameterPrivileges.rows.map((privilege) => ({ - alterSystem: privilege.alter_system, - parameter: privilege.parameter, - set: privilege.set, - })), - role: { - bypassRls: roleRow.bypass_rls, - canCreateDatabases: roleRow.can_create_databases, - canCreateRoles: roleRow.can_create_roles, - canLogin: roleRow.can_login, - inherit: roleRow.inherit, - predefinedRole: roleRow.predefined_role, - replication: roleRow.replication, - superuser: roleRow.superuser, + canAdministerRole: membership.can_administer_role, + canInheritRole: membership.can_inherit_role, + canSetRole: membership.can_set_role, + createSchemas: membership.create_schemas, + databaseCreate: membership.database_create, + ownedRelations: membership.owned_relations, + ownedRoutines: membership.owned_routines, + ownedSchemas: membership.owned_schemas, + ownedTypes: membership.owned_types, + parameterPrivileges: membership.parameter_privileges, + predefinedRole: membership.predefined_role, + relationPrivilegeSchemas: membership.relation_privilege_schemas, + role: membership.role, + securityDefinerRoutines: membership.security_definer_routines, + })), + parameterPrivileges: parameterPrivileges.rows.map((privilege) => ({ + alterSystem: privilege.alter_system, + parameter: privilege.parameter, + set: privilege.set, + })), + role: { + bypassRls: roleRow.bypass_rls, + canCreateDatabases: roleRow.can_create_databases, + canCreateRoles: roleRow.can_create_roles, + canLogin: roleRow.can_login, + inherit: roleRow.inherit, + predefinedRole: roleRow.predefined_role, + replication: roleRow.replication, + superuser: roleRow.superuser, + }, + routines: routines.rows.map((routine) => ({ + executable: routine.executable, + identityArguments: routine.identity_arguments, + kind: routine.kind, + owner: routine.owner, + routine: routine.routine, + schema: routine.schema, + securityDefiner: routine.security_definer, + })), + runtimeRole, + schemas: schemas.rows, + sequences: sequences.rows.map((sequence) => ({ + owner: sequence.owner, + privileges: { + select: sequence.select, + update: sequence.update, + usage: sequence.usage, }, - routines: routines.rows.map((routine) => ({ - executable: routine.executable, - identityArguments: routine.identity_arguments, - kind: routine.kind, - owner: routine.owner, - routine: routine.routine, - schema: routine.schema, - securityDefiner: routine.security_definer, - })), - runtimeRole, - schemas: schemas.rows, - sequences: sequences.rows.map((sequence) => ({ - owner: sequence.owner, - privileges: { - select: sequence.select, - update: sequence.update, - usage: sequence.usage, - }, - schema: sequence.schema, - sequence: sequence.sequence, - })), - tables: tables.rows.map((table) => ({ - deletable: table.deletable, - insertable: table.insertable, - kind: table.kind, - owner: table.owner, - ownerBypassRls: table.owner_bypass_rls, - ownerContextPrivileged: table.owner_context_privileged, - ownerContextRlsBypass: table.owner_context_rls_bypass, - ownerSuperuser: table.owner_superuser, - privileges: { - delete: table.delete, - insert: table.insert, - maintain: table.maintain, - references: table.references, - select: table.select, - trigger: table.trigger, - truncate: table.truncate, - update: table.update, - }, - rlsEnabled: table.rls_enabled, - rlsForced: table.rls_forced, - schema: table.schema, - securityInvoker: table.security_invoker, - table: table.table, - updatable: table.updatable, - })), - trustedContext: { - legalEntitySettingRetainedAfterRollback: - legalEntity.retainedAfterRollback, - legalEntitySettingSettable: legalEntity.settable, - tenantSettingRetainedAfterRollback: tenant.retainedAfterRollback, - tenantSettingSettable: tenant.settable, - transactionLocal: true, + schema: sequence.schema, + sequence: sequence.sequence, + })), + tables: tables.rows.map((table) => ({ + deletable: table.deletable, + insertable: table.insertable, + kind: table.kind, + owner: table.owner, + ownerBypassRls: table.owner_bypass_rls, + ownerContextPrivileged: table.owner_context_privileged, + ownerContextRlsBypass: table.owner_context_rls_bypass, + ownerSuperuser: table.owner_superuser, + privileges: { + delete: table.delete, + insert: table.insert, + maintain: table.maintain, + references: table.references, + select: table.select, + trigger: table.trigger, + truncate: table.truncate, + update: table.update, }, - types: types.rows, - }; - } -); + rlsEnabled: table.rls_enabled, + rlsForced: table.rls_forced, + schema: table.schema, + securityInvoker: table.security_invoker, + table: table.table, + updatable: table.updatable, + })), + trustedContext: { + legalEntitySettingRetainedAfterRollback: legalEntity.retainedAfterRollback, + legalEntitySettingSettable: legalEntity.settable, + tenantSettingRetainedAfterRollback: tenant.retainedAfterRollback, + tenantSettingSettable: tenant.settable, + transactionLocal: true, + }, + types: types.rows, + }; +}); diff --git a/app/scripts/database-trust-audit/report.mts b/app/scripts/database-trust-audit/report.mts index be26381b7..cbdb01a59 100644 --- a/app/scripts/database-trust-audit/report.mts +++ b/app/scripts/database-trust-audit/report.mts @@ -84,12 +84,7 @@ interface SequencePrivilege { interface TablePrivilege { readonly deletable?: boolean; readonly insertable?: boolean; - readonly kind: - | 'foreign-table' - | 'materialized-view' - | 'partitioned-table' - | 'table' - | 'view'; + readonly kind: 'foreign-table' | 'materialized-view' | 'partitioned-table' | 'table' | 'view'; readonly owner: string; readonly ownerBypassRls?: boolean; readonly ownerContextPrivileged?: boolean; @@ -114,13 +109,7 @@ interface TablePrivilege { } interface TypePrivilege { - readonly kind: - | 'base' - | 'composite' - | 'domain' - | 'enum' - | 'multirange' - | 'range'; + readonly kind: 'base' | 'composite' | 'domain' | 'enum' | 'multirange' | 'range'; readonly owner: string; readonly schema: string; readonly type: string; @@ -207,44 +196,33 @@ export interface DatabaseTrustBoundaryReport extends DatabaseTrustBoundarySnapsh export class DatabaseTrustBoundaryAuditError extends Schema.TaggedError()( 'DatabaseTrustBoundaryAuditError', - { reason: Schema.String } + { reason: Schema.String }, ) {} export class DatabaseTargetMismatchError extends Schema.TaggedError()( 'DatabaseTargetMismatchError', - { message: Schema.String } + { message: Schema.String }, ) {} export class DatabaseSessionIdentityError extends Schema.TaggedError()( 'DatabaseSessionIdentityError', - { message: Schema.String } + { message: Schema.String }, ) {} -export const genericAuditFailureMessage = - 'Database trust-boundary audit failed'; +export const genericAuditFailureMessage = 'Database trust-boundary audit failed'; -export const getDatabaseTrustBoundaryFailureMessage = ( - cause: Cause.Cause -): string => { +export const getDatabaseTrustBoundaryFailureMessage = (cause: Cause.Cause): string => { const failure = Cause.findErrorOption(cause); - return Option.isSome(failure) && - Schema.is(DatabaseTrustBoundaryAuditError)(failure.value) + return Option.isSome(failure) && Schema.is(DatabaseTrustBoundaryAuditError)(failure.value) ? failure.value.reason : genericAuditFailureMessage; }; const hasDml = (table: TablePrivilege): boolean => - table.privileges.delete || - table.privileges.insert || - table.privileges.select || - table.privileges.update; + table.privileges.delete || table.privileges.insert || table.privileges.select || table.privileges.update; const hasClusterPrivilege = (role: RoleAttributes): boolean => - role.superuser || - role.bypassRls || - role.canCreateDatabases || - role.canCreateRoles || - role.replication; + role.superuser || role.bypassRls || role.canCreateDatabases || role.canCreateRoles || role.replication; const compareText = (left: string, right: string): number => { if (left < right) { @@ -256,15 +234,10 @@ const compareText = (left: string, right: string): number => { return 0; }; -const sorted = ( - values: Iterable, - compare: (left: Value, right: Value) => number -): Value[] => { +const sorted = (values: Iterable, compare: (left: Value, right: Value) => number): Value[] => { const result: Value[] = []; for (const value of values) { - const insertionIndex = result.findIndex( - (candidate) => compare(value, candidate) < 0 - ); + const insertionIndex = result.findIndex((candidate) => compare(value, candidate) < 0); if (insertionIndex === -1) { result.push(value); } else { @@ -277,7 +250,7 @@ const sorted = ( const addFinding = ( findings: DatabaseTrustBoundaryFinding[], included: boolean, - finding: DatabaseTrustBoundaryFinding + finding: DatabaseTrustBoundaryFinding, ): void => { if (included) { findings.push(finding); @@ -286,7 +259,7 @@ const addFinding = ( export const assertSameDatabaseTarget = ( administrative: DatabaseTargetIdentity, - runtime: DatabaseTargetIdentity + runtime: DatabaseTargetIdentity, ): void => { if ( administrative.database !== runtime.database || @@ -300,15 +273,14 @@ export const assertSameDatabaseTarget = ( Option.none(), () => new DatabaseTargetMismatchError({ - message: - 'DATABASE_ADMIN_URL and DATABASE_URL must target the same PostgreSQL server and database', - }) + message: 'DATABASE_ADMIN_URL and DATABASE_URL must target the same PostgreSQL server and database', + }), ); } }; export const getEffectiveDatabaseEndpoint = ( - client: Client + client: Client, ): Pick => ({ configuredHost: client.host, configuredPort: client.port, @@ -316,19 +288,15 @@ export const getEffectiveDatabaseEndpoint = ( export const assertDatabaseSessionIdentities = ( administrative: DatabaseSessionIdentity, - runtime: DatabaseSessionIdentity + runtime: DatabaseSessionIdentity, ): void => { - if ( - administrative.currentRole !== administrative.sessionRole || - runtime.currentRole !== runtime.sessionRole - ) { + if (administrative.currentRole !== administrative.sessionRole || runtime.currentRole !== runtime.sessionRole) { Option.getOrThrowWith( Option.none(), () => new DatabaseSessionIdentityError({ - message: - 'current_user must equal session_user for both database audit connections', - }) + message: 'current_user must equal session_user for both database audit connections', + }), ); } if (administrative.sessionRole === runtime.sessionRole) { @@ -336,9 +304,8 @@ export const assertDatabaseSessionIdentities = ( Option.none(), () => new DatabaseSessionIdentityError({ - message: - 'DATABASE_ADMIN_URL and DATABASE_URL must authenticate as distinct authenticated PostgreSQL roles', - }) + message: 'DATABASE_ADMIN_URL and DATABASE_URL must authenticate as distinct authenticated PostgreSQL roles', + }), ); } }; @@ -356,8 +323,7 @@ const hasMembershipObjectAuthority = (membership: RoleMembership): boolean => ].some((objects) => objects.length > 0); const hasPrivilegedMembership = (membership: RoleMembership): boolean => - ((membership.canSetRole || membership.canAdministerRole) && - hasClusterPrivilege(membership.attributes)) || + ((membership.canSetRole || membership.canAdministerRole) && hasClusterPrivilege(membership.attributes)) || membership.predefinedRole === true || membership.databaseCreate || hasMembershipObjectAuthority(membership); @@ -368,47 +334,26 @@ const hasUsableViewPrivileges = (table: TablePrivilege): boolean => (table.privileges.update && table.updatable !== false) || (table.privileges.delete && table.deletable !== false); -const hasPrivilegedViewOwner = ( - table: TablePrivilege, - administrativeRole: string -): boolean => +const hasPrivilegedViewOwner = (table: TablePrivilege, administrativeRole: string): boolean => (table.securityInvoker !== true && - (table.owner === administrativeRole || - table.ownerBypassRls === true || - table.ownerSuperuser === true)) || + (table.owner === administrativeRole || table.ownerBypassRls === true || table.ownerSuperuser === true)) || table.ownerContextPrivileged === true || table.ownerContextRlsBypass === true; -const isPrivilegedOwnerView = ( - table: TablePrivilege, - administrativeRole: string -): boolean => - table.kind === 'view' && - hasUsableViewPrivileges(table) && - hasPrivilegedViewOwner(table, administrativeRole); +const isPrivilegedOwnerView = (table: TablePrivilege, administrativeRole: string): boolean => + table.kind === 'view' && hasUsableViewPrivileges(table) && hasPrivilegedViewOwner(table, administrativeRole); const hasDdlAuthority = (snapshot: DatabaseTrustBoundarySnapshot): boolean => { const { memberships, routines, schemas, sequences, tables, types } = snapshot; const ownsRelation = tables.some(({ owner }) => owner === snapshot.runtimeRole) || sequences.some(({ owner }) => owner === snapshot.runtimeRole); - const ownsRoutine = routines.some( - ({ owner }) => owner === snapshot.runtimeRole - ); + const ownsRoutine = routines.some(({ owner }) => owner === snapshot.runtimeRole); const ownsType = types.some(({ owner }) => owner === snapshot.runtimeRole); const inheritsOwnership = memberships.some( - ({ - canInheritRole, - ownedRelations, - ownedRoutines, - ownedSchemas, - ownedTypes, - }) => + ({ canInheritRole, ownedRelations, ownedRoutines, ownedSchemas, ownedTypes }) => canInheritRole && - (ownedRelations.length > 0 || - ownedRoutines.length > 0 || - ownedSchemas.length > 0 || - ownedTypes.length > 0) + (ownedRelations.length > 0 || ownedRoutines.length > 0 || ownedSchemas.length > 0 || ownedTypes.length > 0), ); return ( snapshot.databasePrivileges.create || @@ -421,35 +366,27 @@ const hasDdlAuthority = (snapshot: DatabaseTrustBoundarySnapshot): boolean => { }; export const buildDatabaseTrustBoundaryReport = ( - snapshot: DatabaseTrustBoundarySnapshot + snapshot: DatabaseTrustBoundarySnapshot, ): DatabaseTrustBoundaryReport => { - const schemas = sorted(snapshot.schemas, (left, right) => - compareText(left.schema, right.schema) - ); + const schemas = sorted(snapshot.schemas, (left, right) => compareText(left.schema, right.schema)); const tables = sorted( snapshot.tables, - (left, right) => - compareText(left.schema, right.schema) || - compareText(left.table, right.table) + (left, right) => compareText(left.schema, right.schema) || compareText(left.table, right.table), ); const sequences = sorted( snapshot.sequences, - (left, right) => - compareText(left.schema, right.schema) || - compareText(left.sequence, right.sequence) + (left, right) => compareText(left.schema, right.schema) || compareText(left.sequence, right.sequence), ); const routines = sorted( snapshot.routines, (left, right) => compareText(left.schema, right.schema) || compareText(left.routine, right.routine) || - compareText(left.identityArguments, right.identityArguments) + compareText(left.identityArguments, right.identityArguments), ); const types = sorted( snapshot.types, - (left, right) => - compareText(left.schema, right.schema) || - compareText(left.type, right.type) + (left, right) => compareText(left.schema, right.schema) || compareText(left.type, right.type), ); const defaultPrivileges = sorted( snapshot.defaultPrivileges, @@ -460,36 +397,25 @@ export const buildDatabaseTrustBoundaryReport = ( compareText(left.grantee, right.grantee) || compareText(left.privilege, right.privilege) || compareText(left.source, right.source) || - Number(left.grantable) - Number(right.grantable) - ); - const memberships = sorted(snapshot.memberships, (left, right) => - compareText(left.role, right.role) + Number(left.grantable) - Number(right.grantable), ); + const memberships = sorted(snapshot.memberships, (left, right) => compareText(left.role, right.role)); const grantOptions = sorted(snapshot.grantOptions, compareText); - const grantableDefaultPrivileges = defaultPrivileges.filter( - ({ grantable }) => grantable - ); - const parameterPrivileges = sorted( - snapshot.parameterPrivileges, - (left, right) => compareText(left.parameter, right.parameter) + const grantableDefaultPrivileges = defaultPrivileges.filter(({ grantable }) => grantable); + const parameterPrivileges = sorted(snapshot.parameterPrivileges, (left, right) => + compareText(left.parameter, right.parameter), ); const findings: DatabaseTrustBoundaryFinding[] = []; const dmlTables = tables.filter(hasDml); - addFinding( - findings, - hasClusterPrivilege(snapshot.role) || snapshot.role.predefinedRole === true, - { - code: 'runtime_role_is_privileged', - evidence: - 'The runtime role has a PostgreSQL cluster-level privilege or is a predefined PostgreSQL role.', - severity: 'critical', - } - ); + addFinding(findings, hasClusterPrivilege(snapshot.role) || snapshot.role.predefinedRole === true, { + code: 'runtime_role_is_privileged', + evidence: 'The runtime role has a PostgreSQL cluster-level privilege or is a predefined PostgreSQL role.', + severity: 'critical', + }); const administrativeMembership = memberships.some( ({ canAdministerRole, canInheritRole, canSetRole, role }) => - (canSetRole || canAdministerRole || canInheritRole) && - role === snapshot.administrativeRole + (canSetRole || canAdministerRole || canInheritRole) && role === snapshot.administrativeRole, ); addFinding(findings, administrativeMembership, { code: 'runtime_role_can_assume_administrative_role', @@ -499,28 +425,20 @@ export const buildDatabaseTrustBoundaryReport = ( }); const nonAdministrativeMemberships = memberships.filter( ({ canAdministerRole, canInheritRole, canSetRole, role }) => - (canSetRole || canAdministerRole || canInheritRole) && - role !== snapshot.administrativeRole - ); - const privilegedMemberships = nonAdministrativeMemberships.filter( - hasPrivilegedMembership + (canSetRole || canAdministerRole || canInheritRole) && role !== snapshot.administrativeRole, ); + const privilegedMemberships = nonAdministrativeMemberships.filter(hasPrivilegedMembership); addFinding(findings, privilegedMemberships.length > 0, { code: 'runtime_role_can_assume_privileged_role', evidence: 'The runtime role can reach a non-administrative identity with predefined-role, cluster, database, schema, relation, routine, type, or parameter authority through inheritance, SET ROLE, or ADMIN OPTION.', severity: 'critical', }); - addFinding( - findings, - nonAdministrativeMemberships.length > privilegedMemberships.length, - { - code: 'runtime_role_can_assume_other_role', - evidence: - 'The runtime role can inherit, SET ROLE to, or administer at least one additional identity.', - severity: 'high', - } - ); + addFinding(findings, nonAdministrativeMemberships.length > privilegedMemberships.length, { + code: 'runtime_role_can_assume_other_role', + evidence: 'The runtime role can inherit, SET ROLE to, or administer at least one additional identity.', + severity: 'high', + }); addFinding(findings, hasDdlAuthority(snapshot), { code: 'runtime_role_has_ddl_authority', evidence: @@ -528,31 +446,22 @@ export const buildDatabaseTrustBoundaryReport = ( severity: 'high', }); const relationControlTables = tables.filter( - ({ privileges }) => - privileges.maintain || - privileges.references || - privileges.trigger || - privileges.truncate + ({ privileges }) => privileges.maintain || privileges.references || privileges.trigger || privileges.truncate, ); addFinding(findings, relationControlTables.length > 0, { code: 'runtime_role_has_relation_control_authority', - evidence: - 'The runtime role has MAINTAIN, TRUNCATE, REFERENCES, or TRIGGER on an audited table-like relation.', + evidence: 'The runtime role has MAINTAIN, TRUNCATE, REFERENCES, or TRIGGER on an audited table-like relation.', severity: 'high', }); const executableSecurityDefiners = routines.filter( - ({ executable, owner, securityDefiner }) => - executable && securityDefiner && owner !== snapshot.runtimeRole + ({ executable, owner, securityDefiner }) => executable && securityDefiner && owner !== snapshot.runtimeRole, ); addFinding(findings, executableSecurityDefiners.length > 0, { code: 'runtime_role_can_execute_security_definer', - evidence: - 'The runtime role can execute a SECURITY DEFINER routine owned by another role in an audited schema.', + evidence: 'The runtime role can execute a SECURITY DEFINER routine owned by another role in an audited schema.', severity: 'high', }); - const privilegedOwnerViews = tables.filter((table) => - isPrivilegedOwnerView(table, snapshot.administrativeRole) - ); + const privilegedOwnerViews = tables.filter((table) => isPrivilegedOwnerView(table, snapshot.administrativeRole)); addFinding(findings, privilegedOwnerViews.length > 0, { code: 'runtime_role_can_use_privileged_owner_view', evidence: @@ -565,16 +474,12 @@ export const buildDatabaseTrustBoundaryReport = ( 'The runtime role has an explicit effective SET or ALTER SYSTEM privilege on a PostgreSQL configuration parameter.', severity: 'critical', }); - addFinding( - findings, - grantOptions.length > 0 || grantableDefaultPrivileges.length > 0, - { - code: 'runtime_role_has_grant_authority', - evidence: - 'The runtime role has a grant option on at least one existing or creator-default database object privilege.', - severity: 'high', - } - ); + addFinding(findings, grantOptions.length > 0 || grantableDefaultPrivileges.length > 0, { + code: 'runtime_role_has_grant_authority', + evidence: + 'The runtime role has a grant option on at least one existing or creator-default database object privilege.', + severity: 'high', + }); addFinding( findings, sequences.some(({ privileges }) => privileges.update), @@ -582,18 +487,16 @@ export const buildDatabaseTrustBoundaryReport = ( code: 'runtime_role_has_sequence_mutation_authority', evidence: 'The runtime role has UPDATE on an audited sequence.', severity: 'high', - } + }, ); addFinding( findings, - snapshot.trustedContext.tenantSettingSettable || - snapshot.trustedContext.legalEntitySettingSettable, + snapshot.trustedContext.tenantSettingSettable || snapshot.trustedContext.legalEntitySettingSettable, { code: 'runtime_role_can_forge_trusted_context', - evidence: - 'The ordinary runtime role can set and read at least one custom GUC used by tenant RLS.', + evidence: 'The ordinary runtime role can set and read at least one custom GUC used by tenant RLS.', severity: 'high', - } + }, ); addFinding( findings, @@ -601,16 +504,14 @@ export const buildDatabaseTrustBoundaryReport = ( snapshot.trustedContext.legalEntitySettingRetainedAfterRollback, { code: 'trusted_context_survives_transaction', - evidence: - 'A probed transaction-local trusted context value remained visible after rollback.', + evidence: 'A probed transaction-local trusted context value remained visible after rollback.', severity: 'critical', - } + }, ); const dmlSchemas = new Set(dmlTables.map(({ schema }) => schema)); addFinding(findings, dmlSchemas.size > 1, { code: 'runtime_role_has_cross_schema_dml', - evidence: - 'One runtime role has DML privileges in more than one audited application schema.', + evidence: 'One runtime role has DML privileges in more than one audited application schema.', severity: 'high', }); diff --git a/app/scripts/ensure-local-environment.mts b/app/scripts/ensure-local-environment.mts index 3aed978ed..2a8e805c6 100644 --- a/app/scripts/ensure-local-environment.mts +++ b/app/scripts/ensure-local-environment.mts @@ -1,23 +1,9 @@ #!/usr/bin/env node import { NodeFileSystem, NodePath } from '@effect/platform-node'; -import { - Cause, - Config, - Effect, - Exit, - FileSystem, - Layer, - Option, - Path, - Redacted, - Schema, -} from 'effect'; +import { Cause, Config, Effect, Exit, FileSystem, Layer, Option, Path, Redacted, Schema } from 'effect'; import { APP_ENV_PATH } from '../packages/core-runtime/src/environment/workspace-environment.ts'; -import { - localPublicClientValues, - localSpiceDbValues, -} from './local-environment-values.mts'; +import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; const ShellIdSchema = Schema.String.pipe(Schema.brand('ShellId')); const TopologySchema = Schema.fromJsonString( @@ -25,7 +11,7 @@ const TopologySchema = Schema.fromJsonString( shell: Schema.Struct({ id: ShellIdSchema, }), - }) + }), ); const LocalOverlaySchema = Schema.fromJsonString( Schema.Struct({ @@ -33,7 +19,7 @@ const LocalOverlaySchema = Schema.fromJsonString( 'party-registry': Schema.String, }), ports: Schema.Record(Schema.String, Schema.Number), - }) + }), ); const PublicClientTopologySchema = Schema.Struct({ partyRegistryApiBaseUrl: Schema.String, @@ -41,8 +27,7 @@ const PublicClientTopologySchema = Schema.Struct({ shellPort: Schema.Number, }); -const optionalTrimmedString = (name: string) => - Config.option(Config.schema(Schema.Trim, name)); +const optionalTrimmedString = (name: string) => Config.option(Config.schema(Schema.Trim, name)); const LocalEnvironmentOverrides = Config.all({ grpcPort: optionalTrimmedString('LOCAL_SPICEDB_GRPC_PORT'), httpPort: optionalTrimmedString('LOCAL_SPICEDB_HTTP_PORT'), @@ -51,50 +36,39 @@ const LocalEnvironmentOverrides = Config.all({ Schema.RedactedFromValue(Schema.Trim, { label: 'LOCAL_SPICEDB_PRESHARED_KEY', }), - 'LOCAL_SPICEDB_PRESHARED_KEY' - ) + 'LOCAL_SPICEDB_PRESHARED_KEY', + ), ), }); const nonEmptyValue = (value: Option.Option): string | undefined => value.pipe( Option.filter((candidate) => candidate.length > 0), - Option.getOrUndefined + Option.getOrUndefined, ); -const nonEmptyRedactedValue = ( - value: Option.Option -): Redacted.Redacted | undefined => +const nonEmptyRedactedValue = (value: Option.Option): Redacted.Redacted | undefined => value.pipe( Option.filter((candidate) => Redacted.value(candidate).length > 0), - Option.getOrUndefined + Option.getOrUndefined, ); const main = Effect.gen(function* ensureLocalEnvironment() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const topologyPath = yield* path.fromFileUrl( - new URL('../topology/reference-topology.json', import.meta.url) - ); - const overlayPath = yield* path.fromFileUrl( - new URL('../topology/local-overlays/development.json', import.meta.url) - ); - const [original, topologySource, overlaySource, overrides] = - yield* Effect.all([ - fileSystem.readFileString(APP_ENV_PATH, 'utf-8'), - fileSystem.readFileString(topologyPath, 'utf-8'), - fileSystem.readFileString(overlayPath, 'utf-8'), - LocalEnvironmentOverrides, - ]); - const topology = - yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); - const overlay = - yield* Schema.decodeUnknownEffect(LocalOverlaySchema)(overlaySource); + const topologyPath = yield* path.fromFileUrl(new URL('../topology/reference-topology.json', import.meta.url)); + const overlayPath = yield* path.fromFileUrl(new URL('../topology/local-overlays/development.json', import.meta.url)); + const [original, topologySource, overlaySource, overrides] = yield* Effect.all([ + fileSystem.readFileString(APP_ENV_PATH, 'utf-8'), + fileSystem.readFileString(topologyPath, 'utf-8'), + fileSystem.readFileString(overlayPath, 'utf-8'), + LocalEnvironmentOverrides, + ]); + const topology = yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); + const overlay = yield* Schema.decodeUnknownEffect(LocalOverlaySchema)(overlaySource); const lines = original.replaceAll('\r\n', '\n').split('\n'); const shellId = topology.shell.id; - const publicClientTopology = yield* Schema.decodeUnknownEffect( - PublicClientTopologySchema - )({ + const publicClientTopology = yield* Schema.decodeUnknownEffect(PublicClientTopologySchema)({ partyRegistryApiBaseUrl: overlay.apis['party-registry'], shellId, shellPort: overlay.ports[shellId], @@ -107,7 +81,7 @@ const main = Effect.gen(function* ensureLocalEnvironment() { httpPort: nonEmptyValue(overrides.httpPort), preSharedKey: nonEmptyRedactedValue(overrides.preSharedKey), }), - }) + }), ); const updated = lines.map((line) => { const match = /^(?[A-Z][A-Z0-9_]*)=/u.exec(line); @@ -142,8 +116,8 @@ const NodeServicesLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer); const exit = await Effect.runPromiseExit( main.pipe( Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause))), - Effect.provide(NodeServicesLive) - ) + Effect.provide(NodeServicesLive), + ), ); if (Exit.isFailure(exit)) { process.exitCode = 1; diff --git a/app/scripts/generate-node-backend-federation.mts b/app/scripts/generate-node-backend-federation.mts index a669383fb..5aa364da9 100644 --- a/app/scripts/generate-node-backend-federation.mts +++ b/app/scripts/generate-node-backend-federation.mts @@ -2,28 +2,23 @@ import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; -import { - runUltramodernScript, - ultramodernExitCode, -} from './shared/ultramodern-command.mts'; +import { runUltramodernScript, ultramodernExitCode } from './shared/ultramodern-command.mts'; class BackendFederationGenerationError extends Schema.TaggedError()( 'BackendFederationGenerationError', - { reason: Schema.String } + { reason: Schema.String }, ) {} -const failure = (reason: string): BackendFederationGenerationError => - new BackendFederationGenerationError({ reason }); +const failure = (reason: string): BackendFederationGenerationError => new BackendFederationGenerationError({ reason }); const exit = await Effect.runPromiseExit( runUltramodernScript({ command: 'backend-federation-generate', - directoryFailure: - 'Unable to resolve the backend-federation generator directory', + directoryFailure: 'Unable to resolve the backend-federation generator directory', failure, launchErrorDetail: (error) => `: ${error.message}`, moduleUrl: import.meta.url, nodeExecutable: process.execPath, - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped) + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); process.exitCode = ultramodernExitCode(exit); diff --git a/app/scripts/generate-ontos-module-contract.mts b/app/scripts/generate-ontos-module-contract.mts index 70eca5256..fe220e7b9 100644 --- a/app/scripts/generate-ontos-module-contract.mts +++ b/app/scripts/generate-ontos-module-contract.mts @@ -5,16 +5,7 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { - Effect, - Exit, - FileSystem, - ManagedRuntime, - Path, - Predicate, - Schema, - Stream, -} from 'effect'; +import { Effect, Exit, FileSystem, ManagedRuntime, Path, Predicate, Schema, Stream } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; import { HttpApi } from 'effect/unstable/httpapi'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; @@ -28,10 +19,7 @@ import { OntosModuleIdSchema, extractVerticalRuntimeSafeDescriptors, } from '../packages/core-runtime/src/index.ts'; -import type { - OntosModuleManifest, - VerticalRuntimeRegistration, -} from '../packages/core-runtime/src/index.ts'; +import type { OntosModuleManifest, VerticalRuntimeRegistration } from '../packages/core-runtime/src/index.ts'; import { MODULE_CONTRACT_GENERATOR_HEADER, MODULE_MANIFEST_ACTION_SLOT_END, @@ -49,12 +37,8 @@ import { toPascalCase, } from './scaffolding/shared.mts'; -export const OntosModuleContractTargetSchema = Schema.Literals([ - 'cloudflare-dist', - 'dist', -]); -export type OntosModuleContractTarget = - typeof OntosModuleContractTargetSchema.Type; +export const OntosModuleContractTargetSchema = Schema.Literals(['cloudflare-dist', 'dist']); +export type OntosModuleContractTarget = typeof OntosModuleContractTargetSchema.Type; interface GenerateInput { readonly target: OntosModuleContractTarget; @@ -77,7 +61,7 @@ export class OntosModuleContractGenerationError extends Schema.TaggedError +const isOntosModuleManifest = (cause: unknown): cause is OntosModuleManifest => Predicate.isObject(cause); +const isVerticalRuntimeRegistration = (cause: unknown): cause is VerticalRuntimeRegistration => Predicate.isObject(cause); -const isVerticalRuntimeRegistration = ( - cause: unknown -): cause is VerticalRuntimeRegistration => Predicate.isObject(cause); const LoadedOwnerModuleSchema = Schema.Struct({ manifest: Schema.declare(isOntosModuleManifest), registration: Schema.declare(isVerticalRuntimeRegistration), }); -const decodeLoadedOwnerModule = Schema.decodeUnknownPromise( - LoadedOwnerModuleSchema -); +const decodeLoadedOwnerModule = Schema.decodeUnknownPromise(LoadedOwnerModuleSchema); const PackageJsonTextSchema = Schema.fromJsonString(ModulePackageSchema); -const ReferenceTopologyTextSchema = Schema.fromJsonString( - ReferenceTopologySchema -); -const ContractJsonTextSchema = Schema.fromJsonString( - OntosModuleDeploymentContractSchema, - { - space: 2, - } -); +const ReferenceTopologyTextSchema = Schema.fromJsonString(ReferenceTopologySchema); +const ContractJsonTextSchema = Schema.fromJsonString(OntosModuleDeploymentContractSchema, { + space: 2, +}); const JsonDocumentTextSchema = Schema.fromJsonString(Schema.Unknown, { space: 2, }); const JsonStringTextSchema = Schema.fromJsonString(Schema.String); const canonicalSlugPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; -const outputRootByTarget: Readonly> = - Object.freeze({ - 'cloudflare-dist': 'dist-cloudflare', - dist: 'dist', - }); +const outputRootByTarget: Readonly> = Object.freeze({ + 'cloudflare-dist': 'dist-cloudflare', + dist: 'dist', +}); -const sha256 = (value: string): string => - createHash('sha256').update(value).digest('hex'); +const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex'); const require = createRequire(import.meta.url); -const failure = ( - message: string, - cause?: unknown -): OntosModuleContractGenerationError => +const failure = (message: string, cause?: unknown): OntosModuleContractGenerationError => new OntosModuleContractGenerationError({ cause, message }); const repositoryEsbuildPath = (): string => { @@ -167,9 +133,7 @@ const repositoryEsbuildPath = (): string => { }; const assertPlainTarget = (value: string, label: string, pattern: RegExp) => - pattern.test(value) - ? Effect.succeed(value) - : Effect.fail(failure(`${label} must be one safe generated identifier`)); + pattern.test(value) ? Effect.succeed(value) : Effect.fail(failure(`${label} must be one safe generated identifier`)); const assertOwnerSlots = (verticalDirectory: string) => Effect.gen(function* assertOwnerSlotsEffect() { @@ -202,37 +166,24 @@ const assertOwnerSlots = (verticalDirectory: string) => (owner) => fileSystem.readFileString(owner.path).pipe( Effect.map((content) => ({ ...owner, content })), - Effect.mapError((cause) => - failure(`unable to read module contract owner ${owner.path}`, cause) - ) + Effect.mapError((cause) => failure(`unable to read module contract owner ${owner.path}`, cause)), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); for (const owner of ownerContents) { const { content } = owner; if (!content.startsWith(`${MODULE_CONTRACT_GENERATOR_HEADER}\n`)) { - yield* failure( - `module contract owner is missing its generated header: ${owner.path}` - ); + yield* failure(`module contract owner is missing its generated header: ${owner.path}`); } for (const slot of owner.slots) { - if ( - !content.includes(slot) || - content.indexOf(slot) !== content.lastIndexOf(slot) - ) { - yield* failure( - `module contract owner must contain exactly one ${slot} slot` - ); + if (!content.includes(slot) || content.indexOf(slot) !== content.lastIndexOf(slot)) { + yield* failure(`module contract owner must contain exactly one ${slot} slot`); } } } }); -const loadOwnerValues = ( - workspaceRoot: string, - verticalDirectory: string, - vertical: string -) => +const loadOwnerValues = (workspaceRoot: string, verticalDirectory: string, vertical: string) => Effect.scoped( Effect.gen(function* loadOwnerValuesEffect() { const fileSystem = yield* FileSystem.FileSystem; @@ -243,50 +194,24 @@ const loadOwnerValues = ( directory: verticalDirectory, prefix: '.ontos-contract-', }) - .pipe( - Effect.mapError((cause) => - failure( - 'unable to create the module contract temporary directory', - cause - ) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to create the module contract temporary directory', cause))); const entryPath = platformPath.join(temporaryDirectory, 'entry.mts'); const bundlePath = platformPath.join(temporaryDirectory, 'bundle.mjs'); - const manifestPath = platformPath.join( - verticalDirectory, - 'vertical.manifest.ts' - ); - const registrationPath = platformPath.join( - verticalDirectory, - 'vertical.registration.ts' - ); + const manifestPath = platformPath.join(verticalDirectory, 'vertical.manifest.ts'); + const registrationPath = platformPath.join(verticalDirectory, 'vertical.registration.ts'); const prefix = toCamelCase(vertical); - const encodedManifestPath = yield* Schema.encodeEffect( - JsonStringTextSchema - )(manifestPath).pipe( - Effect.mapError((cause) => - failure('unable to encode the manifest owner path', cause) - ) + const encodedManifestPath = yield* Schema.encodeEffect(JsonStringTextSchema)(manifestPath).pipe( + Effect.mapError((cause) => failure('unable to encode the manifest owner path', cause)), ); - const encodedRegistrationPath = yield* Schema.encodeEffect( - JsonStringTextSchema - )(registrationPath).pipe( - Effect.mapError((cause) => - failure('unable to encode the registration owner path', cause) - ) + const encodedRegistrationPath = yield* Schema.encodeEffect(JsonStringTextSchema)(registrationPath).pipe( + Effect.mapError((cause) => failure('unable to encode the registration owner path', cause)), ); const entry = `import { ${prefix}Manifest as manifest } from ${encodedManifestPath};\nimport { ${prefix}Registration as registration } from ${encodedRegistrationPath};\nexport { manifest, registration };\n`; yield* fileSystem .writeFileString(entryPath, entry) - .pipe( - Effect.mapError((cause) => - failure('unable to write the module contract bundle entry', cause) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to write the module contract bundle entry', cause))); const esbuildPath = yield* Effect.try({ - catch: (cause) => - failure('unable to resolve the repository esbuild executable', cause), + catch: (cause) => failure('unable to resolve the repository esbuild executable', cause), try: repositoryEsbuildPath, }); const handle = yield* spawner @@ -301,42 +226,25 @@ const loadOwnerValues = ( '--packages=external', `--outfile=${bundlePath}`, ], - { cwd: workspaceRoot } - ) - ) - .pipe( - Effect.mapError((cause) => - failure('module contract owner bundle failed', cause) - ) - ); - const bundleOutput = yield* Stream.mkString( - handle.all.pipe(Stream.decodeText()) - ).pipe( - Effect.mapError((cause) => - failure('unable to collect module contract bundle output', cause) + { cwd: workspaceRoot }, + ), ) + .pipe(Effect.mapError((cause) => failure('module contract owner bundle failed', cause))); + const bundleOutput = yield* Stream.mkString(handle.all.pipe(Stream.decodeText())).pipe( + Effect.mapError((cause) => failure('unable to collect module contract bundle output', cause)), ); const bundleExitCode = yield* handle.exitCode.pipe( - Effect.mapError((cause) => - failure('unable to read module contract bundle exit code', cause) - ) + Effect.mapError((cause) => failure('unable to read module contract bundle exit code', cause)), ); if (bundleExitCode !== ChildProcessSpawner.ExitCode(0)) { - return yield* failure( - `module contract owner bundle failed: ${bundleOutput.trim()}` - ); + return yield* failure(`module contract owner bundle failed: ${bundleOutput.trim()}`); } return yield* Effect.tryPromise({ - catch: (cause) => - failure('unable to import the bundled module contract owners', cause), + catch: (cause) => failure('unable to import the bundled module contract owners', cause), try: async (): Promise => - await decodeLoadedOwnerModule( - await import( - `${pathToFileURL(bundlePath).href}?build=${randomUUID()}` - ) - ), + await decodeLoadedOwnerModule(await import(`${pathToFileURL(bundlePath).href}?build=${randomUUID()}`)), }); - }) + }), ); const componentExposes = (verticalDirectory: string) => @@ -344,17 +252,10 @@ const componentExposes = (verticalDirectory: string) => const fileSystem = yield* FileSystem.FileSystem; const platformPath = yield* Path.Path; const config = yield* fileSystem - .readFileString( - platformPath.join(verticalDirectory, 'module-federation.config.ts') - ) - .pipe( - Effect.mapError((cause) => - failure('unable to read the Module Federation configuration', cause) - ) - ); + .readFileString(platformPath.join(verticalDirectory, 'module-federation.config.ts')) + .pipe(Effect.mapError((cause) => failure('unable to read the Module Federation configuration', cause))); const exposes = new Set(); - const pattern = - /['"](?\.\/[A-Za-z][A-Za-z0-9_-]*)['"]\s*:\s*['"][^'"]+['"]/gu; + const pattern = /['"](?\.\/[A-Za-z][A-Za-z0-9_-]*)['"]\s*:\s*['"][^'"]+['"]/gu; for (const match of config.matchAll(pattern)) { const key = match.groups?.key; if (key !== undefined) { @@ -370,15 +271,10 @@ const toKebab = (value: string): string => .replaceAll('_', '-') .toLowerCase(); -const sorted = ( - values: readonly Value[], - compare: (left: Value, right: Value) => number -): readonly Value[] => { +const sorted = (values: readonly Value[], compare: (left: Value, right: Value) => number): readonly Value[] => { const result: Value[] = []; for (const value of values) { - const insertionIndex = result.findIndex( - (existing) => compare(value, existing) < 0 - ); + const insertionIndex = result.findIndex((existing) => compare(value, existing) < 0); if (insertionIndex === -1) { result.push(value); } else { @@ -392,143 +288,80 @@ const deriveApiOperationKeys = (api: HttpApi.Top): readonly string[] => sorted( Object.values(api.groups).flatMap((group) => Object.values(group.endpoints).map((endpoint) => - group.topLevel - ? endpoint.identifier - : `${group.identifier}.${endpoint.identifier}` - ) + group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`, + ), ), - (left, right) => left.localeCompare(right) + (left, right) => left.localeCompare(right), ); -const deriveContract = ( - workspaceRoot: string, - vertical: string, - owner: LoadedOwnerValues -) => +const deriveContract = (workspaceRoot: string, vertical: string, owner: LoadedOwnerValues) => Effect.gen(function* deriveContractEffect() { const fileSystem = yield* FileSystem.FileSystem; const platformPath = yield* Path.Path; - const verticalDirectory = platformPath.join( - workspaceRoot, - 'verticals', - vertical - ); + const verticalDirectory = platformPath.join(workspaceRoot, 'verticals', vertical); const packageJsonSource = yield* fileSystem .readFileString(platformPath.join(verticalDirectory, 'package.json')) - .pipe( - Effect.mapError((cause) => - failure('unable to read the vertical package metadata', cause) - ) - ); - const packageJson = yield* Schema.decodeUnknownEffect( - PackageJsonTextSchema - )(packageJsonSource).pipe( - Effect.mapError((cause) => - failure('vertical package metadata is invalid', cause) - ) + .pipe(Effect.mapError((cause) => failure('unable to read the vertical package metadata', cause))); + const packageJson = yield* Schema.decodeUnknownEffect(PackageJsonTextSchema)(packageJsonSource).pipe( + Effect.mapError((cause) => failure('vertical package metadata is invalid', cause)), ); const topologySource = yield* fileSystem - .readFileString( - platformPath.join(workspaceRoot, 'topology/reference-topology.json') - ) - .pipe( - Effect.mapError((cause) => - failure('unable to read the reference topology', cause) - ) - ); - const topology = yield* Schema.decodeUnknownEffect( - ReferenceTopologyTextSchema - )(topologySource).pipe( - Effect.mapError((cause) => - failure('reference topology is invalid', cause) - ) + .readFileString(platformPath.join(workspaceRoot, 'topology/reference-topology.json')) + .pipe(Effect.mapError((cause) => failure('unable to read the reference topology', cause))); + const topology = yield* Schema.decodeUnknownEffect(ReferenceTopologyTextSchema)(topologySource).pipe( + Effect.mapError((cause) => failure('reference topology is invalid', cause)), ); const matchesOwnerModule = () => - packageJson.modernjs?.ontosModule?.moduleId === - owner.manifest.module.id && - packageJson.modernjs.ontosModule.schemaVersion === - ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION; - const validateDeploymentIdentity = Effect.gen( - function* validateDeploymentIdentityEffect() { - const appId = packageJson.modernjs?.appId; - const topologyEntries = topology.verticals?.filter( - (entry) => - entry.id === appId && - entry.package === packageJson.name && - entry.path === `verticals/${vertical}` - ); - if (appId === undefined || topologyEntries?.length !== 1) { - return yield* failure( - 'vertical package and topology deployment identity do not match exactly' - ); - } - if (!matchesOwnerModule()) { - return yield* failure( - 'generated package module marker does not match the owner manifest' - ); - } - const [topologyEntry] = topologyEntries; - if ( - topologyEntry === undefined || - topologyEntry.moduleFederation?.name === undefined - ) { - return yield* failure( - 'vertical topology Module Federation boundary is missing' - ); - } - const moduleFederationName = topologyEntry.moduleFederation.name; - - return { appId, moduleFederationName, topologyEntry }; + packageJson.modernjs?.ontosModule?.moduleId === owner.manifest.module.id && + packageJson.modernjs.ontosModule.schemaVersion === ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION; + const validateDeploymentIdentity = Effect.gen(function* validateDeploymentIdentityEffect() { + const appId = packageJson.modernjs?.appId; + const topologyEntries = topology.verticals?.filter( + (entry) => entry.id === appId && entry.package === packageJson.name && entry.path === `verticals/${vertical}`, + ); + if (appId === undefined || topologyEntries?.length !== 1) { + return yield* failure('vertical package and topology deployment identity do not match exactly'); } - ); - const { appId, moduleFederationName, topologyEntry } = - yield* validateDeploymentIdentity; + if (!matchesOwnerModule()) { + return yield* failure('generated package module marker does not match the owner manifest'); + } + const [topologyEntry] = topologyEntries; + if (topologyEntry === undefined || topologyEntry.moduleFederation?.name === undefined) { + return yield* failure('vertical topology Module Federation boundary is missing'); + } + const moduleFederationName = topologyEntry.moduleFederation.name; + + return { appId, moduleFederationName, topologyEntry }; + }); + const { appId, moduleFederationName, topologyEntry } = yield* validateDeploymentIdentity; const exposes = yield* componentExposes(verticalDirectory); - const validatePublicDescriptors = Effect.gen( - function* validatePublicDescriptorsEffect() { - const componentKeys = Object.keys( - owner.manifest.publicSurface.components - ); - for (const key of componentKeys) { - if (!exposes.has(`./${toPascalCase(key)}`)) { - return yield* failure( - `public component ${key} has no matching Module Federation exposure` - ); - } + const validatePublicDescriptors = Effect.gen(function* validatePublicDescriptorsEffect() { + const componentKeys = Object.keys(owner.manifest.publicSurface.components); + for (const key of componentKeys) { + if (!exposes.has(`./${toPascalCase(key)}`)) { + return yield* failure(`public component ${key} has no matching Module Federation exposure`); } - const safeRuntime = extractVerticalRuntimeSafeDescriptors( - owner.registration - ); - const manifestActionKeys = sorted( - owner.manifest.publicSurface.actions.map( - ({ descriptor }) => descriptor.actionKey - ), - (left, right) => left.localeCompare(right) - ); - const runtimeActionKeys = safeRuntime.actions.map( - ({ actionKey }) => actionKey - ); - if ( - manifestActionKeys.length !== runtimeActionKeys.length || - manifestActionKeys.some( - (actionKey, index) => actionKey !== runtimeActionKeys[index] - ) - ) { - return yield* failure( - 'manifest Actions and private runtime Action descriptors do not match' - ); - } - - return { componentKeys, safeRuntime }; } - ); + const safeRuntime = extractVerticalRuntimeSafeDescriptors(owner.registration); + const manifestActionKeys = sorted( + owner.manifest.publicSurface.actions.map(({ descriptor }) => descriptor.actionKey), + (left, right) => left.localeCompare(right), + ); + const runtimeActionKeys = safeRuntime.actions.map(({ actionKey }) => actionKey); + if ( + manifestActionKeys.length !== runtimeActionKeys.length || + manifestActionKeys.some((actionKey, index) => actionKey !== runtimeActionKeys[index]) + ) { + return yield* failure('manifest Actions and private runtime Action descriptors do not match'); + } + + return { componentKeys, safeRuntime }; + }); const { componentKeys, safeRuntime } = yield* validatePublicDescriptors; const events = yield* Effect.forEach( owner.manifest.publicSurface.events, (event) => - Schema.encodeEffect(JsonDocumentTextSchema)( - Schema.toJsonSchemaDocument(event.payloadSchema) - ).pipe( + Schema.encodeEffect(JsonDocumentTextSchema)(Schema.toJsonSchemaDocument(event.payloadSchema)).pipe( Effect.map((payloadDocument) => ({ key: event.key, owningModuleId: event.owningModuleId, @@ -537,53 +370,40 @@ const deriveContract = ( tense: event.tense, visibility: event.visibility, })), - Effect.mapError((cause) => - failure( - `unable to encode the ${event.key} event payload contract`, - cause - ) - ) + Effect.mapError((cause) => failure(`unable to encode the ${event.key} event payload contract`, cause)), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); - const apiContracts = yield* Effect.forEach( - Object.entries(owner.manifest.publicSurface.api), - ([key, value]) => { - if (!HttpApi.isHttpApi(value)) { - return Effect.fail(failure(`public API ${key} is not an HttpApi`)); - } - return Effect.succeed({ - key: `${owner.manifest.module.id}.${toKebab(key)}`, - operationKeys: deriveApiOperationKeys(value), - }); + const apiContracts = yield* Effect.forEach(Object.entries(owner.manifest.publicSurface.api), ([key, value]) => { + if (!HttpApi.isHttpApi(value)) { + return Effect.fail(failure(`public API ${key} is not an HttpApi`)); } - ); + return Effect.succeed({ + key: `${owner.manifest.module.id}.${toKebab(key)}`, + operationKeys: deriveApiOperationKeys(value), + }); + }); const contract = { deployment: { appId, buildMarker: - topologyEntry.deliveryUnit?.buildMarker ?? - sha256(`${appId}:${packageJson.version ?? '0.0.0'}`).slice(0, 16), + topologyEntry.deliveryUnit?.buildMarker ?? sha256(`${appId}:${packageJson.version ?? '0.0.0'}`).slice(0, 16), }, manifest: { activation: owner.manifest.activation, module: owner.manifest.module, publicSurface: { actions: safeRuntime.actions, - api: sorted(apiContracts, (left, right) => - left.key.localeCompare(right.key) - ), + api: sorted(apiContracts, (left, right) => left.key.localeCompare(right.key)), components: sorted( componentKeys.map((key) => ({ expose: `./${toPascalCase(key)}`, key: `${owner.manifest.module.id}.${toKebab(key)}`, mfBoundaryId: moduleFederationName, })), - (left, right) => left.key.localeCompare(right.key) - ), - events: sorted(events, (left, right) => - left.key.localeCompare(right.key) + (left, right) => left.key.localeCompare(right.key), ), + events: sorted(events, (left, right) => left.key.localeCompare(right.key)), reports: owner.manifest.publicSurface.reports, resourceTypes: owner.manifest.publicSurface.resourceTypes, search: owner.manifest.publicSurface.search, @@ -593,42 +413,19 @@ const deriveContract = ( runtime: { outboxSubscriptions: safeRuntime.outboxSubscriptions }, schemaVersion: ONTOS_MODULE_CONTRACT_SCHEMA_VERSION, } as const; - return yield* Schema.decodeUnknownEffect( - OntosModuleDeploymentContractSchema, - { - onExcessProperty: 'error', - } - )(contract).pipe( - Effect.mapError((cause) => - failure('derived OntOS module contract is invalid', cause) - ) - ); + return yield* Schema.decodeUnknownEffect(OntosModuleDeploymentContractSchema, { + onExcessProperty: 'error', + })(contract).pipe(Effect.mapError((cause) => failure('derived OntOS module contract is invalid', cause))); }); -export const deriveOntosModuleDeploymentContract = ( - input: DeriveOntosModuleContractInput -) => +export const deriveOntosModuleDeploymentContract = (input: DeriveOntosModuleContractInput) => Effect.gen(function* deriveDeploymentContractProgram() { const platformPath = yield* Path.Path; - const workspaceRoot = platformPath.resolve( - input.workspaceRoot ?? platformPath.join(import.meta.dirname, '..') - ); - const vertical = yield* assertPlainTarget( - input.vertical, - 'vertical', - canonicalSlugPattern - ); - const verticalDirectory = platformPath.join( - workspaceRoot, - 'verticals', - vertical - ); + const workspaceRoot = platformPath.resolve(input.workspaceRoot ?? platformPath.join(import.meta.dirname, '..')); + const vertical = yield* assertPlainTarget(input.vertical, 'vertical', canonicalSlugPattern); + const verticalDirectory = platformPath.join(workspaceRoot, 'verticals', vertical); yield* assertOwnerSlots(verticalDirectory); - const owner = yield* loadOwnerValues( - workspaceRoot, - verticalDirectory, - vertical - ); + const owner = yield* loadOwnerValues(workspaceRoot, verticalDirectory, vertical); return yield* deriveContract(workspaceRoot, vertical, owner); }); @@ -638,88 +435,46 @@ export const generateOntosModuleContract = (input: GenerateInput) => Effect.gen(function* generateContractProgram() { const fileSystem = yield* FileSystem.FileSystem; const platformPath = yield* Path.Path; - const workspaceRoot = platformPath.resolve( - input.workspaceRoot ?? platformPath.join(import.meta.dirname, '..') - ); - const vertical = yield* assertPlainTarget( - input.vertical, - 'vertical', - canonicalSlugPattern - ); - const target = yield* Schema.decodeUnknownEffect( - OntosModuleContractTargetSchema - )(input.target).pipe( - Effect.mapError(() => failure('target must be dist or cloudflare-dist')) - ); - const verticalDirectory = platformPath.join( - workspaceRoot, - 'verticals', - vertical + const workspaceRoot = platformPath.resolve(input.workspaceRoot ?? platformPath.join(import.meta.dirname, '..')); + const vertical = yield* assertPlainTarget(input.vertical, 'vertical', canonicalSlugPattern); + const target = yield* Schema.decodeUnknownEffect(OntosModuleContractTargetSchema)(input.target).pipe( + Effect.mapError(() => failure('target must be dist or cloudflare-dist')), ); + const verticalDirectory = platformPath.join(workspaceRoot, 'verticals', vertical); const contract = yield* deriveOntosModuleDeploymentContract({ vertical, workspaceRoot, }); - const encodedContract = yield* Schema.encodeEffect(ContractJsonTextSchema)( - contract - ).pipe( - Effect.mapError((cause) => - failure('unable to encode the OntOS module contract', cause) - ) + const encodedContract = yield* Schema.encodeEffect(ContractJsonTextSchema)(contract).pipe( + Effect.mapError((cause) => failure('unable to encode the OntOS module contract', cause)), ); const content = `${encodedContract}\n`; const bytes = Buffer.byteLength(content); if (bytes > ONTOS_MODULE_CONTRACT_MAX_BYTES) { - return yield* failure( - 'generated OntOS module contract exceeds the 1 MiB deployment limit' - ); + return yield* failure('generated OntOS module contract exceeds the 1 MiB deployment limit'); } const outputPath = platformPath.join( verticalDirectory, outputRootByTarget[target], 'public', - ONTOS_MODULE_CONTRACT_PATH.slice(1) + ONTOS_MODULE_CONTRACT_PATH.slice(1), ); yield* fileSystem .makeDirectory(platformPath.dirname(outputPath), { recursive: true }) - .pipe( - Effect.mapError((cause) => - failure( - 'unable to create the module contract output directory', - cause - ) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to create the module contract output directory', cause))); const temporaryPath = `${outputPath}.tmp-${randomUUID()}`; yield* fileSystem .writeFileString(temporaryPath, content) - .pipe( - Effect.mapError((cause) => - failure('unable to write the temporary module contract', cause) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to write the temporary module contract', cause))); yield* fileSystem .rename(temporaryPath, outputPath) - .pipe( - Effect.mapError((cause) => - failure('unable to publish the generated module contract', cause) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to publish the generated module contract', cause))); const etag = `"${sha256(content)}"`; - const headersPath = platformPath.join( - verticalDirectory, - outputRootByTarget[target], - 'public', - '_headers' - ); + const headersPath = platformPath.join(verticalDirectory, outputRootByTarget[target], 'public', '_headers'); const headers = `${ONTOS_MODULE_CONTRACT_PATH}\n Cache-Control: no-cache\n Content-Type: application/json\n ETag: ${etag}\n`; yield* fileSystem .writeFileString(headersPath, headers) - .pipe( - Effect.mapError((cause) => - failure('unable to write the module contract response headers', cause) - ) - ); + .pipe(Effect.mapError((cause) => failure('unable to write the module contract response headers', cause))); return { bytes, etag, path: outputPath }; }); @@ -731,22 +486,15 @@ const cli = Command.make( ({ target, vertical }) => generateOntosModuleContract({ target, vertical }).pipe( Effect.flatMap((result) => - Effect.logInfo( - `Generated ${result.path} (${result.bytes} bytes, ETag ${result.etag})` - ) - ) - ) + Effect.logInfo(`Generated ${result.path} (${result.bytes} bytes, ETag ${result.etag})`), + ), + ), ); -if ( - process.argv[1] !== undefined && - import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href -) { +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { const moduleContractRuntime = ManagedRuntime.make(NodeServices.layer); const exit = await moduleContractRuntime.runPromiseExit( - Command.run({ version: '1.0.0' })(cli).pipe( - Effect.tapError((error) => Effect.logError(error)) - ) + Command.run({ version: '1.0.0' })(cli).pipe(Effect.tapError((error) => Effect.logError(error))), ); if (Exit.isFailure(exit)) { process.exitCode = 1; diff --git a/app/scripts/generate-outbox-worker-deployment.mjs b/app/scripts/generate-outbox-worker-deployment.mjs index aeea358da..614754ff2 100644 --- a/app/scripts/generate-outbox-worker-deployment.mjs +++ b/app/scripts/generate-outbox-worker-deployment.mjs @@ -12,9 +12,9 @@ const TopologySchema = Schema.fromJsonString( moduleFederation: Schema.Struct({ manifestUrl: Schema.String }), package: Schema.String, path: Schema.String, - }) + }), ), - }) + }), ); class OutboxWorkerDeploymentError extends Error { @@ -39,22 +39,17 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => Effect.gen(function* generateDeployment() { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const topologySource = yield* fs.readFileString( - path.join(root, 'topology/reference-topology.json') - ); - const topology = - yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); + const topologySource = yield* fs.readFileString(path.join(root, 'topology/reference-topology.json')); + const topology = yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); let result = source.replace( /\n {2}# [\s\S]*? {2}# <\/generated-outbox-worker-deployments>\n?/u, - '\n' + '\n', ); /** @type {string[]} */ const services = []; for (const vertical of topology.verticals) { const delivery = yield* outboxWorkerDelivery(root, vertical).pipe( - Effect.mapError(() => - failure(`Invalid generated worker delivery for ${vertical.id}`) - ) + Effect.mapError(() => failure(`Invalid generated worker delivery for ${vertical.id}`)), ); if (delivery === undefined) { continue; @@ -63,21 +58,15 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => .split(/(?=^ {2}- setup:)/mu) .find((section) => section.startsWith(` - setup: '${vertical.id}'\n`)); if (ownerSection === undefined) { - return yield* Effect.fail( - failure(`Missing owner deployment for ${vertical.id}`) - ); + return yield* Effect.fail(failure(`Missing owner deployment for ${vertical.id}`)); } - const port = /^ {8}PORT: '(?[0-9]+)'$/mu.exec(ownerSection)?.groups - ?.port; + const port = /^ {8}PORT: '(?[0-9]+)'$/mu.exec(ownerSection)?.groups?.port; const topologyPort = yield* Effect.try({ - catch: () => - failure(`Invalid topology manifest URL for ${vertical.id}`), + catch: () => failure(`Invalid topology manifest URL for ${vertical.id}`), try: () => new URL(vertical.moduleFederation.manifestUrl).port, }); if (port === undefined || port.length === 0 || port !== topologyPort) { - return yield* Effect.fail( - failure(`Owner port disagrees with topology for ${vertical.id}`) - ); + return yield* Effect.fail(failure(`Owner port disagrees with topology for ${vertical.id}`)); } const ownerServiceHostname = vertical.id.replaceAll('-', ''); const service = ownerSection @@ -87,31 +76,20 @@ const generateOutboxWorkerDeploymentEffect = (root, source) => .split('\n') .filter( (line) => - !line.includes(' run build') && - !line.includes("- cp 'app/topology/") && - !line.includes('VERTICAL_') + !line.includes(' run build') && !line.includes("- cp 'app/topology/") && !line.includes('VERTICAL_'), ) .map((line) => line.includes('run zerops:materialize') - ? line.replace( - 'cd app && ', - 'cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" ' - ) - : line + ? line.replace('cd app && ', 'cd app && ULTRAMODERN_SOURCE_REVISION="$(git rev-parse HEAD)" ') + : line, ) .join('\n') - .replace( - /(?run zerops:materialize[^\n]*)/u, - '$ --worker' - ) + .replace(/(?run zerops:materialize[^\n]*)/u, '$ --worker') .replaceAll(`/${vertical.id}-api/${vertical.id}/readiness`, '/ready') - .replace( - `ULTRAMODERN_ZEROPS_SERVICE: ${vertical.id}`, - `ULTRAMODERN_ZEROPS_SERVICE: ${delivery.id}` - ) + .replace(`ULTRAMODERN_ZEROPS_SERVICE: ${vertical.id}`, `ULTRAMODERN_ZEROPS_SERVICE: ${delivery.id}`) .replace( ` PORT: '${port}'`, - ` PORT: '${port}'\n OUTBOX_WORKER_HEALTH_PORT: '${port}'\n DATABASE_URL: \${${ownerServiceHostname}_DATABASE_URL}` + ` PORT: '${port}'\n OUTBOX_WORKER_HEALTH_PORT: '${port}'\n DATABASE_URL: \${${ownerServiceHostname}_DATABASE_URL}`, ); services.push(service); } @@ -143,9 +121,7 @@ const runCommand = ({ write }) => yield* fs.writeFileString(file, generated); } else if (source !== generated) { yield* Effect.fail( - failure( - 'Worker deployment drift: run node scripts/generate-outbox-worker-deployment.mjs --write' - ) + failure('Worker deployment drift: run node scripts/generate-outbox-worker-deployment.mjs --write'), ); } }); @@ -153,7 +129,7 @@ const runCommand = ({ write }) => const command = Command.make( 'generate-outbox-worker-deployment', { write: Flag.boolean('write').pipe(Flag.withDefault(false)) }, - runCommand + runCommand, ); if (import.meta.main) { diff --git a/app/scripts/generate-public-surface-assets.mts b/app/scripts/generate-public-surface-assets.mts index 92c0370ca..68ab3e5aa 100644 --- a/app/scripts/generate-public-surface-assets.mts +++ b/app/scripts/generate-public-surface-assets.mts @@ -2,27 +2,22 @@ import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; -import { - runUltramodernScript, - ultramodernExitCode, -} from './shared/ultramodern-command.mts'; +import { runUltramodernScript, ultramodernExitCode } from './shared/ultramodern-command.mts'; class PublicSurfaceGenerationError extends Schema.TaggedError()( 'PublicSurfaceGenerationError', - { reason: Schema.String } + { reason: Schema.String }, ) {} -const failure = (reason: string): PublicSurfaceGenerationError => - new PublicSurfaceGenerationError({ reason }); +const failure = (reason: string): PublicSurfaceGenerationError => new PublicSurfaceGenerationError({ reason }); const exit = await Effect.runPromiseExit( runUltramodernScript({ command: 'public-surface', - directoryFailure: - 'Unable to resolve the public-surface generator directory', + directoryFailure: 'Unable to resolve the public-surface generator directory', failure, launchErrorDetail: (error) => `: ${error.message}`, moduleUrl: import.meta.url, - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped) + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); process.exitCode = ultramodernExitCode(exit); diff --git a/app/scripts/generate-tanstack-routes.mts b/app/scripts/generate-tanstack-routes.mts index 979e965bf..3b8aad83f 100644 --- a/app/scripts/generate-tanstack-routes.mts +++ b/app/scripts/generate-tanstack-routes.mts @@ -1,42 +1,17 @@ #!/usr/bin/env node import { NodeRuntime, NodeServices } from '@effect/platform-node'; -import { - Array as EffectArray, - Console, - Effect, - FileSystem, - Layer, - Order, - Path, - Random, - Schema, -} from 'effect'; +import { Array as EffectArray, Console, Effect, FileSystem, Layer, Order, Path, Random, Schema } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import { ModuleEntrypointSchema } from '../packages/core-runtime/src/modules/module-entrypoint.ts'; -import { - launchUltramodern, - resolveUltramodernInvocation, -} from './shared/ultramodern-command.mts'; +import { launchUltramodern, resolveUltramodernInvocation } from './shared/ultramodern-command.mts'; -const RouteMetadataIdentifierSchema = Schema.String.pipe( - Schema.brand('RouteMetadataIdentifier') -); -const JsonPrimitiveSchema = Schema.Union([ - Schema.Null, - Schema.Number, - Schema.Boolean, - Schema.String, -]); +const RouteMetadataIdentifierSchema = Schema.String.pipe(Schema.brand('RouteMetadataIdentifier')); +const JsonPrimitiveSchema = Schema.Union([Schema.Null, Schema.Number, Schema.Boolean, Schema.String]); const RouteMetadataValueSchema = Schema.Tree(JsonPrimitiveSchema); -const RouteMetadataFieldsSchema = Schema.Record( - Schema.String, - RouteMetadataValueSchema -); +const RouteMetadataFieldsSchema = Schema.Record(Schema.String, RouteMetadataValueSchema); -const RouteEntrypointSchema = Schema.StructWithRest(ModuleEntrypointSchema, [ - RouteMetadataFieldsSchema, -]); +const RouteEntrypointSchema = Schema.StructWithRest(ModuleEntrypointSchema, [RouteMetadataFieldsSchema]); const RouteMetadataSchema = Schema.StructWithRest( Schema.Struct({ @@ -52,7 +27,7 @@ const RouteMetadataSchema = Schema.StructWithRest( public: Schema.Boolean, titleKey: RouteMetadataIdentifierSchema, }), - [RouteMetadataFieldsSchema] + [RouteMetadataFieldsSchema], ); type RouteMetadata = typeof RouteMetadataSchema.Type; @@ -69,10 +44,10 @@ const UltramodernConfigSchema = Schema.Struct({ Schema.Struct({ id: Schema.String, path: Schema.String, - }) - ) + }), + ), ), - }) + }), ), }); @@ -82,61 +57,39 @@ const PackageConfigSchema = Schema.Struct({ ontosModule: Schema.optionalKey( Schema.Struct({ moduleId: Schema.optionalKey(RouteMetadataIdentifierSchema), - }) + }), ), - }) + }), ), }); -class RouteGenerationError extends Schema.TaggedError()( - 'RouteGenerationError', - { reason: Schema.String } -) {} +class RouteGenerationError extends Schema.TaggedError()('RouteGenerationError', { + reason: Schema.String, +}) {} -const failure = (reason: string): RouteGenerationError => - new RouteGenerationError({ reason }); +const failure = (reason: string): RouteGenerationError => new RouteGenerationError({ reason }); -const decodeUltramodernConfig = Schema.decodeUnknownEffect( - Schema.fromJsonString(UltramodernConfigSchema) -); -const decodePackageConfig = Schema.decodeUnknownEffect( - Schema.fromJsonString(PackageConfigSchema) -); -const encodeJsonString = Schema.encodeEffect( - Schema.fromJsonString(Schema.String) -); -const encodeJson = Schema.encodeEffect( - Schema.fromJsonString(RouteMetadataValueSchema, { space: 2 }) -); +const decodeUltramodernConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(UltramodernConfigSchema)); +const decodePackageConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(PackageConfigSchema)); +const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(RouteMetadataValueSchema, { space: 2 })); const isJsonArray = Schema.is(Schema.Array(RouteMetadataValueSchema)); const isJsonObject = Schema.is(RouteMetadataFieldsSchema); -const sortJsonValue = ( - value: typeof RouteMetadataValueSchema.Type -): typeof RouteMetadataValueSchema.Type => { +const sortJsonValue = (value: typeof RouteMetadataValueSchema.Type): typeof RouteMetadataValueSchema.Type => { if (isJsonArray(value)) { return value.map(sortJsonValue); } if (isJsonObject(value)) { - const sortedEntries = EffectArray.sortWith( - Object.entries(value), - ([key]) => key, - Order.String - ); - return Object.fromEntries( - sortedEntries.map(([key, entry]) => [key, sortJsonValue(entry)]) - ); + const sortedEntries = EffectArray.sortWith(Object.entries(value), ([key]) => key, Order.String); + return Object.fromEntries(sortedEntries.map(([key, entry]) => [key, sortJsonValue(entry)])); } return value; }; const findRouteMetadataFiles = ( - directory: string -): Effect.Effect< - string[], - RouteGenerationError, - FileSystem.FileSystem | Path.Path -> => + directory: string, +): Effect.Effect => Effect.gen(function* findRouteMetadataFilesEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -147,79 +100,57 @@ const findRouteMetadataFiles = ( .filter((entry) => path.basename(entry) === 'route.meta.ts') .map((entry) => path.resolve(directory, entry)); return EffectArray.sort(routeFiles, Order.String); - }).pipe( - Effect.mapError(() => - failure(`Unable to discover route metadata beneath ${directory}`) - ) - ); + }).pipe(Effect.mapError(() => failure(`Unable to discover route metadata beneath ${directory}`))); const isGovernedPageEntrypoint = ( route: RouteMetadata, appId: string, moduleId: string, - expectedScope: 'system' | 'tenant' + expectedScope: 'system' | 'tenant', ): boolean => route.ownerAppId === appId && route.entrypoint.moduleKey === moduleId && route.entrypoint.role === 'page' && - (route.entrypoint.access === 'read' || - route.entrypoint.access === 'historical_read') && + (route.entrypoint.access === 'read' || route.entrypoint.access === 'historical_read') && route.entrypoint.scope === expectedScope && route.entrypoint.entrypointKey.startsWith(`${moduleId}.`); const loadRouteMetadataFile = ( metadataFile: string, appId: string, - moduleId: string + moduleId: string, ): Effect.Effect => Effect.gen(function* loadRouteMetadataFileEffect() { const path = yield* Path.Path; const moduleFileUrl = yield* path .toFileUrl(metadataFile) - .pipe( - Effect.mapError(() => failure(`Unable to resolve ${metadataFile}`)) - ); + .pipe(Effect.mapError(() => failure(`Unable to resolve ${metadataFile}`))); const cacheNonce = yield* Random.nextInt; const moduleUrl = `${moduleFileUrl.href}?generated=${cacheNonce}`; const decodedModule = yield* Effect.tryPromise({ - catch: () => - failure(`Unable to import route metadata from ${metadataFile}`), + catch: () => failure(`Unable to import route metadata from ${metadataFile}`), try: async () => { const importedModule: unknown = await import(moduleUrl); - return Schema.decodeUnknownResult(RouteMetadataModuleSchema)( - importedModule - ); + return Schema.decodeUnknownResult(RouteMetadataModuleSchema)(importedModule); }, }); const routeModule = yield* Effect.fromResult(decodedModule).pipe( - Effect.mapError(() => - failure(`Invalid route metadata in ${metadataFile}`) - ) + Effect.mapError(() => failure(`Invalid route metadata in ${metadataFile}`)), ); const route = routeModule.routeMeta ?? routeModule.default; if (route === undefined) { - return yield* Effect.fail( - failure( - `${metadataFile} must export routeMeta or a default route metadata object` - ) - ); + return yield* Effect.fail(failure(`${metadataFile} must export routeMeta or a default route metadata object`)); } const expectedScope = appId.startsWith('shell-') ? 'system' : 'tenant'; if (!isGovernedPageEntrypoint(route, appId, moduleId, expectedScope)) { return yield* Effect.fail( - failure( - `${metadataFile} must declare one governed ${expectedScope} page entrypoint owned by ${appId}` - ) + failure(`${metadataFile} must declare one governed ${expectedScope} page entrypoint owned by ${appId}`), ); } return route; }); -const loadRouteMetadata = ( - appDirectory: string, - appId: string, - moduleId: string -) => +const loadRouteMetadata = (appDirectory: string, appId: string, moduleId: string) => Effect.gen(function* loadRouteMetadataEffect() { const path = yield* Path.Path; const routeDirectory = path.join(appDirectory, 'src/routes'); @@ -227,17 +158,13 @@ const loadRouteMetadata = ( const routes = yield* Effect.forEach( metadataFiles, (metadataFile) => loadRouteMetadataFile(metadataFile, appId, moduleId), - { concurrency: 'unbounded' } - ); - return EffectArray.sortWith( - routes, - (route) => route.canonicalPath, - Order.String + { concurrency: 'unbounded' }, ); + return EffectArray.sortWith(routes, (route) => route.canonicalPath, Order.String); }); const createLocalisedUrls = ( - routes: readonly RouteMetadata[] + routes: readonly RouteMetadata[], ): Readonly>>> => Object.fromEntries( routes.flatMap((route) => { @@ -245,28 +172,17 @@ const createLocalisedUrls = ( return []; } return [[route.canonicalPath, route.localisedPaths]]; - }) + }), ); -const runCommand = ( - executable: string, - args: readonly string[], - options: ChildProcess.CommandOptions -) => +const runCommand = (executable: string, args: readonly string[], options: ChildProcess.CommandOptions) => Effect.gen(function* runCommandEffect() { const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const exitCode = yield* processSpawner.exitCode( - ChildProcess.make(executable, args, options) - ); + const exitCode = yield* processSpawner.exitCode(ChildProcess.make(executable, args, options)); return Number(exitCode); }); -const generateRouteMetadataManifest = ( - appDirectory: string, - appId: string, - moduleId: string, - workspaceRoot: string -) => +const generateRouteMetadataManifest = (appDirectory: string, appId: string, moduleId: string, workspaceRoot: string) => Effect.gen(function* generateRouteMetadataManifestEffect() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -277,21 +193,13 @@ const generateRouteMetadataManifest = ( } const localisedUrls = createLocalisedUrls(routes); const encodedNamespace = yield* encodeJsonString(namespace).pipe( - Effect.mapError(() => - failure(`Unable to encode the route namespace for ${appId}`) - ) + Effect.mapError(() => failure(`Unable to encode the route namespace for ${appId}`)), ); const encodedRoutes = yield* encodeJson(sortJsonValue(routes)).pipe( - Effect.mapError(() => - failure(`Unable to encode route metadata for ${appId}`) - ) + Effect.mapError(() => failure(`Unable to encode route metadata for ${appId}`)), ); - const encodedLocalisedUrls = yield* encodeJson( - sortJsonValue(localisedUrls) - ).pipe( - Effect.mapError(() => - failure(`Unable to encode localised URLs for ${appId}`) - ) + const encodedLocalisedUrls = yield* encodeJson(sortJsonValue(localisedUrls)).pipe( + Effect.mapError(() => failure(`Unable to encode localised URLs for ${appId}`)), ); const content = `// @generated by @modern-js/ultramodern-create. // Author route metadata in colocated src/routes/**/route.meta.ts files. @@ -304,34 +212,19 @@ export const ultramodernRouteMetadata = ${encodedRoutes} as const; export const ultramodernLocalisedUrls = ${encodedLocalisedUrls} as const; `; - const manifestPath = path.join( - appDirectory, - 'src/routes/ultramodern-route-metadata.ts' - ); + const manifestPath = path.join(appDirectory, 'src/routes/ultramodern-route-metadata.ts'); yield* fileSystem .writeFileString(manifestPath, content) .pipe(Effect.mapError(() => failure(`Unable to write ${manifestPath}`))); - const formatStatus = yield* runCommand( - 'pnpm', - ['exec', 'oxfmt', manifestPath], - { - cwd: workspaceRoot, - shell: path.sep === '\\', - stderr: 'inherit', - stdin: 'inherit', - stdout: 'inherit', - } - ).pipe( - Effect.mapError(() => - failure(`Unable to launch the formatter for ${manifestPath}`) - ) - ); + const formatStatus = yield* runCommand('pnpm', ['exec', 'oxfmt', manifestPath], { + cwd: workspaceRoot, + shell: path.sep === '\\', + stderr: 'inherit', + stdin: 'inherit', + stdout: 'inherit', + }).pipe(Effect.mapError(() => failure(`Unable to launch the formatter for ${manifestPath}`))); if (formatStatus !== 0) { - yield* Effect.fail( - failure( - `Failed to format generated route metadata at ${manifestPath}: exit ${formatStatus}` - ) - ); + yield* Effect.fail(failure(`Failed to format generated route metadata at ${manifestPath}: exit ${formatStatus}`)); } }); @@ -346,76 +239,47 @@ const program = Effect.gen(function* generateTanstackRoutesEffect() { moduleUrl: import.meta.url, }); const { forwardedArgs, workspaceRoot } = invocation; - const ultramodernConfigPath = path.join( - workspaceRoot, - '.modernjs/ultramodern.json' - ); + const ultramodernConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json'); const ultramodernConfigText = yield* fileSystem .readFileString(ultramodernConfigPath) - .pipe( - Effect.mapError(() => failure(`Unable to read ${ultramodernConfigPath}`)) - ); - const ultramodernConfig = yield* decodeUltramodernConfig( - ultramodernConfigText - ).pipe(Effect.mapError(() => failure(`${ultramodernConfigPath} is invalid`))); + .pipe(Effect.mapError(() => failure(`Unable to read ${ultramodernConfigPath}`))); + const ultramodernConfig = yield* decodeUltramodernConfig(ultramodernConfigText).pipe( + Effect.mapError(() => failure(`${ultramodernConfigPath} is invalid`)), + ); const appFlagIndex = forwardedArgs.indexOf('--app'); - const selectedAppId = - appFlagIndex === -1 ? undefined : forwardedArgs[appFlagIndex + 1]; + const selectedAppId = appFlagIndex === -1 ? undefined : forwardedArgs[appFlagIndex + 1]; const selectedApps = (ultramodernConfig.topology?.apps ?? []).filter( - (app) => selectedAppId === undefined || selectedAppId === app.id + (app) => selectedAppId === undefined || selectedAppId === app.id, ); yield* Effect.forEach( selectedApps, (app) => Effect.gen(function* generateAppRouteMetadataEffect() { - const packageConfigPath = path.join( - workspaceRoot, - app.path, - 'package.json' - ); + const packageConfigPath = path.join(workspaceRoot, app.path, 'package.json'); const packageConfigText = yield* fileSystem .readFileString(packageConfigPath) - .pipe( - Effect.mapError(() => - failure(`Unable to read ${packageConfigPath}`) - ) - ); - const packageConfig = yield* decodePackageConfig( - packageConfigText - ).pipe( - Effect.mapError(() => failure(`${packageConfigPath} is invalid`)) - ); - const moduleId = - packageConfig.modernjs?.ontosModule?.moduleId ?? app.id; - yield* generateRouteMetadataManifest( - path.join(workspaceRoot, app.path), - app.id, - moduleId, - workspaceRoot - ); - yield* Console.log( - `[ultramodern] Route metadata manifest generated: ${app.id}` + .pipe(Effect.mapError(() => failure(`Unable to read ${packageConfigPath}`))); + const packageConfig = yield* decodePackageConfig(packageConfigText).pipe( + Effect.mapError(() => failure(`${packageConfigPath} is invalid`)), ); + const moduleId = packageConfig.modernjs?.ontosModule?.moduleId ?? app.id; + yield* generateRouteMetadataManifest(path.join(workspaceRoot, app.path), app.id, moduleId, workspaceRoot); + yield* Console.log(`[ultramodern] Route metadata manifest generated: ${app.id}`); }), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); const generationStatus = yield* launchUltramodern(invocation); if (generationStatus !== 0) { - yield* Effect.fail( - failure( - `Framework route-artifact generation failed: exit ${generationStatus}` - ) - ); + yield* Effect.fail(failure(`Framework route-artifact generation failed: exit ${generationStatus}`)); } }); -const reportFailure = (error: RouteGenerationError) => - Console.error(error.reason); -const MainLayer = Layer.effectDiscard( - program.pipe(Effect.tapError(reportFailure)) -).pipe(Layer.provide(NodeServices.layer)); +const reportFailure = (error: RouteGenerationError) => Console.error(error.reason); +const MainLayer = Layer.effectDiscard(program.pipe(Effect.tapError(reportFailure))).pipe( + Layer.provide(NodeServices.layer), +); NodeRuntime.runMain(Effect.scoped(Layer.build(MainLayer)), { disableErrorReporting: true, diff --git a/app/scripts/generated-governed-http-boundary.mts b/app/scripts/generated-governed-http-boundary.mts index 6c58e6681..909f0b836 100644 --- a/app/scripts/generated-governed-http-boundary.mts +++ b/app/scripts/generated-governed-http-boundary.mts @@ -2,11 +2,7 @@ import path from 'node:path'; import { Schema } from 'effect'; -import { - matchingDelimiter, - separatedSource, - topLevelSeparators, -} from './boundary-source-structure.mts'; +import { matchingDelimiter, separatedSource, topLevelSeparators } from './boundary-source-structure.mts'; import { hasGeneratedGovernedClientContract, hasGeneratedSourceHeader, @@ -15,37 +11,25 @@ import { hasGeneratedOperationGatewayContract, hasGeneratedOperationPrincipalContract, } from './generated-module-api-boundary.mts'; -import { - toCamelCase, - toPascalCase, - isCodePosition, - maskNonCode, -} from './scaffolding/shared.mts'; +import { toCamelCase, toPascalCase, isCodePosition, maskNonCode } from './scaffolding/shared.mts'; const GOVERNED_API_SLOT_END = '// '; const GOVERNED_API_SLOT_START = '// '; -const GOVERNED_HTTP_RUNTIME_MODULE = - '@app/shared-contracts/server/effect-bff-runtime'; +const GOVERNED_HTTP_RUNTIME_MODULE = '@app/shared-contracts/server/effect-bff-runtime'; const GOVERNED_READ_HTTP_MODULE = '@app/core-runtime/http/governed-read'; const MANIFEST_API_SLOT_START = '// '; const MANIFEST_API_SLOT_END = '// '; const MODULE_API_HEADER = '// @generated by OntOS Codesmith module-api v1\n'; -const GOVERNED_CONTRIBUTION_HEADER = - '// @generated by OntOS Codesmith Governed Contribution v1\n'; +const GOVERNED_CONTRIBUTION_HEADER = '// @generated by OntOS Codesmith Governed Contribution v1\n'; const MODULE_API_KIND = 'module-api'; const REPORT_KIND = 'report'; const SEARCH_PROVIDER_KIND = 'search-provider'; -const GOVERNED_HANDLER_LAYER_SLOT_START = - '// '; -const GOVERNED_HANDLER_LAYER_SLOT_END = - '// '; +const GOVERNED_HANDLER_LAYER_SLOT_START = '// '; +const GOVERNED_HANDLER_LAYER_SLOT_END = '// '; const HTTP_API_CONTRACT_MODULE = 'effect/unstable/httpapi'; +const GOVERNED_HTTP_API_IDENTITY_ALIAS = 'governedHttpApiIdentity'; -const GovernedReadKindSchema = Schema.Literals([ - MODULE_API_KIND, - REPORT_KIND, - SEARCH_PROVIDER_KIND, -]); +const GovernedReadKindSchema = Schema.Literals([MODULE_API_KIND, REPORT_KIND, SEARCH_PROVIDER_KIND]); type GovernedReadKind = typeof GovernedReadKindSchema.Type; const isGovernedReadKind = Schema.is(GovernedReadKindSchema); @@ -55,33 +39,20 @@ interface GovernedReadContribution { readonly name: string; } -const escapeRegExp = (value: string): string => - value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`); +const escapeRegExp = (value: string): string => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`); const matches = (source: string | undefined, expression: RegExp): boolean => source !== undefined && expression.test(source); -const hasExactlyOne = ( - source: string | undefined, - expression: RegExp -): boolean => +const hasExactlyOne = (source: string | undefined, expression: RegExp): boolean => source !== undefined && [...source.matchAll(expression)].length === 1; -const matchingDelimiterEnd = ( - source: string, - start: number, - opening: string, - closing: string -): number | undefined => +const matchingDelimiterEnd = (source: string, start: number, opening: string, closing: string): number | undefined => matchingDelimiter(maskNonCode(source), start, opening, closing); const maskComments = (source: string): string => maskNonCode(source, true); -const callArgument = ( - source: string | undefined, - declaration: RegExp, - argumentIndex = 0 -): string | undefined => { +const callArgument = (source: string | undefined, declaration: RegExp, argumentIndex = 0): string | undefined => { if (source === undefined) { return undefined; } @@ -99,19 +70,12 @@ const callArgument = ( if (callEnd === undefined) { return undefined; } - return separatedSource( - code, - topLevelSeparators(structure, ',', callStart + 1, callEnd), - callStart + 1, - callEnd - )[argumentIndex]; + return separatedSource(code, topLevelSeparators(structure, ',', callStart + 1, callEnd), callStart + 1, callEnd)[ + argumentIndex + ]; }; -const objectArgument = ( - source: string | undefined, - declaration: RegExp, - argumentIndex = 0 -): string | undefined => { +const objectArgument = (source: string | undefined, declaration: RegExp, argumentIndex = 0): string | undefined => { const argument = callArgument(source, declaration, argumentIndex); if (argument === undefined) { return undefined; @@ -123,55 +87,31 @@ const objectArgument = ( return end === argument.length - 1 ? argument : undefined; }; -const topLevelObjectEntries = ( - source: string -): readonly string[] | undefined => { - if ( - !source.startsWith('{') || - matchingDelimiterEnd(source, 0, '{', '}') !== source.length - 1 - ) { +const topLevelObjectEntries = (source: string): readonly string[] | undefined => { + if (!source.startsWith('{') || matchingDelimiterEnd(source, 0, '{', '}') !== source.length - 1) { return undefined; } - const separators = topLevelSeparators( - maskNonCode(source), - ',', - 1, - source.length - 1 - ); - return separatedSource(source, separators, 1, source.length - 1).filter( - (entry) => entry !== '' - ); + const separators = topLevelSeparators(maskNonCode(source), ',', 1, source.length - 1); + return separatedSource(source, separators, 1, source.length - 1).filter((entry) => entry !== ''); }; -const objectProperty = ( - source: string, - property: string -): string | undefined => { +const objectProperty = (source: string, property: string): string | undefined => { const entries = topLevelObjectEntries(source); const prefix = new RegExp(`^${escapeRegExp(property)}:\\s*`, 'u'); const candidates = entries?.filter((entry) => prefix.test(entry)) ?? []; - return candidates.length === 1 - ? candidates[0]?.replace(prefix, '').trim() - : undefined; + return candidates.length === 1 ? candidates[0]?.replace(prefix, '').trim() : undefined; }; -const objectPropertyValue = ( - source: string, - property: string -): string | undefined => { +const objectPropertyValue = (source: string, property: string): string | undefined => { const explicit = objectProperty(source, property); if (explicit !== undefined) { return explicit; } - const shorthand = - topLevelObjectEntries(source)?.filter((entry) => entry === property) ?? []; + const shorthand = topLevelObjectEntries(source)?.filter((entry) => entry === property) ?? []; return shorthand.length === 1 ? property : undefined; }; -const isWholeCallExpression = ( - source: string, - declaration: RegExp -): boolean => { +const isWholeCallExpression = (source: string, declaration: RegExp): boolean => { const match = declaration.exec(source); if (match?.index === undefined) { return false; @@ -221,16 +161,10 @@ const sourceDepthAnalysis = (source: string): SourceDepthAnalysis => { const codeDepthBeforePosition = (source: string, target: number): number => sourceDepthAnalysis(source).prefixDepths[target] ?? 0; -const codeDepthAtPosition = ( - source: string, - target: number -): number | undefined => - isCodePosition(source, target) - ? codeDepthBeforePosition(source, target) - : undefined; +const codeDepthAtPosition = (source: string, target: number): number | undefined => + isCodePosition(source, target) ? codeDepthBeforePosition(source, target) : undefined; -const isTopLevelCodePosition = (source: string, target: number): boolean => - codeDepthAtPosition(source, target) === 0; +const isTopLevelCodePosition = (source: string, target: number): boolean => codeDepthAtPosition(source, target) === 0; interface SourceRange { readonly end: number; @@ -238,21 +172,12 @@ interface SourceRange { readonly value: string; } -const assignedExpressionRange = ( - source: string, - declaration: RegExp -): SourceRange | undefined => { +const assignedExpressionRange = (source: string, declaration: RegExp): SourceRange | undefined => { const code = maskComments(source); const structure = sourceDepthAnalysis(source).code; - const flags = declaration.flags.includes('g') - ? declaration.flags - : `${declaration.flags}g`; - const declarations = [ - ...structure.matchAll(new RegExp(declaration.source, flags)), - ].filter( - (candidate) => - candidate.index !== undefined && - isTopLevelCodePosition(source, candidate.index) + const flags = declaration.flags.includes('g') ? declaration.flags : `${declaration.flags}g`; + const declarations = [...structure.matchAll(new RegExp(declaration.source, flags))].filter( + (candidate) => candidate.index !== undefined && isTopLevelCodePosition(source, candidate.index), ); if (declarations.length !== 1) { return undefined; @@ -276,10 +201,8 @@ const assignedExpressionRange = ( }; }; -const assignedExpression = ( - source: string, - declaration: RegExp -): string | undefined => assignedExpressionRange(source, declaration)?.value; +const assignedExpression = (source: string, declaration: RegExp): string | undefined => + assignedExpressionRange(source, declaration)?.value; const isEffectFnCallback = (source: string): boolean => { if (!source.startsWith('Effect.fn(')) { @@ -287,68 +210,38 @@ const isEffectFnCallback = (source: string): boolean => { } const opening = source.indexOf('('); const closing = matchingDelimiterEnd(source, opening, '(', ')'); - const invocation = - closing === undefined ? '' : source.slice(closing + 1).trim(); - const callback = invocation.startsWith('(') - ? callArgument(`invoke${invocation}`, /^invoke\(/u) - : undefined; + const invocation = closing === undefined ? '' : source.slice(closing + 1).trim(); + const callback = invocation.startsWith('(') ? callArgument(`invoke${invocation}`, /^invoke\(/u) : undefined; return ( callback !== undefined && matchingDelimiterEnd(invocation, 0, '(', ')') === invocation.length - 1 && - (matches( - callback, - /^(?:async\s+)?function\*?(?:\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\(/u - ) || - matches( - callback, - /^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/u - )) + (matches(callback, /^(?:async\s+)?function\*?(?:\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\(/u) || + matches(callback, /^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/u)) ); }; -const isExecutableCallback = ( - candidate: string | undefined, - ownerSource?: string -): boolean => { - if ( - candidate === undefined || - /^(?:null|undefined)(?:\s+as\b[\s\S]*)?$/u.test(candidate) - ) { +const isExecutableCallback = (candidate: string | undefined, ownerSource?: string): boolean => { + if (candidate === undefined || /^(?:null|undefined)(?:\s+as\b[\s\S]*)?$/u.test(candidate)) { return false; } if ( - matches( - candidate, - /^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/u - ) || + matches(candidate, /^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/u) || isEffectFnCallback(candidate) || - matches( - candidate, - /^(?:async\s+)?function\*?(?:\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\(/u - ) + matches(candidate, /^(?:async\s+)?function\*?(?:\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\(/u) ) { return true; } - if ( - ownerSource === undefined || - !/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(candidate) - ) { + if (ownerSource === undefined || !/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(candidate)) { return false; } const initializer = assignedExpression( ownerSource, - new RegExp(`(?:export\\s+)?const ${escapeRegExp(candidate)}\\s*=\\s*`, 'u') - ); - return ( - initializer !== undefined && - initializer !== candidate && - isExecutableCallback(initializer) + new RegExp(`(?:export\\s+)?const ${escapeRegExp(candidate)}\\s*=\\s*`, 'u'), ); + return initializer !== undefined && initializer !== candidate && isExecutableCallback(initializer); }; -const callbackReturnedExpression = ( - handlerSource: string -): string | undefined => { +const callbackReturnedExpression = (handlerSource: string): string | undefined => { const structure = maskNonCode(handlerSource); const arrow = structure.indexOf('=>'); if (arrow === -1) { @@ -360,9 +253,7 @@ const callbackReturnedExpression = ( } const bodyStructure = maskNonCode(body); const returns = [...bodyStructure.matchAll(/\breturn\s+/gu)].filter( - (match) => - match.index !== undefined && - codeDepthBeforePosition(body, match.index) === 1 + (match) => match.index !== undefined && codeDepthBeforePosition(body, match.index) === 1, ); const finalReturn = returns.at(-1); if (finalReturn?.index === undefined) { @@ -370,29 +261,15 @@ const callbackReturnedExpression = ( } const returned = body.slice(finalReturn.index + finalReturn[0].length); const statementEnd = returned.indexOf(';'); - return ( - statementEnd === -1 ? returned : returned.slice(0, statementEnd) - ).trim(); + return (statementEnd === -1 ? returned : returned.slice(0, statementEnd)).trim(); }; -const isReadHandlerCallback = ( - candidate: string | undefined, - ownerSource: string -): boolean => { - if ( - !isExecutableCallback(candidate, ownerSource) || - candidate === undefined - ) { +const isReadHandlerCallback = (candidate: string | undefined, ownerSource: string): boolean => { + if (!isExecutableCallback(candidate, ownerSource) || candidate === undefined) { return false; } const resolved = /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(candidate) - ? assignedExpression( - ownerSource, - new RegExp( - `(?:export\\s+)?const ${escapeRegExp(candidate)}\\s*=\\s*`, - 'u' - ) - ) + ? assignedExpression(ownerSource, new RegExp(`(?:export\\s+)?const ${escapeRegExp(candidate)}\\s*=\\s*`, 'u')) : candidate; if (resolved === undefined) { return false; @@ -401,9 +278,7 @@ const isReadHandlerCallback = ( return /\byield\*|\.pipe\(/u.test(maskNonCode(resolved, true)); } const expression = callbackReturnedExpression(resolved); - return /\bEffect\.[A-Za-z_$][A-Za-z0-9_$]*\s*\(|\.pipe\(/u.test( - maskNonCode(expression ?? '', true) - ); + return /\bEffect\.[A-Za-z_$][A-Za-z0-9_$]*\s*\(|\.pipe\(/u.test(maskNonCode(expression ?? '', true)); }; interface GeneratedSlotRange { @@ -414,17 +289,10 @@ interface GeneratedSlotRange { readonly markerStart: number; } -const generatedSlotRange = ( - source: string, - startMarker: string, - endMarker: string -): GeneratedSlotRange | undefined => { +const generatedSlotRange = (source: string, startMarker: string, endMarker: string): GeneratedSlotRange | undefined => { const markerStart = source.indexOf(startMarker); const markerEnd = source.indexOf(endMarker); - const depth = - markerStart === -1 - ? undefined - : codeDepthBeforePosition(source, markerStart); + const depth = markerStart === -1 ? undefined : codeDepthBeforePosition(source, markerStart); if ( markerStart === -1 || markerEnd <= markerStart || @@ -449,7 +317,7 @@ const generatedSlotRange = ( const generatedSlotEntries = ( source: string, startMarker: string, - endMarker: string + endMarker: string, ): readonly string[] | undefined => { const slot = generatedSlotRange(source, startMarker, endMarker); if (slot === undefined) { @@ -459,10 +327,7 @@ const generatedSlotEntries = ( const entries: string[] = []; let start = slot.bodyStart; for (let index = slot.bodyStart; index < slot.bodyEnd; index += 1) { - if ( - structure[index] === ',' && - codeDepthBeforePosition(source, index) === slot.depth - ) { + if (structure[index] === ',' && codeDepthBeforePosition(source, index) === slot.depth) { const entry = source.slice(start, index).trim(); if (entry !== '') { entries.push(entry); @@ -479,21 +344,16 @@ const generatedSlotEntries = ( const entriesAcrossSlots = ( source: string, - slots: readonly (readonly [string, string])[] + slots: readonly (readonly [string, string])[], ): readonly string[] | undefined => { - const entries = slots.map(([start, end]) => - generatedSlotEntries(source, start, end) - ); + const entries = slots.map(([start, end]) => generatedSlotEntries(source, start, end)); return entries.some((candidate) => candidate === undefined) ? undefined : entries.flatMap((candidate) => candidate ?? []); }; const objectEntryKey = (entry: string): string | undefined => { - const match = - /^(?:'(?[^']+)'|"(?[^"]+)"|(?[A-Za-z][A-Za-z0-9-]*))\s*:/u.exec( - entry - ); + const match = /^(?:'(?[^']+)'|"(?[^"]+)"|(?[A-Za-z][A-Za-z0-9-]*))\s*:/u.exec(entry); return match?.groups?.single ?? match?.groups?.double ?? match?.groups?.bare; }; @@ -502,33 +362,23 @@ const slotHasExactlyOneCodeMatch = ( startMarker: string, endMarker: string, expression: RegExp, - depthOffset: number | null = 0 + depthOffset: number | null = 0, ): boolean => { const slot = generatedSlotRange(source, startMarker, endMarker); if (slot === undefined) { return false; } - const flags = expression.flags.includes('g') - ? expression.flags - : `${expression.flags}g`; + const flags = expression.flags.includes('g') ? expression.flags : `${expression.flags}g`; return ( - [ - ...source - .slice(slot.bodyStart, slot.bodyEnd) - .matchAll(new RegExp(expression.source, flags)), - ].filter((candidate) => { - if (candidate.index === undefined) { - return false; - } - const depth = codeDepthAtPosition( - source, - slot.bodyStart + candidate.index - ); - return ( - depth !== undefined && - (depthOffset === null || depth === slot.depth + depthOffset) - ); - }).length === 1 + [...source.slice(slot.bodyStart, slot.bodyEnd).matchAll(new RegExp(expression.source, flags))].filter( + (candidate) => { + if (candidate.index === undefined) { + return false; + } + const depth = codeDepthAtPosition(source, slot.bodyStart + candidate.index); + return depth !== undefined && (depthOffset === null || depth === slot.depth + depthOffset); + }, + ).length === 1 ); }; @@ -536,17 +386,14 @@ const slotIsInsideObjectProperty = ( source: string, property: string, startMarker: string, - endMarker: string + endMarker: string, ): boolean => { const code = maskComments(source); const slot = generatedSlotRange(source, startMarker, endMarker); if (slot === undefined) { return false; } - const propertyPattern = new RegExp( - `\\b${escapeRegExp(property)}:\\s*\\{`, - 'gu' - ); + const propertyPattern = new RegExp(`\\b${escapeRegExp(property)}:\\s*\\{`, 'gu'); const containers = [...code.matchAll(propertyPattern)].filter((match) => { const opening = code.indexOf('{', match.index); const closing = matchingDelimiterEnd(code, opening, '{', '}'); @@ -566,28 +413,18 @@ const defaultExportExpression = (source: string): string | undefined => const returnedEffectBffDefinition = (source: string): string | undefined => { const structure = maskNonCode(source); - const returnedCalls = [ - ...structure.matchAll( - /\breturn\s+(?:defineEffectBff|assembleEffectBffRuntime)\(/gu - ), - ]; + const returnedCalls = [...structure.matchAll(/\breturn\s+(?:defineEffectBff|assembleEffectBffRuntime)\(/gu)]; if (returnedCalls.length !== 1 || returnedCalls[0]?.index === undefined) { return undefined; } const callStart = - returnedCalls[0].index + - returnedCalls[0][0].search( - /(?:defineEffectBff|assembleEffectBffRuntime)\(/u - ); + returnedCalls[0].index + returnedCalls[0][0].search(/(?:defineEffectBff|assembleEffectBffRuntime)\(/u); const opening = structure.indexOf('(', callStart); const closing = matchingDelimiterEnd(structure, opening, '(', ')'); if (closing === undefined) { return undefined; } - return objectArgument( - source.slice(callStart, closing + 1), - /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u - ); + return objectArgument(source.slice(callStart, closing + 1), /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u); }; const exportedRuntimeFactory = (source: string): SourceRange | undefined => { @@ -595,26 +432,15 @@ const exportedRuntimeFactory = (source: string): SourceRange | undefined => { if (exported === undefined || !/^[A-Za-z][A-Za-z0-9]*$/u.test(exported)) { return undefined; } - const runtimeInitializer = assignedExpression( - source, - new RegExp(`const ${escapeRegExp(exported)}\\s*=\\s*`, 'u') - ); - const factory = /^(?make[A-Za-z][A-Za-z0-9]*ApiRuntime)\(/u.exec( - runtimeInitializer ?? '' - )?.groups?.factory; + const runtimeInitializer = assignedExpression(source, new RegExp(`const ${escapeRegExp(exported)}\\s*=\\s*`, 'u')); + const factory = /^(?make[A-Za-z][A-Za-z0-9]*ApiRuntime)\(/u.exec(runtimeInitializer ?? '')?.groups?.factory; const factoryExpression = factory === undefined ? undefined - : assignedExpressionRange( - source, - new RegExp(`export const ${escapeRegExp(factory)}\\s*=\\s*`, 'u') - ); + : assignedExpressionRange(source, new RegExp(`export const ${escapeRegExp(factory)}\\s*=\\s*`, 'u')); return factoryExpression === undefined || runtimeInitializer === undefined || - !isWholeCallExpression( - runtimeInitializer, - new RegExp(`^${escapeRegExp(factory ?? '')}\\(`, 'u') - ) + !isWholeCallExpression(runtimeInitializer, new RegExp(`^${escapeRegExp(factory ?? '')}\\(`, 'u')) ? undefined : factoryExpression; }; @@ -625,56 +451,37 @@ const effectBffDefinition = (source: string): string | undefined => { return undefined; } if (/^(?:defineEffectBff|assembleEffectBffRuntime)\(/u.test(exported)) { - return isWholeCallExpression( - exported, - /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u - ) - ? objectArgument( - exported, - /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u - ) + return isWholeCallExpression(exported, /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u) + ? objectArgument(exported, /^(?:defineEffectBff|assembleEffectBffRuntime)\(/u) : undefined; } if (!/^[A-Za-z][A-Za-z0-9]*$/u.test(exported)) { return undefined; } const factory = exportedRuntimeFactory(source); - return factory === undefined - ? undefined - : returnedEffectBffDefinition(factory.value); + return factory === undefined ? undefined : returnedEffectBffDefinition(factory.value); }; const hasExactValueImport = ( source: string, value: string, - modulePath: string + modulePath: string, + allowAdditionalImports = false, ): boolean => { const code = maskComments(source); const imports = [ ...source.matchAll( - new RegExp( - `^\\s*import\\s*\\{(?[^}]*)\\}\\s*from\\s*'${escapeRegExp(modulePath)}';`, - 'gmu' - ) + new RegExp(`^\\s*import\\s*\\{(?[^}]*)\\}\\s*from\\s*'${escapeRegExp(modulePath)}';`, 'gmu'), ), - ].filter( - (candidate) => - candidate.index !== undefined && - isTopLevelCodePosition(source, candidate.index) - ); + ].filter((candidate) => candidate.index !== undefined && isTopLevelCodePosition(source, candidate.index)); const importedValues = imports.flatMap((candidate) => - (candidate.groups?.values ?? '').split(',').map((entry) => entry.trim()) + (candidate.groups?.values ?? '').split(',').map((entry) => entry.trim()), ); + const importedBinding = /\bas\s+(?[A-Za-z_$][A-Za-z0-9_$]*)\s*$/u.exec(value)?.groups?.binding ?? value; return ( - imports.length === 1 && + (allowAdditionalImports || imports.length === 1) && importedValues.filter((candidate) => candidate === value).length === 1 && - !matches( - code, - new RegExp( - `\\b(?:class|const|function|let|var)\\s+${escapeRegExp(value)}\\b`, - 'u' - ) - ) + !matches(code, new RegExp(`\\b(?:class|const|function|let|var)\\s+${escapeRegExp(importedBinding)}\\b`, 'u')) ); }; @@ -682,7 +489,7 @@ const slotIsMountedByAssembler = ( source: string, runtimeSource: string, layerName: string, - expectedApi: string + expectedApi: string, ): boolean => { const definition = effectBffDefinition(source); if ( @@ -690,11 +497,7 @@ const slotIsMountedByAssembler = ( definition.includes('...') || objectPropertyValue(definition, 'api') !== expectedApi || !hasExactlyOne(maskNonCode(source), /\bassembleEffectBffRuntime\(/gu) || - !hasExactValueImport( - source, - 'assembleEffectBffRuntime', - GOVERNED_HTTP_RUNTIME_MODULE - ) + !hasExactValueImport(source, 'assembleEffectBffRuntime', GOVERNED_HTTP_RUNTIME_MODULE) ) { return false; } @@ -705,24 +508,13 @@ const slotIsMountedByAssembler = ( if (handlers === undefined || !/^[A-Za-z][A-Za-z0-9]*$/u.test(handlers)) { return false; } - const resolved = assignedExpression( - runtimeSource, - new RegExp(`const ${escapeRegExp(handlers)}\\s*=\\s*`, 'u') - ); + const resolved = assignedExpression(runtimeSource, new RegExp(`const ${escapeRegExp(handlers)}\\s*=\\s*`, 'u')); return ( - resolved !== undefined && - isWholeCallExpression( - resolved, - new RegExp(`^${escapeRegExp(layerName)}\\.pipe\\(`, 'u') - ) + resolved !== undefined && isWholeCallExpression(resolved, new RegExp(`^${escapeRegExp(layerName)}\\.pipe\\(`, 'u')) ); }; -const definesExpectedRuntime = ( - definition: string | undefined, - expectedApi: string, - runtimeName: string -): boolean => +const definesExpectedRuntime = (definition: string | undefined, expectedApi: string, runtimeName: string): boolean => definition !== undefined && !definition.includes('...') && objectPropertyValue(definition, 'api') === expectedApi && @@ -730,10 +522,7 @@ const definesExpectedRuntime = ( definition, runtimeName === 'layer' ? /(?:\{|,)\s*layer(?:\s*:\s*layer)?\s*(?:,|\})/gu - : new RegExp( - `(?:\\{|,)\\s*layer\\s*:\\s*${escapeRegExp(runtimeName)}\\s*(?:,|\\})`, - 'gu' - ) + : new RegExp(`(?:\\{|,)\\s*layer\\s*:\\s*${escapeRegExp(runtimeName)}\\s*(?:,|\\})`, 'gu'), ); const legacyRuntimeMount = ( @@ -742,12 +531,9 @@ const legacyRuntimeMount = ( runtimeSource: string, closing: number, layerName: string, - expectedApi: string + expectedApi: string, ): boolean => { - const runtimeDeclaration = - /const\s+(?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApiBuilder\.layer\(/u.exec( - runtimeSource - ); + const runtimeDeclaration = /const\s+(?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApiBuilder\.layer\(/u.exec(runtimeSource); const runtimeName = runtimeDeclaration?.groups?.name; if (runtimeDeclaration === null || runtimeName === undefined) { return false; @@ -761,13 +547,7 @@ const legacyRuntimeMount = ( const definition = effectBffDefinition(source); return ( callArgument(runtimeSlice, /HttpApiBuilder\.layer\(/u) === expectedApi && - matches( - runtimeSlice, - new RegExp( - `(?:GovernedReadLayer|Layer)\\.provide\\(${escapeRegExp(layerName)}\\)`, - 'u' - ) - ) && + matches(runtimeSlice, new RegExp(`(?:GovernedReadLayer|Layer)\\.provide\\(${escapeRegExp(layerName)}\\)`, 'u')) && hasExactlyOne(code, /\bdefineEffectBff\(/gu) && definesExpectedRuntime(definition, expectedApi, runtimeName) ); @@ -777,7 +557,7 @@ const slotIsInsideMountedLayer = ( source: string, startMarker: string, endMarker: string, - expectedApi: string + expectedApi: string, ): boolean => { const code = maskComments(source); const slot = generatedSlotRange(source, startMarker, endMarker); @@ -787,9 +567,7 @@ const slotIsInsideMountedLayer = ( const declarations = [ ...code .slice(0, slot.markerStart) - .matchAll( - /(?:export\s+)?const\s+(?[A-Za-z][A-Za-z0-9]*)\s*=\s*(?:GovernedReadLayer|Layer)\.mergeAll\(/gu - ), + .matchAll(/(?:export\s+)?const\s+(?[A-Za-z][A-Za-z0-9]*)\s*=\s*(?:GovernedReadLayer|Layer)\.mergeAll\(/gu), ]; let declaration: RegExpExecArray | undefined; for (const candidate of declarations) { @@ -811,14 +589,7 @@ const slotIsInsideMountedLayer = ( if (slotIsMountedByAssembler(source, runtimeSource, layerName, expectedApi)) { return true; } - return legacyRuntimeMount( - source, - code, - runtimeSource, - closing, - layerName, - expectedApi - ); + return legacyRuntimeMount(source, code, runtimeSource, closing, layerName, expectedApi); }; // A trailing slot comment may precede an ASI-terminated root. Stop at the next @@ -826,8 +597,7 @@ const slotIsInsideMountedLayer = ( const apiStatementEnd = (source: string, start: number): number | undefined => { const [semicolon] = topLevelSeparators(maskNonCode(source), ';', start); const nextExport = [...maskComments(source).matchAll(/\bexport\s/gu)].find( - (match) => - match.index > start && isTopLevelCodePosition(source, match.index) + (match) => match.index > start && isTopLevelCodePosition(source, match.index), )?.index; if (semicolon === undefined) { return nextExport; @@ -837,18 +607,12 @@ const apiStatementEnd = (source: string, start: number): number | undefined => { /** Resolve the actual exported root containing the generated slot, never an alias or decoy. */ export const governedApiBinding = (source: string): string | undefined => { - const slot = generatedSlotRange( - source, - GOVERNED_API_SLOT_START, - GOVERNED_API_SLOT_END - ); + const slot = generatedSlotRange(source, GOVERNED_API_SLOT_START, GOVERNED_API_SLOT_END); if (slot === undefined) { return undefined; } const candidates = [ - ...maskComments(source).matchAll( - /export const (?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApi\.make\(/gu - ), + ...maskComments(source).matchAll(/export const (?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApi\.make\(/gu), ] .filter((match) => isTopLevelCodePosition(source, match.index)) .map((match) => match.groups?.name) @@ -856,12 +620,8 @@ export const governedApiBinding = (source: string): string | undefined => { if (name === undefined) { return false; } - const root = assignedExpressionRange( - source, - new RegExp(`export const ${escapeRegExp(name)}\\s*=\\s*`, 'u') - ); - const statementEnd = - root === undefined ? undefined : apiStatementEnd(source, root.start); + const root = assignedExpressionRange(source, new RegExp(`export const ${escapeRegExp(name)}\\s*=\\s*`, 'u')); + const statementEnd = root === undefined ? undefined : apiStatementEnd(source, root.start); return ( root !== undefined && statementEnd !== undefined && @@ -877,15 +637,8 @@ const governedSharedApiRoot = (source: string): SourceRange | undefined => { const apiRoot = binding === undefined ? undefined - : assignedExpressionRange( - source, - new RegExp(`export const ${escapeRegExp(binding)}\\s*=\\s*`, 'u') - ); - const slot = generatedSlotRange( - source, - GOVERNED_API_SLOT_START, - GOVERNED_API_SLOT_END - ); + : assignedExpressionRange(source, new RegExp(`export const ${escapeRegExp(binding)}\\s*=\\s*`, 'u')); + const slot = generatedSlotRange(source, GOVERNED_API_SLOT_START, GOVERNED_API_SLOT_END); if (apiRoot === undefined || slot === undefined) { return undefined; } @@ -893,23 +646,20 @@ const governedSharedApiRoot = (source: string): SourceRange | undefined => { if (statementEnd === undefined) { return undefined; } - const additions = maskNonCode( - source.slice(slot.bodyStart, slot.bodyEnd), - true - ).trim(); + const additions = maskNonCode(source.slice(slot.bodyStart, slot.bodyEnd), true).trim(); const trailing = maskComments( - source - .slice(slot.markerEnd + GOVERNED_API_SLOT_END.length, statementEnd) - .replace(/^;(?=\r?\n)/u, '') + source.slice(slot.markerEnd + GOVERNED_API_SLOT_END.length, statementEnd).replace(/^;(?=\r?\n)/u, ''), ).trim(); + const hasStableIdentityTail = + trailing === `.pipe(${GOVERNED_HTTP_API_IDENTITY_ALIAS})` && + hasExactValueImport(source, `identity as ${GOVERNED_HTTP_API_IDENTITY_ALIAS}`, 'effect', true); return /^(?:\.addHttpApi\([A-Za-z][A-Za-z0-9]*\)\s*)*$/u.test(additions) && - (trailing === '' || trailing === '.pipe(identity)') + (trailing === '' || trailing === '.pipe(identity)' || hasStableIdentityTail) ? apiRoot : undefined; }; -const hasGovernedSharedApiRoot = (source: string): boolean => - governedSharedApiRoot(source) !== undefined; +const hasGovernedSharedApiRoot = (source: string): boolean => governedSharedApiRoot(source) !== undefined; const hasInjectedGovernedReadRuntime = (source: string): boolean => { const factory = exportedRuntimeFactory(source); @@ -920,9 +670,7 @@ const hasInjectedGovernedReadRuntime = (source: string): boolean => { // The generated binding may be the first explicitly typed injection in an owner runtime // factory. Its caller and mounted layers are checked separately, and TS checks the layer type. const injection = - /const\s+\[\s*governedReadRuntimeLive,\s*[A-Za-z0-9_,\s]+\]\s*=\s*(?[A-Za-z][A-Za-z0-9]*)\s*;/u.exec( - code - ); + /const\s+\[\s*governedReadRuntimeLive,\s*[A-Za-z0-9_,\s]+\]\s*=\s*(?[A-Za-z][A-Za-z0-9]*)\s*;/u.exec(code); const args = injection?.groups?.args; if ( args === undefined || @@ -931,16 +679,13 @@ const hasInjectedGovernedReadRuntime = (source: string): boolean => { ) { return false; } - const signature = new RegExp( - `\\.\\.\\.${escapeRegExp(args)}:\\s*(?[A-Za-z][A-Za-z0-9]*)`, - 'u' - ).exec(code); + const signature = new RegExp(`\\.\\.\\.${escapeRegExp(args)}:\\s*(?[A-Za-z][A-Za-z0-9]*)`, 'u').exec(code); const type = signature?.groups?.type; return ( type !== undefined && new RegExp( `type ${escapeRegExp(type)}\\s*=\\s*readonly\\s*\\[\\s*readRuntime:\\s*Layer\\.Layer<\\s*ReadRuntime\\s*[,>]`, - 'u' + 'u', ).test(maskNonCode(source)) && effectBffDefinition(source) !== undefined ); @@ -953,26 +698,18 @@ const hasGovernedHandlerRoot = (source: string): boolean => { if (hasInjectedGovernedReadRuntime(source)) { return true; } - const runtime = assignedExpression( - source, - /const governedReadRuntimeLive\s*=\s*/u - ); + const runtime = assignedExpression(source, /const governedReadRuntimeLive\s*=\s*/u); if ( runtime === undefined || - (!/^readRuntimeLive$/u.test(runtime) && - !runtime.startsWith('GovernedReadRuntimeLive.pipe(')) + (!/^readRuntimeLive$/u.test(runtime) && !runtime.startsWith('GovernedReadRuntimeLive.pipe(')) ) { return false; } if (runtime === 'readRuntimeLive') { - const ownerRuntime = assignedExpression( - source, - /const readRuntimeLive\s*=\s*/u - ); + const ownerRuntime = assignedExpression(source, /const readRuntimeLive\s*=\s*/u); if ( !wholeCall(ownerRuntime, /^ReadRuntimeLive\.pipe\(/u) || - callArgument(ownerRuntime, /^ReadRuntimeLive\.pipe\(/u) !== - 'Layer.provide(readRuntimeDependenciesLive)' + callArgument(ownerRuntime, /^ReadRuntimeLive\.pipe\(/u) !== 'Layer.provide(readRuntimeDependenciesLive)' ) { return false; } @@ -987,28 +724,15 @@ const hasGovernedHandlerRoot = (source: string): boolean => { }; const hasGovernedSupportSlots = (handlerRoot: string): boolean => { - const supportImportStart = - '// '; - const supportLayerStart = - '// '; + const supportImportStart = '// '; + const supportLayerStart = '// '; const supportImports = handlerRoot.includes(supportImportStart) - ? generatedSlotRange( - handlerRoot, - supportImportStart, - '// ' - ) + ? generatedSlotRange(handlerRoot, supportImportStart, '// ') : null; const supportLayers = handlerRoot.includes(supportLayerStart) - ? generatedSlotRange( - handlerRoot, - supportLayerStart, - '// ' - ) + ? generatedSlotRange(handlerRoot, supportLayerStart, '// ') : null; - const generatedHandlers = assignedExpressionRange( - handlerRoot, - /export const governedReadApiHandlersLive\s*=\s*/u - ); + const generatedHandlers = assignedExpressionRange(handlerRoot, /export const governedReadApiHandlersLive\s*=\s*/u); return ( (supportImports === null || supportImports?.depth === 0) && (supportLayers === null || @@ -1019,21 +743,18 @@ const hasGovernedSupportSlots = (handlerRoot: string): boolean => { ); }; -export const hasValidGovernedHttpCompositionRoot = ( - sharedApi: string, - handlerRoot: string -): boolean => { +export const hasValidGovernedHttpCompositionRoot = (sharedApi: string, handlerRoot: string): boolean => { const sharedRoot = governedSharedApiRoot(sharedApi); const expectedApi = governedApiBinding(sharedApi); const sharedImports = generatedSlotRange( sharedApi, '// ', - '// ' + '// ', ); const handlerImports = generatedSlotRange( handlerRoot, '// ', - '// ' + '// ', ); const mounted = expectedApi !== undefined && @@ -1041,7 +762,7 @@ export const hasValidGovernedHttpCompositionRoot = ( handlerRoot, GOVERNED_HANDLER_LAYER_SLOT_START, GOVERNED_HANDLER_LAYER_SLOT_END, - expectedApi + expectedApi, ); return ( sharedRoot !== undefined && @@ -1053,21 +774,12 @@ export const hasValidGovernedHttpCompositionRoot = ( ); }; -const governedReadContribution = ( - contractStem: string, - source: string -): GovernedReadContribution | undefined => { +const governedReadContribution = (contractStem: string, source: string): GovernedReadContribution | undefined => { if (hasGeneratedSourceHeader(source, MODULE_API_HEADER)) { return { contractStem, kind: MODULE_API_KIND, name: contractStem }; } - const candidateKind = - /^\/\/ @ontos-contribution-kind (?report|search-provider)$/mu.exec( - source - )?.groups?.kind; - if ( - !hasGeneratedSourceHeader(source, GOVERNED_CONTRIBUTION_HEADER) || - !isGovernedReadKind(candidateKind) - ) { + const candidateKind = /^\/\/ @ontos-contribution-kind (?report|search-provider)$/mu.exec(source)?.groups?.kind; + if (!hasGeneratedSourceHeader(source, GOVERNED_CONTRIBUTION_HEADER) || !isGovernedReadKind(candidateKind)) { return undefined; } const kind = candidateKind; @@ -1083,9 +795,7 @@ const governedReadContribution = ( }; const generatedHeader = (kind: GovernedReadKind): string => - kind === MODULE_API_KIND - ? MODULE_API_HEADER - : `${GOVERNED_CONTRIBUTION_HEADER}// @ontos-contribution-kind ${kind}\n`; + kind === MODULE_API_KIND ? MODULE_API_HEADER : `${GOVERNED_CONTRIBUTION_HEADER}// @ontos-contribution-kind ${kind}\n`; const contributionProfiles = { 'module-api': { @@ -1120,83 +830,47 @@ const contributionProfiles = { }, } as const; -const contributionServerStem = ( - contribution: GovernedReadContribution -): string => - contribution.kind === MODULE_API_KIND - ? `${contribution.name}-read` - : contribution.contractStem; +const contributionServerStem = (contribution: GovernedReadContribution): string => + contribution.kind === MODULE_API_KIND ? `${contribution.name}-read` : contribution.contractStem; -const contributionRole = (kind: GovernedReadKind): string => - contributionProfiles[kind].role; +const contributionRole = (kind: GovernedReadKind): string => contributionProfiles[kind].role; const contributionGroup = (kind: GovernedReadKind, name: string): string => `${toCamelCase(name)}${contributionProfiles[kind].typeSuffix}`; const contributionApiValue = (kind: GovernedReadKind, name: string): string => `${toPascalCase(name)}${contributionProfiles[kind].typeSuffix}Api`; -const contributionReadDirectory = (kind: GovernedReadKind): string => - contributionProfiles[kind].directory; +const contributionReadDirectory = (kind: GovernedReadKind): string => contributionProfiles[kind].directory; const contributionSchemaStem = (kind: GovernedReadKind, name: string): string => `${toPascalCase(name)}${contributionProfiles[kind].schemaSuffix}`; -const contributionEndpoint = ( - contribution: GovernedReadContribution, - moduleId: string -): string => +const contributionEndpoint = (contribution: GovernedReadContribution, moduleId: string): string => contribution.kind === MODULE_API_KIND ? `/reads/${contribution.name}` : `/${moduleId}/${contributionProfiles[contribution.kind].directory}/${contribution.name}`; -const hasObjectProperties = ( - source: string, - expected: Readonly> -): boolean => - Object.entries(expected).every( - ([property, value]) => objectProperty(source, property) === value - ); +const hasObjectProperties = (source: string, expected: Readonly>): boolean => + Object.entries(expected).every(([property, value]) => objectProperty(source, property) === value); -const hasContractImports = ( - source: string, - expected: Readonly> -): boolean => - Object.entries(expected).every(([name, specifier]) => - hasExactValueImport(source, name, specifier) - ); +const hasContractImports = (source: string, expected: Readonly>): boolean => + Object.entries(expected).every(([name, specifier]) => hasExactValueImport(source, name, specifier)); -const isWholeObjectCall = ( - expression: string | undefined, - value: string | undefined, - callee: RegExp -): boolean => +const isWholeObjectCall = (expression: string | undefined, value: string | undefined, callee: RegExp): boolean => expression !== undefined && value !== undefined && !value.includes('...') && isWholeCallExpression(expression, callee); -const hasReadDescriptorPolicy = ( - read: string, - allowedAccessKinds: ReadonlySet -): boolean => { +const hasReadDescriptorPolicy = (read: string, allowedAccessKinds: ReadonlySet): boolean => { const policies = objectProperty(read, 'policies'); return ( allowedAccessKinds.has(objectProperty(read, 'accessKind') ?? '') && - matches( - objectProperty(read, 'legalEntityScope'), - /^'(?:required|optional|forbidden)'$/u - ) && - matches( - objectProperty(read, 'permissionTarget'), - /^'(?:legal_entity|module|resource|tenant)'$/u - ) && + matches(objectProperty(read, 'legalEntityScope'), /^'(?:required|optional|forbidden)'$/u) && + matches(objectProperty(read, 'permissionTarget'), /^'(?:legal_entity|module|resource|tenant)'$/u) && policies !== undefined && policies.startsWith('[') && matchingDelimiterEnd(policies, 0, '[', ']') === policies.length - 1 ); }; -const hasReadCallbacks = ( - source: string, - readExpression: string, - kind: GovernedReadKind -): boolean => { +const hasReadCallbacks = (source: string, readExpression: string, kind: GovernedReadKind): boolean => { const handler = callArgument(readExpression, /^defineRead\(/u, 1); const callbacks = [2, 3]; if (kind === SEARCH_PROVIDER_KIND) { @@ -1204,69 +878,36 @@ const hasReadCallbacks = ( } return ( isReadHandlerCallback(handler, source) && - callbacks.every((index) => - isExecutableCallback( - callArgument(readExpression, /^defineRead\(/u, index), - source - ) - ) + callbacks.every((index) => isExecutableCallback(callArgument(readExpression, /^defineRead\(/u, index), source)) ); }; -const hasReadEntrypoint = ( - entrypoint: string, - identity: Readonly> -): boolean => { +const hasReadEntrypoint = (entrypoint: string, identity: Readonly>): boolean => { const access = objectProperty(entrypoint, 'access'); - return ( - (access === "'read'" || access === "'historical_read'") && - hasObjectProperties(entrypoint, identity) - ); + return (access === "'read'" || access === "'historical_read'") && hasObjectProperties(entrypoint, identity); }; -const hasReadContract = ( - source: string, - contribution: GovernedReadContribution, - moduleId: string -): boolean => { +const hasReadContract = (source: string, contribution: GovernedReadContribution, moduleId: string): boolean => { const camel = toCamelCase(contribution.name); - const schemaStem = contributionSchemaStem( - contribution.kind, - contribution.name - ); + const schemaStem = contributionSchemaStem(contribution.kind, contribution.name); const escapedCamel = escapeRegExp(camel); const role = contributionRole(contribution.kind); - const allowedAccessKinds = - contributionProfiles[contribution.kind].accessKinds; + const allowedAccessKinds = contributionProfiles[contribution.kind].accessKinds; const contributionKey = `${moduleId}.${role}.${contribution.name}`; const entrypointExpression = assignedExpression( source, - new RegExp(`(?:export )?const ${escapedCamel}Entrypoint\\s*=\\s*`, 'u') - ); - const readExpression = assignedExpression( - source, - new RegExp(`export const ${escapedCamel}Read\\s*=\\s*`, 'u') - ); - const entrypoint = objectArgument( - entrypointExpression, - /^defineTenantModuleEntrypoint\(/u + new RegExp(`(?:export )?const ${escapedCamel}Entrypoint\\s*=\\s*`, 'u'), ); + const readExpression = assignedExpression(source, new RegExp(`export const ${escapedCamel}Read\\s*=\\s*`, 'u')); + const entrypoint = objectArgument(entrypointExpression, /^defineTenantModuleEntrypoint\(/u); const read = objectArgument(readExpression, /^defineRead\(/u); const inputSchema = `${schemaStem}RequestSchema`; const resultSchema = `${schemaStem}ResponseSchema`; - if ( - entrypoint === undefined || - read === undefined || - readExpression === undefined - ) { + if (entrypoint === undefined || read === undefined || readExpression === undefined) { return false; } return ( - isWholeObjectCall( - entrypointExpression, - entrypoint, - /^defineTenantModuleEntrypoint\(/u - ) && + isWholeObjectCall(entrypointExpression, entrypoint, /^defineTenantModuleEntrypoint\(/u) && isWholeObjectCall(readExpression, read, /^defineRead\(/u) && hasContractImports(source, { defineRead: '@app/core-runtime', @@ -1294,31 +935,23 @@ const hasClientContract = ( contribution: GovernedReadContribution, endpointGroup: string, apiValue: string, - deploymentAppId: string + deploymentAppId: string, ): boolean => { const type = toPascalCase(contribution.name); - const operation = - contribution.kind === MODULE_API_KIND - ? `execute${type}` - : `load${type}Client`; + const operation = contribution.kind === MODULE_API_KIND ? `execute${type}` : `load${type}Client`; return hasGeneratedGovernedClientContract(source, { authorizedOperation: `${operation}WithAuthorization`, defaultApiPrefix: `/${deploymentAppId}-api`, endpointGroup, generatedHeader: generatedHeader(contribution.kind), - invocationKind: - contribution.kind === MODULE_API_KIND ? 'module-api' : 'provider', + invocationKind: contribution.kind === MODULE_API_KIND ? 'module-api' : 'provider', ownerApiValue: apiValue, ownerContractImport: `../../shared/apis/${contribution.contractStem}.ts`, publicOperation: operation, }); }; -const hasProblemSet = ( - source: string, - schemaStem: string, - contractImport: string -): boolean => { +const hasProblemSet = (source: string, schemaStem: string, contractImport: string): boolean => { const expression = assignedExpression(source, /const problems\s*=\s*/u); const call = /^makeGovernedReadProblems\(/u; const schemas = objectArgument(expression, call); @@ -1326,11 +959,7 @@ const hasProblemSet = ( expression === undefined || schemas === undefined || !isWholeCallExpression(expression, call) || - !hasExactValueImport( - source, - 'makeGovernedReadProblems', - GOVERNED_HTTP_RUNTIME_MODULE - ) + !hasExactValueImport(source, 'makeGovernedReadProblems', GOVERNED_HTTP_RUNTIME_MODULE) ) { return false; } @@ -1351,7 +980,7 @@ const hasProblemSet = ( Object.entries(constructors).every( ([key, suffix]) => objectProperty(schemas, key) === `${schemaStem}${suffix}` && - hasExactValueImport(source, `${schemaStem}${suffix}`, contractImport) + hasExactValueImport(source, `${schemaStem}${suffix}`, contractImport), ) ); }; @@ -1359,7 +988,7 @@ const hasProblemSet = ( const hasOnlyThinServerStatements = ( source: string, exportedName: string, - allowedImports: ReadonlySet + allowedImports: ReadonlySet, ): boolean => { const spans: { readonly end: number; readonly start: number }[] = []; for (const declaration of [ @@ -1367,37 +996,28 @@ const hasOnlyThinServerStatements = ( new RegExp(`export const ${escapeRegExp(exportedName)}\\s*=\\s*`, 'u'), ]) { const range = assignedExpressionRange(source, declaration); - const match = [ - ...source.matchAll(new RegExp(declaration.source, 'gu')), - ].find((candidate) => isTopLevelCodePosition(source, candidate.index)); + const match = [...source.matchAll(new RegExp(declaration.source, 'gu'))].find((candidate) => + isTopLevelCodePosition(source, candidate.index), + ); if (range === undefined || match === undefined) { return false; } spans.push({ end: source.indexOf(';', range.end) + 1, start: match.index }); } - const imports = - /\bimport\s+\{[^}]*\}\s+from\s+['"](?[^'"]+)['"]\s*;/gu; + const imports = /\bimport\s+\{[^}]*\}\s+from\s+['"](?[^'"]+)['"]\s*;/gu; for (const match of source.matchAll(imports)) { - if ( - isTopLevelCodePosition(source, match.index) && - allowedImports.has(match.groups?.module ?? '') - ) { + if (isTopLevelCodePosition(source, match.index) && allowedImports.has(match.groups?.module ?? '')) { spans.push({ end: match.index + match[0].length, start: match.index }); } } let remaining = maskComments(source); for (const { end, start } of spans) { - remaining = - remaining.slice(0, start) + - ' '.repeat(end - start) + - remaining.slice(end); + remaining = remaining.slice(0, start) + ' '.repeat(end - start) + remaining.slice(end); } return remaining.trim() === ''; }; -const optionsFromHandlerCallback = ( - callbackSource: string | undefined -): string | undefined => { +const optionsFromHandlerCallback = (callbackSource: string | undefined): string | undefined => { const prefix = /^\(\s*handlers\s*\)\s*=>\s*/u.exec(callbackSource ?? ''); if (callbackSource === undefined || prefix === null) { return undefined; @@ -1419,21 +1039,16 @@ const optionsFromHandlerCallback = ( const serverHandlerOptions = ( layerExpression: string | undefined, expectedGroup: string, - apiBinding: string + apiBinding: string, ): string | undefined => { if ( !wholeCall(layerExpression, /^HttpApiBuilder\.group\(/u) || callArgument(layerExpression, /^HttpApiBuilder\.group\(/u) !== apiBinding || - callArgument(layerExpression, /^HttpApiBuilder\.group\(/u, 1) !== - `'${expectedGroup}'` + callArgument(layerExpression, /^HttpApiBuilder\.group\(/u, 1) !== `'${expectedGroup}'` ) { return undefined; } - const callback = callArgument( - layerExpression, - /^HttpApiBuilder\.group\(/u, - 2 - ); + const callback = callArgument(layerExpression, /^HttpApiBuilder\.group\(/u, 2); return optionsFromHandlerCallback(callback); }; @@ -1453,18 +1068,11 @@ const hasServerContract = ( readImport: string, schemaStem: string, contractImport: string, - apiBinding: string + apiBinding: string, ): boolean => { const code = maskComments(source); - const layerExpression = assignedExpression( - code, - new RegExp(`export const ${escapedCamel}ReadApiLive\\s*=\\s*`, 'u') - ); - const options = serverHandlerOptions( - layerExpression, - escapedGroup, - apiBinding - ); + const layerExpression = assignedExpression(code, new RegExp(`export const ${escapedCamel}ReadApiLive\\s*=\\s*`, 'u')); + const options = serverHandlerOptions(layerExpression, escapedGroup, apiBinding); return ( options !== undefined && hasServerOptions(options, `${escapedCamel}Read`) && @@ -1479,7 +1087,7 @@ const hasServerContract = ( '../shared/api.ts', readImport, contractImport, - ]) + ]), ) && hasContractImports(source, { [`${escapedCamel}Read`]: readImport, @@ -1489,33 +1097,17 @@ const hasServerContract = ( makeGovernedReadHttpHandler: GOVERNED_READ_HTTP_MODULE, }) && hasProblemSet(source, schemaStem, contractImport) && - [/HttpApiBuilder\.group\(/gu, /makeGovernedReadHttpHandler\(/gu].every( - (pattern) => hasExactlyOne(code, pattern) - ) + [/HttpApiBuilder\.group\(/gu, /makeGovernedReadHttpHandler\(/gu].every((pattern) => hasExactlyOne(code, pattern)) ); }; -const hasProblemSchemaContract = ( - source: string, - schema: string, - status: number, - retryable: boolean -): boolean => { - const expression = assignedExpression( - source, - new RegExp(`export const ${escapeRegExp(schema)}\\s*=\\s*`, 'u') - ); - const factory = retryable - ? 'makeRetryableProblemDetailsSchema' - : 'makeProblemDetailsSchema'; +const hasProblemSchemaContract = (source: string, schema: string, status: number, retryable: boolean): boolean => { + const expression = assignedExpression(source, new RegExp(`export const ${escapeRegExp(schema)}\\s*=\\s*`, 'u')); + const factory = retryable ? 'makeRetryableProblemDetailsSchema' : 'makeProblemDetailsSchema'; const call = new RegExp(`^${factory}\\(`, 'u'); return ( expression !== undefined && - hasExactValueImport( - source, - factory, - '@app/shared-contracts/problem-details' - ) && + hasExactValueImport(source, factory, '@app/shared-contracts/problem-details') && isWholeCallExpression(expression, call) && callArgument(expression, call) === `'${schema.replace(/Schema$/u, '')}'` && callArgument(expression, call, 1) === String(status) && @@ -1524,11 +1116,7 @@ const hasProblemSchemaContract = ( ); }; -const addedContractMember = ( - expression: string | undefined, - factory: string, - name: string -): string | undefined => { +const addedContractMember = (expression: string | undefined, factory: string, name: string): string | undefined => { if (expression === undefined || !expression.startsWith(`${factory}.make(`)) { return undefined; } @@ -1536,57 +1124,36 @@ const addedContractMember = ( if (callArgument(expression, make) !== `'${name}'`) { return undefined; } - const close = matchingDelimiterEnd( - expression, - expression.indexOf('('), - '(', - ')' - ); + const close = matchingDelimiterEnd(expression, expression.indexOf('('), '(', ')'); if (close === undefined) { return undefined; } const addition = expression.slice(close + 1).trim(); - return addition.startsWith('.add(') && - isWholeCallExpression(addition, /^\.add\(/u) + return addition.startsWith('.add(') && isWholeCallExpression(addition, /^\.add\(/u) ? callArgument(addition, /^\.add\(/u) : undefined; }; -const isExecuteEndpoint = ( - expression: string | undefined, - endpointPath: string -): boolean => +const isExecuteEndpoint = (expression: string | undefined, endpointPath: string): boolean => wholeCall(expression, /^HttpApiEndpoint\.post\(/u) && callArgument(expression, /^HttpApiEndpoint\.post\(/u) === "'execute'" && - callArgument(expression, /^HttpApiEndpoint\.post\(/u, 1) === - `'${endpointPath}'`; + callArgument(expression, /^HttpApiEndpoint\.post\(/u, 1) === `'${endpointPath}'`; const hasHttpContract = ( source: string, apiValue: string, group: string, schemaStem: string, - endpointPath: string + endpointPath: string, ): boolean => { const escapedApiValue = escapeRegExp(apiValue); - const expression = assignedExpression( - source, - new RegExp(`export const ${escapedApiValue}\\s*=\\s*`, 'u') - ); + const expression = assignedExpression(source, new RegExp(`export const ${escapedApiValue}\\s*=\\s*`, 'u')); const groupExpression = addedContractMember(expression, 'HttpApi', apiValue); - const endpointExpression = addedContractMember( - groupExpression, - 'HttpApiGroup', - group - ); + const endpointExpression = addedContractMember(groupExpression, 'HttpApiGroup', group); if (!isExecuteEndpoint(endpointExpression, endpointPath)) { return false; } - const options = objectArgument( - endpointExpression, - /^HttpApiEndpoint\.post\(/u, - 2 - ); + const options = objectArgument(endpointExpression, /^HttpApiEndpoint\.post\(/u, 2); const problems = [ `${schemaStem}InvalidProblemSchema`, `${schemaStem}AuthenticationProblemSchema`, @@ -1609,12 +1176,7 @@ const hasHttpContract = ( HttpApiGroup: HTTP_API_CONTRACT_MODULE, }) && problems.every((problem, index) => - hasProblemSchemaContract( - source, - problem, - problemStatuses[index] ?? -1, - index === 6 - ) + hasProblemSchemaContract(source, problem, problemStatuses[index] ?? -1, index === 6), ) && objectProperty(options, 'payload') === `${schemaStem}RequestSchema` && objectProperty(options, 'success') === `${schemaStem}ResponseSchema` && @@ -1622,41 +1184,26 @@ const hasHttpContract = ( ); }; -const hasManifestContract = ( - manifest: string, - contribution: GovernedReadContribution, - moduleId: string -): boolean => { +const hasManifestContract = (manifest: string, contribution: GovernedReadContribution, moduleId: string): boolean => { const allOwnerManifestEntries = entriesAcrossSlots(manifest, [ [MANIFEST_API_SLOT_START, MANIFEST_API_SLOT_END], - [ - '// ', - '// ', - ], - [ - '// ', - '// ', - ], + ['// ', '// '], + ['// ', '// '], ]); if (contribution.kind === MODULE_API_KIND) { - const apiEntries = generatedSlotEntries( - manifest, - MANIFEST_API_SLOT_START, - MANIFEST_API_SLOT_END - ); + const apiEntries = generatedSlotEntries(manifest, MANIFEST_API_SLOT_START, MANIFEST_API_SLOT_END); return ( hasExactValueImport( manifest, contributionApiValue(contribution.kind, contribution.name), - `./shared/apis/${contribution.contractStem}.ts` + `./shared/apis/${contribution.contractStem}.ts`, ) && [apiEntries, allOwnerManifestEntries].every( (entries) => entries?.filter( (entry) => - entry === - `'${contribution.contractStem}': ${contributionApiValue(contribution.kind, contribution.name)}` - ).length === 1 + entry === `'${contribution.contractStem}': ${contributionApiValue(contribution.kind, contribution.name)}`, + ).length === 1, ) ); } @@ -1665,34 +1212,21 @@ const hasManifestContract = ( const manifestEntries = generatedSlotEntries( manifest, `// `, - `// ` + `// `, ); const shellEntries = generatedSlotEntries( manifest, `// `, - `// ` + `// `, ); const manifestKey = `${moduleId}.${contribution.name}`; const entrypointKey = `${moduleId}.${role}.${contribution.name}`; - const published = manifestEntries?.filter( - (entry) => objectProperty(entry, 'key') === `'${manifestKey}'` - ); - const allPublished = allOwnerManifestEntries?.filter( - (entry) => objectProperty(entry, 'key') === `'${manifestKey}'` - ); - const shellFunction = - contribution.kind === REPORT_KIND - ? 'reportContribution' - : 'searchContribution'; + const published = manifestEntries?.filter((entry) => objectProperty(entry, 'key') === `'${manifestKey}'`); + const allPublished = allOwnerManifestEntries?.filter((entry) => objectProperty(entry, 'key') === `'${manifestKey}'`); + const shellFunction = contribution.kind === REPORT_KIND ? 'reportContribution' : 'searchContribution'; const shell = shellEntries?.filter((entry) => { - const shellObject = objectArgument( - entry, - new RegExp(`^${shellFunction}\\(`, 'u') - ); - const entrypoint = - shellObject === undefined - ? undefined - : objectProperty(shellObject, 'entrypoint'); + const shellObject = objectArgument(entry, new RegExp(`^${shellFunction}\\(`, 'u')); + const entrypoint = shellObject === undefined ? undefined : objectProperty(shellObject, 'entrypoint'); return ( shellObject !== undefined && isWholeCallExpression(entry, new RegExp(`^${shellFunction}\\(`, 'u')) && @@ -1702,47 +1236,31 @@ const hasManifestContract = ( objectProperty(entrypoint, 'role') === `'${role}'` ); }); - return ( - published?.length === 1 && allPublished?.length === 1 && shell?.length === 1 - ); + return published?.length === 1 && allPublished?.length === 1 && shell?.length === 1; }; -const hasPublishedRegistration = ( - registration: string, - contribution: GovernedReadContribution -): boolean => { +const hasPublishedRegistration = (registration: string, contribution: GovernedReadContribution): boolean => { const escapedName = escapeRegExp(contribution.name); const escapedContractStem = escapeRegExp(contribution.contractStem); - const { registrationCategory, registrationSection } = - contributionProfiles[contribution.kind]; + const { registrationCategory, registrationSection } = contributionProfiles[contribution.kind]; const registrationEntries = generatedSlotEntries( registration, `// `, - `// ` + `// `, ); const allRegistrationEntries = entriesAcrossSlots(registration, [ - [ - '// ', - '// ', - ], - [ - '// ', - '// ', - ], - [ - '// ', - '// ', - ], + ['// ', '// '], + ['// ', '// '], + ['// ', '// '], ]); const registrationPattern = new RegExp( `^(?:'${escapedName}'|${escapedName})\\s*:\\s*\\(\\s*\\)\\s*=>\\s*import\\(\\s*'./src/api/${escapedContractStem}-client\\.ts'\\s*\\)$`, - 'u' + 'u', ); const canonicalRegistrationPattern = /^(?:'(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)'|(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*))\s*:\s*\(\s*\)\s*=>\s*import\(\s*'\.\/src\/api\/(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)-client\.ts'\s*\)$/u; return ( - registrationEntries?.filter((entry) => registrationPattern.test(entry)) - .length === 1 && + registrationEntries?.filter((entry) => registrationPattern.test(entry)).length === 1 && allRegistrationEntries?.every((entry) => { const match = canonicalRegistrationPattern.exec(entry); const name = match?.groups?.quoted ?? match?.groups?.bare; @@ -1750,40 +1268,29 @@ const hasPublishedRegistration = ( return ( name !== undefined && file !== undefined && - (file === name || - file === `${name}-report` || - file === `${name}-search`) + (file === name || file === `${name}-report` || file === `${name}-search`) ); }) === true && - allRegistrationEntries?.filter( - (entry) => objectEntryKey(entry) === contribution.name - ).length === 1 && + allRegistrationEntries?.filter((entry) => objectEntryKey(entry) === contribution.name).length === 1 && slotIsInsideObjectProperty( registration, registrationCategory, `// `, - `// ` + `// `, ) ); }; -const publishesSharedApiContribution = ( - sharedApi: string, - apiValue: string, - contractStem: string -): boolean => { +const publishesSharedApiContribution = (sharedApi: string, apiValue: string, contractStem: string): boolean => { const escapedApiValue = escapeRegExp(apiValue); return ( - hasExactlyOne( - maskComments(sharedApi), - new RegExp(`\\.addHttpApi\\(${escapedApiValue}\\)`, 'gu') - ) && + hasExactlyOne(maskComments(sharedApi), new RegExp(`\\.addHttpApi\\(${escapedApiValue}\\)`, 'gu')) && hasExactValueImport(sharedApi, apiValue, `./apis/${contractStem}.ts`) && slotHasExactlyOneCodeMatch( sharedApi, GOVERNED_API_SLOT_START, GOVERNED_API_SLOT_END, - new RegExp(`\\.addHttpApi\\(${escapedApiValue}\\)`, 'gu') + new RegExp(`\\.addHttpApi\\(${escapedApiValue}\\)`, 'gu'), ) ); }; @@ -1795,7 +1302,7 @@ const hasPublishedContract = ( handlerRoot: string, contribution: GovernedReadContribution, moduleId: string, - diagnostics?: string[] + diagnostics?: string[], ): boolean => { const expectedApi = governedApiBinding(sharedApi); const apiValue = contributionApiValue(contribution.kind, contribution.name); @@ -1805,60 +1312,34 @@ const hasPublishedContract = ( const handlerLayers = generatedSlotEntries( handlerRoot, GOVERNED_HANDLER_LAYER_SLOT_START, - GOVERNED_HANDLER_LAYER_SLOT_END + GOVERNED_HANDLER_LAYER_SLOT_END, ); const expectedLayer = `${camel}ReadApiLive`; const isExpectedHandlerLayer = (entry: string): boolean => { - if ( - !isWholeCallExpression( - entry, - new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u') - ) - ) { + if (!isWholeCallExpression(entry, new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'))) { return false; } - const first = callArgument( - entry, - new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u') - ); + const first = callArgument(entry, new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u')); if (first !== 'GovernedReadLayer.provide(governedReadRuntimeLive)') { return false; } let index = 1; - let argument = callArgument( - entry, - new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), - index - ); + let argument = callArgument(entry, new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), index); while (argument !== undefined) { if (argument === '') { - return ( - callArgument( - entry, - new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), - index + 1 - ) === undefined - ); + return callArgument(entry, new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), index + 1) === undefined; } if (!/^Layer\.provide\([A-Za-z][A-Za-z0-9]*\)$/u.test(argument)) { return false; } index += 1; - argument = callArgument( - entry, - new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), - index - ); + argument = callArgument(entry, new RegExp(`^${escapedCamel}ReadApiLive\\.pipe\\(`, 'u'), index); } return entry.startsWith(`${expectedLayer}.pipe(`); }; const publicationChecks = { apiBinding: expectedApi !== undefined, - handlerImport: hasExactValueImport( - handlerRoot, - `${camel}ReadApiLive`, - `./${serverStem}-server.ts` - ), + handlerImport: hasExactValueImport(handlerRoot, `${camel}ReadApiLive`, `./${serverStem}-server.ts`), handlerLayer: handlerLayers?.filter(isExpectedHandlerLayer).length === 1, handlerRoot: hasGovernedHandlerRoot(handlerRoot), manifest: hasManifestContract(manifest, contribution, moduleId), @@ -1868,14 +1349,10 @@ const hasPublishedContract = ( handlerRoot, GOVERNED_HANDLER_LAYER_SLOT_START, GOVERNED_HANDLER_LAYER_SLOT_END, - expectedApi + expectedApi, ), registration: hasPublishedRegistration(registration, contribution), - sharedContribution: publishesSharedApiContribution( - sharedApi, - apiValue, - contribution.contractStem - ), + sharedContribution: publishesSharedApiContribution(sharedApi, apiValue, contribution.contractStem), sharedRoot: hasGovernedSharedApiRoot(sharedApi), }; const failed = Object.entries(publicationChecks) @@ -1892,74 +1369,48 @@ const hasPublishedContract = ( * File presence alone is intentionally insufficient: each contract must remain * owner-published, privately registered, authenticated, gated, and contract-derived. */ -const actionBoundaryHeader = - '// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n'; -const hasActionBoundaryAuthentication = ( - source: string | undefined -): boolean => { +const actionBoundaryHeader = '// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n'; +const hasActionBoundaryAuthentication = (source: string | undefined): boolean => { if (source === undefined || !source.startsWith(actionBoundaryHeader)) { return false; } - const expression = assignedExpression( - source, - /export const authenticateOperationPrincipal\s*=\s*/u - ); + const expression = assignedExpression(source, /export const authenticateOperationPrincipal\s*=\s*/u); return ( expression !== undefined && - isWholeCallExpression( - expression, - /^makeMicroverticalHttpPrincipalAuthentication\(/u - ) && + isWholeCallExpression(expression, /^makeMicroverticalHttpPrincipalAuthentication\(/u) && hasExactValueImport( source, 'makeMicroverticalHttpPrincipalAuthentication', - '@app/core-runtime/http/principal-authentication' + '@app/core-runtime/http/principal-authentication', ) && hasGeneratedOperationPrincipalContract(source) ); }; -const hasActionBoundaryGateway = ( - source: string | undefined, - appId: string -): boolean => +const hasActionBoundaryGateway = (source: string | undefined, appId: string): boolean => source !== undefined && source.startsWith(actionBoundaryHeader) && - assignedExpression(source, /export const operationGateway\s*=\s*/u) === - 'makeOperationGateway()' && + assignedExpression(source, /export const operationGateway\s*=\s*/u) === 'makeOperationGateway()' && hasGeneratedOperationGatewayContract(source, appId); -type GeneratedContribution = readonly [ - string, - string, - GovernedReadContribution | undefined, -]; +type GeneratedContribution = readonly [string, string, GovernedReadContribution | undefined]; const hasMatchingModuleApiSlots = ( manifest: string, registration: string, - contributions: readonly GeneratedContribution[] + contributions: readonly GeneratedContribution[], ): boolean => { - const moduleApiCount = contributions.filter( - (entry) => entry[2]?.kind === MODULE_API_KIND - ).length; - const manifestApis = generatedSlotEntries( - manifest, - MANIFEST_API_SLOT_START, - MANIFEST_API_SLOT_END - ); + const moduleApiCount = contributions.filter((entry) => entry[2]?.kind === MODULE_API_KIND).length; + const manifestApis = generatedSlotEntries(manifest, MANIFEST_API_SLOT_START, MANIFEST_API_SLOT_END); const registrationApis = generatedSlotEntries( registration, '// ', - '// ' - ); - return ( - manifestApis?.length === moduleApiCount && - registrationApis?.length === moduleApiCount + '// ', ); + return manifestApis?.length === moduleApiCount && registrationApis?.length === moduleApiCount; }; const generatedReadContributions = ( sources: ReadonlyMap, - verticalPath: string + verticalPath: string, ): readonly GeneratedContribution[] => { const contractPrefix = `${verticalPath}/shared/apis/`; const generatedContracts = [...sources.entries()].filter( @@ -1968,29 +1419,22 @@ const generatedReadContributions = ( candidate.endsWith('.ts') && !candidate.slice(contractPrefix.length).includes('/') && (hasGeneratedSourceHeader(source, MODULE_API_HEADER) || - hasGeneratedSourceHeader(source, GOVERNED_CONTRIBUTION_HEADER)) + hasGeneratedSourceHeader(source, GOVERNED_CONTRIBUTION_HEADER)), ); return generatedContracts.map( ([candidate, source]) => - [ - candidate, - source, - governedReadContribution(path.posix.basename(candidate, '.ts'), source), - ] as const + [candidate, source, governedReadContribution(path.posix.basename(candidate, '.ts'), source)] as const, ); }; -const governedOwnerModuleId = ( - manifest: string | undefined -): string | undefined => - /^\/\/ @ontos-module-id (?[^\s]+)$/mu.exec(manifest ?? '')?.groups - ?.moduleId; +const governedOwnerModuleId = (manifest: string | undefined): string | undefined => + /^\/\/ @ontos-module-id (?[^\s]+)$/mu.exec(manifest ?? '')?.groups?.moduleId; const loadContributionSources = ( sources: ReadonlyMap, verticalPath: string, contribution: GovernedReadContribution, - contractSource: string + contractSource: string, ): | { readonly clientSource: string; @@ -2003,19 +1447,15 @@ const loadContributionSources = ( const { readSuffix } = contributionProfiles[contribution.kind]; const readPath = `src/${readDirectory}/${contribution.name}${readSuffix}.ts`; const readSource = sources.get(`${verticalPath}/${readPath}`); - const clientSource = sources.get( - `${verticalPath}/src/api/${contribution.contractStem}-client.ts` - ); - const serverSource = sources.get( - `${verticalPath}/api/${contributionServerStem(contribution)}-server.ts` - ); + const clientSource = sources.get(`${verticalPath}/src/api/${contribution.contractStem}-client.ts`); + const serverSource = sources.get(`${verticalPath}/api/${contributionServerStem(contribution)}-server.ts`); const header = generatedHeader(contribution.kind); if ( readSource === undefined || clientSource === undefined || serverSource === undefined || ![contractSource, readSource, clientSource, serverSource].every((source) => - hasGeneratedSourceHeader(source, header) + hasGeneratedSourceHeader(source, header), ) ) { return undefined; @@ -2031,10 +1471,8 @@ const loadContributionSources = ( export const hasCompleteGeneratedModuleApiSeam = ( sources: ReadonlyMap, sharedApiFile: string, - deploymentAppId = path.posix.basename( - sharedApiFile.slice(0, -'/shared/api.ts'.length) - ), - diagnostics?: string[] + deploymentAppId = path.posix.basename(sharedApiFile.slice(0, -'/shared/api.ts'.length)), + diagnostics?: string[], ): boolean => { const verticalPath = sharedApiFile.slice(0, -'/shared/api.ts'.length); const sharedApi = sources.get(sharedApiFile); @@ -2053,10 +1491,7 @@ export const hasCompleteGeneratedModuleApiSeam = ( ) { return false; } - if ( - !hasActionBoundaryAuthentication(principal) || - !hasActionBoundaryGateway(gateway, deploymentAppId) - ) { + if (!hasActionBoundaryAuthentication(principal) || !hasActionBoundaryGateway(gateway, deploymentAppId)) { diagnostics?.push('owner authentication or gateway'); return false; } @@ -2078,50 +1513,23 @@ export const hasCompleteGeneratedModuleApiSeam = ( const camel = toCamelCase(contribution.name); const group = contributionGroup(contribution.kind, contribution.name); const apiValue = contributionApiValue(contribution.kind, contribution.name); - const loaded = loadContributionSources( - sources, - verticalPath, - contribution, - contractSource - ); + const loaded = loadContributionSources(sources, verticalPath, contribution, contractSource); if (loaded === undefined) { - diagnostics?.push( - `${contribution.name}: missing source or provenance marker` - ); + diagnostics?.push(`${contribution.name}: missing source or provenance marker`); return false; } const { clientSource, readImport, readSource, serverSource } = loaded; const escapedCamel = escapeRegExp(camel); const escapedGroup = escapeRegExp(group); - const schemaStem = contributionSchemaStem( - contribution.kind, - contribution.name - ); + const schemaStem = contributionSchemaStem(contribution.kind, contribution.name); const endpointPath = contributionEndpoint(contribution, moduleId); const checks = { - client: hasClientContract( - clientSource, - contribution, - group, - apiValue, - deploymentAppId - ), + client: hasClientContract(clientSource, contribution, group, apiValue, deploymentAppId), contract: - hasHttpContract( - contractSource, - apiValue, - group, - schemaStem, - endpointPath - ) && + hasHttpContract(contractSource, apiValue, group, schemaStem, endpointPath) && (contribution.kind !== MODULE_API_KIND || - hasGeneratedModuleApiContract( - contractSource, - apiValue, - camel, - contribution.name - )), + hasGeneratedModuleApiContract(contractSource, apiValue, camel, contribution.name)), published: hasPublishedContract( sharedApi, manifest, @@ -2129,16 +1537,12 @@ export const hasCompleteGeneratedModuleApiSeam = ( handlerRoot, contribution, moduleId, - diagnostics + diagnostics, ), read: hasReadContract(readSource, contribution, moduleId) && (contribution.kind !== MODULE_API_KIND || - hasGeneratedModuleApiReadContract( - readSource, - moduleId, - contribution.name - )), + hasGeneratedModuleApiReadContract(readSource, moduleId, contribution.name)), server: hasServerContract( serverSource, escapedCamel, @@ -2146,7 +1550,7 @@ export const hasCompleteGeneratedModuleApiSeam = ( readImport, schemaStem, `../shared/apis/${contribution.contractStem}.ts`, - governedApiBinding(sharedApi) ?? '' + governedApiBinding(sharedApi) ?? '', ), }; const failed = Object.entries(checks) @@ -2168,28 +1572,22 @@ const readDirectoryKinds = new Map([ export const hasGeneratedGovernedServerContract = ( source: string, exportedName: string, - sharedApi: string + sharedApi: string, ): boolean => { const camel = exportedName.replace(/ReadApiLive$/u, ''); const readImport = /from '(?\.\.\/src\/(?api|search|reports)\/(?[a-z0-9-]+)\.(?:read|provider)\.ts)'/u.exec( - source + source, ); const [, readPath, directory, name] = readImport ?? []; - if ( - readPath === undefined || - name === undefined || - directory === undefined || - camel !== toCamelCase(name) - ) { + if (readPath === undefined || name === undefined || directory === undefined || camel !== toCamelCase(name)) { return false; } const kind = readDirectoryKinds.get(directory); if (kind === undefined) { return false; } - const stem = - kind === MODULE_API_KIND ? name : `${name}-${contributionRole(kind)}`; + const stem = kind === MODULE_API_KIND ? name : `${name}-${contributionRole(kind)}`; const schemaStem = contributionSchemaStem(kind, name); return ( hasGeneratedSourceHeader(source, generatedHeader(kind)) && @@ -2200,7 +1598,7 @@ export const hasGeneratedGovernedServerContract = ( readPath, schemaStem, `../shared/apis/${stem}.ts`, - governedApiBinding(sharedApi) ?? '' + governedApiBinding(sharedApi) ?? '', ) ); }; diff --git a/app/scripts/generated-module-api-boundary.mts b/app/scripts/generated-module-api-boundary.mts index 0eebaa9b6..dcf76ef4e 100644 --- a/app/scripts/generated-module-api-boundary.mts +++ b/app/scripts/generated-module-api-boundary.mts @@ -1,8 +1,4 @@ -import { - LanguageVariant, - SyntaxKind, - createScanner, -} from '@typescript/native/unstable/ast'; +import { LanguageVariant, SyntaxKind, createScanner } from '@typescript/native/unstable/ast'; import { DelimiterDepth, toCamelCase } from './boundary-source-structure.mts'; @@ -43,14 +39,9 @@ export interface GovernedClientToken { } /** Out-of-range lookahead is a nonmatching token, never an invented identifier. */ -const tokenKind = ( - tokens: readonly GovernedClientToken[], - index: number -): SyntaxKind | undefined => tokens[index]?.kind; -const tokenValue = ( - tokens: readonly GovernedClientToken[], - index: number -): string | undefined => tokens[index]?.value; +const tokenKind = (tokens: readonly GovernedClientToken[], index: number): SyntaxKind | undefined => + tokens[index]?.kind; +const tokenValue = (tokens: readonly GovernedClientToken[], index: number): string | undefined => tokens[index]?.value; const tokenDelimiter = new Map([ [SyntaxKind.OpenBraceToken, '{'], @@ -83,15 +74,14 @@ const REGULAR_EXPRESSION_PRECEDERS = new Set([ const scanTemplateDelimiter = ( scanner: ReturnType, scannedKind: SyntaxKind, - templateExpressionBraceDepths: number[] + templateExpressionBraceDepths: number[], ): SyntaxKind => { let kind = scannedKind; const templateDepthIndex = templateExpressionBraceDepths.length - 1; if (kind === SyntaxKind.TemplateHead) { templateExpressionBraceDepths.push(0); } else if (kind === SyntaxKind.OpenBraceToken && templateDepthIndex >= 0) { - templateExpressionBraceDepths[templateDepthIndex] = - (templateExpressionBraceDepths[templateDepthIndex] ?? 0) + 1; + templateExpressionBraceDepths[templateDepthIndex] = (templateExpressionBraceDepths[templateDepthIndex] ?? 0) + 1; } else if (kind === SyntaxKind.CloseBraceToken && templateDepthIndex >= 0) { const braceDepth = templateExpressionBraceDepths[templateDepthIndex] ?? 0; if (braceDepth === 0) { @@ -106,9 +96,7 @@ const scanTemplateDelimiter = ( return kind; }; -export const tokenizeGovernedClient = ( - source: string -): readonly GovernedClientToken[] => { +export const tokenizeGovernedClient = (source: string): readonly GovernedClientToken[] => { const scanner = createScanner(true, LanguageVariant.Standard, source); const tokens: GovernedClientToken[] = []; const templateExpressionBraceDepths: number[] = []; @@ -117,10 +105,7 @@ export const tokenizeGovernedClient = ( let kind: SyntaxKind = scannedKind; if ( kind === SyntaxKind.SlashToken && - (tokens.length === 0 || - REGULAR_EXPRESSION_PRECEDERS.has( - tokens.at(-1)?.kind ?? SyntaxKind.Unknown - )) + (tokens.length === 0 || REGULAR_EXPRESSION_PRECEDERS.has(tokens.at(-1)?.kind ?? SyntaxKind.Unknown)) ) { kind = scanner.reScanSlashToken(); } @@ -131,14 +116,8 @@ export const tokenizeGovernedClient = ( return tokens; }; -const importStateAfter = ( - kind: SyntaxKind, - inImport: boolean -): boolean | undefined => { - if ( - kind === SyntaxKind.SingleLineCommentTrivia || - kind === SyntaxKind.MultiLineCommentTrivia - ) { +const importStateAfter = (kind: SyntaxKind, inImport: boolean): boolean | undefined => { + if (kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia) { return inImport; } if (kind === SyntaxKind.ImportKeyword) { @@ -148,25 +127,14 @@ const importStateAfter = ( }; // Comments from the leading import section, with undefined marking interruptions. -const leadingSourceComments = function* leadingSourceComments( - source: string -): Generator { +const leadingSourceComments = function* leadingSourceComments(source: string): Generator { const scanner = createScanner(false, LanguageVariant.Standard, source); let inImport = false; - for ( - let kind = scanner.scan(); - kind !== SyntaxKind.EndOfFile; - kind = scanner.scan() - ) { - if ( - kind === SyntaxKind.WhitespaceTrivia || - kind === SyntaxKind.NewLineTrivia - ) { + for (let kind = scanner.scan(); kind !== SyntaxKind.EndOfFile; kind = scanner.scan()) { + if (kind === SyntaxKind.WhitespaceTrivia || kind === SyntaxKind.NewLineTrivia) { continue; } - yield kind === SyntaxKind.SingleLineCommentTrivia - ? scanner.getTokenText().trim() - : undefined; + yield kind === SyntaxKind.SingleLineCommentTrivia ? scanner.getTokenText().trim() : undefined; const nextState = importStateAfter(kind, inImport); if (nextState === undefined) { return; @@ -176,10 +144,7 @@ const leadingSourceComments = function* leadingSourceComments( }; /** Import sorting may move provenance comments between leading imports. */ -export const hasGeneratedSourceHeader = ( - source: string, - header: string -): boolean => { +export const hasGeneratedSourceHeader = (source: string, header: string): boolean => { const expected = header.trim().split(/\r?\n/u); let matched = 0; for (const comment of leadingSourceComments(source)) { @@ -195,35 +160,23 @@ export const hasGeneratedSourceHeader = ( return false; }; -const matchesToken = ( - token: GovernedClientToken | undefined, - expected: ExpectedToken -): boolean => - token?.kind === expected[0] && - (expected[1] === undefined || token.value === expected[1]); +const matchesToken = (token: GovernedClientToken | undefined, expected: ExpectedToken): boolean => + token?.kind === expected[0] && (expected[1] === undefined || token.value === expected[1]); const matchesSequence = ( tokens: readonly GovernedClientToken[], start: number, - expected: readonly ExpectedToken[] -): boolean => - expected.every((token, offset) => - matchesToken(tokens[start + offset], token) - ); + expected: readonly ExpectedToken[], +): boolean => expected.every((token, offset) => matchesToken(tokens[start + offset], token)); -const isOptionalTrailingComma = ( - tokens: readonly GovernedClientToken[], - next: number, - close: number -): boolean => - next === close || - (tokenKind(tokens, next) === SyntaxKind.CommaToken && next + 1 === close); +const isOptionalTrailingComma = (tokens: readonly GovernedClientToken[], next: number, close: number): boolean => + next === close || (tokenKind(tokens, next) === SyntaxKind.CommaToken && next + 1 === close); const findSequence = ( tokens: readonly GovernedClientToken[], expected: readonly ExpectedToken[], start = 0, - end = tokens.length + end = tokens.length, ): number | undefined => { for (let index = start; index < end; index += 1) { if (matchesSequence(tokens, index, expected)) { @@ -238,14 +191,11 @@ const findSequenceAtBraceDepth = ( expected: readonly ExpectedToken[], start: number, end: number, - expectedDepth: number + expectedDepth: number, ): number | undefined => { let braceDepth = 0; for (let index = start; index < end; index += 1) { - if ( - braceDepth === expectedDepth && - matchesSequence(tokens, index, expected) - ) { + if (braceDepth === expectedDepth && matchesSequence(tokens, index, expected)) { return index; } braceDepth += tokenBraceDelta(tokenKind(tokens, index)); @@ -256,15 +206,12 @@ const findSequenceAtBraceDepth = ( const sequenceOccurrencesAtBraceDepth = ( tokens: readonly GovernedClientToken[], expected: readonly ExpectedToken[], - expectedDepth: number + expectedDepth: number, ): number => { let count = 0; let braceDepth = 0; for (let index = 0; index < tokens.length; index += 1) { - if ( - braceDepth === expectedDepth && - matchesSequence(tokens, index, expected) - ) { + if (braceDepth === expectedDepth && matchesSequence(tokens, index, expected)) { count += 1; } braceDepth += tokenBraceDelta(tokenKind(tokens, index)); @@ -272,10 +219,7 @@ const sequenceOccurrencesAtBraceDepth = ( return count; }; -const generatedSlotSource = ( - source: string, - [start, end]: readonly [string, string] -): string | undefined => { +const generatedSlotSource = (source: string, [start, end]: readonly [string, string]): string | undefined => { const startIndex = source.indexOf(start); const endIndex = source.indexOf(end); if ( @@ -294,38 +238,29 @@ const findTopLevelSequence = ( tokens: readonly GovernedClientToken[], expected: readonly ExpectedToken[], start: number, - end: number -): number | undefined => - findSequenceAtBraceDepth(tokens, expected, start, end, 0); + end: number, +): number | undefined => findSequenceAtBraceDepth(tokens, expected, start, end, 0); -const hasTopLevelSequence = ( - tokens: readonly GovernedClientToken[], - expected: readonly ExpectedToken[] -): boolean => +const hasTopLevelSequence = (tokens: readonly GovernedClientToken[], expected: readonly ExpectedToken[]): boolean => findTopLevelSequence(tokens, expected, 0, tokens.length) !== undefined; const findRootExpressionSequence = ( tokens: readonly GovernedClientToken[], expected: readonly ExpectedToken[], start: number, - end: number + end: number, ): number | undefined => { const depth = new DelimiterDepth(); for (let index = start; index < end; index += 1) { if (depth.isTopLevel() && matchesSequence(tokens, index, expected)) { return index; } - depth.update( - tokenDelimiter.get(tokenKind(tokens, index) ?? SyntaxKind.Unknown) - ); + depth.update(tokenDelimiter.get(tokenKind(tokens, index) ?? SyntaxKind.Unknown)); } return undefined; }; -const findClosingBrace = ( - tokens: readonly GovernedClientToken[], - openBraceIndex: number -): number | undefined => { +const findClosingBrace = (tokens: readonly GovernedClientToken[], openBraceIndex: number): number | undefined => { let depth = 0; for (let index = openBraceIndex; index < tokens.length; index += 1) { const token = tokens[index]; @@ -344,7 +279,7 @@ const findClosingBrace = ( const findClosingParenthesis = ( tokens: readonly GovernedClientToken[], openParenthesisIndex: number, - end: number + end: number, ): number | undefined => { let depth = 0; for (let index = openParenthesisIndex; index < end; index += 1) { @@ -361,15 +296,10 @@ const findClosingParenthesis = ( return undefined; }; -const isNamedObjectProperty = ( - tokens: readonly GovernedClientToken[], - index: number, - property: string -): boolean => { +const isNamedObjectProperty = (tokens: readonly GovernedClientToken[], index: number, property: string): boolean => { const token = tokens[index]; return ( - (token?.kind === SyntaxKind.Identifier || - token?.kind === SyntaxKind.StringLiteral) && + (token?.kind === SyntaxKind.Identifier || token?.kind === SyntaxKind.StringLiteral) && token.value === property && tokenKind(tokens, index + 1) === SyntaxKind.ColonToken ); @@ -379,7 +309,7 @@ const objectPropertyValuePositions = ( tokens: readonly GovernedClientToken[], openBraceIndex: number, closeBraceIndex: number, - property: string + property: string, ): readonly number[] => { const positions: number[] = []; let depth = 0; @@ -400,50 +330,32 @@ const findObjectPropertyValue = ( tokens: readonly GovernedClientToken[], openBraceIndex: number, closeBraceIndex: number, - property: string -): number | undefined => - objectPropertyValuePositions( - tokens, - openBraceIndex, - closeBraceIndex, - property - )[0]; + property: string, +): number | undefined => objectPropertyValuePositions(tokens, openBraceIndex, closeBraceIndex, property)[0]; const directObjectPropertyOccurrences = ( tokens: readonly GovernedClientToken[], openBraceIndex: number, closeBraceIndex: number, - property: string -): number => - objectPropertyValuePositions( - tokens, - openBraceIndex, - closeBraceIndex, - property - ).length; + property: string, +): number => objectPropertyValuePositions(tokens, openBraceIndex, closeBraceIndex, property).length; const hasExactObjectPropertyValue = ( tokens: readonly GovernedClientToken[], valueStart: number | undefined, - expected: readonly ExpectedToken[] + expected: readonly ExpectedToken[], ): boolean => { - if ( - valueStart === undefined || - !matchesSequence(tokens, valueStart, expected) - ) { + if (valueStart === undefined || !matchesSequence(tokens, valueStart, expected)) { return false; } const follower = tokenKind(tokens, valueStart + expected.length); - return ( - follower === SyntaxKind.CommaToken || - follower === SyntaxKind.CloseBraceToken - ); + return follower === SyntaxKind.CommaToken || follower === SyntaxKind.CloseBraceToken; }; const directObjectPropertyNames = ( tokens: readonly GovernedClientToken[], openBraceIndex: number, - closeBraceIndex: number + closeBraceIndex: number, ): readonly string[] | undefined => { const properties: string[] = []; let depth = 0; @@ -453,10 +365,7 @@ const directObjectPropertyNames = ( if (depth !== 1) { continue; } - if ( - kind === SyntaxKind.DotDotDotToken || - kind === SyntaxKind.OpenBracketToken - ) { + if (kind === SyntaxKind.DotDotDotToken || kind === SyntaxKind.OpenBracketToken) { return undefined; } const value = tokenValue(tokens, index); @@ -467,10 +376,7 @@ const directObjectPropertyNames = ( return properties; }; -const hasExactProperties = ( - properties: readonly string[] | undefined, - expected: ReadonlySet -): boolean => +const hasExactProperties = (properties: readonly string[] | undefined, expected: ReadonlySet): boolean => properties !== undefined && properties.length === expected.size && properties.every((property) => expected.has(property)); @@ -478,7 +384,7 @@ const hasExactProperties = ( const directObjectHasNoSpread = ( tokens: readonly GovernedClientToken[], openBraceIndex: number, - closeBraceIndex: number + closeBraceIndex: number, ): boolean => { let depth = 0; for (let index = openBraceIndex; index < closeBraceIndex; index += 1) { @@ -507,29 +413,17 @@ interface GovernedClientExpectation { const MODULE_API_INVOCATION_KIND = 'module-api'; -export const hasUniqueExactNamedImport = ( - source: string, - importedName: string, - moduleSpecifier: string -): boolean => { +export const hasUniqueExactNamedImport = (source: string, importedName: string, moduleSpecifier: string): boolean => { const tokens = tokenizeGovernedClient(source); let matchCount = 0; let bindingCount = 0; for (let index = 0; index < tokens.length; index += 1) { - if ( - matchesSequence(tokens, index, [ - [SyntaxKind.ImportKeyword], - [SyntaxKind.OpenBraceToken], - ]) - ) { + if (matchesSequence(tokens, index, [[SyntaxKind.ImportKeyword], [SyntaxKind.OpenBraceToken]])) { const closeBrace = findClosingBrace(tokens, index + 1); if (closeBrace !== undefined) { bindingCount += tokens .slice(index + 2, closeBrace) - .filter( - ({ kind, value }) => - kind === SyntaxKind.Identifier && value === importedName - ).length; + .filter(({ kind, value }) => kind === SyntaxKind.Identifier && value === importedName).length; } } if ( @@ -549,13 +443,11 @@ export const hasUniqueExactNamedImport = ( return matchCount === 1 && bindingCount === 1; }; -export const hasGeneratedOperationPrincipalContract = ( - source: string -): boolean => +export const hasGeneratedOperationPrincipalContract = (source: string): boolean => hasUniqueExactNamedImport( source, 'makeMicroverticalHttpPrincipalAuthentication', - '@app/core-runtime/http/principal-authentication' + '@app/core-runtime/http/principal-authentication', ) && hasTopLevelSequence(tokenizeGovernedClient(source), [ [SyntaxKind.ExportKeyword], @@ -569,21 +461,12 @@ export const hasGeneratedOperationPrincipalContract = ( [SyntaxKind.SemicolonToken], ]); -const hasExclusiveNamedImportFrom = ( - source: string, - importedName: string, - moduleSpecifier: string -): boolean => { +const hasExclusiveNamedImportFrom = (source: string, importedName: string, moduleSpecifier: string): boolean => { const tokens = tokenizeGovernedClient(source); let bindingCount = 0; let exactBindingCount = 0; for (let index = 0; index < tokens.length; index += 1) { - if ( - !matchesSequence(tokens, index, [ - [SyntaxKind.ImportKeyword], - [SyntaxKind.OpenBraceToken], - ]) - ) { + if (!matchesSequence(tokens, index, [[SyntaxKind.ImportKeyword], [SyntaxKind.OpenBraceToken]])) { continue; } const closeBrace = findClosingBrace(tokens, index + 1); @@ -592,10 +475,7 @@ const hasExclusiveNamedImportFrom = ( } const importBindingCount = tokens .slice(index + 2, closeBrace) - .filter( - ({ kind, value }) => - kind === SyntaxKind.Identifier && value === importedName - ).length; + .filter(({ kind, value }) => kind === SyntaxKind.Identifier && value === importedName).length; bindingCount += importBindingCount; if ( importBindingCount === 1 && @@ -611,18 +491,10 @@ const hasExclusiveNamedImportFrom = ( return bindingCount === 1 && exactBindingCount === 1; }; -export const hasNamedImportBinding = ( - source: string, - importedName: string -): boolean => { +export const hasNamedImportBinding = (source: string, importedName: string): boolean => { const tokens = tokenizeGovernedClient(source); for (let index = 0; index < tokens.length; index += 1) { - if ( - !matchesSequence(tokens, index, [ - [SyntaxKind.ImportKeyword], - [SyntaxKind.OpenBraceToken], - ]) - ) { + if (!matchesSequence(tokens, index, [[SyntaxKind.ImportKeyword], [SyntaxKind.OpenBraceToken]])) { continue; } const closeBrace = findClosingBrace(tokens, index + 1); @@ -630,10 +502,7 @@ export const hasNamedImportBinding = ( closeBrace !== undefined && tokens .slice(index + 2, closeBrace) - .some( - ({ kind, value }) => - kind === SyntaxKind.Identifier && value === importedName - ) + .some(({ kind, value }) => kind === SyntaxKind.Identifier && value === importedName) ) { return true; } @@ -648,7 +517,7 @@ const governedRequestType = (expectation: GovernedClientExpectation): string => const hasExactGeneratedImports = ( tokens: readonly GovernedClientToken[], - expectation: GovernedClientExpectation + expectation: GovernedClientExpectation, ): boolean => { const expected = [ [ @@ -707,19 +576,11 @@ const hasExactGeneratedImports = ( } cursor += statement.length; } - return ( - tokens.filter(({ kind }) => kind === SyntaxKind.ImportKeyword).length === - expected.length - ); + return tokens.filter(({ kind }) => kind === SyntaxKind.ImportKeyword).length === expected.length; }; -const identifierOccurrences = ( - tokens: readonly GovernedClientToken[], - identifier: string -): number => - tokens.filter( - ({ kind, value }) => kind === SyntaxKind.Identifier && value === identifier - ).length; +const identifierOccurrences = (tokens: readonly GovernedClientToken[], identifier: string): number => + tokens.filter(({ kind, value }) => kind === SyntaxKind.Identifier && value === identifier).length; interface ExportedConst { readonly end: number; @@ -736,9 +597,7 @@ interface GovernedClientHelper { readonly start: number; } -const exportedConsts = ( - tokens: readonly GovernedClientToken[] -): readonly ExportedConst[] => { +const exportedConsts = (tokens: readonly GovernedClientToken[]): readonly ExportedConst[] => { const declarations: ExportedConst[] = []; for (let index = 0; index < tokens.length; index += 1) { if ( @@ -751,12 +610,7 @@ const exportedConsts = ( ) { const name = tokenValue(tokens, index + 2); if (name !== undefined) { - const nextExport = findSequence( - tokens, - [[SyntaxKind.ExportKeyword]], - index + 1, - tokens.length - ); + const nextExport = findSequence(tokens, [[SyntaxKind.ExportKeyword]], index + 1, tokens.length); declarations.push({ end: nextExport ?? tokens.length, name, @@ -771,7 +625,7 @@ const exportedConsts = ( const clientHelperAt = ( tokens: readonly GovernedClientToken[], index: number, - end: number + end: number, ): GovernedClientHelper | undefined => { if ( !matchesSequence(tokens, index, [ @@ -811,7 +665,7 @@ const clientHelperAt = ( const findClientHelper = ( tokens: readonly GovernedClientToken[], - authorizedExportStart: number + authorizedExportStart: number, ): GovernedClientHelper | undefined => { for (let index = 0; index < authorizedExportStart; index += 1) { const helper = clientHelperAt(tokens, index, authorizedExportStart); @@ -822,44 +676,26 @@ const findClientHelper = ( return undefined; }; -const findStatementSemicolon = ( - tokens: readonly GovernedClientToken[], - start: number -): number | undefined => - findRootExpressionSequence( - tokens, - [[SyntaxKind.SemicolonToken]], - start, - tokens.length - ); +const findStatementSemicolon = (tokens: readonly GovernedClientToken[], start: number): number | undefined => + findRootExpressionSequence(tokens, [[SyntaxKind.SemicolonToken]], start, tokens.length); const hasOnlyAllowedModuleStatements = ( tokens: readonly GovernedClientToken[], helper: GovernedClientHelper, - expectation: GovernedClientExpectation + expectation: GovernedClientExpectation, ): boolean => { - const allowedOperations = new Set([ - expectation.authorizedOperation, - expectation.publicOperation, - ]); + const allowedOperations = new Set([expectation.authorizedOperation, expectation.publicOperation]); const apiStem = expectation.ownerApiValue.replace(/Api$/u, ''); const operationStem = expectation.authorizedOperation .replace(/WithAuthorization$/u, '') .replace(/^execute/u, '') .replace(/^load/u, '') .replace(/Client$/u, ''); - const allowedOptionsInterfaces = new Set([ - `${apiStem}ClientOptions`, - `${operationStem}ClientOptions`, - ]); + const allowedOptionsInterfaces = new Set([`${apiStem}ClientOptions`, `${operationStem}ClientOptions`]); let optionsInterfaceSeen = false; const consumeOptionsInterface = (index: number): number | undefined => { const name = tokenValue(tokens, index + 2); - if ( - name === undefined || - optionsInterfaceSeen || - !allowedOptionsInterfaces.has(name) - ) { + if (name === undefined || optionsInterfaceSeen || !allowedOptionsInterfaces.has(name)) { return undefined; } optionsInterfaceSeen = true; @@ -871,9 +707,7 @@ const hasOnlyAllowedModuleStatements = ( if (close === undefined) { return undefined; } - return tokenKind(tokens, close + 1) === SyntaxKind.SemicolonToken - ? close + 2 - : close + 1; + return tokenKind(tokens, close + 1) === SyntaxKind.SemicolonToken ? close + 2 : close + 1; }; const isAllowedOperation = (index: number): boolean => matchesSequence(tokens, index, [ @@ -886,11 +720,7 @@ const hasOnlyAllowedModuleStatements = ( if (index === tokens.length) { return true; } - if ( - [SyntaxKind.ImportKeyword, SyntaxKind.TypeKeyword].includes( - tokenKind(tokens, index) ?? SyntaxKind.Unknown - ) - ) { + if ([SyntaxKind.ImportKeyword, SyntaxKind.TypeKeyword].includes(tokenKind(tokens, index) ?? SyntaxKind.Unknown)) { const end = findStatementSemicolon(tokens, index); return end !== undefined && acceptsFrom(end + 1); } @@ -921,7 +751,7 @@ const hasGovernedTransportInvocation = ( tokens: readonly GovernedClientToken[], helper: GovernedClientHelper, ownerApiValue: string, - defaultApiPrefix: string + defaultApiPrefix: string, ): boolean => { const expected = (objectTrailingComma: boolean, callTrailingComma: boolean) => [ @@ -950,11 +780,8 @@ const hasGovernedTransportInvocation = ( return [false, true].some((objectTrailingComma) => [false, true].some((callTrailingComma) => { const sequence = expected(objectTrailingComma, callTrailingComma); - return ( - helper.end === helper.start + sequence.length && - matchesSequence(tokens, helper.start, sequence) - ); - }) + return helper.end === helper.start + sequence.length && matchesSequence(tokens, helper.start, sequence); + }), ); }; @@ -962,7 +789,7 @@ const parametersBindIdentifier = ( tokens: readonly GovernedClientToken[], start: number, end: number, - names: ReadonlySet + names: ReadonlySet, ): boolean => { const bindingFollowers = new Set([ SyntaxKind.CloseParenToken, @@ -977,9 +804,7 @@ const parametersBindIdentifier = ( ({ kind, value }, offset) => kind === SyntaxKind.Identifier && names.has(value) && - bindingFollowers.has( - tokenKind(tokens, start + offset + 1) ?? SyntaxKind.Unknown - ) + bindingFollowers.has(tokenKind(tokens, start + offset + 1) ?? SyntaxKind.Unknown), ); }; @@ -987,7 +812,7 @@ const hasTopLevelDeclaration = ( tokens: readonly GovernedClientToken[], start: number, end: number, - names: ReadonlySet + names: ReadonlySet, ): boolean => { const declarationKinds = new Set([ SyntaxKind.ClassKeyword, @@ -1016,10 +841,8 @@ const hasExactParameterTokens = ( tokens: readonly GovernedClientToken[], open: number, close: number, - expected: readonly ExpectedToken[] -): boolean => - close === open + expected.length + 1 && - matchesSequence(tokens, open + 1, expected); + expected: readonly ExpectedToken[], +): boolean => close === open + expected.length + 1 && matchesSequence(tokens, open + 1, expected); interface GeneratedClientTypeNames { authorizedInvocation: string; @@ -1028,9 +851,7 @@ interface GeneratedClientTypeNames { request: string; } -const generatedClientTypeNames = ( - expectation: GovernedClientExpectation -): GeneratedClientTypeNames => { +const generatedClientTypeNames = (expectation: GovernedClientExpectation): GeneratedClientTypeNames => { const operationBase = expectation.invocationKind === MODULE_API_INVOCATION_KIND ? expectation.ownerApiValue.slice(0, -'Api'.length) @@ -1038,10 +859,7 @@ const generatedClientTypeNames = ( return { authorizedInvocation: `${expectation.ownerApiValue.slice(0, -'Api'.length)}AuthorizedInvocation`, operationInvocation: `${expectation.ownerApiValue.slice(0, -'Api'.length)}OperationInvocation`, - options: [ - `${operationBase}ClientOptions`, - `${expectation.ownerApiValue.slice(0, -'Api'.length)}ClientOptions`, - ], + options: [`${operationBase}ClientOptions`, `${expectation.ownerApiValue.slice(0, -'Api'.length)}ClientOptions`], request: governedRequestType(expectation), }; }; @@ -1053,22 +871,14 @@ const hasExactGeneratedOperationParameters = ( authorizedArrow: number, operation: ExportedConst, operationArrow: number, - expectation: GovernedClientExpectation + expectation: GovernedClientExpectation, ): boolean => { const types = generatedClientTypeNames(expectation); const helperOpen = helper.parametersStart - 1; const authorizedOpen = authorized.start + 4; - const authorizedClose = findClosingParenthesis( - tokens, - authorizedOpen, - authorizedArrow - ); + const authorizedClose = findClosingParenthesis(tokens, authorizedOpen, authorizedArrow); const operationOpen = operation.start + 4; - const operationClose = findClosingParenthesis( - tokens, - operationOpen, - operationArrow - ); + const operationClose = findClosingParenthesis(tokens, operationOpen, operationArrow); const authorizedExpected = (trailingComma: boolean) => [ [SyntaxKind.Identifier, 'payload'], @@ -1130,43 +940,27 @@ const hasExactGeneratedOperationParameters = ( [SyntaxKind.ColonToken], [SyntaxKind.Identifier, optionsType], ...(trailingComma ? ([[SyntaxKind.CommaToken]] as const) : []), - ]) - ) + ]), + ), ) && authorizedClose !== undefined && [false, true].some((trailingComma) => - hasExactParameterTokens( - tokens, - authorizedOpen, - authorizedClose, - authorizedExpected(trailingComma) - ) + hasExactParameterTokens(tokens, authorizedOpen, authorizedClose, authorizedExpected(trailingComma)), ) && operationClose !== undefined && [false, true].some((trailingComma) => - hasExactParameterTokens( - tokens, - operationOpen, - operationClose, - operationExpected(trailingComma) - ) + hasExactParameterTokens(tokens, operationOpen, operationClose, operationExpected(trailingComma)), ) ); }; const generatedOperationDeclarations = ( tokens: readonly GovernedClientToken[], - expectation: GovernedClientExpectation -): - | readonly [authorized: ExportedConst, operation: ExportedConst] - | undefined => { + expectation: GovernedClientExpectation, +): readonly [authorized: ExportedConst, operation: ExportedConst] | undefined => { const declarations = exportedConsts(tokens); - const authorized = declarations.filter( - ({ name }) => name === expectation.authorizedOperation - ); - const operations = declarations.filter( - ({ name }) => name === expectation.publicOperation - ); + const authorized = declarations.filter(({ name }) => name === expectation.authorizedOperation); + const operations = declarations.filter(({ name }) => name === expectation.publicOperation); const [authorizedDeclaration] = authorized; const [operationDeclaration] = operations; return declarations.length === 2 && @@ -1181,18 +975,13 @@ const generatedOperationDeclarations = ( const matchingSequenceEnd = ( tokens: readonly GovernedClientToken[], start: number, - alternatives: readonly (readonly ExpectedToken[])[] + alternatives: readonly (readonly ExpectedToken[])[], ): number | undefined => { - const match = alternatives.find((sequence) => - matchesSequence(tokens, start, sequence) - ); + const match = alternatives.find((sequence) => matchesSequence(tokens, start, sequence)); return match === undefined ? undefined : start + match.length; }; -const hasInvocationClosure = ( - tokens: readonly GovernedClientToken[], - start: number | undefined -): boolean => +const hasInvocationClosure = (tokens: readonly GovernedClientToken[], start: number | undefined): boolean => start !== undefined && [false, true].some((trailingComma) => [false, true].some((outerTrailingComma) => @@ -1202,12 +991,12 @@ const hasInvocationClosure = ( ...(outerTrailingComma ? [[SyntaxKind.CommaToken] as const] : []), [SyntaxKind.CloseParenToken], [SyntaxKind.SemicolonToken], - ]) - ) + ]), + ), ); const generatedInvocationPayloads = ( - kind: GovernedClientExpectation['invocationKind'] + kind: GovernedClientExpectation['invocationKind'], ): readonly (readonly ExpectedToken[])[] => kind === MODULE_API_INVOCATION_KIND ? ([false, true] as const).map( @@ -1233,7 +1022,7 @@ const generatedInvocationPayloads = ( ...(hasTrailingComma ? ([[SyntaxKind.CommaToken]] as const) : []), [SyntaxKind.CloseBraceToken], [SyntaxKind.CloseParenToken], - ] satisfies readonly ExpectedToken[] + ] satisfies readonly ExpectedToken[], ) : ([false, true] as const).map( (hasTrailingComma) => @@ -1243,13 +1032,13 @@ const generatedInvocationPayloads = ( ...(hasTrailingComma ? ([[SyntaxKind.CommaToken]] as const) : []), [SyntaxKind.CloseBraceToken], [SyntaxKind.CloseParenToken], - ] satisfies readonly ExpectedToken[] + ] satisfies readonly ExpectedToken[], ); const clientHelperShadowsImports = ( tokens: readonly GovernedClientToken[], helper: GovernedClientHelper, - expectation: GovernedClientExpectation + expectation: GovernedClientExpectation, ): boolean => { const requiredHelperImports = new Set([ 'Effect', @@ -1258,18 +1047,8 @@ const clientHelperShadowsImports = ( expectation.ownerApiValue, ]); return ( - parametersBindIdentifier( - tokens, - helper.parametersStart, - helper.parametersEnd, - requiredHelperImports - ) || - hasTopLevelDeclaration( - tokens, - helper.start, - helper.end, - requiredHelperImports - ) + parametersBindIdentifier(tokens, helper.parametersStart, helper.parametersEnd, requiredHelperImports) || + hasTopLevelDeclaration(tokens, helper.start, helper.end, requiredHelperImports) ); }; @@ -1280,19 +1059,19 @@ const operationParametersShadowBindings = ( authorized: ExportedConst, authorizedArrow: number, operation: ExportedConst, - operationArrow: number + operationArrow: number, ): boolean => { const authorizedShadowsBindings = parametersBindIdentifier( tokens, authorized.start, authorizedArrow, - new Set([helper.name, 'Effect', 'Redacted']) + new Set([helper.name, 'Effect', 'Redacted']), ); const operationShadowsBindings = parametersBindIdentifier( tokens, operation.start, operationArrow, - new Set(['operationGateway', expectation.authorizedOperation]) + new Set(['operationGateway', expectation.authorizedOperation]), ); return authorizedShadowsBindings || operationShadowsBindings; }; @@ -1300,31 +1079,19 @@ const operationParametersShadowBindings = ( const exportedOperationsUseClientHelperAndGateway = ( tokens: readonly GovernedClientToken[], helper: GovernedClientHelper, - expectation: GovernedClientExpectation + expectation: GovernedClientExpectation, ): boolean => { const declarations = generatedOperationDeclarations(tokens, expectation); if (declarations === undefined) { return false; } const [authorized, operation] = declarations; - const authorizedArrow = findSequence( - tokens, - [[SyntaxKind.EqualsGreaterThanToken]], - authorized.start, - authorized.end - ); - const operationArrow = findSequence( - tokens, - [[SyntaxKind.EqualsGreaterThanToken]], - operation.start, - operation.end - ); + const authorizedArrow = findSequence(tokens, [[SyntaxKind.EqualsGreaterThanToken]], authorized.start, authorized.end); + const operationArrow = findSequence(tokens, [[SyntaxKind.EqualsGreaterThanToken]], operation.start, operation.end); if (authorizedArrow === undefined || operationArrow === undefined) { return false; } - const invocationPayloads = generatedInvocationPayloads( - expectation.invocationKind - ); + const invocationPayloads = generatedInvocationPayloads(expectation.invocationKind); const authorizedInvocation = [ [SyntaxKind.Identifier, helper.name], [SyntaxKind.OpenParenToken], @@ -1357,13 +1124,8 @@ const exportedOperationsUseClientHelperAndGateway = ( [SyntaxKind.Identifier, 'execute'], [SyntaxKind.OpenParenToken], ] satisfies readonly ExpectedToken[]; - const authorizedInvocationEnd = - authorizedArrow + 1 + authorizedInvocation.length; - const authorizedInvocationTail = matchingSequenceEnd( - tokens, - authorizedInvocationEnd, - invocationPayloads - ); + const authorizedInvocationEnd = authorizedArrow + 1 + authorizedInvocation.length; + const authorizedInvocationTail = matchingSequenceEnd(tokens, authorizedInvocationEnd, invocationPayloads); const authorizedUsesHelper = matchesSequence(tokens, authorizedArrow + 1, authorizedInvocation) && hasInvocationClosure(tokens, authorizedInvocationTail); @@ -1390,11 +1152,7 @@ const exportedOperationsUseClientHelperAndGateway = ( const operationUsesGateway = matchesSequence(tokens, operationArrow + 1, gatewayInvocation) && hasInvocationClosure(tokens, gatewayInvocationEnd); - const helperShadowsImports = clientHelperShadowsImports( - tokens, - helper, - expectation - ); + const helperShadowsImports = clientHelperShadowsImports(tokens, helper, expectation); const shadowsBindings = operationParametersShadowBindings( tokens, helper, @@ -1402,7 +1160,7 @@ const exportedOperationsUseClientHelperAndGateway = ( authorized, authorizedArrow, operation, - operationArrow + operationArrow, ); return ( hasExactGeneratedOperationParameters( @@ -1412,7 +1170,7 @@ const exportedOperationsUseClientHelperAndGateway = ( authorizedArrow, operation, operationArrow, - expectation + expectation, ) && authorizedUsesHelper && operationUsesGateway && @@ -1421,10 +1179,7 @@ const exportedOperationsUseClientHelperAndGateway = ( ); }; -export const generatedApiGroup = ( - source: string, - ownerApiValue: string -): string | undefined => { +export const generatedApiGroup = (source: string, ownerApiValue: string): string | undefined => { const tokens = tokenizeGovernedClient(source); const apiDeclaration = findTopLevelSequence( tokens, @@ -1438,7 +1193,7 @@ export const generatedApiGroup = ( [SyntaxKind.Identifier, 'make'], ], 0, - tokens.length + tokens.length, ); if (apiDeclaration === undefined) { return undefined; @@ -1447,7 +1202,7 @@ export const generatedApiGroup = ( tokens, [[SyntaxKind.SemicolonToken]], apiDeclaration + 7, - tokens.length + tokens.length, ); if (declarationEnd === undefined) { return undefined; @@ -1472,8 +1227,7 @@ export const generatedApiGroup = ( const endpointAdd = group + sequence.length; if ( endpointAdd === outerCallClose || - (tokenKind(tokens, endpointAdd) === SyntaxKind.CommaToken && - endpointAdd + 1 === outerCallClose) + (tokenKind(tokens, endpointAdd) === SyntaxKind.CommaToken && endpointAdd + 1 === outerCallClose) ) { return true; } @@ -1486,15 +1240,8 @@ export const generatedApiGroup = ( ) { return false; } - const endpointAddClose = findClosingParenthesis( - tokens, - endpointAdd + 2, - outerCallClose + 1 - ); - return ( - endpointAddClose !== undefined && - isOptionalTrailingComma(tokens, endpointAddClose + 1, outerCallClose) - ); + const endpointAddClose = findClosingParenthesis(tokens, endpointAdd + 2, outerCallClose + 1); + return endpointAddClose !== undefined && isOptionalTrailingComma(tokens, endpointAddClose + 1, outerCallClose); }; const makeOpen = apiDeclaration + 7; const makeClose = @@ -1503,17 +1250,13 @@ export const generatedApiGroup = ( : undefined; const hasExactApiRoot = makeClose !== undefined && - matchesSequence(tokens, makeOpen + 1, [ - [SyntaxKind.StringLiteral, ownerApiValue], - ]) && + matchesSequence(tokens, makeOpen + 1, [[SyntaxKind.StringLiteral, ownerApiValue]]) && isOptionalTrailingComma(tokens, makeOpen + 2, makeClose); if (makeClose === undefined) { return undefined; } const group = makeClose + 1; - return hasExactApiRoot && - matchesSequence(tokens, group, sequence) && - isExactGroupArgument(group) + return hasExactApiRoot && matchesSequence(tokens, group, sequence) && isExactGroupArgument(group) ? tokenValue(tokens, group + 7) : undefined; }; @@ -1522,7 +1265,7 @@ const hasGeneratedEndpointContract = ( source: string, ownerApiValue: string, groupName: string, - endpointPath: string + endpointPath: string, ): boolean => { const tokens = tokenizeGovernedClient(source); const declaration = findTopLevelSequence( @@ -1534,12 +1277,9 @@ const hasGeneratedEndpointContract = ( [SyntaxKind.EqualsToken], ], 0, - tokens.length + tokens.length, ); - const declarationEnd = - declaration === undefined - ? undefined - : findStatementSemicolon(tokens, declaration); + const declarationEnd = declaration === undefined ? undefined : findStatementSemicolon(tokens, declaration); if (declaration === undefined || declarationEnd === undefined) { return false; } @@ -1557,7 +1297,7 @@ const hasGeneratedEndpointContract = ( [SyntaxKind.OpenParenToken], ], declaration, - declarationEnd + declarationEnd, ); if (groupMake === undefined) { return false; @@ -1577,11 +1317,7 @@ const hasGeneratedEndpointContract = ( const endpointClose = matchesSequence(tokens, endpoint, endpointSequence) ? findClosingParenthesis(tokens, endpointOpen, declarationEnd) : undefined; - const groupAddClose = findClosingParenthesis( - tokens, - groupAddOpen, - declarationEnd - ); + const groupAddClose = findClosingParenthesis(tokens, groupAddOpen, declarationEnd); return ( endpointClose !== undefined && groupAddClose !== undefined && @@ -1593,7 +1329,7 @@ export const hasGeneratedProviderApiContract = ( ownerApiValue: string, moduleId: string, name: string, - kind: 'report' | 'search' + kind: 'report' | 'search', ): boolean => { const groupName = generatedApiGroup(source, ownerApiValue); return ( @@ -1602,7 +1338,7 @@ export const hasGeneratedProviderApiContract = ( source, ownerApiValue, groupName, - `/${moduleId}/${kind === 'report' ? 'reports' : 'search'}/${name}` + `/${moduleId}/${kind === 'report' ? 'reports' : 'search'}/${name}`, ) ); }; @@ -1611,23 +1347,15 @@ export const hasGeneratedModuleApiContract = ( source: string, ownerApiValue: string, groupName: string, - stem: string -): boolean => - hasGeneratedEndpointContract( - source, - ownerApiValue, - groupName, - `/reads/${stem}` - ); + stem: string, +): boolean => hasGeneratedEndpointContract(source, ownerApiValue, groupName, `/reads/${stem}`); const topLevelCallObject = ( tokens: readonly GovernedClientToken[], exportedName: string, callee: string, - requireExport = true -): - | readonly [open: number, close: number, declarationEnd: number] - | undefined => { + requireExport = true, +): readonly [open: number, close: number, declarationEnd: number] | undefined => { const declaration = findTopLevelSequence( tokens, [ @@ -1640,7 +1368,7 @@ const topLevelCallObject = ( [SyntaxKind.OpenBraceToken], ], 0, - tokens.length + tokens.length, ); if (declaration === undefined) { return undefined; @@ -1652,10 +1380,7 @@ const topLevelCallObject = ( const open = declaration + (requireExport ? 6 : 5); const close = findClosingBrace(tokens, open); const callClose = findClosingParenthesis(tokens, open - 1, declarationEnd); - return close !== undefined && - close < declarationEnd && - callClose !== undefined && - callClose + 1 === declarationEnd + return close !== undefined && close < declarationEnd && callClose !== undefined && callClose + 1 === declarationEnd ? [open, close, declarationEnd] : undefined; }; @@ -1708,7 +1433,7 @@ const withoutTrailingCommas = (tokens: readonly GovernedClientToken[]) => SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.GreaterThanToken, - ].includes(tokens[index + 1]?.kind ?? SyntaxKind.Unknown) + ].includes(tokens[index + 1]?.kind ?? SyntaxKind.Unknown), ); const SOURCE_VALUE_TOKEN_KINDS = new Set([ @@ -1723,21 +1448,15 @@ const SOURCE_VALUE_TOKEN_KINDS = new Set([ SyntaxKind.RegularExpressionLiteral, ]); -const hasExactSourceTokens = ( - tokens: readonly GovernedClientToken[], - expected: string -): boolean => { +const hasExactSourceTokens = (tokens: readonly GovernedClientToken[], expected: string): boolean => { const actualTokens = withoutTrailingCommas(tokens); - const expectedTokens = withoutTrailingCommas( - tokenizeGovernedClient(expected) - ); + const expectedTokens = withoutTrailingCommas(tokenizeGovernedClient(expected)); return ( actualTokens.length === expectedTokens.length && expectedTokens.every( (token, index) => actualTokens[index]?.kind === token.kind && - (!SOURCE_VALUE_TOKEN_KINDS.has(token.kind) || - actualTokens[index]?.value === token.value) + (!SOURCE_VALUE_TOKEN_KINDS.has(token.kind) || actualTokens[index]?.value === token.value), ) ); }; @@ -1745,23 +1464,18 @@ const hasExactSourceTokens = ( export const hasEngagementLifecycleRegistrationContract = ( source: string, registrationSource: string, - action: string + action: string, ): boolean => { - const identity = - /^(?archive|unarchive)-(?organization|person)-engagement$/u.exec( - action - )?.groups; + const identity = /^(?archive|unarchive)-(?organization|person)-engagement$/u.exec( + action, + )?.groups; if ( identity === undefined || - !hasExactSourceTokens( - tokenizeGovernedClient(registrationSource), - engagementLifecycleRegistrationContract - ) + !hasExactSourceTokens(tokenizeGovernedClient(registrationSource), engagementLifecycleRegistrationContract) ) { return false; } - const subject = - identity.subject === 'organization' ? 'Organization' : 'Person'; + const subject = identity.subject === 'organization' ? 'Organization' : 'Person'; const exportedName = `${toCamelCase(action)}Action`; const payload = `${subject}EngagementLifecyclePayload`; const result = `${subject}EngagementProfile`; @@ -1784,7 +1498,7 @@ export const hasEngagementLifecycleRegistrationContract = ( import { handleEngagementLifecycle } from './engagement-lifecycle-handler.ts'; import { engagementLifecycleRegistration } from './engagement-lifecycle-registration.ts'; export const ${exportedName} = defineAction( - ` + `, ) && hasExactSourceTokens( tokens.slice(open, close + 1), @@ -1792,14 +1506,11 @@ export const hasEngagementLifecycleRegistrationContract = ( ...engagementLifecycleRegistration<${payload}>('party.registry.${action}'), payloadSchema: ${payload}Schema, resultSchema: ${result}Schema, - }` + }`, ) && - [ - 'defineAction', - 'engagementLifecycleRegistration', - `${payload}Schema`, - `${result}Schema`, - ].every((binding) => identifierOccurrences(tokens, binding) === 2) + ['defineAction', 'engagementLifecycleRegistration', `${payload}Schema`, `${result}Schema`].every( + (binding) => identifierOccurrences(tokens, binding) === 2, + ) ); }; @@ -1808,37 +1519,31 @@ const objectHasExactString = ( open: number, close: number, property: string, - value: string + value: string, ): boolean => directObjectPropertyOccurrences(tokens, open, close, property) === 1 && - hasExactObjectPropertyValue( - tokens, - findObjectPropertyValue(tokens, open, close, property), - [[SyntaxKind.StringLiteral, value]] - ); + hasExactObjectPropertyValue(tokens, findObjectPropertyValue(tokens, open, close, property), [ + [SyntaxKind.StringLiteral, value], + ]); const objectHasExactStrings = ( tokens: readonly GovernedClientToken[], open: number, close: number, - expected: Readonly> + expected: Readonly>, ): boolean => - Object.entries(expected).every(([property, value]) => - objectHasExactString(tokens, open, close, property, value) - ); + Object.entries(expected).every(([property, value]) => objectHasExactString(tokens, open, close, property, value)); const objectReferencesEntrypoint = ( tokens: readonly GovernedClientToken[], open: number, close: number, - entrypoint: string + entrypoint: string, ): boolean => directObjectPropertyOccurrences(tokens, open, close, 'entrypoint') === 1 && - hasExactObjectPropertyValue( - tokens, - findObjectPropertyValue(tokens, open, close, 'entrypoint'), - [[SyntaxKind.Identifier, entrypoint]] - ); + hasExactObjectPropertyValue(tokens, findObjectPropertyValue(tokens, open, close, 'entrypoint'), [ + [SyntaxKind.Identifier, entrypoint], + ]); export interface GeneratedReadAuthorization { readonly kind: 'authenticated_principal' | 'context_permission' | 'public'; @@ -1849,11 +1554,10 @@ const objectStringProperty = ( tokens: readonly GovernedClientToken[], open: number, close: number, - property: string + property: string, ): string | undefined => { const start = findObjectPropertyValue(tokens, open, close, property); - return start !== undefined && - hasExactObjectPropertyValue(tokens, start, [[SyntaxKind.StringLiteral]]) + return start !== undefined && hasExactObjectPropertyValue(tokens, start, [[SyntaxKind.StringLiteral]]) ? tokenValue(tokens, start) : undefined; }; @@ -1861,7 +1565,7 @@ const objectStringProperty = ( const authorizationObject = ( tokens: readonly GovernedClientToken[], open: number, - close: number + close: number, ): GeneratedReadAuthorization | undefined => { const kind = objectStringProperty(tokens, open, close, 'kind'); const properties = directObjectPropertyNames(tokens, open, close); @@ -1876,22 +1580,17 @@ const authorizationObject = ( if (kind !== 'authenticated_principal' && kind !== 'public') { return undefined; } - return hasExactProperties(properties, new Set(['kind'])) - ? { kind } - : undefined; + return hasExactProperties(properties, new Set(['kind'])) ? { kind } : undefined; }; const nestedObjectRange = ( tokens: readonly GovernedClientToken[], open: number, close: number, - property: string + property: string, ): readonly [number, number] | undefined => { const value = findObjectPropertyValue(tokens, open, close, property); - if ( - value === undefined || - tokenKind(tokens, value) !== SyntaxKind.OpenBraceToken - ) { + if (value === undefined || tokenKind(tokens, value) !== SyntaxKind.OpenBraceToken) { return undefined; } const end = findClosingBrace(tokens, value); @@ -1901,11 +1600,9 @@ const nestedObjectRange = ( const generatedReadAuthorization = ( tokens: readonly GovernedClientToken[], open: number, - close: number + close: number, ): GeneratedReadAuthorization | undefined => { - if ( - directObjectPropertyOccurrences(tokens, open, close, 'authorization') !== 1 - ) { + if (directObjectPropertyOccurrences(tokens, open, close, 'authorization') !== 1) { return undefined; } const range = nestedObjectRange(tokens, open, close, 'authorization'); @@ -1916,29 +1613,22 @@ const generatedReadAuthorization = ( const matchesGeneratedReadAuthorization = ( actual: GeneratedReadAuthorization | undefined, - expected: GeneratedReadAuthorization | undefined + expected: GeneratedReadAuthorization | undefined, ): boolean => actual !== undefined && - (expected === undefined || - (actual.kind === expected.kind && - actual.permission === expected.permission)); + (expected === undefined || (actual.kind === expected.kind && actual.permission === expected.permission)); const hasGeneratedReadContract = ( source: string, moduleId: string, name: string, role: 'api' | 'report' | 'search', - authorization?: GeneratedReadAuthorization + authorization?: GeneratedReadAuthorization, ): boolean => { const tokens = tokenizeGovernedClient(source); const camel = toCamelCase(name); const entrypointName = `${camel}Entrypoint`; - const entrypoint = topLevelCallObject( - tokens, - entrypointName, - 'defineTenantModuleEntrypoint', - false - ); + const entrypoint = topLevelCallObject(tokens, entrypointName, 'defineTenantModuleEntrypoint', false); const read = topLevelCallObject(tokens, `${camel}Read`, 'defineRead'); if (entrypoint === undefined || read === undefined) { return false; @@ -1950,12 +1640,9 @@ const hasGeneratedReadContract = ( directObjectHasNoSpread(tokens, readOpen, readClose) && matchesGeneratedReadAuthorization( generatedReadAuthorization(tokens, entrypointOpen, entrypointClose), - authorization + authorization, ) && - matchesSequence(tokens, entrypointClose + 1, [ - [SyntaxKind.CloseParenToken], - [SyntaxKind.SemicolonToken], - ]) && + matchesSequence(tokens, entrypointClose + 1, [[SyntaxKind.CloseParenToken], [SyntaxKind.SemicolonToken]]) && entrypointClose + 2 === entrypointEnd && objectHasExactStrings(tokens, entrypointOpen, entrypointClose, { access: 'read', @@ -1977,31 +1664,22 @@ export const hasGeneratedProviderReadContract = ( moduleId: string, name: string, kind: 'report' | 'search', - authorization?: GeneratedReadAuthorization + authorization?: GeneratedReadAuthorization, ): boolean => hasGeneratedReadContract(source, moduleId, name, kind, authorization) && (() => { const tokens = tokenizeGovernedClient(source); - const read = topLevelCallObject( - tokens, - `${toCamelCase(name)}Read`, - 'defineRead' - ); + const read = topLevelCallObject(tokens, `${toCamelCase(name)}Read`, 'defineRead'); return ( read !== undefined && objectHasExactString(tokens, read[0], read[1], 'accessKind', kind) && - directObjectPropertyOccurrences( - tokens, - read[0], - read[1], - 'legalEntityScope' - ) === 1 + directObjectPropertyOccurrences(tokens, read[0], read[1], 'legalEntityScope') === 1 ); })(); const enclosingBraceRange = ( tokens: readonly GovernedClientToken[], - index: number + index: number, ): readonly [start: number, end: number] | undefined => { const openBraces: number[] = []; for (let cursor = 0; cursor <= index; cursor += 1) { @@ -2021,18 +1699,18 @@ export const hasMatchingGeneratedProviderAuthorization = ( manifest: string, moduleId: string, name: string, - kind: 'report' | 'search' + kind: 'report' | 'search', ): boolean => { const providerTokens = tokenizeGovernedClient(providerSource); const entrypoint = topLevelCallObject( providerTokens, `${toCamelCase(name)}Entrypoint`, 'defineTenantModuleEntrypoint', - false + false, ); const shellSlot = generatedSlotSource( manifest, - kind === 'report' ? MANIFEST_SHELL_REPORT_SLOT : MANIFEST_SHELL_SEARCH_SLOT + kind === 'report' ? MANIFEST_SHELL_REPORT_SLOT : MANIFEST_SHELL_SEARCH_SLOT, ); if (entrypoint === undefined || shellSlot === undefined) { return false; @@ -2047,70 +1725,43 @@ export const hasMatchingGeneratedProviderAuthorization = ( ], 0, shellTokens.length, - 1 + 1, ); - const contribution = - identity === undefined - ? undefined - : enclosingBraceRange(shellTokens, identity); + const contribution = identity === undefined ? undefined : enclosingBraceRange(shellTokens, identity); const manifestEntrypoint = - contribution === undefined - ? undefined - : nestedObjectRange(shellTokens, ...contribution, 'entrypoint'); + contribution === undefined ? undefined : nestedObjectRange(shellTokens, ...contribution, 'entrypoint'); if (manifestEntrypoint === undefined) { return false; } - const providerAuthorization = generatedReadAuthorization( - providerTokens, - entrypoint[0], - entrypoint[1] - ); - const manifestAuthorization = generatedReadAuthorization( - shellTokens, - ...manifestEntrypoint - ); - return matchesGeneratedReadAuthorization( - providerAuthorization, - manifestAuthorization - ); + const providerAuthorization = generatedReadAuthorization(providerTokens, entrypoint[0], entrypoint[1]); + const manifestAuthorization = generatedReadAuthorization(shellTokens, ...manifestEntrypoint); + return matchesGeneratedReadAuthorization(providerAuthorization, manifestAuthorization); }; export const hasGeneratedModuleApiReadContract = ( source: string, moduleId: string, name: string, - authorization?: GeneratedReadAuthorization + authorization?: GeneratedReadAuthorization, ): boolean => { const tokens = tokenizeGovernedClient(source); const camel = toCamelCase(name); const entrypointName = `${camel}Entrypoint`; - const entrypoint = topLevelCallObject( - tokens, - entrypointName, - 'defineTenantModuleEntrypoint', - false - ); + const entrypoint = topLevelCallObject(tokens, entrypointName, 'defineTenantModuleEntrypoint', false); const read = topLevelCallObject(tokens, `${camel}Read`, 'defineRead'); if (entrypoint === undefined || read === undefined) { return false; } - const access = findObjectPropertyValue( - tokens, - entrypoint[0], - entrypoint[1], - 'access' - ); + const access = findObjectPropertyValue(tokens, entrypoint[0], entrypoint[1], 'access'); return ( directObjectHasNoSpread(tokens, entrypoint[0], entrypoint[1]) && directObjectHasNoSpread(tokens, read[0], read[1]) && matchesGeneratedReadAuthorization( generatedReadAuthorization(tokens, entrypoint[0], entrypoint[1]), - authorization + authorization, ) && ['read', 'historical_read'].some((value) => - hasExactObjectPropertyValue(tokens, access, [ - [SyntaxKind.StringLiteral, value], - ]) + hasExactObjectPropertyValue(tokens, access, [[SyntaxKind.StringLiteral, value]]), ) && objectHasExactStrings(tokens, entrypoint[0], entrypoint[1], { entrypointKey: `${moduleId}.api.${name}`, @@ -2119,9 +1770,7 @@ export const hasGeneratedModuleApiReadContract = ( }) && objectReferencesEntrypoint(tokens, read[0], read[1], entrypointName) && ['legalEntityScope', 'permissionTarget', 'policies'].every( - (property) => - directObjectPropertyOccurrences(tokens, read[0], read[1], property) === - 1 + (property) => directObjectPropertyOccurrences(tokens, read[0], read[1], property) === 1, ) && objectHasExactStrings(tokens, read[0], read[1], { owningModuleKey: moduleId, @@ -2131,18 +1780,13 @@ export const hasGeneratedModuleApiReadContract = ( ); }; -export const hasGeneratedGovernedClientContract = ( - source: string, - expectation: GovernedClientExpectation -): boolean => { +export const hasGeneratedGovernedClientContract = (source: string, expectation: GovernedClientExpectation): boolean => { if (!hasGeneratedSourceHeader(source, expectation.generatedHeader)) { return false; } const tokens = tokenizeGovernedClient(source); const declarations = exportedConsts(tokens); - const authorized = declarations.find(({ name }) => - name.endsWith('WithAuthorization') - ); + const authorized = declarations.find(({ name }) => name.endsWith('WithAuthorization')); if (authorized === undefined) { return false; } @@ -2150,45 +1794,24 @@ export const hasGeneratedGovernedClientContract = ( return ( helper !== undefined && hasExactGeneratedImports(tokens, expectation) && - hasGovernedTransportInvocation( - tokens, - helper, - expectation.ownerApiValue, - expectation.defaultApiPrefix - ) && + hasGovernedTransportInvocation(tokens, helper, expectation.ownerApiValue, expectation.defaultApiPrefix) && exportedOperationsUseClientHelperAndGateway(tokens, helper, expectation) && hasOnlyAllowedModuleStatements(tokens, helper, expectation) && - [ - 'makeGovernedEffectBffClient', - 'operationGateway', - expectation.ownerApiValue, - 'Effect', - helper.name, - ].every((name) => identifierOccurrences(tokens, name) === 2) && - !tokens.some( - ({ value }) => - value === 'makeEffectHttpApiClient' || value === 'HttpClientRequest' - ) + ['makeGovernedEffectBffClient', 'operationGateway', expectation.ownerApiValue, 'Effect', helper.name].every( + (name) => identifierOccurrences(tokens, name) === 2, + ) && + !tokens.some(({ value }) => value === 'makeEffectHttpApiClient' || value === 'HttpClientRequest') ); }; -const slotHasDirectPropertyKey = ( - source: string | undefined, - key: string -): boolean => { +const slotHasDirectPropertyKey = (source: string | undefined, key: string): boolean => { if (source === undefined) { return false; } const tokens = tokenizeGovernedClient(source); return [SyntaxKind.StringLiteral, SyntaxKind.Identifier].some( (keyKind) => - findSequenceAtBraceDepth( - tokens, - [[keyKind, key], [SyntaxKind.ColonToken]], - 0, - tokens.length, - 0 - ) !== undefined + findSequenceAtBraceDepth(tokens, [[keyKind, key], [SyntaxKind.ColonToken]], 0, tokens.length, 0) !== undefined, ); }; @@ -2196,17 +1819,13 @@ const directPropertyKeyOccurrences = (source: string, key: string): number => { const tokens = tokenizeGovernedClient(source); let count = 0; for (const keyKind of [SyntaxKind.StringLiteral, SyntaxKind.Identifier]) { - count += sequenceOccurrencesAtBraceDepth( - tokens, - [[keyKind, key], [SyntaxKind.ColonToken]], - 0 - ); + count += sequenceOccurrencesAtBraceDepth(tokens, [[keyKind, key], [SyntaxKind.ColonToken]], 0); } return count; }; const topLevelCommaSeparatedRanges = ( - tokens: readonly GovernedClientToken[] + tokens: readonly GovernedClientToken[], ): readonly (readonly [start: number, end: number])[] | undefined => { const ranges: (readonly [number, number])[] = []; const depth = new DelimiterDepth(); @@ -2233,9 +1852,7 @@ const topLevelCommaSeparatedRanges = ( return ranges; }; -const directSlotPropertyNames = ( - source: string | undefined -): readonly string[] | undefined => { +const directSlotPropertyNames = (source: string | undefined): readonly string[] | undefined => { if (source === undefined) { return undefined; } @@ -2248,8 +1865,7 @@ const directSlotPropertyNames = ( for (const [start] of ranges) { const key = tokens[start]; if ( - (key?.kind !== SyntaxKind.Identifier && - key?.kind !== SyntaxKind.StringLiteral) || + (key?.kind !== SyntaxKind.Identifier && key?.kind !== SyntaxKind.StringLiteral) || tokenKind(tokens, start + 1) !== SyntaxKind.ColonToken ) { return undefined; @@ -2262,59 +1878,37 @@ const directSlotPropertyNames = ( const hasRelatedSequenceInObject = ( tokens: readonly GovernedClientToken[], anchor: readonly ExpectedToken[], - related: readonly ExpectedToken[] + related: readonly ExpectedToken[], ): boolean => { - const anchorIndex = findSequenceAtBraceDepth( - tokens, - anchor, - 0, - tokens.length, - 1 - ); - const range = - anchorIndex === undefined - ? undefined - : enclosingBraceRange(tokens, anchorIndex); - return ( - range !== undefined && - findSequenceAtBraceDepth(tokens, related, range[0], range[1], 1) !== - undefined - ); + const anchorIndex = findSequenceAtBraceDepth(tokens, anchor, 0, tokens.length, 1); + const range = anchorIndex === undefined ? undefined : enclosingBraceRange(tokens, anchorIndex); + return range !== undefined && findSequenceAtBraceDepth(tokens, related, range[0], range[1], 1) !== undefined; }; const registrationIdentityIsExclusiveToSlot = ( registration: string, name: string, - intendedSlot: readonly [string, string] + intendedSlot: readonly [string, string], ): boolean => - [ - REGISTRATION_API_SLOT, - REGISTRATION_REPORT_SLOT, - REGISTRATION_SEARCH_SLOT, - ].every((slot) => { + [REGISTRATION_API_SLOT, REGISTRATION_REPORT_SLOT, REGISTRATION_SEARCH_SLOT].every((slot) => { const source = generatedSlotSource(registration, slot); - return slot === intendedSlot - ? source !== undefined - : !slotHasDirectPropertyKey(source, name); + return slot === intendedSlot ? source !== undefined : !slotHasDirectPropertyKey(source, name); }); export const hasGeneratedProviderRegistration = ( registration: string, name: string, - kind: 'report' | 'search' + kind: 'report' | 'search', ): boolean => { const slot = generatedSlotSource( registration, - kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT + kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT, ); if (slot === undefined) { return false; } - const intendedSlot = - kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT; - if ( - !registrationIdentityIsExclusiveToSlot(registration, name, intendedSlot) - ) { + const intendedSlot = kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT; + if (!registrationIdentityIsExclusiveToSlot(registration, name, intendedSlot)) { return false; } const tokens = tokenizeGovernedClient(slot); @@ -2334,15 +1928,7 @@ export const hasGeneratedProviderRegistration = ( directPropertyKeyOccurrences(slot, name) === 1 && [SyntaxKind.StringLiteral, SyntaxKind.Identifier].some((keyKind) => { const key: ExpectedToken = [keyKind, name]; - return ( - findSequenceAtBraceDepth( - tokens, - [key, ...registrationTail], - 0, - tokens.length, - 0 - ) !== undefined - ); + return findSequenceAtBraceDepth(tokens, [key, ...registrationTail], 0, tokens.length, 0) !== undefined; }) ); }; @@ -2350,7 +1936,7 @@ export const hasGeneratedProviderRegistration = ( const hasProviderShellEntrypoint = ( tokens: readonly GovernedClientToken[], contribution: readonly [number, number], - identity: Readonly> + identity: Readonly>, ): boolean => { const range = nestedObjectRange(tokens, ...contribution, 'entrypoint'); return ( @@ -2360,21 +1946,13 @@ const hasProviderShellEntrypoint = ( ); }; -const slotOmitsIdentity = ( - source: string | undefined, - identity: readonly ExpectedToken[] -): boolean => - source === undefined || - sequenceOccurrencesAtBraceDepth( - tokenizeGovernedClient(source), - identity, - 1 - ) === 0; +const slotOmitsIdentity = (source: string | undefined, identity: readonly ExpectedToken[]): boolean => + source === undefined || sequenceOccurrencesAtBraceDepth(tokenizeGovernedClient(source), identity, 1) === 0; const hasOwnedProviderDescriptor = ( tokens: readonly GovernedClientToken[], identity: readonly ExpectedToken[], - moduleId: string + moduleId: string, ): boolean => sequenceOccurrencesAtBraceDepth(tokens, identity, 1) === 1 && hasRelatedSequenceInObject(tokens, identity, [ @@ -2390,7 +1968,7 @@ const hasOwnedProviderShellContribution = ( shellContributionKey: string, moduleId: string, kind: 'report' | 'search', - descriptorKey: string + descriptorKey: string, ): boolean => sequenceOccurrencesAtBraceDepth(shellTokens, shellIdentity, 1) === 1 && shellContribution !== undefined && @@ -2411,27 +1989,12 @@ export const hasGeneratedProviderManifest = ( manifest: string, moduleId: string, name: string, - kind: 'report' | 'search' + kind: 'report' | 'search', ): boolean => { - const [ - descriptorMarkers, - shellMarkers, - otherDescriptorMarkers, - otherShellMarkers, - ] = + const [descriptorMarkers, shellMarkers, otherDescriptorMarkers, otherShellMarkers] = kind === 'report' - ? [ - MANIFEST_REPORT_SLOT, - MANIFEST_SHELL_REPORT_SLOT, - MANIFEST_SEARCH_SLOT, - MANIFEST_SHELL_SEARCH_SLOT, - ] - : [ - MANIFEST_SEARCH_SLOT, - MANIFEST_SHELL_SEARCH_SLOT, - MANIFEST_REPORT_SLOT, - MANIFEST_SHELL_REPORT_SLOT, - ]; + ? [MANIFEST_REPORT_SLOT, MANIFEST_SHELL_REPORT_SLOT, MANIFEST_SEARCH_SLOT, MANIFEST_SHELL_SEARCH_SLOT] + : [MANIFEST_SEARCH_SLOT, MANIFEST_SHELL_SEARCH_SLOT, MANIFEST_REPORT_SLOT, MANIFEST_SHELL_REPORT_SLOT]; const descriptorSlot = generatedSlotSource(manifest, descriptorMarkers); const shellSlot = generatedSlotSource(manifest, shellMarkers); if (descriptorSlot === undefined || shellSlot === undefined) { @@ -2451,28 +2014,13 @@ export const hasGeneratedProviderManifest = ( [SyntaxKind.ColonToken], [SyntaxKind.StringLiteral, shellContributionKey], ] satisfies readonly ExpectedToken[]; - const otherDescriptorSlot = generatedSlotSource( - manifest, - otherDescriptorMarkers - ); + const otherDescriptorSlot = generatedSlotSource(manifest, otherDescriptorMarkers); const otherShellSlot = generatedSlotSource(manifest, otherShellMarkers); - const shellIdentityIndex = findSequenceAtBraceDepth( - shellTokens, - shellIdentity, - 0, - shellTokens.length, - 1 - ); + const shellIdentityIndex = findSequenceAtBraceDepth(shellTokens, shellIdentity, 0, shellTokens.length, 1); const shellContribution = - shellIdentityIndex === undefined - ? undefined - : enclosingBraceRange(shellTokens, shellIdentityIndex); + shellIdentityIndex === undefined ? undefined : enclosingBraceRange(shellTokens, shellIdentityIndex); return ( - hasOwnedProviderDescriptor( - descriptorTokens, - descriptorIdentity, - moduleId - ) && + hasOwnedProviderDescriptor(descriptorTokens, descriptorIdentity, moduleId) && hasOwnedProviderShellContribution( shellTokens, shellIdentity, @@ -2480,7 +2028,7 @@ export const hasGeneratedProviderManifest = ( shellContributionKey, moduleId, kind, - descriptorKey + descriptorKey, ) && slotOmitsIdentity(otherDescriptorSlot, descriptorIdentity) && slotOmitsIdentity(otherShellSlot, shellIdentity) @@ -2497,15 +2045,12 @@ const slotProviderNames = ( moduleId: string, kind: GeneratedProviderIdentity['kind'], identityProperty: 'contributionKey' | 'key', - depth: number + depth: number, ): readonly string[] => { if (source === undefined || moduleId.length === 0) { return []; } - const prefix = - identityProperty === 'contributionKey' - ? `${moduleId}.${kind}.` - : `${moduleId}.`; + const prefix = identityProperty === 'contributionKey' ? `${moduleId}.${kind}.` : `${moduleId}.`; const tokens = tokenizeGovernedClient(source); const names: string[] = []; let braceDepth = 0; @@ -2529,33 +2074,23 @@ const slotProviderNames = ( return names; }; -const registrationProviderNames = ( - source: string | undefined -): readonly string[] => - (directSlotPropertyNames(source) ?? []).filter((name) => - /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) - ); +const registrationProviderNames = (source: string | undefined): readonly string[] => + (directSlotPropertyNames(source) ?? []).filter((name) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name)); const providerIdentitySlotIsExact = ( source: string | undefined, names: readonly string[], identityProperty: 'contributionKey' | 'key', - depth: number + depth: number, ): boolean => source !== undefined && sequenceOccurrencesAtBraceDepth( tokenizeGovernedClient(source), - [ - [SyntaxKind.Identifier, identityProperty], - [SyntaxKind.ColonToken], - [SyntaxKind.StringLiteral], - ], - depth + [[SyntaxKind.Identifier, identityProperty], [SyntaxKind.ColonToken], [SyntaxKind.StringLiteral]], + depth, ) === names.length; -const sameUniqueNames = ( - ...collections: readonly (readonly string[])[] -): boolean => { +const sameUniqueNames = (...collections: readonly (readonly string[])[]): boolean => { const [first, ...remaining] = collections; if (first === undefined || new Set(first).size !== first.length) { return false; @@ -2565,24 +2100,21 @@ const sameUniqueNames = ( (names) => names.length === expected.size && new Set(names).size === names.length && - names.every((name) => expected.has(name)) + names.every((name) => expected.has(name)), ); }; export const hasExactGeneratedProviderIdentityTopology = ( manifest: string, registration: string, - moduleId: string + moduleId: string, ): boolean => { const slotsPresent = - [ - MANIFEST_REPORT_SLOT, - MANIFEST_SEARCH_SLOT, - MANIFEST_SHELL_REPORT_SLOT, - MANIFEST_SHELL_SEARCH_SLOT, - ].some((slot) => generatedSlotSource(manifest, slot) !== undefined) || + [MANIFEST_REPORT_SLOT, MANIFEST_SEARCH_SLOT, MANIFEST_SHELL_REPORT_SLOT, MANIFEST_SHELL_SEARCH_SLOT].some( + (slot) => generatedSlotSource(manifest, slot) !== undefined, + ) || [REGISTRATION_REPORT_SLOT, REGISTRATION_SEARCH_SLOT].some( - (slot) => generatedSlotSource(registration, slot) !== undefined + (slot) => generatedSlotSource(registration, slot) !== undefined, ); if (!slotsPresent) { return true; @@ -2590,44 +2122,25 @@ export const hasExactGeneratedProviderIdentityTopology = ( return (['report', 'search'] as const).every((kind) => { const descriptorSlot = generatedSlotSource( manifest, - kind === 'report' ? MANIFEST_REPORT_SLOT : MANIFEST_SEARCH_SLOT + kind === 'report' ? MANIFEST_REPORT_SLOT : MANIFEST_SEARCH_SLOT, ); const shellSlot = generatedSlotSource( manifest, - kind === 'report' - ? MANIFEST_SHELL_REPORT_SLOT - : MANIFEST_SHELL_SEARCH_SLOT + kind === 'report' ? MANIFEST_SHELL_REPORT_SLOT : MANIFEST_SHELL_SEARCH_SLOT, ); const registrationSlot = generatedSlotSource( registration, - kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT - ); - const descriptorNames = slotProviderNames( - descriptorSlot, - moduleId, - kind, - 'key', - 1 - ); - const shellNames = slotProviderNames( - shellSlot, - moduleId, - kind, - 'contributionKey', - 1 + kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT, ); + const descriptorNames = slotProviderNames(descriptorSlot, moduleId, kind, 'key', 1); + const shellNames = slotProviderNames(shellSlot, moduleId, kind, 'contributionKey', 1); const directRegistrationNames = directSlotPropertyNames(registrationSlot); const registrationNames = registrationProviderNames(registrationSlot); return ( directRegistrationNames !== undefined && directRegistrationNames.length === registrationNames.length && providerIdentitySlotIsExact(descriptorSlot, descriptorNames, 'key', 1) && - providerIdentitySlotIsExact( - shellSlot, - shellNames, - 'contributionKey', - 1 - ) && + providerIdentitySlotIsExact(shellSlot, shellNames, 'contributionKey', 1) && sameUniqueNames(descriptorNames, shellNames, registrationNames) ); }); @@ -2636,23 +2149,21 @@ export const hasExactGeneratedProviderIdentityTopology = ( export const generatedProviderIdentities = ( manifest: string, registration: string, - moduleId: string + moduleId: string, ): readonly GeneratedProviderIdentity[] => { const identities = new Map(); for (const kind of ['report', 'search'] as const) { const descriptorSlot = generatedSlotSource( manifest, - kind === 'report' ? MANIFEST_REPORT_SLOT : MANIFEST_SEARCH_SLOT + kind === 'report' ? MANIFEST_REPORT_SLOT : MANIFEST_SEARCH_SLOT, ); const shellSlot = generatedSlotSource( manifest, - kind === 'report' - ? MANIFEST_SHELL_REPORT_SLOT - : MANIFEST_SHELL_SEARCH_SLOT + kind === 'report' ? MANIFEST_SHELL_REPORT_SLOT : MANIFEST_SHELL_SEARCH_SLOT, ); const registrationSlot = generatedSlotSource( registration, - kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT + kind === 'report' ? REGISTRATION_REPORT_SLOT : REGISTRATION_SEARCH_SLOT, ); const names = [ ...slotProviderNames(descriptorSlot, moduleId, kind, 'key', 1), @@ -2669,7 +2180,7 @@ export const generatedProviderIdentities = ( const hasExactGeneratedGatewayFactory = ( tokens: readonly GovernedClientToken[], start: number, - end: number + end: number, ): boolean => { const expected = [ [SyntaxKind.ExportKeyword], @@ -2692,25 +2203,14 @@ const hasExactGeneratedGatewayFactory = ( [SyntaxKind.CloseParenToken], [SyntaxKind.SemicolonToken], ] satisfies readonly ExpectedToken[]; - return ( - start + expected.length === end + 1 && - matchesSequence(tokens, start, expected) - ); + return start + expected.length === end + 1 && matchesSequence(tokens, start, expected); }; -const hasGatewayBindingMutation = ( - tokens: readonly GovernedClientToken[] -): boolean => { - const protectedBindings = new Set([ - 'makeOperationGateway', - 'operationGateway', - ]); +const hasGatewayBindingMutation = (tokens: readonly GovernedClientToken[]): boolean => { + const protectedBindings = new Set(['makeOperationGateway', 'operationGateway']); for (let index = 0; index < tokens.length; index += 1) { const binding = tokens[index]; - if ( - binding?.kind !== SyntaxKind.Identifier || - !protectedBindings.has(binding.value) - ) { + if (binding?.kind !== SyntaxKind.Identifier || !protectedBindings.has(binding.value)) { continue; } if ( @@ -2721,7 +2221,7 @@ const hasGatewayBindingMutation = ( [SyntaxKind.Identifier, operation], [SyntaxKind.OpenParenToken], [SyntaxKind.Identifier, binding.value], - ]) + ]), ) || matchesSequence(tokens, index - 4, [ [SyntaxKind.Identifier, 'Reflect'], @@ -2735,7 +2235,7 @@ const hasGatewayBindingMutation = ( tokens, [[SyntaxKind.EqualsToken]], index + 2, - findStatementSemicolon(tokens, index) ?? tokens.length + findStatementSemicolon(tokens, index) ?? tokens.length, ) !== undefined) ) { return true; @@ -2744,10 +2244,7 @@ const hasGatewayBindingMutation = ( return false; }; -export const hasGeneratedOperationGatewayContract = ( - source: string, - deploymentAppId: string -): boolean => { +export const hasGeneratedOperationGatewayContract = (source: string, deploymentAppId: string): boolean => { const header = `// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n// @ontos-action-boundary-owner ${deploymentAppId}\n`; if (!source.startsWith(header)) { return false; @@ -2766,7 +2263,7 @@ export const hasGeneratedOperationGatewayContract = ( [SyntaxKind.SemicolonToken], ], 0, - tokens.length + tokens.length, ); const factory = findTopLevelSequence( tokens, @@ -2777,16 +2274,15 @@ export const hasGeneratedOperationGatewayContract = ( [SyntaxKind.EqualsToken], ], 0, - tokens.length + tokens.length, ); - const factoryEnd = - factory === undefined ? undefined : findStatementSemicolon(tokens, factory); + const factoryEnd = factory === undefined ? undefined : findStatementSemicolon(tokens, factory); const invokesFreshIssuer = factory !== undefined && factoryEnd !== undefined && hasExactGeneratedGatewayFactory(tokens, factory, factoryEnd) && ['issueGatewayContext', 'makeSharedOperationGateway'].every((name) => - hasExclusiveNamedImportFrom(source, name, '@app/shared-contracts') + hasExclusiveNamedImportFrom(source, name, '@app/shared-contracts'), ) && findSequence( tokens, @@ -2796,7 +2292,7 @@ export const hasGeneratedOperationGatewayContract = ( [SyntaxKind.Identifier, 'makeSharedOperationGateway'], ], 0, - tokens.length + tokens.length, ) !== undefined; return ( audience !== undefined && diff --git a/app/scripts/initialize-local-development.mts b/app/scripts/initialize-local-development.mts index d35a5e452..8fa534291 100644 --- a/app/scripts/initialize-local-development.mts +++ b/app/scripts/initialize-local-development.mts @@ -8,29 +8,13 @@ import { betterAuth } from 'better-auth'; import { verifyPassword } from 'better-auth/crypto'; import { admin } from 'better-auth/plugins/admin'; import { and, eq, or } from 'drizzle-orm'; -import { - Config, - ConfigProvider, - Console, - Effect, - FileSystem, - Layer, - Path, - Redacted, - Schema, -} from 'effect'; +import { Config, ConfigProvider, Console, Effect, FileSystem, Layer, Path, Redacted, Schema } from 'effect'; import { isSqlError } from 'effect/unstable/sql/SqlError'; import { AuthConfig } from '../apps/shell-super-app/api/auth/config.ts'; -import { - AuthDatabase, - AuthDatabaseLive, -} from '../apps/shell-super-app/api/auth/db/client.ts'; +import { AuthDatabase, AuthDatabaseLive } from '../apps/shell-super-app/api/auth/db/client.ts'; import { account, user } from '../apps/shell-super-app/api/auth/db/schema.ts'; -import { - CoreDatabase, - CoreDatabaseLive, -} from '../packages/core-runtime/src/db/client.ts'; +import { CoreDatabase, CoreDatabaseLive } from '../packages/core-runtime/src/db/client.ts'; import { DatabaseConfig, parseDatabaseConfig, @@ -43,10 +27,7 @@ import { tenantModuleStates, tenants, } from '../packages/core-runtime/src/db/schema.ts'; -import type { - CoreDatabaseExecutor, - CoreTransaction, -} from '../packages/core-runtime/src/db/types.ts'; +import type { CoreDatabaseExecutor, CoreTransaction } from '../packages/core-runtime/src/db/types.ts'; import { bootstrapPrincipalRecord, bootstrapRelationshipRequest, @@ -93,9 +74,7 @@ export const LOCAL_DEVELOPMENT_CONTEXT = Object.freeze({ tenantSlug: 'techsio', }); -export const LOCAL_DEVELOPMENT_VERTICALS = Object.freeze([ - 'party-registry', -] as const); +export const LOCAL_DEVELOPMENT_VERTICALS = Object.freeze(['party-registry'] as const); export interface LocalDevelopmentConfiguration { readonly authBaseUrl: string; @@ -136,20 +115,17 @@ export class LocalDevelopmentInitializationError extends Schema.TaggedError - new LocalDevelopmentInitializationError({ code, reason }); + reason: string, +): LocalDevelopmentInitializationError => new LocalDevelopmentInitializationError({ code, reason }); const loopbackHosts = new Set(['127.0.0.1', '::1', '[::1]', 'localhost']); -const validateLoopbackHttpOrigin = ( - value: string -): Effect.Effect => { +const validateLoopbackHttpOrigin = (value: string): Effect.Effect => { const parsed = URL.parse(value); if ( parsed === null || @@ -157,12 +133,7 @@ const validateLoopbackHttpOrigin = ( parsed.origin !== value || !loopbackHosts.has(parsed.hostname) ) { - return Effect.fail( - failure( - 'local_configuration_invalid', - 'BETTER_AUTH_URL must be an exact local HTTP origin' - ) - ); + return Effect.fail(failure('local_configuration_invalid', 'BETTER_AUTH_URL must be an exact local HTTP origin')); } return Effect.succeed(value); }; @@ -174,85 +145,54 @@ const localDevelopmentConfigSource = Config.all({ authSecret: Config.redacted('BETTER_AUTH_SECRET'), databaseAdminUrl: Config.schema(TrimmedNonEmptyString, 'DATABASE_ADMIN_URL'), databaseUrl: Config.schema(TrimmedNonEmptyString, 'DATABASE_URL'), - deploymentEnvironment: Config.schema( - Schema.Trim, - 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT' - ).pipe(Config.withDefault('development')), + deploymentEnvironment: Config.schema(Schema.Trim, 'ULTRAMODERN_DEPLOYMENT_ENVIRONMENT').pipe( + Config.withDefault('development'), + ), spiceDbEndpoint: Config.schema(TrimmedNonEmptyString, 'SPICEDB_ENDPOINT'), spiceDbInsecure: Config.schema(Schema.Trim, 'SPICEDB_INSECURE'), spiceDbPreSharedKey: Config.redacted('SPICEDB_PRESHARED_KEY'), }); -const environmentProvider = (environment: LocalDevelopmentEnvironment) => - ConfigProvider.fromUnknown(environment); +const environmentProvider = (environment: LocalDevelopmentEnvironment) => ConfigProvider.fromUnknown(environment); -const parseLocalDevelopmentConfigurationFromProvider = ( - provider: ConfigProvider.ConfigProvider -) => +const parseLocalDevelopmentConfigurationFromProvider = (provider: ConfigProvider.ConfigProvider) => Effect.gen(function* parseConfiguration() { const source = yield* localDevelopmentConfigSource .parse(provider) .pipe( - Effect.mapError(() => - failure( - 'local_configuration_invalid', - 'The local development configuration is invalid' - ) - ) + Effect.mapError(() => failure('local_configuration_invalid', 'The local development configuration is invalid')), ); if (source.deploymentEnvironment !== 'development') { return yield* failure( 'local_configuration_invalid', - 'Local initialization can run only in the development environment' + 'Local initialization can run only in the development environment', ); } const authSecret = Redacted.make(Redacted.value(source.authSecret).trim()); if (Redacted.value(authSecret).length < 32) { - return yield* failure( - 'local_configuration_invalid', - 'BETTER_AUTH_SECRET must contain at least 32 characters' - ); + return yield* failure('local_configuration_invalid', 'BETTER_AUTH_SECRET must contain at least 32 characters'); } const databasePair = yield* parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: source.databaseAdminUrl, DATABASE_URL: source.databaseUrl, - }).pipe( - Effect.mapError((error) => - failure('local_configuration_invalid', error.reason) - ) - ); - if ( - !loopbackHosts.has(databasePair.admin.host) || - !loopbackHosts.has(databasePair.runtime.host) - ) { - return yield* failure( - 'local_configuration_invalid', - 'Both PostgreSQL endpoints must be local' - ); + }).pipe(Effect.mapError((error) => failure('local_configuration_invalid', error.reason))); + if (!loopbackHosts.has(databasePair.admin.host) || !loopbackHosts.has(databasePair.runtime.host)) { + return yield* failure('local_configuration_invalid', 'Both PostgreSQL endpoints must be local'); } - const spiceDbPreSharedKey = Redacted.make( - Redacted.value(source.spiceDbPreSharedKey).trim() - ); + const spiceDbPreSharedKey = Redacted.make(Redacted.value(source.spiceDbPreSharedKey).trim()); const spiceDb = yield* parseSpiceDbConfig({ SPICEDB_ENDPOINT: source.spiceDbEndpoint, SPICEDB_INSECURE: source.spiceDbInsecure, SPICEDB_PRESHARED_KEY: Redacted.value(spiceDbPreSharedKey), ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: source.deploymentEnvironment, - }).pipe( - Effect.mapError((error) => - failure('local_configuration_invalid', error.reason) - ) - ); + }).pipe(Effect.mapError((error) => failure('local_configuration_invalid', error.reason))); const parsedSpiceDbEndpoint = URL.parse(`http://${spiceDb.endpoint}`); if ( parsedSpiceDbEndpoint === null || !loopbackHosts.has(parsedSpiceDbEndpoint.hostname) || !spiceDb.insecureLocal ) { - return yield* failure( - 'local_configuration_invalid', - 'SpiceDB must use insecure transport on a local endpoint' - ); + return yield* failure('local_configuration_invalid', 'SpiceDB must use insecure transport on a local endpoint'); } const authBaseUrl = yield* validateLoopbackHttpOrigin(source.authBaseUrl); return { @@ -269,19 +209,14 @@ const parseLocalDevelopmentConfigurationFromProvider = ( }); export const parseLocalDevelopmentConfiguration = ( - environment: LocalDevelopmentEnvironment -): Effect.Effect< - LocalDevelopmentConfiguration, - LocalDevelopmentInitializationError -> => - parseLocalDevelopmentConfigurationFromProvider( - environmentProvider(environment) - ); + environment: LocalDevelopmentEnvironment, +): Effect.Effect => + parseLocalDevelopmentConfigurationFromProvider(environmentProvider(environment)); export const classifyExactLocalRecord = ( label: string, existing: ExactRecord | undefined, - expected: Expected + expected: Expected, ): Effect.Effect<'create' | 'existing', LocalDevelopmentInitializationError> => Effect.gen(function* classifyRecord() { if (existing === undefined) { @@ -293,7 +228,7 @@ export const classifyExactLocalRecord = ( if (conflictingFields.length > 0) { return yield* failure( 'local_conflict', - `Existing ${label} conflicts with the local development definition (${conflictingFields.join(', ')})` + `Existing ${label} conflicts with the local development definition (${conflictingFields.join(', ')})`, ); } return 'existing' as const; @@ -302,7 +237,7 @@ export const classifyExactLocalRecord = ( export const classifyLocalModuleState = ( label: string, existing: ExactRecord | undefined, - expected: ExactRecord + expected: ExactRecord, ): Effect.Effect<'create' | 'existing', LocalDevelopmentInitializationError> => classifyExactLocalRecord(label, existing, { moduleKey: expected.moduleKey ?? null, @@ -314,7 +249,7 @@ const TopologySchema = Schema.Struct({ verticals: Schema.Array( Schema.Struct({ id: TrimmedNonEmptyString, - }) + }), ), }); @@ -323,62 +258,34 @@ type DeriveContract = typeof deriveOntosModuleDeploymentContract; export const deriveActivatedModuleIds = ( workspaceRoot: string, deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, - activatedVerticals: readonly string[] = LOCAL_DEVELOPMENT_VERTICALS + activatedVerticals: readonly string[] = LOCAL_DEVELOPMENT_VERTICALS, ) => Effect.gen(function* deriveModuleIds() { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const topologySource = yield* fileSystem - .readFileString( - pathService.join(workspaceRoot, 'topology/reference-topology.json') - ) - .pipe( - Effect.mapError(() => - failure( - 'local_contract_invalid', - 'The authoritative topology could not be read' - ) - ) - ); - const topology = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(TopologySchema), - { - onExcessProperty: 'preserve', - } - )(topologySource).pipe( - Effect.mapError(() => - failure( - 'local_contract_invalid', - 'The authoritative topology is invalid' - ) - ) + .readFileString(pathService.join(workspaceRoot, 'topology/reference-topology.json')) + .pipe(Effect.mapError(() => failure('local_contract_invalid', 'The authoritative topology could not be read'))); + const topology = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(TopologySchema), { + onExcessProperty: 'preserve', + })(topologySource).pipe( + Effect.mapError(() => failure('local_contract_invalid', 'The authoritative topology is invalid')), ); if (topology.verticals.length === 0) { - return yield* failure( - 'local_contract_invalid', - 'The authoritative topology has no MicroVerticals' - ); + return yield* failure('local_contract_invalid', 'The authoritative topology has no MicroVerticals'); } const verticals = topology.verticals.map(({ id }) => id); if (new Set(verticals).size !== verticals.length) { - return yield* failure( - 'local_contract_invalid', - 'The authoritative topology has duplicate verticals' - ); + return yield* failure('local_contract_invalid', 'The authoritative topology has duplicate verticals'); } if (new Set(activatedVerticals).size !== activatedVerticals.length) { - return yield* failure( - 'local_contract_invalid', - 'Local activation contains duplicate MicroVerticals' - ); + return yield* failure('local_contract_invalid', 'Local activation contains duplicate MicroVerticals'); } - const missingVerticals = activatedVerticals.filter( - (vertical) => !verticals.includes(vertical) - ); + const missingVerticals = activatedVerticals.filter((vertical) => !verticals.includes(vertical)); if (missingVerticals.length > 0) { return yield* failure( 'local_contract_invalid', - `Configured local MicroVerticals are missing from the topology (${missingVerticals.join(', ')})` + `Configured local MicroVerticals are missing from the topology (${missingVerticals.join(', ')})`, ); } const contracts = yield* Effect.forEach( @@ -386,26 +293,18 @@ export const deriveActivatedModuleIds = ( (vertical) => deriveContract({ vertical, workspaceRoot }).pipe( Effect.mapError(() => - failure( - 'local_contract_invalid', - `The ${vertical} deployment contract could not be derived` - ) - ) + failure('local_contract_invalid', `The ${vertical} deployment contract could not be derived`), + ), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const moduleIds = contracts.map((contract) => contract.manifest.module.id); if (new Set(moduleIds).size !== moduleIds.length) { - return yield* failure( - 'local_contract_invalid', - 'Generated contracts contain duplicate module IDs' - ); + return yield* failure('local_contract_invalid', 'Generated contracts contain duplicate module IDs'); } const sortedModuleIds: string[] = []; for (const moduleId of moduleIds) { - const insertionIndex = sortedModuleIds.findIndex( - (existing) => moduleId.localeCompare(existing) < 0 - ); + const insertionIndex = sortedModuleIds.findIndex((existing) => moduleId.localeCompare(existing) < 0); if (insertionIndex === -1) { sortedModuleIds.push(moduleId); } else { @@ -425,191 +324,141 @@ export const moduleStateIdFor = (moduleId: string): string => { return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`; }; -export const buildLocalDevelopmentRelationships = Effect.fn( - 'LocalDevelopment.buildRelationships' -)(function* buildRelationships( - moduleIds: readonly string[] -): Effect.fn.Return< - readonly LocalDevelopmentRelationship[], - LocalDevelopmentInitializationError -> { - const context = LOCAL_DEVELOPMENT_CONTEXT; - const legalEntityObjectId = toLegalEntityAccessObjectId( - context.tenantId, - context.legalEntityId - ); - if (legalEntityObjectId === undefined) { - return yield* failure( - 'local_contract_invalid', - 'The local Legal Entity authorization ID is invalid' - ); - } - const shared: LocalDevelopmentRelationship[] = [ - { - relation: 'member', - resourceId: context.tenantId, - resourceType: 'tenant', - subjectId: context.principalId, - subjectType: 'principal', - }, - { - relation: 'tenant', - resourceId: legalEntityObjectId, - resourceType: 'legal_entity', - subjectId: context.tenantId, - subjectType: 'tenant', - }, - { - relation: 'member', - resourceId: legalEntityObjectId, - resourceType: 'legal_entity', - subjectId: context.principalId, - subjectType: 'principal', - }, - ]; - for (const moduleId of moduleIds) { - const moduleObjectId = toModuleAccessObjectId( - context.tenantId, - context.legalEntityId, - moduleId - ); - if (moduleObjectId === undefined) { - return yield* failure( - 'local_contract_invalid', - `Module ${moduleId} has an invalid authorization ID` - ); +export const buildLocalDevelopmentRelationships = Effect.fn('LocalDevelopment.buildRelationships')( + function* buildRelationships( + moduleIds: readonly string[], + ): Effect.fn.Return { + const context = LOCAL_DEVELOPMENT_CONTEXT; + const legalEntityObjectId = toLegalEntityAccessObjectId(context.tenantId, context.legalEntityId); + if (legalEntityObjectId === undefined) { + return yield* failure('local_contract_invalid', 'The local Legal Entity authorization ID is invalid'); } - shared.push( + const shared: LocalDevelopmentRelationship[] = [ { - relation: 'legal_entity', - resourceId: moduleObjectId, - resourceType: 'module_access', - subjectId: legalEntityObjectId, - subjectType: 'legal_entity', + relation: 'member', + resourceId: context.tenantId, + resourceType: 'tenant', + subjectId: context.principalId, + subjectType: 'principal', }, { - relation: 'accessor', - resourceId: moduleObjectId, - resourceType: 'module_access', + relation: 'tenant', + resourceId: legalEntityObjectId, + resourceType: 'legal_entity', + subjectId: context.tenantId, + subjectType: 'tenant', + }, + { + relation: 'member', + resourceId: legalEntityObjectId, + resourceType: 'legal_entity', subjectId: context.principalId, subjectType: 'principal', + }, + ]; + for (const moduleId of moduleIds) { + const moduleObjectId = toModuleAccessObjectId(context.tenantId, context.legalEntityId, moduleId); + if (moduleObjectId === undefined) { + return yield* failure('local_contract_invalid', `Module ${moduleId} has an invalid authorization ID`); } - ); - } - return shared; -}); + shared.push( + { + relation: 'legal_entity', + resourceId: moduleObjectId, + resourceType: 'module_access', + subjectId: legalEntityObjectId, + subjectType: 'legal_entity', + }, + { + relation: 'accessor', + resourceId: moduleObjectId, + resourceType: 'module_access', + subjectId: context.principalId, + subjectType: 'principal', + }, + ); + } + return shared; + }, +); -const ensureAuthUser = Effect.fn('LocalDevelopment.ensureAuthUser')( - function* ensureAuthUserEffect(configuration: LocalDevelopmentConfiguration) { - const { adapter, executor: database } = yield* AuthDatabase; - const existingUsers = yield* database - .select({ email: user.email, id: user.id, name: user.name }) - .from(user) - .where(eq(user.email, configuration.email)) +const ensureAuthUser = Effect.fn('LocalDevelopment.ensureAuthUser')(function* ensureAuthUserEffect( + configuration: LocalDevelopmentConfiguration, +) { + const { adapter, executor: database } = yield* AuthDatabase; + const existingUsers = yield* database + .select({ email: user.email, id: user.id, name: user.name }) + .from(user) + .where(eq(user.email, configuration.email)) + .limit(2) + .pipe(Effect.mapError(() => failure('local_persistence_failed', 'The local Better Auth user could not be loaded'))); + if (existingUsers.length > 1) { + return yield* failure('local_conflict', 'Multiple Better Auth users use the local email'); + } + const [existingUser] = existingUsers; + if (existingUser !== undefined) { + yield* classifyExactLocalRecord('Better Auth user', existingUser, { + email: configuration.email, + name: configuration.principalDisplayName, + }); + const credentials = yield* database + .select({ password: account.password }) + .from(account) + .where(and(eq(account.userId, existingUser.id), eq(account.providerId, 'credential'))) .limit(2) .pipe( Effect.mapError(() => - failure( - 'local_persistence_failed', - 'The local Better Auth user could not be loaded' - ) - ) - ); - if (existingUsers.length > 1) { - return yield* failure( - 'local_conflict', - 'Multiple Better Auth users use the local email' + failure('local_persistence_failed', 'The local Better Auth credentials could not be loaded'), + ), ); + const [credential] = credentials.length === 1 ? credentials : []; + if (credential?.password === null || credential?.password === undefined) { + return yield* failure('local_conflict', 'The existing local user has conflicting credentials'); } - const [existingUser] = existingUsers; - if (existingUser !== undefined) { - yield* classifyExactLocalRecord('Better Auth user', existingUser, { - email: configuration.email, - name: configuration.principalDisplayName, - }); - const credentials = yield* database - .select({ password: account.password }) - .from(account) - .where( - and( - eq(account.userId, existingUser.id), - eq(account.providerId, 'credential') - ) - ) - .limit(2) - .pipe( - Effect.mapError(() => - failure( - 'local_persistence_failed', - 'The local Better Auth credentials could not be loaded' - ) - ) - ); - const [credential] = credentials.length === 1 ? credentials : []; - if (credential?.password === null || credential?.password === undefined) { - return yield* failure( - 'local_conflict', - 'The existing local user has conflicting credentials' - ); - } - const storedPassword = credential.password; - const validPassword = yield* Effect.tryPromise({ - catch: () => - failure( - 'local_persistence_failed', - 'The local Better Auth password could not be verified' - ), - try: async () => - await verifyPassword({ - hash: storedPassword, - password: Redacted.value(configuration.password), - }), - }); - if (!validPassword) { - return yield* failure( - 'local_conflict', - 'The existing local user has conflicting credentials' - ); - } - return { status: 'existing' as const, userId: existingUser.id }; - } - const created = yield* Effect.tryPromise({ - catch: () => - failure( - 'local_persistence_failed', - 'The local Better Auth user could not be created' - ), - try: async () => { - const authentication = betterAuth({ - baseURL: configuration.authBaseUrl, - database: adapter, - emailAndPassword: { - autoSignIn: false, - disableSignUp: true, - enabled: true, - }, - logger: { disabled: true }, - plugins: [admin()], - secret: Redacted.value(configuration.authSecret), - }); - return await authentication.api.createUser({ - body: { - email: configuration.email, - name: configuration.principalDisplayName, - password: Redacted.value(configuration.password), - }, - }); - }, + const storedPassword = credential.password; + const validPassword = yield* Effect.tryPromise({ + catch: () => failure('local_persistence_failed', 'The local Better Auth password could not be verified'), + try: async () => + await verifyPassword({ + hash: storedPassword, + password: Redacted.value(configuration.password), + }), }); - return { status: 'created' as const, userId: created.user.id }; + if (!validPassword) { + return yield* failure('local_conflict', 'The existing local user has conflicting credentials'); + } + return { status: 'existing' as const, userId: existingUser.id }; } -); + const created = yield* Effect.tryPromise({ + catch: () => failure('local_persistence_failed', 'The local Better Auth user could not be created'), + try: async () => { + const authentication = betterAuth({ + baseURL: configuration.authBaseUrl, + database: adapter, + emailAndPassword: { + autoSignIn: false, + disableSignUp: true, + enabled: true, + }, + logger: { disabled: true }, + plugins: [admin()], + secret: Redacted.value(configuration.authSecret), + }); + return await authentication.api.createUser({ + body: { + email: configuration.email, + name: configuration.principalDisplayName, + password: Redacted.value(configuration.password), + }, + }); + }, + }); + return { status: 'created' as const, userId: created.user.id }; +}); -const reconcileLocalModules = Effect.fn( - 'LocalDevelopment.reconcileLocalModules' -)(function* reconcileModuleStates( +const reconcileLocalModules = Effect.fn('LocalDevelopment.reconcileLocalModules')(function* reconcileModuleStates( transaction: CoreTransaction, - moduleIds: readonly string[] + moduleIds: readonly string[], ) { const context = LOCAL_DEVELOPMENT_CONTEXT; for (const moduleId of moduleIds) { @@ -625,18 +474,12 @@ const reconcileLocalModules = Effect.fn( .where( or( eq(tenantModuleStates.tenantModuleStateId, moduleStateId), - and( - eq(tenantModuleStates.tenantId, context.tenantId), - eq(tenantModuleStates.moduleKey, moduleId) - ) - ) + and(eq(tenantModuleStates.tenantId, context.tenantId), eq(tenantModuleStates.moduleKey, moduleId)), + ), ) .limit(2); if (moduleCandidates.length > 1) { - return yield* failure( - 'local_conflict', - `The ${moduleId} module-state identity conflicts` - ); + return yield* failure('local_conflict', `The ${moduleId} module-state identity conflicts`); } const expectedModuleState = { moduleKey: moduleId, @@ -645,11 +488,8 @@ const reconcileLocalModules = Effect.fn( tenantModuleStateId: moduleStateId, } as const; if ( - (yield* classifyLocalModuleState( - `${moduleId} module state`, - moduleCandidates[0], - expectedModuleState - )) === 'create' + (yield* classifyLocalModuleState(`${moduleId} module state`, moduleCandidates[0], expectedModuleState)) === + 'create' ) { yield* transaction.insert(tenantModuleStates).values(expectedModuleState); } @@ -660,161 +500,98 @@ const reconcileLocalModules = Effect.fn( export const reconcileCoreContext = ( database: CoreDatabaseExecutor, authUserId: string, - moduleIds: readonly string[] + moduleIds: readonly string[], ): Effect.Effect => database .transaction( - Effect.fn('LocalDevelopment.reconcileCoreContext')( - function* reconcileContext(transaction) { - const context = LOCAL_DEVELOPMENT_CONTEXT; - const tenantCandidates = yield* transaction - .select({ - defaultLocale: tenants.defaultLocale, - name: tenants.name, - slug: tenants.slug, - status: tenants.status, - tenantId: tenants.tenantId, - }) - .from(tenants) - .where( - or( - eq(tenants.tenantId, context.tenantId), - eq(tenants.slug, context.tenantSlug) - ) - ) - .limit(2); - if (tenantCandidates.length > 1) { - return yield* failure( - 'local_conflict', - 'The local tenant identity conflicts' - ); - } - const expectedTenant = { - defaultLocale: context.defaultLocale, - name: context.tenantName, - slug: context.tenantSlug, - status: 'active', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactLocalRecord( - 'tenant', - tenantCandidates[0], - expectedTenant - )) === 'create' - ) { - yield* transaction.insert(tenants).values(expectedTenant); - } - - const legalCandidates = yield* selectBootstrapLegalEntities( - transaction, - context - ); - if (legalCandidates.length > 1) { - return yield* failure( - 'local_conflict', - 'The local Legal Entity identity conflicts' - ); - } - const expectedLegalEntity = { - legalEntityId: context.legalEntityId, - legalName: context.legalName, - registrationCountry: context.registrationCountry, - registrationNumber: context.registrationNumber, - status: 'active', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactLocalRecord( - 'Legal Entity', - legalCandidates[0], - expectedLegalEntity - )) === 'create' - ) { - yield* transaction - .insert(legalEntities) - .values(expectedLegalEntity); - } - - const expectedPrincipal = bootstrapPrincipalRecord(context); - const principalCandidates = yield* selectBootstrapPrincipals( - transaction, - context - ); - if ( - (yield* classifyExactLocalRecord( - 'principal', - principalCandidates[0], - expectedPrincipal - )) === 'create' - ) { - yield* transaction.insert(principals).values(expectedPrincipal); - } - - const bindingCandidates = yield* selectBootstrapAuthBindings( - transaction, - context, - authUserId - ); - if (bindingCandidates.length > 1) { - return yield* failure( - 'local_conflict', - 'The local authentication binding conflicts' - ); - } - const expectedBinding = { - principalAuthBindingId: context.authBindingId, - principalId: context.principalId, - provider: 'better_auth', - providerSubjectId: authUserId, - status: 'active', - subjectType: 'user', - tenantId: context.tenantId, - } as const; - if ( - (yield* classifyExactLocalRecord( - 'authentication binding', - bindingCandidates[0], - expectedBinding - )) === 'create' - ) { - yield* transaction - .insert(principalAuthBindings) - .values(expectedBinding); - } - - yield* reconcileLocalModules(transaction, moduleIds); - return yield* Effect.void; + Effect.fn('LocalDevelopment.reconcileCoreContext')(function* reconcileContext(transaction) { + const context = LOCAL_DEVELOPMENT_CONTEXT; + const tenantCandidates = yield* transaction + .select({ + defaultLocale: tenants.defaultLocale, + name: tenants.name, + slug: tenants.slug, + status: tenants.status, + tenantId: tenants.tenantId, + }) + .from(tenants) + .where(or(eq(tenants.tenantId, context.tenantId), eq(tenants.slug, context.tenantSlug))) + .limit(2); + if (tenantCandidates.length > 1) { + return yield* failure('local_conflict', 'The local tenant identity conflicts'); } - ) + const expectedTenant = { + defaultLocale: context.defaultLocale, + name: context.tenantName, + slug: context.tenantSlug, + status: 'active', + tenantId: context.tenantId, + } as const; + if ((yield* classifyExactLocalRecord('tenant', tenantCandidates[0], expectedTenant)) === 'create') { + yield* transaction.insert(tenants).values(expectedTenant); + } + + const legalCandidates = yield* selectBootstrapLegalEntities(transaction, context); + if (legalCandidates.length > 1) { + return yield* failure('local_conflict', 'The local Legal Entity identity conflicts'); + } + const expectedLegalEntity = { + legalEntityId: context.legalEntityId, + legalName: context.legalName, + registrationCountry: context.registrationCountry, + registrationNumber: context.registrationNumber, + status: 'active', + tenantId: context.tenantId, + } as const; + if ((yield* classifyExactLocalRecord('Legal Entity', legalCandidates[0], expectedLegalEntity)) === 'create') { + yield* transaction.insert(legalEntities).values(expectedLegalEntity); + } + + const expectedPrincipal = bootstrapPrincipalRecord(context); + const principalCandidates = yield* selectBootstrapPrincipals(transaction, context); + if ((yield* classifyExactLocalRecord('principal', principalCandidates[0], expectedPrincipal)) === 'create') { + yield* transaction.insert(principals).values(expectedPrincipal); + } + + const bindingCandidates = yield* selectBootstrapAuthBindings(transaction, context, authUserId); + if (bindingCandidates.length > 1) { + return yield* failure('local_conflict', 'The local authentication binding conflicts'); + } + const expectedBinding = { + principalAuthBindingId: context.authBindingId, + principalId: context.principalId, + provider: 'better_auth', + providerSubjectId: authUserId, + status: 'active', + subjectType: 'user', + tenantId: context.tenantId, + } as const; + if ( + (yield* classifyExactLocalRecord('authentication binding', bindingCandidates[0], expectedBinding)) === + 'create' + ) { + yield* transaction.insert(principalAuthBindings).values(expectedBinding); + } + + yield* reconcileLocalModules(transaction, moduleIds); + return yield* Effect.void; + }), ) .pipe( // Native SQL commit/rollback errors are defects; preserve unrelated defects. - Effect.catchDefect((defect) => - isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect) - ), + Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))), Effect.catchTag('EffectDrizzleQueryError', () => - failure( - 'local_persistence_failed', - 'The local Core context could not be reconciled' - ) + failure('local_persistence_failed', 'The local Core context could not be reconciled'), ), Effect.catchTag('SqlError', () => - failure( - 'local_persistence_failed', - 'The local Core context could not be reconciled' - ) - ) + failure('local_persistence_failed', 'The local Core context could not be reconciled'), + ), ); const acquireSpiceDbClient = (configuration: LocalDevelopmentConfiguration) => Effect.acquireRelease( Effect.try({ - catch: () => - failure( - 'local_persistence_failed', - 'The local authorization client could not be created' - ), + catch: () => failure('local_persistence_failed', 'The local authorization client could not be created'), try: () => { const preSharedKey = Redacted.value(configuration.spiceDbPreSharedKey); return v1.NewClient( @@ -823,56 +600,41 @@ const acquireSpiceDbClient = (configuration: LocalDevelopmentConfiguration) => spiceDbClientSecurity({ endpoint: configuration.spiceDbEndpoint, insecureLocal: configuration.spiceDbInsecureLocal, - }) + }), ); }, }), - (client) => Effect.sync(() => client.close()) + (client) => Effect.sync(() => client.close()), ); const touchRelationships = ( configuration: LocalDevelopmentConfiguration, - relationships: readonly LocalDevelopmentRelationship[] + relationships: readonly LocalDevelopmentRelationship[], ): Effect.Effect => Effect.scoped( Effect.gen(function* touchLocalRelationships() { const client = yield* acquireSpiceDbClient(configuration); yield* Effect.tryPromise({ catch: () => - failure( - 'local_persistence_failed', - 'The local authorization relationships could not be reconciled' - ), + failure('local_persistence_failed', 'The local authorization relationships could not be reconciled'), try: async () => { - await client.promises.writeRelationships( - bootstrapRelationshipRequest(relationships) - ); + await client.promises.writeRelationships(bootstrapRelationshipRequest(relationships)); }, }); - }) + }), ); const loadRootConfiguration = () => Effect.gen(function* loadConfiguration() { const fileProvider = yield* ConfigProvider.fromDotEnv({ path: path.join(import.meta.dirname, '..', '.env'), - }).pipe( - Effect.mapError(() => - failure('local_configuration_invalid', 'Unable to load app/.env') - ) - ); - const provider = ConfigProvider.orElse( - ConfigProvider.fromEnv(), - fileProvider - ); + }).pipe(Effect.mapError(() => failure('local_configuration_invalid', 'Unable to load app/.env'))); + const provider = ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider); return yield* parseLocalDevelopmentConfigurationFromProvider(provider); }); export const initializeLocalDevelopment = ( - environmentEffect?: Effect.Effect< - LocalDevelopmentEnvironment, - LocalDevelopmentInitializationError - > + environmentEffect?: Effect.Effect, ): Effect.Effect< LocalDevelopmentInitializationResult, LocalDevelopmentInitializationError, @@ -882,13 +644,11 @@ export const initializeLocalDevelopment = ( const configuration = environmentEffect === undefined ? yield* loadRootConfiguration() - : yield* environmentEffect.pipe( - Effect.flatMap(parseLocalDevelopmentConfiguration) - ); + : yield* environmentEffect.pipe(Effect.flatMap(parseLocalDevelopmentConfiguration)); const moduleIds = yield* deriveActivatedModuleIds( path.join(import.meta.dirname, '..'), deriveOntosModuleDeploymentContract, - LOCAL_DEVELOPMENT_VERTICALS + LOCAL_DEVELOPMENT_VERTICALS, ); const relationships = yield* buildLocalDevelopmentRelationships(moduleIds); const authUser = yield* ensureAuthUser(configuration).pipe( @@ -902,46 +662,27 @@ export const initializeLocalDevelopment = ( secureCookies: false, supportUserIds: [], trustedOrigins: [configuration.authBaseUrl], - }) - ) - ) + }), + ), + ), ), Effect.catchTag('AuthDatabaseConnectionError', () => - failure( - 'local_persistence_failed', - 'The local authentication database could not be opened' - ) - ) + failure('local_persistence_failed', 'The local authentication database could not be opened'), + ), ); const databaseConfiguration = yield* parseDatabaseConfig({ DATABASE_URL: configuration.databaseAdminUrl, }).pipe( - Effect.mapError(() => - failure( - 'local_configuration_invalid', - 'The local Core database configuration is invalid' - ) - ) + Effect.mapError(() => failure('local_configuration_invalid', 'The local Core database configuration is invalid')), ); yield* Effect.gen(function* initializeCore() { const database = yield* CoreDatabase; - yield* reconcileCoreContext( - database.executor, - authUser.userId, - moduleIds - ); + yield* reconcileCoreContext(database.executor, authUser.userId, moduleIds); }).pipe( - Effect.provide( - CoreDatabaseLive.pipe( - Layer.provide(Layer.succeed(DatabaseConfig, databaseConfiguration)) - ) - ), + Effect.provide(CoreDatabaseLive.pipe(Layer.provide(Layer.succeed(DatabaseConfig, databaseConfiguration)))), Effect.catchTag('DatabaseConnectionError', () => - failure( - 'local_persistence_failed', - 'The local Core database could not be opened' - ) - ) + failure('local_persistence_failed', 'The local Core database could not be opened'), + ), ); yield* touchRelationships(configuration, relationships); return { @@ -954,24 +695,16 @@ export const initializeLocalDevelopment = ( }; }); -const runLocalDevelopmentInitialization = Effect.matchEffect( - initializeLocalDevelopment(), - { - onFailure: (error) => Console.error(error.reason).pipe(Effect.as(false)), - onSuccess: (result) => - Console.log( - `Local development initialized for ${result.email}; auth user ${result.authUser}; ${result.moduleIds.length} module(s) active.` - ).pipe(Effect.as(true)), - } -); +const runLocalDevelopmentInitialization = Effect.matchEffect(initializeLocalDevelopment(), { + onFailure: (error) => Console.error(error.reason).pipe(Effect.as(false)), + onSuccess: (result) => + Console.log( + `Local development initialized for ${result.email}; auth user ${result.authUser}; ${result.moduleIds.length} module(s) active.`, + ).pipe(Effect.as(true)), +}); -if ( - process.argv[1] !== undefined && - import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href -) { - const succeeded = await Effect.runPromise( - runLocalDevelopmentInitialization.pipe(Effect.provide(NodeServices.layer)) - ); +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + const succeeded = await Effect.runPromise(runLocalDevelopmentInitialization.pipe(Effect.provide(NodeServices.layer))); if (!succeeded) { process.exitCode = 1; } diff --git a/app/scripts/local-environment-values.mts b/app/scripts/local-environment-values.mts index 5c05e29e0..8532a5c93 100644 --- a/app/scripts/local-environment-values.mts +++ b/app/scripts/local-environment-values.mts @@ -2,18 +2,14 @@ import { Redacted } from 'effect'; const assignmentPattern = /^(?[A-Z][A-Z0-9_]*)=(?.*)$/u; -const existingValues = ( - lines: readonly string[] -): Readonly> => +const existingValues = (lines: readonly string[]): Readonly> => Object.fromEntries( lines.flatMap((line) => { const match = assignmentPattern.exec(line); const key = match?.groups?.key; const value = match?.groups?.value; - return key === undefined || value === undefined - ? [] - : [[key, value] as const]; - }) + return key === undefined || value === undefined ? [] : [[key, value] as const]; + }), ); export interface LocalEnvironmentOverrides { @@ -28,40 +24,28 @@ export interface LocalPublicClientTopology { readonly shellPort: number; } -export const localPublicClientValues = ( - lines: readonly string[], - topology: LocalPublicClientTopology -) => { +export const localPublicClientValues = (lines: readonly string[], topology: LocalPublicClientTopology) => { const existing = existingValues(lines); return { - ONTOS_PARTY_REGISTRY_API_BASE_URL: - existing.ONTOS_PARTY_REGISTRY_API_BASE_URL ?? - topology.partyRegistryApiBaseUrl, + ONTOS_PARTY_REGISTRY_API_BASE_URL: existing.ONTOS_PARTY_REGISTRY_API_BASE_URL ?? topology.partyRegistryApiBaseUrl, ONTOS_SHELL_GATEWAY_BASE_URL: - existing.ONTOS_SHELL_GATEWAY_BASE_URL ?? - `http://localhost:${topology.shellPort}/${topology.shellId}-api`, + existing.ONTOS_SHELL_GATEWAY_BASE_URL ?? `http://localhost:${topology.shellPort}/${topology.shellId}-api`, }; }; -export const localSpiceDbValues = ( - lines: readonly string[], - overrides: LocalEnvironmentOverrides -) => { +export const localSpiceDbValues = (lines: readonly string[], overrides: LocalEnvironmentOverrides) => { const existing = existingValues(lines); const grpcPort = overrides.grpcPort ?? existing.SPICEDB_GRPC_PORT ?? '50051'; const httpPort = overrides.httpPort ?? existing.SPICEDB_HTTP_PORT ?? '8443'; const resolvePreSharedKey = () => - (overrides.preSharedKey === undefined - ? undefined - : Redacted.value(overrides.preSharedKey)) ?? + (overrides.preSharedKey === undefined ? undefined : Redacted.value(overrides.preSharedKey)) ?? existing.SPICEDB_PRESHARED_KEY ?? 'ontos-local-development-key'; const preSharedKey = resolvePreSharedKey(); return { SPICEDB_ENDPOINT: - overrides.grpcPort === undefined && - existing.SPICEDB_ENDPOINT !== undefined + overrides.grpcPort === undefined && existing.SPICEDB_ENDPOINT !== undefined ? existing.SPICEDB_ENDPOINT : `localhost:${grpcPort}`, SPICEDB_GRPC_PORT: grpcPort, diff --git a/app/scripts/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index 3589eb14b..e5842b9e6 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,9 +1,6 @@ import { expect, it } from 'effect-rstest'; -import { - localPublicClientValues, - localSpiceDbValues, -} from './local-environment-values.mts'; +import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; const spiceDbGrpcPort = '50052'; const spiceDbHttpPort = '8444'; @@ -18,7 +15,7 @@ it('preserves canonical SpiceDB values when no local override is supplied', () = 'SPICEDB_INSECURE=true', 'SPICEDB_PRESHARED_KEY=existing-key', ], - {} + {}, ); expect(values).toEqual({ @@ -31,10 +28,10 @@ it('preserves canonical SpiceDB values when no local override is supplied', () = }); it('applies explicit local port overrides as one consistent endpoint', () => { - const values = localSpiceDbValues( - ['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], - { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort } - ); + const values = localSpiceDbValues(['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], { + grpcPort: spiceDbGrpcPort, + httpPort: spiceDbHttpPort, + }); expect(values.SPICEDB_ENDPOINT).toBe(spiceDbEndpoint); expect(values.SPICEDB_GRPC_PORT).toBe(spiceDbGrpcPort); @@ -47,7 +44,7 @@ it('derives local public-client URLs from configured Shell identity/port and Par partyRegistryApiBaseUrl: 'http://localhost:4199/party-api', shellId: 'staff-shell', shellPort: 3099, - }) + }), ).toEqual({ ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', @@ -65,12 +62,10 @@ it('preserves explicitly configured public-client URLs', () => { partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', shellId: 'shell-super-app', shellPort: 3020, - } - ) + }, + ), ).toEqual({ - ONTOS_PARTY_REGISTRY_API_BASE_URL: - 'https://party.example.test/party-registry-api', - ONTOS_SHELL_GATEWAY_BASE_URL: - 'https://gateway.example.test/shell-super-app-api', + ONTOS_PARTY_REGISTRY_API_BASE_URL: 'https://party.example.test/party-registry-api', + ONTOS_SHELL_GATEWAY_BASE_URL: 'https://gateway.example.test/shell-super-app-api', }); }); diff --git a/app/scripts/materialize-outbox-worker.mjs b/app/scripts/materialize-outbox-worker.mjs index 43069d24b..3c6c0f5d9 100644 --- a/app/scripts/materialize-outbox-worker.mjs +++ b/app/scripts/materialize-outbox-worker.mjs @@ -1,14 +1,7 @@ import { isBuiltin } from 'node:module'; import { NodeServices } from '@effect/platform-node'; -import { - Config, - Effect, - FileSystem, - ManagedRuntime, - Path, - Schema, -} from 'effect'; +import { Config, Effect, FileSystem, ManagedRuntime, Path, Schema } from 'effect'; import { build } from 'esbuild'; import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; @@ -21,18 +14,16 @@ const TopologySchema = Schema.fromJsonString( moduleFederation: Schema.Struct({ manifestUrl: Schema.String }), package: Schema.String, path: Schema.String, - }) + }), ), - }) + }), ); const PackageManifestSchema = Schema.fromJsonString( Schema.Struct({ - dependencies: Schema.optionalKey( - Schema.Record(Schema.String, Schema.String) - ), + dependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), name: Schema.String, - }) + }), ); const MetafileInputsSchema = Schema.Struct({ @@ -52,7 +43,7 @@ const WorkerArtifactSchema = Schema.fromJsonString( sourceInputs: Schema.Array(Schema.String), sourceRevision: Schema.OptionFromNullOr(Schema.String), }), - { space: 2 } + { space: 2 }, ); class OutboxWorkerMaterializationError extends Error { @@ -76,8 +67,7 @@ const nodeRuntime = ManagedRuntime.make(NodeServices.layer); * @param {RegExp} pattern Unicode-aware source expression. * @returns {RegExp} Equivalent esbuild-compatible filter. */ -const esbuildFilter = (pattern) => - new RegExp(pattern.source, pattern.flags.replaceAll('u', '')); +const esbuildFilter = (pattern) => new RegExp(pattern.source, pattern.flags.replaceAll('u', '')); /** * @param {{ @@ -87,20 +77,13 @@ const esbuildFilter = (pattern) => * }} context Plugin dependencies. * @returns {import('esbuild').Plugin} Production dependency externalization plugin. */ -const makeProductionDependenciesPlugin = ({ - packages, - path, - workspaceRoot, -}) => ({ +const makeProductionDependenciesPlugin = ({ packages, path, workspaceRoot }) => ({ name: 'worker-production-dependencies', setup(builder) { - builder.onResolve( - { filter: esbuildFilter(/^@effect\/platform-node$/u) }, - () => ({ - namespace: 'worker-platform-node', - path: '@effect/platform-node', - }) - ); + builder.onResolve({ filter: esbuildFilter(/^@effect\/platform-node$/u) }, () => ({ + namespace: 'worker-platform-node', + path: '@effect/platform-node', + })); builder.onLoad( { filter: esbuildFilter(/.*/u), @@ -115,21 +98,16 @@ const makeProductionDependenciesPlugin = ({ ].join('\n'), loader: 'js', resolveDir: workspaceRoot, - }) + }), ); builder.onResolve({ filter: esbuildFilter(/^[^./]/u) }, (args) => { if (isBuiltin(args.path)) { return { external: true, path: args.path }; } - const name = args.path.startsWith('@') - ? args.path.split('/').slice(0, 2).join('/') - : args.path.split('/').at(0); + const name = args.path.startsWith('@') ? args.path.split('/').slice(0, 2).join('/') : args.path.split('/').at(0); if (args.path === '@app/core-runtime') { return { - path: path.join( - workspaceRoot, - 'packages/core-runtime/src/outbox/worker-entrypoint.ts' - ), + path: path.join(workspaceRoot, 'packages/core-runtime/src/outbox/worker-entrypoint.ts'), }; } if (name !== undefined && packages.has(name)) { @@ -154,29 +132,19 @@ const collectProductionDependency = (importedPath, packages, dependencies) => ? importedPath.split('/').slice(0, 2).join('/') : importedPath.split('/').at(0); if (name === undefined) { - return yield* Effect.fail( - failure(`Invalid worker dependency ${importedPath}`) - ); + return yield* Effect.fail(failure(`Invalid worker dependency ${importedPath}`)); } const versions = [...packages.values()].flatMap(({ manifest }) => { const version = manifest.dependencies?.[name]; - return version !== undefined && !version.startsWith('workspace:') - ? [version] - : []; + return version !== undefined && !version.startsWith('workspace:') ? [version] : []; }); const uniqueVersions = [...new Set(versions)]; if (uniqueVersions.length !== 1) { - return yield* Effect.fail( - failure( - `Worker dependency ${name} must have one declared production version` - ) - ); + return yield* Effect.fail(failure(`Worker dependency ${name} must have one declared production version`)); } const [version] = uniqueVersions; if (version === undefined) { - return yield* Effect.fail( - failure(`Worker dependency ${name} has no version`) - ); + return yield* Effect.fail(failure(`Worker dependency ${name} has no version`)); } dependencies[name] = version; return null; @@ -196,68 +164,43 @@ const collectProductionDependency = (importedPath, packages, dependencies) => * Bundle owner + Core code; retain exact production dependencies, never workspace links. * @param {MaterializeOptions} options Materialization identity and paths. */ -const materializeOutboxWorkerEffect = ({ - appId, - packageDir, - packageName, - runtimeDir, - workspaceRoot, -}) => +const materializeOutboxWorkerEffect = ({ appId, packageDir, packageName, runtimeDir, workspaceRoot }) => Effect.gen(function* materializeWorker() { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const topologySource = yield* fs.readFileString( - path.join(workspaceRoot, 'topology/reference-topology.json') - ); - const topology = - yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); - const vertical = topology.verticals.find( - (candidate) => candidate.id === appId - ); - if ( - vertical === undefined || - vertical.package !== packageName || - vertical.path !== packageDir - ) { - return yield* Effect.fail( - failure('Worker identity must match its topology owner') - ); + const topologySource = yield* fs.readFileString(path.join(workspaceRoot, 'topology/reference-topology.json')); + const topology = yield* Schema.decodeUnknownEffect(TopologySchema)(topologySource); + const vertical = topology.verticals.find((candidate) => candidate.id === appId); + if (vertical === undefined || vertical.package !== packageName || vertical.path !== packageDir) { + return yield* Effect.fail(failure('Worker identity must match its topology owner')); } const delivery = yield* outboxWorkerDelivery(workspaceRoot, vertical).pipe( - Effect.mapError(() => - failure(`Invalid generated worker delivery for ${appId}`) - ) + Effect.mapError(() => failure(`Invalid generated worker delivery for ${appId}`)), ); if (delivery === undefined) { - return yield* Effect.fail( - failure(`${appId} has no generated Outbox Worker host`) - ); + return yield* Effect.fail(failure(`${appId} has no generated Outbox Worker host`)); } /** @type {Record} */ const dependencies = {}; /** @type {Map }>} */ const packages = new Map(); - const collectWorkspacePackages = Effect.gen( - function* collectWorkspacePackagesEffect() { - for (const directory of ['packages', 'apps', 'verticals']) { - const parent = path.join(workspaceRoot, directory); - if (!(yield* fs.exists(parent))) { + const collectWorkspacePackages = Effect.gen(function* collectWorkspacePackagesEffect() { + for (const directory of ['packages', 'apps', 'verticals']) { + const parent = path.join(workspaceRoot, directory); + if (!(yield* fs.exists(parent))) { + continue; + } + for (const entry of yield* fs.readDirectory(parent)) { + const manifestPath = path.join(parent, entry, 'package.json'); + if (!(yield* fs.exists(manifestPath))) { continue; } - for (const entry of yield* fs.readDirectory(parent)) { - const manifestPath = path.join(parent, entry, 'package.json'); - if (!(yield* fs.exists(manifestPath))) { - continue; - } - const manifestSource = yield* fs.readFileString(manifestPath); - const manifest = yield* Schema.decodeUnknownEffect( - PackageManifestSchema - )(manifestSource); - packages.set(manifest.name, { manifest }); - } + const manifestSource = yield* fs.readFileString(manifestPath); + const manifest = yield* Schema.decodeUnknownEffect(PackageManifestSchema)(manifestSource); + packages.set(manifest.name, { manifest }); } } - ); + }); yield* collectWorkspacePackages; const result = yield* Effect.tryPromise({ catch: () => failure(`Unable to bundle the ${appId} Outbox Worker`), @@ -288,24 +231,19 @@ const materializeOutboxWorkerEffect = ({ }); const { metafile } = result; const externalImports = Object.values(metafile.outputs).flatMap((output) => - output.imports.filter((item) => item.external === true) + output.imports.filter((item) => item.external === true), ); for (const imported of externalImports) { yield* collectProductionDependency(imported.path, packages, dependencies); } yield* fs.copyFile( path.join(workspaceRoot, 'topology/reference-topology.json'), - path.join(runtimeDir, 'topology.json') - ); - const sourceRevision = yield* Config.option( - Config.string('ULTRAMODERN_SOURCE_REVISION') + path.join(runtimeDir, 'topology.json'), ); + const sourceRevision = yield* Config.option(Config.string('ULTRAMODERN_SOURCE_REVISION')); const artifactAppId = yield* Schema.decodeUnknownEffect(AppIdSchema)(appId); - const artifactServiceId = yield* Schema.decodeUnknownEffect( - ServiceIdSchema - )(delivery.id); - const { inputs: sourceInputMetadata } = - yield* Schema.decodeUnknownEffect(MetafileInputsSchema)(metafile); + const artifactServiceId = yield* Schema.decodeUnknownEffect(ServiceIdSchema)(delivery.id); + const { inputs: sourceInputMetadata } = yield* Schema.decodeUnknownEffect(MetafileInputsSchema)(metafile); /** @type {string[]} */ const sourceInputs = []; for (const sourceInput in sourceInputMetadata) { @@ -322,10 +260,7 @@ const materializeOutboxWorkerEffect = ({ sourceInputs, sourceRevision, }); - yield* fs.writeFileString( - path.join(runtimeDir, 'worker-artifact.json'), - `${artifactSource}\n` - ); + yield* fs.writeFileString(path.join(runtimeDir, 'worker-artifact.json'), `${artifactSource}\n`); return { dependencies, name: `${delivery.id}-runtime`, @@ -345,5 +280,4 @@ const materializeOutboxWorkerEffect = ({ * type: string, * }>} Materialized runtime package manifest. */ -export const materializeOutboxWorker = (options) => - nodeRuntime.runPromise(materializeOutboxWorkerEffect(options)); +export const materializeOutboxWorker = (options) => nodeRuntime.runPromise(materializeOutboxWorkerEffect(options)); diff --git a/app/scripts/materialize-zerops-runtime.mjs b/app/scripts/materialize-zerops-runtime.mjs index 56a3b2b8d..ea9b7cd00 100644 --- a/app/scripts/materialize-zerops-runtime.mjs +++ b/app/scripts/materialize-zerops-runtime.mjs @@ -2,25 +2,14 @@ /// import { NodeServices } from '@effect/platform-node'; -import { - Config, - Effect, - FileSystem, - Layer, - Path, - Predicate, - Schema, -} from 'effect'; +import { Config, Effect, FileSystem, Layer, Path, Predicate, Schema } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; const packageJsonFile = 'package.json'; const workspacePackageDirectories = ['packages', 'apps', 'verticals']; const DependencyMapSchema = Schema.Record(Schema.String, Schema.String); -const PlatformFieldSchema = Schema.Union([ - Schema.String, - Schema.Array(Schema.String), -]); +const PlatformFieldSchema = Schema.Union([Schema.String, Schema.Array(Schema.String)]); const RuntimePackageSchema = Schema.Struct({ cpu: Schema.optional(PlatformFieldSchema), dependencies: Schema.optional(DependencyMapSchema), @@ -38,20 +27,18 @@ const CompactConfigSchema = Schema.Struct({ aliasPackageNamePrefix: Schema.optional(Schema.String), aliasScope: Schema.optional(Schema.String), modernPackageVersion: Schema.optional(Schema.String), - }) + }), ), }); const decodeRuntimePackage = Schema.decodeUnknownEffect(RuntimePackageSchema, { onExcessProperty: 'preserve', }); -const decodeRuntimePackageJson = Schema.decodeUnknownEffect( - Schema.fromJsonString(RuntimePackageSchema), - { onExcessProperty: 'preserve' } -); -const decodeCompactConfigJson = Schema.decodeUnknownEffect( - Schema.fromJsonString(CompactConfigSchema), - { onExcessProperty: 'preserve' } -); +const decodeRuntimePackageJson = Schema.decodeUnknownEffect(Schema.fromJsonString(RuntimePackageSchema), { + onExcessProperty: 'preserve', +}); +const decodeCompactConfigJson = Schema.decodeUnknownEffect(Schema.fromJsonString(CompactConfigSchema), { + onExcessProperty: 'preserve', +}); /** @typedef {typeof Schema.Json.Type} JsonValue */ /** @type {import('effect/Schema').Codec} */ const JsonValueSchema = Schema.suspend(() => @@ -62,13 +49,11 @@ const JsonValueSchema = Schema.suspend(() => Schema.String, Schema.Array(JsonValueSchema), Schema.Record(Schema.String, JsonValueSchema), - ]) + ]), ); const JsonRecordSchema = Schema.Record(Schema.String, JsonValueSchema); const decodeDependencyMap = Schema.decodeUnknownEffect(DependencyMapSchema); -const encodeJson = Schema.encodeEffect( - Schema.fromJsonString(JsonValueSchema, { space: 2 }) -); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(JsonValueSchema, { space: 2 })); const isJsonRecord = Schema.is(JsonRecordSchema); /** @typedef {typeof RuntimePackageSchema.Type} RuntimePackage */ @@ -91,10 +76,7 @@ const fail = (message) => Effect.fail(new MaterializationError(message)); * @param {import('effect/Path').Path} pathService - Path service. */ const assertRelativePath = (label, candidate, pathService) => { - if ( - pathService.isAbsolute(candidate) || - candidate.split(/[\\/]/u).includes('..') - ) { + if (pathService.isAbsolute(candidate) || candidate.split(/[\\/]/u).includes('..')) { return fail(`${label} must be a workspace-relative path`); } return Effect.void; @@ -106,12 +88,7 @@ const assertRelativePath = (label, candidate, pathService) => { * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const assertInsideWorkspace = ( - label, - targetPath, - workspaceRoot, - pathService -) => { +const assertInsideWorkspace = (label, targetPath, workspaceRoot, pathService) => { const relativePath = pathService.relative(workspaceRoot, targetPath); if (relativePath.startsWith('..') || pathService.isAbsolute(relativePath)) { return fail(`${label} resolved outside the workspace`); @@ -163,30 +140,21 @@ const writeJson = (filePath, json) => * @param {string} aliasPrefix - Generated package alias prefix. * @param {string} modernPackageVersion - Modern.js package version. */ -const normalizeDependencySection = ( - dependencies, - aliasPrefix, - modernPackageVersion -) => { +const normalizeDependencySection = (dependencies, aliasPrefix, modernPackageVersion) => { if (dependencies === undefined) { return null; } return Object.fromEntries( - Object.entries(dependencies).flatMap( - ([dependencyName, dependencyVersion]) => { - if (!dependencyName.startsWith(aliasPrefix)) { - return [[dependencyName, dependencyVersion]]; - } - const officialPackageName = `@modern-js/${dependencyName.slice(aliasPrefix.length)}`; - return [ - [dependencyName, modernPackageVersion], - [ - officialPackageName, - `npm:${dependencyName}@${modernPackageVersion}`, - ], - ]; + Object.entries(dependencies).flatMap(([dependencyName, dependencyVersion]) => { + if (!dependencyName.startsWith(aliasPrefix)) { + return [[dependencyName, dependencyVersion]]; } - ) + const officialPackageName = `@modern-js/${dependencyName.slice(aliasPrefix.length)}`; + return [ + [dependencyName, modernPackageVersion], + [officialPackageName, `npm:${dependencyName}@${modernPackageVersion}`], + ]; + }), ); }; @@ -195,38 +163,24 @@ const normalizeDependencySection = ( * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const normalizeRuntimePackageDependencies = ( - runtimeManifest, - workspaceRoot, - pathService -) => +const normalizeRuntimePackageDependencies = (runtimeManifest, workspaceRoot, pathService) => Effect.gen(function* normalizeRuntimePackageDependenciesEffect() { const compactConfig = yield* readOptionalCompactConfig( - pathService.join(workspaceRoot, '.modernjs/ultramodern.json') + pathService.join(workspaceRoot, '.modernjs/ultramodern.json'), ); - const modernPackageVersion = - compactConfig?.packageSource?.modernPackageVersion; + const modernPackageVersion = compactConfig?.packageSource?.modernPackageVersion; const aliasScope = compactConfig?.packageSource?.aliasScope; - const aliasPackageNamePrefix = - compactConfig?.packageSource?.aliasPackageNamePrefix; - if ( - modernPackageVersion === undefined || - aliasScope === undefined || - aliasPackageNamePrefix === undefined - ) { + const aliasPackageNamePrefix = compactConfig?.packageSource?.aliasPackageNamePrefix; + if (modernPackageVersion === undefined || aliasScope === undefined || aliasPackageNamePrefix === undefined) { return runtimeManifest; } const aliasPrefix = `@${aliasScope}/${aliasPackageNamePrefix}`; - const dependencies = normalizeDependencySection( - runtimeManifest.dependencies, - aliasPrefix, - modernPackageVersion - ); + const dependencies = normalizeDependencySection(runtimeManifest.dependencies, aliasPrefix, modernPackageVersion); const optionalDependencies = normalizeDependencySection( runtimeManifest.optionalDependencies, aliasPrefix, - modernPackageVersion + modernPackageVersion, ); const normalizedManifest = { ...runtimeManifest }; if (dependencies !== null) { @@ -254,11 +208,7 @@ const platformFieldAllows = (field, currentValue) => { return false; } const allowed = values.filter((item) => !item.startsWith('!')); - return ( - allowed.length === 0 || - allowed.includes(currentValue) || - allowed.includes('any') - ); + return allowed.length === 0 || allowed.includes(currentValue) || allowed.includes('any'); }; /** @param {RuntimePackage} dependencyManifest - Installed dependency manifest. */ @@ -273,36 +223,17 @@ const isCurrentPlatformSupported = (dependencyManifest) => * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const readInstalledDependencyPackage = ( - dependencyName, - dependencyVersion, - appRoot, - workspaceRoot, - pathService -) => +const readInstalledDependencyPackage = (dependencyName, dependencyVersion, appRoot, workspaceRoot, pathService) => Effect.gen(function* readInstalledDependencyPackageEffect() { const fileSystem = yield* FileSystem.FileSystem; const dependencySegments = dependencyName.split('/'); const directCandidates = [ - pathService.join( - appRoot, - 'node_modules', - ...dependencySegments, - packageJsonFile - ), - pathService.join( - workspaceRoot, - 'node_modules', - ...dependencySegments, - packageJsonFile - ), + pathService.join(appRoot, 'node_modules', ...dependencySegments, packageJsonFile), + pathService.join(workspaceRoot, 'node_modules', ...dependencySegments, packageJsonFile), ]; for (const candidate of directCandidates) { const dependencyManifest = yield* readOptionalRuntimePackage(candidate); - if ( - dependencyManifest?.name === dependencyName && - dependencyManifest.version === dependencyVersion - ) { + if (dependencyManifest?.name === dependencyName && dependencyManifest.version === dependencyVersion) { return dependencyManifest; } } @@ -313,21 +244,16 @@ const readInstalledDependencyPackage = ( } const encodedName = dependencyName.replaceAll('/', '+'); const storeEntries = yield* fileSystem.readDirectory(virtualStore); - for (const storeEntry of storeEntries.filter((item) => - item.startsWith(`${encodedName}@`) - )) { + for (const storeEntry of storeEntries.filter((item) => item.startsWith(`${encodedName}@`))) { const candidate = pathService.join( virtualStore, storeEntry, 'node_modules', ...dependencySegments, - packageJsonFile + packageJsonFile, ); const dependencyManifest = yield* readOptionalRuntimePackage(candidate); - if ( - dependencyManifest?.name === dependencyName && - dependencyManifest.version === dependencyVersion - ) { + if (dependencyManifest?.name === dependencyName && dependencyManifest.version === dependencyVersion) { return dependencyManifest; } } @@ -340,12 +266,7 @@ const readInstalledDependencyPackage = ( * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const removeIncompatibleDependencySection = ( - dependencies, - appRoot, - workspaceRoot, - pathService -) => { +const removeIncompatibleDependencySection = (dependencies, appRoot, workspaceRoot, pathService) => { if (dependencies === undefined) { return Effect.succeed(null); } @@ -358,24 +279,18 @@ const removeIncompatibleDependencySection = ( dependencyVersion, appRoot, workspaceRoot, - pathService + pathService, ); - const compatible = - dependencyManifest === null || - isCurrentPlatformSupported(dependencyManifest); + const compatible = dependencyManifest === null || isCurrentPlatformSupported(dependencyManifest); if (!compatible) { yield* Effect.log( - `[ultramodern:zerops] excluded ${dependencyName}@${dependencyVersion} from ${process.platform}/${process.arch} runtime` + `[ultramodern:zerops] excluded ${dependencyName}@${dependencyVersion} from ${process.platform}/${process.arch} runtime`, ); } return compatible; }), - { concurrency: 'unbounded' } - ).pipe( - Effect.flatMap((entries) => - decodeDependencyMap(Object.fromEntries(entries)) - ) - ); + { concurrency: 'unbounded' }, + ).pipe(Effect.flatMap((entries) => decodeDependencyMap(Object.fromEntries(entries)))); }; /** @@ -384,28 +299,13 @@ const removeIncompatibleDependencySection = ( * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const removeIncompatiblePlatformDependencies = ( - runtimeManifest, - appRoot, - workspaceRoot, - pathService -) => +const removeIncompatiblePlatformDependencies = (runtimeManifest, appRoot, workspaceRoot, pathService) => Effect.all( [ - removeIncompatibleDependencySection( - runtimeManifest.dependencies, - appRoot, - workspaceRoot, - pathService - ), - removeIncompatibleDependencySection( - runtimeManifest.optionalDependencies, - appRoot, - workspaceRoot, - pathService - ), + removeIncompatibleDependencySection(runtimeManifest.dependencies, appRoot, workspaceRoot, pathService), + removeIncompatibleDependencySection(runtimeManifest.optionalDependencies, appRoot, workspaceRoot, pathService), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ).pipe( Effect.flatMap(([dependencies, optionalDependencies]) => { const compatibleManifest = { ...runtimeManifest }; @@ -416,7 +316,7 @@ const removeIncompatiblePlatformDependencies = ( compatibleManifest.optionalDependencies = optionalDependencies; } return decodeRuntimePackage(compatibleManifest); - }) + }), ); /** @@ -436,13 +336,11 @@ const collectPackageDirectoryEntries = (absoluteDirectory, pathService) => Effect.gen(function* collectPackageManifestEffect() { const packageDirectory = pathService.join(absoluteDirectory, item); const packageManifest = yield* readOptionalRuntimePackage( - pathService.join(packageDirectory, packageJsonFile) + pathService.join(packageDirectory, packageJsonFile), ); - return packageManifest?.name === undefined - ? null - : [packageManifest.name, packageDirectory]; + return packageManifest?.name === undefined ? null : [packageManifest.name, packageDirectory]; }), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); return packageEntries.filter((entry) => entry !== null); }); @@ -459,7 +357,7 @@ const collectWorkspacePackages = (workspaceRoot, pathService) => const absoluteDirectory = pathService.join(workspaceRoot, directory); return collectPackageDirectoryEntries(absoluteDirectory, pathService); }, - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); return new Map(packageEntries.flat()); }); @@ -476,34 +374,24 @@ const removeWorkspaceDependencies = (runtimeManifest, workspacePackages) => { } const entries = Object.entries(dependencies); return { - dependencies: Object.fromEntries( - entries.filter( - ([dependencyName]) => !workspacePackages.has(dependencyName) - ) - ), + dependencies: Object.fromEntries(entries.filter(([dependencyName]) => !workspacePackages.has(dependencyName))), localDependencies: entries .filter(([dependencyName]) => workspacePackages.has(dependencyName)) .map(([dependencyName]) => dependencyName), }; }; const runtimeDependencies = filterSection(runtimeManifest.dependencies); - const runtimeOptionalDependencies = filterSection( - runtimeManifest.optionalDependencies - ); + const runtimeOptionalDependencies = filterSection(runtimeManifest.optionalDependencies); const installPackage = structuredClone(runtimeManifest); if (runtimeDependencies.dependencies !== null) { installPackage.dependencies = runtimeDependencies.dependencies; } if (runtimeOptionalDependencies.dependencies !== null) { - installPackage.optionalDependencies = - runtimeOptionalDependencies.dependencies; + installPackage.optionalDependencies = runtimeOptionalDependencies.dependencies; } return { installPackage, - localDependencies: [ - ...runtimeDependencies.localDependencies, - ...runtimeOptionalDependencies.localDependencies, - ], + localDependencies: [...runtimeDependencies.localDependencies, ...runtimeOptionalDependencies.localDependencies], }; }; @@ -513,11 +401,7 @@ const removeWorkspaceDependencies = (runtimeManifest, workspacePackages) => { * @param {import('effect/Path').Path} pathService - Path service. * @returns {import('effect/Effect').Effect} Copy effect. */ -const copyDirectoryWithoutNodeModules = ( - sourceDirectory, - targetDirectory, - pathService -) => +const copyDirectoryWithoutNodeModules = (sourceDirectory, targetDirectory, pathService) => Effect.gen(function* copyDirectoryWithoutNodeModulesEffect() { const fileSystem = yield* FileSystem.FileSystem; yield* fileSystem.makeDirectory(targetDirectory, { recursive: true }); @@ -532,16 +416,12 @@ const copyDirectoryWithoutNodeModules = ( .pipe( Effect.flatMap((info) => info.type === 'Directory' - ? copyDirectoryWithoutNodeModules( - sourcePath, - targetPath, - pathService - ) - : fileSystem.copy(sourcePath, targetPath, { overwrite: true }) - ) + ? copyDirectoryWithoutNodeModules(sourcePath, targetPath, pathService) + : fileSystem.copy(sourcePath, targetPath, { overwrite: true }), + ), ); }, - { concurrency: 'unbounded', discard: true } + { concurrency: 'unbounded', discard: true }, ); }); @@ -559,10 +439,7 @@ const rewriteTsExports = (exportTarget) => { if (isJsonRecord(exportTarget)) { /** @type {Readonly>} */ const rewritten = Object.fromEntries( - Object.entries(exportTarget).map(([key, entry]) => [ - key, - rewriteTsExports(entry), - ]) + Object.entries(exportTarget).map(([key, entry]) => [key, rewriteTsExports(entry)]), ); return rewritten; } @@ -583,18 +460,13 @@ const listTsFiles = (directory) => /** @param {string} parameters - TypeScript parameter source. */ const stripParameterTypes = (parameters) => - parameters.replaceAll( - /(?[A-Za-z_$][\w$]*)\??:\s*[^,]+/gu, - '$' - ); + parameters.replaceAll(/(?[A-Za-z_$][\w$]*)\??:\s*[^,]+/gu, '$'); /** @param {string} _match - Full match. @param {string} parameters - Parameter source. */ -const rewriteArrowParameters = (_match, parameters) => - `(${stripParameterTypes(parameters)}) =>`; +const rewriteArrowParameters = (_match, parameters) => `(${stripParameterTypes(parameters)}) =>`; /** @param {string} _match - Full match. @param {string} name - Function name. @param {string} parameters - Parameter source. */ -const rewriteFunctionParameters = (_match, name, parameters) => - `function${name}(${stripParameterTypes(parameters)})`; +const rewriteFunctionParameters = (_match, name, parameters) => `function${name}(${stripParameterTypes(parameters)})`; /** @param {string} source - TypeScript source. */ const transpileGeneratedPackageTs = (source) => @@ -605,17 +477,11 @@ const transpileGeneratedPackageTs = (source) => .replaceAll(/^\s*interface\s+\w+\s*\{[^}]*\}\s*$/gmsu, '') .replaceAll( /\b(?const|let|var)\s+(?[A-Za-z_$][\w$]*)\s*:\s*[^=]+=/gu, - '$ $ =' - ) - .replaceAll( - /\((?[^)]*)\)\s*:\s*[^=]+=>/gu, - rewriteArrowParameters + '$ $ =', ) + .replaceAll(/\((?[^)]*)\)\s*:\s*[^=]+=>/gu, rewriteArrowParameters) .replaceAll(/\((?[^)]*)\)\s*=>/gu, rewriteArrowParameters) - .replaceAll( - /function(?\s+\w+\s*)\((?[^)]*)\)/gu, - rewriteFunctionParameters - ) + .replaceAll(/function(?\s+\w+\s*)\((?[^)]*)\)/gu, rewriteFunctionParameters) .replaceAll(/\s+as\s+const\b/gu, '') .replaceAll(/\s+satisfies\s+[A-Za-z_$][\w$]*(?:<[^>]+>)?/gu, ''); @@ -624,19 +490,14 @@ const makeWorkspacePackageRuntimeSafe = (packageDirectory) => Effect.gen(function* makeWorkspacePackageRuntimeSafeEffect() { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; - const packageManifestPath = pathService.join( - packageDirectory, - packageJsonFile - ); - const packageManifest = - yield* readOptionalRuntimePackage(packageManifestPath); + const packageManifestPath = pathService.join(packageDirectory, packageJsonFile); + const packageManifest = yield* readOptionalRuntimePackage(packageManifestPath); if (packageManifest !== null) { const runtimeSafePackage = { ...packageManifest }; if (packageManifest.exports !== undefined) { runtimeSafePackage.exports = rewriteTsExports(packageManifest.exports); } - const runtimeSafeManifest = - yield* decodeRuntimePackage(runtimeSafePackage); + const runtimeSafeManifest = yield* decodeRuntimePackage(runtimeSafePackage); yield* writeJson(packageManifestPath, runtimeSafeManifest); } const tsFiles = yield* listTsFiles(packageDirectory); @@ -645,12 +506,9 @@ const makeWorkspacePackageRuntimeSafe = (packageDirectory) => (tsFile) => Effect.gen(function* transpileWorkspacePackageFileEffect() { const source = yield* fileSystem.readFileString(tsFile); - yield* fileSystem.writeFileString( - tsFile.replace(/\.ts$/u, '.js'), - transpileGeneratedPackageTs(source) - ); + yield* fileSystem.writeFileString(tsFile.replace(/\.ts$/u, '.js'), transpileGeneratedPackageTs(source)); }), - { concurrency: 'unbounded', discard: true } + { concurrency: 'unbounded', discard: true }, ); }); @@ -660,32 +518,19 @@ const makeWorkspacePackageRuntimeSafe = (packageDirectory) => * @param {string} runtimeDir - Runtime directory. * @param {import('effect/Path').Path} pathService - Path service. */ -const copyWorkspacePackage = ( - workspacePackageName, - workspacePackages, - runtimeDir, - pathService -) => { +const copyWorkspacePackage = (workspacePackageName, workspacePackages, runtimeDir, pathService) => { const sourceDirectory = workspacePackages.get(workspacePackageName); if (sourceDirectory === undefined) { return Effect.void; } return Effect.gen(function* copyWorkspacePackageEffect() { const fileSystem = yield* FileSystem.FileSystem; - const targetDirectory = pathService.join( - runtimeDir, - 'node_modules', - ...workspacePackageName.split('/') - ); + const targetDirectory = pathService.join(runtimeDir, 'node_modules', ...workspacePackageName.split('/')); yield* fileSystem.remove(targetDirectory, { force: true, recursive: true }); yield* fileSystem.makeDirectory(pathService.dirname(targetDirectory), { recursive: true, }); - yield* copyDirectoryWithoutNodeModules( - sourceDirectory, - targetDirectory, - pathService - ); + yield* copyDirectoryWithoutNodeModules(sourceDirectory, targetDirectory, pathService); yield* makeWorkspacePackageRuntimeSafe(targetDirectory); }); }; @@ -697,46 +542,25 @@ const copyWorkspacePackage = ( * @param {string} workspaceRoot - Workspace root. * @param {import('effect/Path').Path} pathService - Path service. */ -const installRuntimeDependencies = ( - runtimeManifest, - appId, - runtimeDir, - workspaceRoot, - pathService -) => +const installRuntimeDependencies = (runtimeManifest, appId, runtimeDir, workspaceRoot, pathService) => Effect.gen(function* installRuntimeDependenciesEffect() { const fileSystem = yield* FileSystem.FileSystem; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const installDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: `ultramodern-zerops-${appId}-`, }); - const workspacePackages = yield* collectWorkspacePackages( - workspaceRoot, - pathService - ); - const { installPackage, localDependencies } = removeWorkspaceDependencies( - runtimeManifest, - workspacePackages - ); - yield* writeJson( - pathService.join(installDir, packageJsonFile), - installPackage - ); + const workspacePackages = yield* collectWorkspacePackages(workspaceRoot, pathService); + const { installPackage, localDependencies } = removeWorkspaceDependencies(runtimeManifest, workspacePackages); + yield* writeJson(pathService.join(installDir, packageJsonFile), installPackage); const installCommand = ChildProcess.make( process.platform === 'win32' ? 'npm.cmd' : 'npm', - [ - 'install', - '--omit=dev', - '--no-audit', - '--fund=false', - '--legacy-peer-deps', - ], + ['install', '--omit=dev', '--no-audit', '--fund=false', '--legacy-peer-deps'], { cwd: installDir, stderr: 'inherit', stdin: 'inherit', stdout: 'inherit', - } + }, ); const installExitCode = yield* childProcessSpawner.exitCode(installCommand); if (installExitCode !== 0) { @@ -748,21 +572,12 @@ const installRuntimeDependencies = ( }); const installedModules = pathService.join(installDir, 'node_modules'); if (yield* fileSystem.exists(installedModules)) { - yield* fileSystem.copy( - installedModules, - pathService.join(runtimeDir, 'node_modules') - ); + yield* fileSystem.copy(installedModules, pathService.join(runtimeDir, 'node_modules')); } yield* Effect.forEach( localDependencies, - (dependency) => - copyWorkspacePackage( - dependency, - workspacePackages, - runtimeDir, - pathService - ), - { discard: true } + (dependency) => copyWorkspacePackage(dependency, workspacePackages, runtimeDir, pathService), + { discard: true }, ); }); @@ -779,73 +594,50 @@ const materializeCommand = Command.make( const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const workspaceRoot = pathService.resolve( - yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( - Config.withDefault(process.cwd()) - ) + yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe(Config.withDefault(process.cwd())), ); yield* assertRelativePath('--package-dir', packageDir, pathService); const appRoot = pathService.resolve(workspaceRoot, packageDir); const appOutputDir = pathService.join(appRoot, '.output'); - const runtimeDir = pathService.join( - workspaceRoot, - '.zerops/runtime', - worker ? `${appId}-worker` : appId - ); + const runtimeDir = pathService.join(workspaceRoot, '.zerops/runtime', worker ? `${appId}-worker` : appId); yield* Effect.all( [ - assertInsideWorkspace( - 'package directory', - appRoot, - workspaceRoot, - pathService - ), - assertInsideWorkspace( - 'runtime directory', - runtimeDir, - workspaceRoot, - pathService - ), + assertInsideWorkspace('package directory', appRoot, workspaceRoot, pathService), + assertInsideWorkspace('runtime directory', runtimeDir, workspaceRoot, pathService), ], - { discard: true } + { discard: true }, ); - const appPackage = yield* readRuntimePackage( - pathService.join(appRoot, packageJsonFile) - ); - const prepareRuntimeDirectory = Effect.gen( - function* prepareRuntimeDirectoryEffect() { - if (appPackage.name !== packageName) { - yield* fail(`--package must match ${packageDir}/package.json name`); - } - if (!worker && !(yield* fileSystem.exists(appOutputDir))) { - yield* fail( - `Modern.js package build must produce ${pathService.relative(workspaceRoot, appOutputDir)} before runtime materialization` - ); - } - - yield* fileSystem.remove(runtimeDir, { - force: true, - recursive: true, - }); - yield* fileSystem.makeDirectory(pathService.dirname(runtimeDir), { - recursive: true, - }); - yield* worker - ? fileSystem.makeDirectory(runtimeDir, { recursive: true }) - : fileSystem.copy(appOutputDir, runtimeDir); - const entryPath = pathService.join(runtimeDir, 'index.js'); - if (!worker && !(yield* fileSystem.exists(entryPath))) { - yield* fail( - `Modern.js Node deploy output is missing ${pathService.relative(workspaceRoot, entryPath)}` - ); - } + const appPackage = yield* readRuntimePackage(pathService.join(appRoot, packageJsonFile)); + const prepareRuntimeDirectory = Effect.gen(function* prepareRuntimeDirectoryEffect() { + if (appPackage.name !== packageName) { + yield* fail(`--package must match ${packageDir}/package.json name`); } - ); + if (!worker && !(yield* fileSystem.exists(appOutputDir))) { + yield* fail( + `Modern.js package build must produce ${pathService.relative(workspaceRoot, appOutputDir)} before runtime materialization`, + ); + } + + yield* fileSystem.remove(runtimeDir, { + force: true, + recursive: true, + }); + yield* fileSystem.makeDirectory(pathService.dirname(runtimeDir), { + recursive: true, + }); + yield* worker + ? fileSystem.makeDirectory(runtimeDir, { recursive: true }) + : fileSystem.copy(appOutputDir, runtimeDir); + const entryPath = pathService.join(runtimeDir, 'index.js'); + if (!worker && !(yield* fileSystem.exists(entryPath))) { + yield* fail(`Modern.js Node deploy output is missing ${pathService.relative(workspaceRoot, entryPath)}`); + } + }); yield* prepareRuntimeDirectory; const packageJsonPath = pathService.join(runtimeDir, packageJsonFile); /** @type {RuntimePackage} */ - let runtimePackage = - (yield* readOptionalRuntimePackage(packageJsonPath)) ?? {}; + let runtimePackage = (yield* readOptionalRuntimePackage(packageJsonPath)) ?? {}; if (worker) { const outboxWorkerModule = yield* Effect.tryPromise({ catch: (cause) => new MaterializationError(String(cause)), @@ -863,16 +655,12 @@ const materializeCommand = Command.make( }), }).pipe(Effect.flatMap(decodeRuntimePackage)); } - runtimePackage = yield* normalizeRuntimePackageDependencies( - runtimePackage, - workspaceRoot, - pathService - ); + runtimePackage = yield* normalizeRuntimePackageDependencies(runtimePackage, workspaceRoot, pathService); runtimePackage = yield* removeIncompatiblePlatformDependencies( runtimePackage, appRoot, workspaceRoot, - pathService + pathService, ); runtimePackage = yield* decodeRuntimePackage({ ...runtimePackage, @@ -884,25 +672,14 @@ const materializeCommand = Command.make( }, }); yield* writeJson(packageJsonPath, runtimePackage); - yield* installRuntimeDependencies( - runtimePackage, - appId, - runtimeDir, - workspaceRoot, - pathService - ); + yield* installRuntimeDependencies(runtimePackage, appId, runtimeDir, workspaceRoot, pathService); yield* Effect.log( - `[ultramodern:zerops] materialized ${appId} runtime at ${pathService.relative( - workspaceRoot, - runtimeDir - )}` + `[ultramodern:zerops] materialized ${appId} runtime at ${pathService.relative(workspaceRoot, runtimeDir)}`, ); - }) + }), ); const program = Command.run(materializeCommand, { version: '1.0.0' }); -const executableLayer = Layer.effectDiscard(program).pipe( - Layer.provide(NodeServices.layer) -); +const executableLayer = Layer.effectDiscard(program).pipe(Layer.provide(NodeServices.layer)); await Effect.runPromise(Effect.scoped(Layer.build(executableLayer))); diff --git a/app/scripts/microvertical-api-baseline-boundary.mts b/app/scripts/microvertical-api-baseline-boundary.mts index 05eac848d..aad300996 100644 --- a/app/scripts/microvertical-api-baseline-boundary.mts +++ b/app/scripts/microvertical-api-baseline-boundary.mts @@ -29,9 +29,7 @@ import type { import { API as TypeScriptApi } from '@typescript/native/unstable/sync'; const camelCaseStem = (stem: string): string => - stem.replaceAll(/-(?[a-z0-9])/gu, (_match, letter: string) => - letter.toUpperCase() - ); + stem.replaceAll(/-(?[a-z0-9])/gu, (_match, letter: string) => letter.toUpperCase()); const pascalCaseStem = (stem: string): string => { const camelStem = camelCaseStem(stem); @@ -42,12 +40,7 @@ const identifierName = (node: Node | undefined): string | undefined => node !== undefined && isIdentifier(node) ? node.text : undefined; const propertyName = (node: Node | undefined): string | undefined => { - if ( - node !== undefined && - (isIdentifier(node) || - isStringLiteralLikeNode(node) || - isNumericLiteral(node)) - ) { + if (node !== undefined && (isIdentifier(node) || isStringLiteralLikeNode(node) || isNumericLiteral(node))) { return node.text; } return undefined; @@ -69,19 +62,13 @@ const isAccessPath = (node: Expression, expected: readonly string[]): boolean => const unwrapExpression = (expression: Expression): Expression => { let current = expression; - while ( - isAsExpression(current) || - isParenthesizedExpression(current) || - isSatisfiesExpression(current) - ) { + while (isAsExpression(current) || isParenthesizedExpression(current) || isSatisfiesExpression(current)) { current = current.expression; } return current; }; -const stringLiteral = ( - expression: Expression | undefined -): string | undefined => { +const stringLiteral = (expression: Expression | undefined): string | undefined => { if (expression === undefined) { return undefined; } @@ -89,9 +76,7 @@ const stringLiteral = ( return isStringLiteralLikeNode(unwrapped) ? unwrapped.text : undefined; }; -const numericLiteral = ( - expression: Expression | undefined -): number | undefined => { +const numericLiteral = (expression: Expression | undefined): number | undefined => { if (expression === undefined) { return undefined; } @@ -99,38 +84,25 @@ const numericLiteral = ( return isNumericLiteral(unwrapped) ? Number(unwrapped.text) : undefined; }; -const callExpression = ( - expression: Expression | undefined, - callee: readonly string[] -): CallExpression | undefined => { +const callExpression = (expression: Expression | undefined, callee: readonly string[]): CallExpression | undefined => { if (expression === undefined) { return undefined; } const unwrapped = unwrapExpression(expression); - return isCallExpression(unwrapped) && - isAccessPath(unwrapped.expression, callee) - ? unwrapped - : undefined; + return isCallExpression(unwrapped) && isAccessPath(unwrapped.expression, callee) ? unwrapped : undefined; }; -const exportedConst = ( - sourceFile: SourceFile, - name: string -): VariableDeclaration | undefined => { +const exportedConst = (sourceFile: SourceFile, name: string): VariableDeclaration | undefined => { for (const statement of sourceFile.statements) { if ( !isVariableStatement(statement) || - !( - statement.modifiers?.some( - (modifier) => modifier.kind === SyntaxKind.ExportKeyword - ) ?? false - ) || + !(statement.modifiers?.some((modifier) => modifier.kind === SyntaxKind.ExportKeyword) ?? false) || statement.declarationList.flags !== NodeFlags.Const ) { continue; } const declarations = statement.declarationList.declarations.filter( - (declaration) => identifierName(declaration.name) === name + (declaration) => identifierName(declaration.name) === name, ); if (declarations.length === 1) { return declarations[0]; @@ -139,19 +111,13 @@ const exportedConst = ( return undefined; }; -const localConst = ( - sourceFile: SourceFile, - name: string -): VariableDeclaration | undefined => { +const localConst = (sourceFile: SourceFile, name: string): VariableDeclaration | undefined => { for (const statement of sourceFile.statements) { - if ( - !isVariableStatement(statement) || - statement.declarationList.flags !== NodeFlags.Const - ) { + if (!isVariableStatement(statement) || statement.declarationList.flags !== NodeFlags.Const) { continue; } const declarations = statement.declarationList.declarations.filter( - (declaration) => identifierName(declaration.name) === name + (declaration) => identifierName(declaration.name) === name, ); if (declarations.length === 1) { return declarations[0]; @@ -160,9 +126,7 @@ const localConst = ( return undefined; }; -const objectLiteral = ( - expression: Expression | undefined -): ObjectLiteralExpression | undefined => { +const objectLiteral = (expression: Expression | undefined): ObjectLiteralExpression | undefined => { if (expression === undefined) { return undefined; } @@ -171,7 +135,7 @@ const objectLiteral = ( }; const propertyAssignments = ( - properties: readonly ObjectLiteralElementLike[] + properties: readonly ObjectLiteralElementLike[], ): ReadonlyMap | undefined => { const assignments = new Map(); for (const property of properties) { @@ -190,7 +154,7 @@ const propertyAssignments = ( const exactCall = ( expression: Expression | undefined, callee: readonly string[], - argumentCount: number + argumentCount: number, ): CallExpression | undefined => { const call = callExpression(expression, callee); return call?.arguments.length === argumentCount ? call : undefined; @@ -204,7 +168,7 @@ interface SharedSchemaObject { const sharedSchemaObject = ( declaration: VariableDeclaration | undefined, sharedSchemaName: string, - protectedFields: readonly string[] + protectedFields: readonly string[], ): SharedSchemaObject | undefined => { if (declaration?.initializer === undefined) { return undefined; @@ -219,14 +183,10 @@ const sharedSchemaObject = ( return undefined; } const spreads = schemaObject.properties.filter(isSpreadAssignment); - const assignments = propertyAssignments( - schemaObject.properties.filter((property) => !isSpreadAssignment(property)) - ); + const assignments = propertyAssignments(schemaObject.properties.filter((property) => !isSpreadAssignment(property))); if ( spreads.length !== 1 || - !spreads.every((spread) => - isAccessPath(spread.expression, [sharedSchemaName, 'fields']) - ) || + !spreads.every((spread) => isAccessPath(spread.expression, [sharedSchemaName, 'fields'])) || assignments === undefined || protectedFields.some((field) => assignments.has(field)) ) { @@ -243,9 +203,7 @@ interface DirectCallChain { }[]; } -const directCallChain = ( - expression: Expression | undefined -): DirectCallChain | undefined => { +const directCallChain = (expression: Expression | undefined): DirectCallChain | undefined => { if (expression === undefined) { return undefined; } @@ -271,10 +229,7 @@ const directCallChain = ( return { base: current, methods }; }; -const brandedStringSchemaIsExact = ( - declaration: VariableDeclaration | undefined, - brand: string -): boolean => { +const brandedStringSchemaIsExact = (declaration: VariableDeclaration | undefined, brand: string): boolean => { const initializer = declaration?.initializer; if (initializer === undefined) { return false; @@ -293,62 +248,42 @@ const brandedStringSchemaIsExact = ( return stringLiteral(brandCall?.arguments[0]) === brand; }; -const importedRuntimeNames = ( - statement: Node, - expectedPackage: string -): readonly string[] => { - if ( - !isImportDeclaration(statement) || - stringLiteral(statement.moduleSpecifier) !== expectedPackage - ) { +const importedRuntimeNames = (statement: Node, expectedPackage: string): readonly string[] => { + if (!isImportDeclaration(statement) || stringLiteral(statement.moduleSpecifier) !== expectedPackage) { return []; } const clause = statement.importClause; const bindings = clause?.namedBindings; - if ( - clause?.phaseModifier === SyntaxKind.TypeKeyword || - bindings === undefined || - !isNamedImports(bindings) - ) { + if (clause?.phaseModifier === SyntaxKind.TypeKeyword || bindings === undefined || !isNamedImports(bindings)) { return []; } return bindings.elements - .filter( - (element) => !element.isTypeOnly && element.propertyName === undefined - ) + .filter((element) => !element.isTypeOnly && element.propertyName === undefined) .map((element) => element.name.text); }; const importsExactBindings = ( sourceFile: SourceFile, expectedPackage: string, - expectedBindings: readonly string[] + expectedBindings: readonly string[], ): boolean => { - const names = new Set( - sourceFile.statements.flatMap((statement) => - importedRuntimeNames(statement, expectedPackage) - ) - ); + const names = new Set(sourceFile.statements.flatMap((statement) => importedRuntimeNames(statement, expectedPackage))); return expectedBindings.every((name) => names.has(name)); }; -const importsSharedBaselinePrimitives = ( - sourceFile: SourceFile, - expectedPackage: string -): boolean => - [expectedPackage, `${expectedPackage}/microvertical-api-baseline`].some( - (specifier) => - importsExactBindings(sourceFile, specifier, [ - 'MicroVerticalBuildMarkerSchema', - 'MicroVerticalReadinessSchema', - 'createMicroVerticalOperationContext', - ]) +const importsSharedBaselinePrimitives = (sourceFile: SourceFile, expectedPackage: string): boolean => + [expectedPackage, `${expectedPackage}/microvertical-api-baseline`].some((specifier) => + importsExactBindings(sourceFile, specifier, [ + 'MicroVerticalBuildMarkerSchema', + 'MicroVerticalReadinessSchema', + 'createMicroVerticalOperationContext', + ]), ); const singleAddedArgument = ( expression: Expression | undefined, factory: readonly string[], - names: readonly string[] + names: readonly string[], ): Expression | undefined => { const chain = directCallChain(expression); if (chain === undefined || !isAccessPath(chain.base.expression, factory)) { @@ -370,34 +305,24 @@ const singleAddedArgument = ( const foundationIsExact = ( declaration: VariableDeclaration | undefined, stem: string, - readinessSchemaName: string + readinessSchemaName: string, ): boolean => { const group = singleAddedArgument( declaration?.initializer, ['HttpApi', 'make'], - [ - `${pascalCaseStem(stem)}FoundationApi`, - `${pascalCaseStem(stem)}ApiFoundation`, - ] - ); - const endpointExpression = singleAddedArgument( - group, - ['HttpApiGroup', 'make'], - ['foundation'] + [`${pascalCaseStem(stem)}FoundationApi`, `${pascalCaseStem(stem)}ApiFoundation`], ); + const endpointExpression = singleAddedArgument(group, ['HttpApiGroup', 'make'], ['foundation']); const endpoint = exactCall(endpointExpression, ['HttpApiEndpoint', 'get'], 3); const endpointOptions = objectLiteral(endpoint?.arguments[2]); const endpointProperties = - endpointOptions === undefined - ? undefined - : propertyAssignments(endpointOptions.properties); + endpointOptions === undefined ? undefined : propertyAssignments(endpointOptions.properties); return ( endpoint !== undefined && stringLiteral(endpoint.arguments[0]) === 'readiness' && stringLiteral(endpoint.arguments[1]) === `/${stem}/readiness` && endpointProperties?.size === 1 && - identifierName(endpointProperties.get('success')?.initializer) === - readinessSchemaName + identifierName(endpointProperties.get('success')?.initializer) === readinessSchemaName ); }; @@ -405,7 +330,7 @@ const rootComposesFoundation = ( sourceFile: SourceFile, declaration: VariableDeclaration | undefined, stem: string, - foundationName: string + foundationName: string, ): boolean => { const chain = directCallChain(declaration?.initializer); const first = chain?.methods[0]; @@ -423,7 +348,7 @@ const rootComposesFoundation = ( (index === chain.methods.length - 1 && method.name === 'pipe' && identifierName(method.arguments[0]) === 'identity' && - importsExactBindings(sourceFile, 'effect', ['identity']))) + importsExactBindings(sourceFile, 'effect', ['identity']))), ) && first?.name === 'addHttpApi' && identifierName(first.arguments[0]) === foundationName @@ -431,19 +356,13 @@ const rootComposesFoundation = ( }; const operationContextFields = (property: PropertyAssignment) => { - const constructorCall = exactCall( - property.initializer, - ['createMicroVerticalOperationContext'], - 1 - ); + const constructorCall = exactCall(property.initializer, ['createMicroVerticalOperationContext'], 1); const input = objectLiteral(constructorCall?.arguments[0]); - return input === undefined - ? undefined - : propertyAssignments(input.properties); + return input === undefined ? undefined : propertyAssignments(input.properties); }; const operationContextIdentity = ( - property: PropertyAssignment + property: PropertyAssignment, ): | { readonly method: string; @@ -455,31 +374,19 @@ const operationContextIdentity = ( const method = stringLiteral(fields?.get('method')?.initializer); const operationId = stringLiteral(fields?.get('operationId')?.initializer); const routePath = stringLiteral(fields?.get('routePath')?.initializer); - if ( - fields?.size !== 3 || - method === undefined || - operationId === undefined || - routePath === undefined - ) { + if (fields?.size !== 3 || method === undefined || operationId === undefined || routePath === undefined) { return undefined; } return { method, operationId, routePath }; }; -const operationContextIsConstructed = ( - property: PropertyAssignment, - stem: string, - propertyKey: string -): boolean => { +const operationContextIsConstructed = (property: PropertyAssignment, stem: string, propertyKey: string): boolean => { const identity = operationContextIdentity(property); if (identity === undefined) { return false; } const { method, operationId, routePath } = identity; - if ( - !/^[A-Z]+$/u.test(method) || - !/^\/(?!.*(?:^|\/)\.\.?\/)[^\s?#]*$/u.test(routePath) - ) { + if (!/^[A-Z]+$/u.test(method) || !/^\/(?!.*(?:^|\/)\.\.?\/)[^\s?#]*$/u.test(routePath)) { return false; } const apiName = `${pascalCaseStem(stem)}Api`; @@ -494,45 +401,29 @@ const operationContextIsConstructed = ( ['removeCartItem', ['POST', `/${stem}/cart/remove`]], ]).get(propertyKey); const requestedOperation = [method, routePath]; - const matchesGenerated = - generatedOperation?.every( - (value, index) => value === requestedOperation[index] - ) === true; + const matchesGenerated = generatedOperation?.every((value, index) => value === requestedOperation[index]) === true; if (propertyKey === 'readiness' && !matchesGenerated) { return false; } return ( operationId === `${apiName}:${routePath}` || - (operationId === `${apiName}:${camelCaseStem(stem)}:${propertyKey}` && - matchesGenerated) + (operationId === `${apiName}:${camelCaseStem(stem)}:${propertyKey}` && matchesGenerated) ); }; -const operationMapIsConnected = ( - declaration: VariableDeclaration | undefined, - stem: string -): boolean => { +const operationMapIsConnected = (declaration: VariableDeclaration | undefined, stem: string): boolean => { const map = objectLiteral(declaration?.initializer); - const properties = - map === undefined ? undefined : propertyAssignments(map.properties); + const properties = map === undefined ? undefined : propertyAssignments(map.properties); return ( properties !== undefined && properties.has('readiness') && - [...properties].every(([key, property]) => - operationContextIsConstructed(property, stem, key) - ) + [...properties].every(([key, property]) => operationContextIsConstructed(property, stem, key)) ); }; -const constAssertionObject = ( - declaration: VariableDeclaration | undefined -): ObjectLiteralExpression | undefined => { +const constAssertionObject = (declaration: VariableDeclaration | undefined): ObjectLiteralExpression | undefined => { const initializer = declaration?.initializer; - if ( - initializer === undefined || - !isAsExpression(initializer) || - initializer.type.getText() !== 'const' - ) { + if (initializer === undefined || !isAsExpression(initializer) || initializer.type.getText() !== 'const') { return undefined; } return objectLiteral(initializer.expression); @@ -540,11 +431,10 @@ const constAssertionObject = ( const metadataIsExact = ( declaration: VariableDeclaration | undefined, - expectation: MicroVerticalApiBaselineExpectation + expectation: MicroVerticalApiBaselineExpectation, ): boolean => { const object = constAssertionObject(declaration); - const fields = - object === undefined ? undefined : propertyAssignments(object.properties); + const fields = object === undefined ? undefined : propertyAssignments(object.properties); const expectedFields = [ ['apiPrefix', expectation.apiPrefix], ['basePath', expectation.basePath], @@ -555,93 +445,64 @@ const metadataIsExact = ( return ( fields !== undefined && fields.size === new Map(expectedFields).size && - [...expectedFields].every( - ([field, value]) => - stringLiteral(fields.get(field)?.initializer) === value - ) + [...expectedFields].every(([field, value]) => stringLiteral(fields.get(field)?.initializer) === value) ); }; -const markerSchemaIsShared = ( - sourceFile: SourceFile, - declaration: VariableDeclaration | undefined -): boolean => { - const schema = sharedSchemaObject( - declaration, - 'MicroVerticalBuildMarkerSchema', - [ - 'build', - 'buildMarker', - 'deployProfile', - 'packageName', - 'sourceRevision', - 'surface', - 'version', - ] - ); +const markerSchemaIsShared = (sourceFile: SourceFile, declaration: VariableDeclaration | undefined): boolean => { + const schema = sharedSchemaObject(declaration, 'MicroVerticalBuildMarkerSchema', [ + 'build', + 'buildMarker', + 'deployProfile', + 'packageName', + 'sourceRevision', + 'surface', + 'version', + ]); if (schema === undefined || schema.identity) { return schema?.identity === true; } const brandedField = (property: PropertyAssignment, brand: string): boolean => identifierName(property.initializer) === `${brand}Schema` && brandedStringSchemaIsExact(localConst(sourceFile, `${brand}Schema`), brand); - const validators = new Map boolean>( + const validators = new Map boolean>([ + ['appId', (property) => brandedField(property, 'AppId')], + ['unitId', (property) => brandedField(property, 'UnitId')], [ - ['appId', (property) => brandedField(property, 'AppId')], - ['unitId', (property) => brandedField(property, 'UnitId')], - [ - 'kind', - (property) => - stringLiteral( - exactCall(property.initializer, ['Schema', 'Literal'], 1) - ?.arguments[0] - ) === 'microvertical-delivery-unit', - ], - [ - 'schemaVersion', - (property) => - numericLiteral( - exactCall(property.initializer, ['Schema', 'Literal'], 1) - ?.arguments[0] - ) === 1, - ], - ] - ); - return [...schema.assignments].every( - ([field, property]) => validators.get(field)?.(property) === true - ); + 'kind', + (property) => + stringLiteral(exactCall(property.initializer, ['Schema', 'Literal'], 1)?.arguments[0]) === + 'microvertical-delivery-unit', + ], + [ + 'schemaVersion', + (property) => numericLiteral(exactCall(property.initializer, ['Schema', 'Literal'], 1)?.arguments[0]) === 1, + ], + ]); + return [...schema.assignments].every(([field, property]) => validators.get(field)?.(property) === true); }; const readinessSchemaIsShared = ( declaration: VariableDeclaration | undefined, markerSchemaName: string, - ownerMarkerIsIdentity: boolean + ownerMarkerIsIdentity: boolean, ): boolean => { - const schema = sharedSchemaObject( - declaration, - 'MicroVerticalReadinessSchema', - ['checks', 'status', 'versionSkew'] - ); + const schema = sharedSchemaObject(declaration, 'MicroVerticalReadinessSchema', ['checks', 'status', 'versionSkew']); return ( schema !== undefined && ((schema.identity && ownerMarkerIsIdentity) || (schema.assignments.size === 1 && - identifierName(schema.assignments.get('marker')?.initializer) === - markerSchemaName)) + identifierName(schema.assignments.get('marker')?.initializer) === markerSchemaName)) ); }; -const declarationIsIdentifier = ( - declaration: VariableDeclaration | undefined, - identifier: string -): boolean => - declaration?.initializer !== undefined && - identifierName(unwrapExpression(declaration.initializer)) === identifier; +const declarationIsIdentifier = (declaration: VariableDeclaration | undefined, identifier: string): boolean => + declaration?.initializer !== undefined && identifierName(unwrapExpression(declaration.initializer)) === identifier; const validateParsedContract = ( sourceFile: SourceFile, stem: string, - expectation: MicroVerticalApiBaselineExpectation + expectation: MicroVerticalApiBaselineExpectation, ): string | undefined => { const exportStem = camelCaseStem(stem); const foundationName = `${exportStem}FoundationApi`; @@ -658,12 +519,7 @@ const validateParsedContract = ( ) { return 'MicroVertical root contract must import exact Effect API primitives from the framework client package'; } - if ( - !importsSharedBaselinePrimitives( - sourceFile, - expectation.sharedContractsPackage - ) - ) { + if (!importsSharedBaselinePrimitives(sourceFile, expectation.sharedContractsPackage)) { return 'MicroVertical root contract must import exact baseline primitives from the shared contracts package'; } if (!markerSchemaIsShared(sourceFile, markerDeclaration)) { @@ -673,47 +529,21 @@ const validateParsedContract = ( !readinessSchemaIsShared( exportedConst(sourceFile, readinessSchemaName), markerSchemaName, - declarationIsIdentifier( - markerDeclaration, - 'MicroVerticalBuildMarkerSchema' - ) + declarationIsIdentifier(markerDeclaration, 'MicroVerticalBuildMarkerSchema'), ) ) { return 'MicroVertical readiness schema must consume the shared readiness schema without overriding shared fields'; } - if ( - !foundationIsExact( - exportedConst(sourceFile, foundationName), - stem, - readinessSchemaName - ) - ) { + if (!foundationIsExact(exportedConst(sourceFile, foundationName), stem, readinessSchemaName)) { return 'MicroVertical readiness foundation API must directly compose its exact readiness endpoint and foundation identity'; } - if ( - !rootComposesFoundation( - sourceFile, - exportedConst(sourceFile, `${exportStem}Api`), - stem, - foundationName - ) - ) { + if (!rootComposesFoundation(sourceFile, exportedConst(sourceFile, `${exportStem}Api`), stem, foundationName)) { return 'MicroVertical root API must explicitly compose its readiness foundation API'; } - if ( - !operationMapIsConnected( - exportedConst(sourceFile, `${exportStem}OperationContexts`), - stem - ) - ) { + if (!operationMapIsConnected(exportedConst(sourceFile, `${exportStem}OperationContexts`), stem)) { return 'MicroVertical operation map must construct every operation with the shared context constructor'; } - if ( - !metadataIsExact( - exportedConst(sourceFile, `${exportStem}ApiContract`), - expectation - ) - ) { + if (!metadataIsExact(exportedConst(sourceFile, `${exportStem}ApiContract`), expectation)) { return 'MicroVertical root contract must keep exact owner and API path metadata without forbidden fields'; } return undefined; @@ -722,7 +552,7 @@ const validateParsedContract = ( const parseAndValidate = ( stem: string, filePath: string, - expectation: MicroVerticalApiBaselineExpectation + expectation: MicroVerticalApiBaselineExpectation, ): string | undefined => { const compiler = new TypeScriptApi(); try { @@ -760,20 +590,17 @@ export interface MicroVerticalTopologyEntry { export const configuredMicroVerticalApiStem = ( verticalPath: string, - verticals: readonly MicroVerticalTopologyEntry[] + verticals: readonly MicroVerticalTopologyEntry[], ): string | undefined => { - const topologyVertical = verticals.find( - (vertical) => (vertical.path ?? `verticals/${vertical.id}`) === verticalPath - ); + const topologyVertical = verticals.find((vertical) => (vertical.path ?? `verticals/${vertical.id}`) === verticalPath); const endpoint = topologyVertical?.api?.readiness?.endpoint; - return endpoint?.match(/^\/(?[a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u) - ?.groups?.stem; + return endpoint?.match(/^\/(?[a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.groups?.stem; }; export const microVerticalApiBaselineViolation = ( stem: string, filePath: string, - expectation?: MicroVerticalApiBaselineExpectation + expectation?: MicroVerticalApiBaselineExpectation, ): string | undefined => parseAndValidate( stem, @@ -786,5 +613,5 @@ export const microVerticalApiBaselineViolation = ( ownerId: stem, readinessPath: `/${stem}-api/${stem}/readiness`, sharedContractsPackage: '@app/shared-contracts', - } + }, ); diff --git a/app/scripts/migrate-contacts-authorization.mts b/app/scripts/migrate-contacts-authorization.mts index 9f8b48e24..a3f8275ce 100644 --- a/app/scripts/migrate-contacts-authorization.mts +++ b/app/scripts/migrate-contacts-authorization.mts @@ -3,24 +3,12 @@ import { pathToFileURL } from 'node:url'; import { v1 } from '@authzed/authzed-node'; import { NodeServices } from '@effect/platform-node'; -import { - Console, - Effect, - Exit, - ManagedRuntime, - Number as EffectNumber, - Redacted, - Result, - Schema, -} from 'effect'; +import { Console, Effect, Exit, ManagedRuntime, Number as EffectNumber, Redacted, Result, Schema } from 'effect'; import { Argument, Command } from 'effect/unstable/cli'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../packages/core-runtime/src/db/config.ts'; -import { - fullyConsistent, - spiceDbClientSecurity, -} from '../packages/core-runtime/src/permissions/client.ts'; +import { fullyConsistent, spiceDbClientSecurity } from '../packages/core-runtime/src/permissions/client.ts'; import type { SpiceDbConfigValue } from '../packages/core-runtime/src/permissions/config.ts'; import { loadSpiceDbConfig } from '../packages/core-runtime/src/permissions/config.ts'; import { @@ -34,21 +22,15 @@ const MAX_CONTEXTS = 500; const MAX_PRINCIPALS = 5000; const MAX_RELATIONSHIPS_PER_CONTEXT = 100; const DENIED_PROBE_PRINCIPAL_ID = 'contacts-identity-migration-denied-probe'; -const OUTSIDE_AUTHORITATIVE_CONTEXT_MESSAGE = - 'A module-access relationship is outside the authoritative context'; +const OUTSIDE_AUTHORITATIVE_CONTEXT_MESSAGE = 'A module-access relationship is outside the authoritative context'; -const ContactsAuthorizationMigrationModeSchema = Schema.Literals([ - 'finalize', - 'prepare', - 'verify', -]); +const ContactsAuthorizationMigrationModeSchema = Schema.Literals(['finalize', 'prepare', 'verify']); -export type ContactsAuthorizationMigrationMode = - typeof ContactsAuthorizationMigrationModeSchema.Type; +export type ContactsAuthorizationMigrationMode = typeof ContactsAuthorizationMigrationModeSchema.Type; export class ContactsAuthorizationMigrationError extends Schema.TaggedError()( 'ContactsAuthorizationMigrationError', - { message: Schema.String } + { message: Schema.String }, ) {} export interface ContactsAuthorizationRelationship { @@ -59,11 +41,7 @@ export interface ContactsAuthorizationRelationship { export interface ContactsAuthorizationContextPlan { readonly deleteLegacy: boolean; - readonly state: - | 'already_finalized' - | 'already_prepared' - | 'legacy_only' - | 'unconfigured'; + readonly state: 'already_finalized' | 'already_prepared' | 'legacy_only' | 'unconfigured'; readonly touchContacts: boolean; } @@ -98,36 +76,26 @@ interface ContactsAuthorizationMigrationResult { type SpiceDbClient = ReturnType; -const migrationFailure = ( - message: string -): ContactsAuthorizationMigrationError => +const migrationFailure = (message: string): ContactsAuthorizationMigrationError => new ContactsAuthorizationMigrationError({ message }); -const relationshipKey = ( - relationship: ContactsAuthorizationRelationship -): string => +const relationshipKey = (relationship: ContactsAuthorizationRelationship): string => `${relationship.relation}\0${relationship.subjectType}\0${relationship.subjectId}`; const sameRelationshipSet = ( left: readonly ContactsAuthorizationRelationship[], - right: readonly ContactsAuthorizationRelationship[] + right: readonly ContactsAuthorizationRelationship[], ): boolean => { const leftKeys = new Set(left.map(relationshipKey)); const rightKeys = new Set(right.map(relationshipKey)); - return ( - leftKeys.size === rightKeys.size && - [...leftKeys].every((key) => rightKeys.has(key)) - ); + return leftKeys.size === rightKeys.size && [...leftKeys].every((key) => rightKeys.has(key)); }; const planContactsAuthorizationContextResult = ( mode: ContactsAuthorizationMigrationMode, legacy: readonly ContactsAuthorizationRelationship[], - contacts: readonly ContactsAuthorizationRelationship[] -): Result.Result< - ContactsAuthorizationContextPlan, - ContactsAuthorizationMigrationError -> => { + contacts: readonly ContactsAuthorizationRelationship[], +): Result.Result => { if (legacy.length === 0 && contacts.length === 0) { return Result.succeed({ deleteLegacy: false, @@ -144,11 +112,7 @@ const planContactsAuthorizationContextResult = ( } if (contacts.length === 0) { if (mode !== 'prepare') { - return Result.fail( - migrationFailure( - 'Contacts authorization is missing while legacy authorization still exists' - ) - ); + return Result.fail(migrationFailure('Contacts authorization is missing while legacy authorization still exists')); } return Result.succeed({ deleteLegacy: false, @@ -157,9 +121,7 @@ const planContactsAuthorizationContextResult = ( }); } if (!sameRelationshipSet(legacy, contacts)) { - return Result.fail( - migrationFailure('Legacy and Contacts authorization relationships differ') - ); + return Result.fail(migrationFailure('Legacy and Contacts authorization relationships differ')); } return Result.succeed({ deleteLegacy: mode === 'finalize', @@ -171,33 +133,26 @@ const planContactsAuthorizationContextResult = ( export const planContactsAuthorizationContext = ( mode: ContactsAuthorizationMigrationMode, legacy: readonly ContactsAuthorizationRelationship[], - contacts: readonly ContactsAuthorizationRelationship[] + contacts: readonly ContactsAuthorizationRelationship[], ): ContactsAuthorizationContextPlan => - Result.getOrThrow( - planContactsAuthorizationContextResult(mode, legacy, contacts) - ); + Result.getOrThrow(planContactsAuthorizationContextResult(mode, legacy, contacts)); const acquirePool = (connection: Redacted.Redacted) => Effect.acquireRelease( Effect.try({ - catch: () => - migrationFailure( - 'The authorization migration database pool could not open' - ), - try: () => - new Pool({ connectionString: Redacted.value(connection), max: 1 }), + catch: () => migrationFailure('The authorization migration database pool could not open'), + try: () => new Pool({ connectionString: Redacted.value(connection), max: 1 }), }), - (pool) => Effect.promise(async () => await pool.end()) + (pool) => Effect.promise(async () => await pool.end()), ); const loadAuthoritativeContexts = ( - connection: Redacted.Redacted + connection: Redacted.Redacted, ): Effect.Effect => Effect.gen(function* loadAuthoritativeContextsEffect() { const pool = yield* acquirePool(connection); const contextResult = yield* Effect.tryPromise({ - catch: () => - migrationFailure('The authoritative module contexts could not be read'), + catch: () => migrationFailure('The authoritative module contexts could not be read'), try: async () => await pool.query( `select @@ -210,28 +165,21 @@ const loadAuthoritativeContexts = ( where module_state.module_key in ($1, $2) order by module_state.tenant_id, legal_entity.legal_entity_id limit $3`, - [LEGACY_MODULE_ID, CONTACTS_MODULE_ID, MAX_CONTEXTS + 1] + [LEGACY_MODULE_ID, CONTACTS_MODULE_ID, MAX_CONTEXTS + 1], ), }); if (contextResult.rows.length > MAX_CONTEXTS) { - return yield* migrationFailure( - `Authorization migration exceeds the ${MAX_CONTEXTS}-context safety bound` - ); + return yield* migrationFailure(`Authorization migration exceeds the ${MAX_CONTEXTS}-context safety bound`); } if (contextResult.rows.some((row) => row.module_key === LEGACY_MODULE_ID)) { - return yield* migrationFailure( - 'Core module identity migration must complete before authorization migration' - ); + return yield* migrationFailure('Core module identity migration must complete before authorization migration'); } - const tenantIds = [ - ...new Set(contextResult.rows.map((row) => row.tenant_id)), - ]; + const tenantIds = [...new Set(contextResult.rows.map((row) => row.tenant_id))]; if (tenantIds.length === 0) { return []; } const principalResult = yield* Effect.tryPromise({ - catch: () => - migrationFailure('The authoritative Principals could not be read'), + catch: () => migrationFailure('The authoritative Principals could not be read'), try: async () => await pool.query( `select principal_id::text, status, tenant_id::text @@ -239,36 +187,28 @@ const loadAuthoritativeContexts = ( where tenant_id = any($1::uuid[]) order by tenant_id, principal_id limit $2`, - [tenantIds, MAX_PRINCIPALS + 1] + [tenantIds, MAX_PRINCIPALS + 1], ), }); if (principalResult.rows.length > MAX_PRINCIPALS) { - return yield* migrationFailure( - `Authorization migration exceeds the ${MAX_PRINCIPALS}-principal safety bound` - ); + return yield* migrationFailure(`Authorization migration exceeds the ${MAX_PRINCIPALS}-principal safety bound`); } const activePrincipalsByTenant = new Map>(); for (const principal of principalResult.rows) { if (principal.status === 'active') { - const ids = - activePrincipalsByTenant.get(principal.tenant_id) ?? - new Set(); + const ids = activePrincipalsByTenant.get(principal.tenant_id) ?? new Set(); ids.add(principal.principal_id); activePrincipalsByTenant.set(principal.tenant_id, ids); } } return contextResult.rows.map((row) => ({ - activePrincipalIds: - activePrincipalsByTenant.get(row.tenant_id) ?? new Set(), + activePrincipalIds: activePrincipalsByTenant.get(row.tenant_id) ?? new Set(), legalEntityId: row.legal_entity_id, tenantId: row.tenant_id, })); }).pipe(Effect.scoped); -const hasExpectedRelationshipEnvelope = ( - relationship: v1.Relationship, - resourceId: string -): boolean => +const hasExpectedRelationshipEnvelope = (relationship: v1.Relationship, resourceId: string): boolean => relationship.subject !== undefined && relationship.subject.object !== undefined && relationship.subject.optionalRelation === '' && @@ -281,21 +221,16 @@ const hasExpectedRelationshipEnvelope = ( const matchesRelationshipSubject = ( relation: string, subjectType: string | undefined, - expectedRelation: ContactsAuthorizationRelationship['relation'] + expectedRelation: ContactsAuthorizationRelationship['relation'], ): boolean => - relation === expectedRelation && - subjectType === - (expectedRelation === 'accessor' ? 'principal' : 'legal_entity'); + relation === expectedRelation && subjectType === (expectedRelation === 'accessor' ? 'principal' : 'legal_entity'); const decodeRelationship = ( relationship: v1.Relationship | undefined, resourceId: string, legalEntityObjectId: string, - activePrincipalIds: ReadonlySet -): Result.Result< - ContactsAuthorizationRelationship, - ContactsAuthorizationMigrationError -> => { + activePrincipalIds: ReadonlySet, +): Result.Result => { if (relationship === undefined) { return Result.fail(migrationFailure(OUTSIDE_AUTHORITATIVE_CONTEXT_MESSAGE)); } @@ -306,11 +241,8 @@ const decodeRelationship = ( const subjectType = relationship.subject?.object?.objectType; const { relation } = relationship; const isLegalEntity = - matchesRelationshipSubject(relation, subjectType, 'legal_entity') && - subjectId === legalEntityObjectId; - const isAccessor = - matchesRelationshipSubject(relation, subjectType, 'accessor') && - activePrincipalIds.has(subjectId); + matchesRelationshipSubject(relation, subjectType, 'legal_entity') && subjectId === legalEntityObjectId; + const isAccessor = matchesRelationshipSubject(relation, subjectType, 'accessor') && activePrincipalIds.has(subjectId); if (isLegalEntity) { return Result.succeed({ relation: 'legal_entity', @@ -331,15 +263,11 @@ const decodeRelationship = ( const readRelationships = ( client: SpiceDbClient, resourceId: string, - context: AuthoritativeContext -): Effect.Effect< - ContactsAuthorizationRelationship[], - ContactsAuthorizationMigrationError -> => + context: AuthoritativeContext, +): Effect.Effect => Effect.gen(function* readRelationshipsEffect() { const responses = yield* Effect.tryPromise({ - catch: () => - migrationFailure('Module-access relationships could not be read'), + catch: () => migrationFailure('Module-access relationships could not be read'), try: async () => await client.promises.readRelationships( v1.ReadRelationshipsRequest.create({ @@ -349,42 +277,27 @@ const readRelationships = ( optionalResourceId: resourceId, resourceType: 'module_access', }), - }) + }), ), }); if (responses.length > MAX_RELATIONSHIPS_PER_CONTEXT) { - return yield* migrationFailure( - 'A module-access context exceeds the relationship safety bound' - ); + return yield* migrationFailure('A module-access context exceeds the relationship safety bound'); } - const legalEntityObjectId = toLegalEntityAccessObjectId( - context.tenantId, - context.legalEntityId - ); + const legalEntityObjectId = toLegalEntityAccessObjectId(context.tenantId, context.legalEntityId); if (legalEntityObjectId === undefined) { - return yield* migrationFailure( - 'Invalid authoritative legal-entity context' - ); + return yield* migrationFailure('Invalid authoritative legal-entity context'); } return yield* Effect.forEach( responses, ({ relationship }) => Effect.fromResult( - decodeRelationship( - relationship, - resourceId, - legalEntityObjectId, - context.activePrincipalIds - ) + decodeRelationship(relationship, resourceId, legalEntityObjectId, context.activePrincipalIds), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); }); -const toRelationship = ( - resourceId: string, - item: ContactsAuthorizationRelationship -): v1.Relationship => +const toRelationship = (resourceId: string, item: ContactsAuthorizationRelationship): v1.Relationship => v1.Relationship.create({ relation: item.relation, resource: v1.ObjectReference.create({ @@ -403,14 +316,13 @@ const writeRelationships = ( client: SpiceDbClient, operation: v1.RelationshipUpdate_Operation, resourceId: string, - relationships: readonly ContactsAuthorizationRelationship[] + relationships: readonly ContactsAuthorizationRelationship[], ): Effect.Effect => { if (relationships.length === 0) { return Effect.void; } return Effect.tryPromise({ - catch: () => - migrationFailure('Module-access relationships could not be written'), + catch: () => migrationFailure('Module-access relationships could not be written'), try: async () => await client.promises.writeRelationships( v1.WriteRelationshipsRequest.create({ @@ -418,9 +330,9 @@ const writeRelationships = ( v1.RelationshipUpdate.create({ operation, relationship: toRelationship(resourceId, item), - }) + }), ), - }) + }), ), }).pipe(Effect.asVoid); }; @@ -428,14 +340,10 @@ const writeRelationships = ( const checkContactsPermission = ( client: SpiceDbClient, resourceId: string, - principalId: string -): Effect.Effect< - v1.CheckPermissionResponse_Permissionship, - ContactsAuthorizationMigrationError -> => + principalId: string, +): Effect.Effect => Effect.tryPromise({ - catch: () => - migrationFailure('Contacts permission verification could not complete'), + catch: () => migrationFailure('Contacts permission verification could not complete'), try: async () => await client.promises.checkPermission( v1.CheckPermissionRequest.create({ @@ -451,47 +359,31 @@ const checkContactsPermission = ( objectType: 'principal', }), }), - }) + }), ), }).pipe(Effect.map((response) => response.permissionship)); const assertContactsPermissions = ( client: SpiceDbClient, resourceId: string, - relationships: readonly ContactsAuthorizationRelationship[] + relationships: readonly ContactsAuthorizationRelationship[], ): Effect.Effect => Effect.gen(function* assertContactsPermissionsEffect() { - const accessorIds = relationships - .filter((item) => item.relation === 'accessor') - .map((item) => item.subjectId); + const accessorIds = relationships.filter((item) => item.relation === 'accessor').map((item) => item.subjectId); yield* Effect.forEach( accessorIds, (principalId) => checkContactsPermission(client, resourceId, principalId).pipe( Effect.filterOrFail( - (permissionship) => - permissionship === - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, - () => - migrationFailure( - 'Contacts permission verification did not preserve an allowed principal' - ) - ) + (permissionship) => permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, + () => migrationFailure('Contacts permission verification did not preserve an allowed principal'), + ), ), - { concurrency: 1, discard: true } - ); - const deniedPermissionship = yield* checkContactsPermission( - client, - resourceId, - DENIED_PROBE_PRINCIPAL_ID + { concurrency: 1, discard: true }, ); - if ( - deniedPermissionship !== - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION - ) { - yield* migrationFailure( - 'Contacts permission verification did not preserve the denied boundary' - ); + const deniedPermissionship = yield* checkContactsPermission(client, resourceId, DENIED_PROBE_PRINCIPAL_ID); + if (deniedPermissionship !== v1.CheckPermissionResponse_Permissionship.NO_PERMISSION) { + yield* migrationFailure('Contacts permission verification did not preserve the denied boundary'); } }); @@ -499,15 +391,10 @@ const deleteLegacyRelationships = ( client: SpiceDbClient, resourceId: string, relationships: readonly ContactsAuthorizationRelationship[], - context: AuthoritativeContext + context: AuthoritativeContext, ) => Effect.gen(function* deleteLegacyRelationshipsEffect() { - yield* writeRelationships( - client, - v1.RelationshipUpdate_Operation.DELETE, - resourceId, - relationships - ); + yield* writeRelationships(client, v1.RelationshipUpdate_Operation.DELETE, resourceId, relationships); const remaining = yield* readRelationships(client, resourceId, context); if (remaining.length > 0) { yield* migrationFailure('Legacy relationship cleanup was incomplete'); @@ -517,66 +404,31 @@ const deleteLegacyRelationships = ( const migrateContext = ( client: SpiceDbClient, mode: ContactsAuthorizationMigrationMode, - context: AuthoritativeContext + context: AuthoritativeContext, ): Effect.Effect => Effect.gen(function* migrateContextEffect() { - const legacyResourceId = toModuleAccessObjectId( - context.tenantId, - context.legalEntityId, - LEGACY_MODULE_ID - ); - const contactsResourceId = toModuleAccessObjectId( - context.tenantId, - context.legalEntityId, - CONTACTS_MODULE_ID - ); + const legacyResourceId = toModuleAccessObjectId(context.tenantId, context.legalEntityId, LEGACY_MODULE_ID); + const contactsResourceId = toModuleAccessObjectId(context.tenantId, context.legalEntityId, CONTACTS_MODULE_ID); if (legacyResourceId === undefined || contactsResourceId === undefined) { - return yield* migrationFailure( - 'Invalid authoritative module-access context' - ); + return yield* migrationFailure('Invalid authoritative module-access context'); } const [legacy, contactsBefore] = yield* Effect.all( - [ - readRelationships(client, legacyResourceId, context), - readRelationships(client, contactsResourceId, context), - ], - { concurrency: 'unbounded' } - ); - const plan = yield* Effect.fromResult( - planContactsAuthorizationContextResult(mode, legacy, contactsBefore) + [readRelationships(client, legacyResourceId, context), readRelationships(client, contactsResourceId, context)], + { concurrency: 'unbounded' }, ); + const plan = yield* Effect.fromResult(planContactsAuthorizationContextResult(mode, legacy, contactsBefore)); if (plan.touchContacts) { - yield* writeRelationships( - client, - v1.RelationshipUpdate_Operation.TOUCH, - contactsResourceId, - legacy - ); + yield* writeRelationships(client, v1.RelationshipUpdate_Operation.TOUCH, contactsResourceId, legacy); } - const contactsAfter = yield* readRelationships( - client, - contactsResourceId, - context - ); + const contactsAfter = yield* readRelationships(client, contactsResourceId, context); if (legacy.length > 0 && !sameRelationshipSet(legacy, contactsAfter)) { - return yield* migrationFailure( - 'Contacts relationship verification failed after preparation' - ); + return yield* migrationFailure('Contacts relationship verification failed after preparation'); } if (contactsAfter.length > 0) { - yield* assertContactsPermissions( - client, - contactsResourceId, - contactsAfter - ); + yield* assertContactsPermissions(client, contactsResourceId, contactsAfter); } if (plan.deleteLegacy) { - yield* deleteLegacyRelationships( - client, - legacyResourceId, - legacy, - context - ); + yield* deleteLegacyRelationships(client, legacyResourceId, legacy, context); } return { deleted: plan.deleteLegacy ? legacy.length : 0, @@ -587,55 +439,32 @@ const migrateContext = ( const acquireSpiceDbClient = (configuration: SpiceDbConfigValue) => Effect.acquireRelease( Effect.try({ - catch: () => - migrationFailure( - 'The authorization migration SpiceDB client could not open' - ), - try: () => - v1.NewClient( - configuration.preSharedKey, - configuration.endpoint, - spiceDbClientSecurity(configuration) - ), + catch: () => migrationFailure('The authorization migration SpiceDB client could not open'), + try: () => v1.NewClient(configuration.preSharedKey, configuration.endpoint, spiceDbClientSecurity(configuration)), }), - (client) => Effect.sync(() => client.close()) + (client) => Effect.sync(() => client.close()), ); const migrateContactsAuthorization = ( - mode: ContactsAuthorizationMigrationMode -): Effect.Effect< - ContactsAuthorizationMigrationResult, - ContactsAuthorizationMigrationError -> => + mode: ContactsAuthorizationMigrationMode, +): Effect.Effect => Effect.gen(function* migrateContactsAuthorizationProgram() { const [database, spiceDb] = yield* Effect.all( [ - loadDatabaseConnectionPair().pipe( - Effect.mapError((error) => migrationFailure(error.reason)) - ), - loadSpiceDbConfig().pipe( - Effect.mapError((error) => migrationFailure(error.reason)) - ), + loadDatabaseConnectionPair().pipe(Effect.mapError((error) => migrationFailure(error.reason))), + loadSpiceDbConfig().pipe(Effect.mapError((error) => migrationFailure(error.reason))), ], - { concurrency: 'unbounded' } - ); - const contexts = yield* loadAuthoritativeContexts( - Redacted.make(database.admin.connectionString) + { concurrency: 'unbounded' }, ); + const contexts = yield* loadAuthoritativeContexts(Redacted.make(database.admin.connectionString)); const client = yield* acquireSpiceDbClient(spiceDb); - const contextResults = yield* Effect.forEach( - contexts, - (context) => migrateContext(client, mode, context), - { concurrency: 1 } - ); + const contextResults = yield* Effect.forEach(contexts, (context) => migrateContext(client, mode, context), { + concurrency: 1, + }); return { contexts: contextResults.length, - deleted: EffectNumber.sumAll( - contextResults.map((result) => result.deleted) - ), - touched: EffectNumber.sumAll( - contextResults.map((result) => result.touched) - ), + deleted: EffectNumber.sumAll(contextResults.map((result) => result.deleted)), + touched: EffectNumber.sumAll(contextResults.map((result) => result.touched)), }; }).pipe(Effect.scoped); @@ -645,22 +474,17 @@ const command = Command.make( ({ mode }) => Effect.gen(function* migrateContactsAuthorizationCommand() { const result = yield* migrateContactsAuthorization(mode).pipe( - Effect.tapError((error) => Console.error(error.message)) + Effect.tapError((error) => Console.error(error.message)), ); yield* Console.log( - `Contacts authorization ${mode} completed (${result.contexts} contexts, ${result.touched} touched, ${result.deleted} deleted)` + `Contacts authorization ${mode} completed (${result.contexts} contexts, ${result.touched} touched, ${result.deleted} deleted)`, ); - }) + }), ); const [, invokedPath] = process.argv; -if ( - invokedPath !== undefined && - import.meta.url === pathToFileURL(invokedPath).href -) { +if (invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href) { const migrationRuntime = ManagedRuntime.make(NodeServices.layer); - const exit = await migrationRuntime.runPromiseExit( - Command.run(command, { version: '1.0.0' }) - ); + const exit = await migrationRuntime.runPromiseExit(Command.run(command, { version: '1.0.0' })); process.exitCode = Exit.isSuccess(exit) ? 0 : 1; } diff --git a/app/scripts/migrate-strict-effect.mts b/app/scripts/migrate-strict-effect.mts index d9109e7c9..2155e96d2 100644 --- a/app/scripts/migrate-strict-effect.mts +++ b/app/scripts/migrate-strict-effect.mts @@ -2,10 +2,7 @@ import { NodeServices } from '@effect/platform-node'; import { Effect } from 'effect'; -import { - runUltramodernScript, - ultramodernExitCode, -} from './shared/ultramodern-command.mts'; +import { runUltramodernScript, ultramodernExitCode } from './shared/ultramodern-command.mts'; import { ultramodernCommandFailure } from './ultramodern-command-failure.mts'; const exit = await Effect.runPromiseExit( @@ -14,6 +11,6 @@ const exit = await Effect.runPromiseExit( directoryFailure: 'Unable to resolve the strict-Effect migration directory', failure: ultramodernCommandFailure, moduleUrl: import.meta.url, - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped) + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); process.exitCode = ultramodernExitCode(exit); diff --git a/app/scripts/module-federation-bridge-boundary.mts b/app/scripts/module-federation-bridge-boundary.mts index ded61167e..940e23fac 100644 --- a/app/scripts/module-federation-bridge-boundary.mts +++ b/app/scripts/module-federation-bridge-boundary.mts @@ -6,53 +6,34 @@ interface RouterDependencies { readonly devDependencies?: Readonly>; } -const property = ( - object: ObjectExpression, - name: string -): Expression | undefined => { +const property = (object: ObjectExpression, name: string): Expression | undefined => { // Computed keys, spreads and duplicates can overwrite an apparently literal capability. - if ( - object.properties.some( - (entry) => entry.type === 'SpreadElement' || entry.computed - ) - ) { + if (object.properties.some((entry) => entry.type === 'SpreadElement' || entry.computed)) { return undefined; } const entries = object.properties.filter( (entry) => entry.type === 'Property' && ((entry.key.type === 'Identifier' && entry.key.name === name) || - (entry.key.type === 'Literal' && entry.key.value === name)) + (entry.key.type === 'Literal' && entry.key.value === name)), ); const entry = entries.length === 1 ? entries[0] : undefined; - return entry?.type === 'Property' && entry.kind === 'init' && !entry.method - ? entry.value - : undefined; + return entry?.type === 'Property' && entry.kind === 'init' && !entry.method ? entry.value : undefined; }; const exportedConfiguration = (program: Program) => { - const exported = program.body.find( - (statement) => statement.type === 'ExportDefaultDeclaration' - ); - let config = - exported?.type === 'ExportDefaultDeclaration' - ? exported.declaration - : undefined; + const exported = program.body.find((statement) => statement.type === 'ExportDefaultDeclaration'); + let config = exported?.type === 'ExportDefaultDeclaration' ? exported.declaration : undefined; if (config?.type === 'Identifier') { const { name } = config; const declarations = program.body.flatMap((statement) => statement.type === 'VariableDeclaration' && statement.kind === 'const' ? statement.declarations.filter( - (declaration) => - declaration.id.type === 'Identifier' && - declaration.id.name === name + (declaration) => declaration.id.type === 'Identifier' && declaration.id.name === name, ) - : [] + : [], ); - config = - declarations.length === 1 - ? (declarations[0]?.init ?? undefined) - : undefined; + config = declarations.length === 1 ? (declarations[0]?.init ?? undefined) : undefined; } return config; }; @@ -68,14 +49,12 @@ const configurationBindings = (program: Program) => specifier.imported.type === 'Identifier' && specifier.imported.name === 'createModuleFederationConfig' ? [specifier.local.name] - : [] + : [], ) - : [] + : [], ); -const configurationObject = ( - program: Program -): ObjectExpression | undefined => { +const configurationObject = (program: Program): ObjectExpression | undefined => { const bindings = configurationBindings(program); const config = exportedConfiguration(program); if ( @@ -92,30 +71,20 @@ const configurationObject = ( const bridgeRouterEnabled = (config: ObjectExpression): boolean | undefined => { const bridge = property(config, 'bridge'); - const enabled = - bridge?.type === 'ObjectExpression' - ? property(bridge, 'enableBridgeRouter') - : undefined; + const enabled = bridge?.type === 'ObjectExpression' ? property(bridge, 'enableBridgeRouter') : undefined; if (enabled?.type !== 'Literal') { return undefined; } - return enabled.value === true || enabled.value === false - ? enabled.value - : undefined; + return enabled.value === true || enabled.value === false ? enabled.value : undefined; }; const declaresBridgeRouter = (manifest: RouterDependencies): boolean => ['react-router', 'react-router-dom'].some( - (name) => - Object.hasOwn(manifest.dependencies ?? {}, name) || - Object.hasOwn(manifest.devDependencies ?? {}, name) + (name) => Object.hasOwn(manifest.dependencies ?? {}, name) || Object.hasOwn(manifest.devDependencies ?? {}, name), ); /** Check the exported configuration, not an unexecuted decoy or obsolete always-on bridge rule. */ -export const moduleFederationBridgeViolation = ( - source: string, - manifest: RouterDependencies -): string | undefined => { +export const moduleFederationBridgeViolation = (source: string, manifest: RouterDependencies): string | undefined => { const parsed = parseSync('module-federation.config.ts', source); if (parsed.errors.length !== 0) { return 'Module Federation configuration must parse.'; diff --git a/app/scripts/outbox-worker-delivery.mjs b/app/scripts/outbox-worker-delivery.mjs index 55f9d2895..ad4576d0f 100644 --- a/app/scripts/outbox-worker-delivery.mjs +++ b/app/scripts/outbox-worker-delivery.mjs @@ -8,7 +8,7 @@ const OwnerPackageSchema = Schema.Struct({ scripts: Schema.optional( Schema.Struct({ 'worker:start': Schema.optional(Schema.String), - }) + }), ), }); @@ -16,12 +16,9 @@ const OwnerPackageSchema = Schema.Struct({ /** @typedef {{ readonly _tag: 'OutboxWorkerDeliveryInvalid', readonly reason: string }} OutboxWorkerDeliveryInvalidValue */ /** @typedef {{ readonly entry: string, readonly id: string, readonly ownerId: string, readonly packageName: string, readonly path: string, readonly serviceIdEnv: string, readonly stageSetup: string }} OutboxWorkerDelivery */ -class OutboxWorkerDeliveryInvalid extends Schema.TaggedError()( - 'OutboxWorkerDeliveryInvalid', - { - reason: Schema.String, - } -) {} +class OutboxWorkerDeliveryInvalid extends Schema.TaggedError()('OutboxWorkerDeliveryInvalid', { + reason: Schema.String, +}) {} /** * A generated worker host is the deployment capability; topology owns its identity. @@ -42,17 +39,15 @@ export const outboxWorkerDelivery = Effect.fn('outboxWorkerDelivery')( return yield* Effect.undefined; } - const ownerPackage = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(OwnerPackageSchema) - )(yield* fileSystem.readFileString(packagePath)); + const ownerPackage = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OwnerPackageSchema))( + yield* fileSystem.readFileString(packagePath), + ); const workerStart = ownerPackage.scripts?.['worker:start']; if (workerStart === undefined || workerStart.length === 0) { return yield* Effect.undefined; } - const host = yield* fileSystem.readFileString( - path.join(root, vertical.path, workerEntry) - ); + const host = yield* fileSystem.readFileString(path.join(root, vertical.path, workerEntry)); if ( ownerPackage.name !== vertical.package || workerStart !== workerStartCommand || @@ -73,5 +68,5 @@ export const outboxWorkerDelivery = Effect.fn('outboxWorkerDelivery')( serviceIdEnv: `ZEROPS_${vertical.id.replaceAll('-', '_').toUpperCase()}_WORKER_SERVICE_ID`, stageSetup: `${vertical.id}-worker`, }; - } + }, ); diff --git a/app/scripts/plan-deployment-impact.mts b/app/scripts/plan-deployment-impact.mts index b8ac230c4..005e1ef13 100644 --- a/app/scripts/plan-deployment-impact.mts +++ b/app/scripts/plan-deployment-impact.mts @@ -28,11 +28,7 @@ import { hashAuthorizationEvidence } from './check-authorization-readiness.mts'; import { outboxWorkerDelivery } from './outbox-worker-delivery.mjs'; import type { AuthorizationImpactReport } from './report-fail-closed-authorization-impact.mts'; -export const DeploymentPhaseKindSchema = Schema.Literals([ - 'infrastructure', - 'provider', - 'shell', -]); +export const DeploymentPhaseKindSchema = Schema.Literals(['infrastructure', 'provider', 'shell']); export type DeploymentPhaseKind = typeof DeploymentPhaseKindSchema.Type; interface TopologyUnit { @@ -65,7 +61,7 @@ const ReferenceTopologySchema = Schema.Struct({ id: Schema.optional(Schema.String), package: Schema.optional(Schema.String), verticalRefs: Schema.optional(Schema.Array(Schema.String)), - }) + }), ), verticals: Schema.optional( Schema.Array( @@ -73,18 +69,14 @@ const ReferenceTopologySchema = Schema.Struct({ id: Schema.optional(Schema.String), moduleFederation: Schema.optional( Schema.Struct({ - remotes: Schema.optional( - Schema.Array( - Schema.Struct({ id: Schema.optional(Schema.String) }) - ) - ), + remotes: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) }))), verticalRefs: Schema.optional(Schema.Array(Schema.String)), - }) + }), ), package: Schema.optional(Schema.String), path: Schema.optional(Schema.String), - }) - ) + }), + ), ), }); @@ -96,19 +88,10 @@ const OwnershipSchema = Schema.Struct({ type Ownership = typeof OwnershipSchema.Type; -const AuthorizationEnvironmentSchema = Schema.Literals([ - 'development', - 'production', - 'stage', -]); +const AuthorizationEnvironmentSchema = Schema.Literals(['development', 'production', 'stage']); const AuthorizationModeSchema = Schema.Literals(['enforced', 'report_only']); const AuthorizationCredentialSchema = Schema.Literals(['api_key', 'session']); -const AuthorizationSurfaceSchema = Schema.Literals([ - 'action', - 'capability_issuance', - 'route', - 'worker', -]); +const AuthorizationSurfaceSchema = Schema.Literals(['action', 'capability_issuance', 'route', 'worker']); const EntrypointKeySchema = Schema.String.pipe(Schema.brand('EntrypointKey')); const CanonicalTimestampStringSchema = Schema.String.check( Schema.makeFilter((value) => { @@ -116,11 +99,9 @@ const CanonicalTimestampStringSchema = Schema.String.check( return Option.isSome(parsed) && DateTime.formatIso(parsed.value) === value ? undefined : 'timestamp must use canonical UTC ISO 8601 encoding'; - }) -); -const CanonicalTimestampCodec = CanonicalTimestampStringSchema.pipe( - Schema.decodeTo(Schema.DateTimeUtcFromString) + }), ); +const CanonicalTimestampCodec = CanonicalTimestampStringSchema.pipe(Schema.decodeTo(Schema.DateTimeUtcFromString)); const CanonicalTimestampWireSchema = Schema.toEncoded(CanonicalTimestampCodec); export interface DeploymentImpactPlan { @@ -191,7 +172,7 @@ const ProtectedEntrypointInventorySchema = Schema.Struct({ entrypointKey: EntrypointKeySchema, owner: Schema.String, surface: AuthorizationSurfaceSchema, - }) + }), ), inventoryHash: Schema.String, schemaVersion: Schema.Literal(1), @@ -233,7 +214,7 @@ const AuthorizationImpactReportSchema = Schema.Struct({ 'public', ]), surface: AuthorizationSurfaceSchema, - }) + }), ), inventoryHash: Schema.String, observation: Schema.Struct({ @@ -253,7 +234,7 @@ const AuthorizationNegativeSmokeEvidenceSchema = Schema.Struct({ credential: AuthorizationCredentialSchema, outcome: Schema.Literal('denied'), scenario: Schema.String, - }) + }), ), schemaVersion: Schema.Literal(1), sourceRevision: Schema.String, @@ -294,7 +275,7 @@ const DeploymentImpactPlanSchema = Schema.Struct({ environment: AuthorizationEnvironmentSchema, mode: AuthorizationModeSchema, status: Schema.Literals(['observing', 'ready']), - }) + }), ), changedPaths: Schema.Array(Schema.String), comparison: Schema.Struct({ @@ -317,7 +298,7 @@ class DeploymentImpactPlanningError extends Schema.TaggedError @@ -325,37 +306,21 @@ const fail = (message: string): never => Result.fail( new DeploymentImpactPlanningError({ message: `Deployment impact planning failed: ${message}`, - }) - ) + }), + ), ); const requireAuthorizationEvidence = ( - input: AuthorizationPromotionGateInput -): Required< - Pick< - AuthorizationPromotionGateInput, - 'impact' | 'negativeSmoke' | 'readiness' - > -> => { + input: AuthorizationPromotionGateInput, +): Required> => { const { impact, negativeSmoke, readiness } = input; - if ( - impact === undefined || - negativeSmoke === undefined || - readiness === undefined - ) { - return fail( - 'enforced authorization promotion requires impact, readiness, and negative-smoke evidence' - ); + if (impact === undefined || negativeSmoke === undefined || readiness === undefined) { + return fail('enforced authorization promotion requires impact, readiness, and negative-smoke evidence'); } return { impact, negativeSmoke, readiness }; }; -type PromotionEvidence = Required< - Pick< - AuthorizationPromotionGateInput, - 'impact' | 'negativeSmoke' | 'readiness' - > ->; +type PromotionEvidence = Required>; const evidenceHasInventoryIdentity = ( inventory: ProtectedEntrypointInventory, @@ -363,16 +328,13 @@ const evidenceHasInventoryIdentity = ( readonly inventoryHash: string; readonly schemaVersion: number; readonly sourceRevision: string; - } + }, ): boolean => evidence.schemaVersion === 1 && evidence.sourceRevision === inventory.sourceRevision && evidence.inventoryHash === inventory.inventoryHash; -const readinessMatchesPromotion = ( - input: AuthorizationPromotionGateInput, - evidence: PromotionEvidence -): boolean => { +const readinessMatchesPromotion = (input: AuthorizationPromotionGateInput, evidence: PromotionEvidence): boolean => { const { impact, negativeSmoke, readiness } = evidence; return ( readiness.status === 'ready' && @@ -383,33 +345,26 @@ const readinessMatchesPromotion = ( ); }; -const authorizationEvidenceMatches = ( - input: AuthorizationPromotionGateInput, - evidence: PromotionEvidence -): boolean => +const authorizationEvidenceMatches = (input: AuthorizationPromotionGateInput, evidence: PromotionEvidence): boolean => [evidence.impact, evidence.negativeSmoke, evidence.readiness].every((item) => - evidenceHasInventoryIdentity(input.inventory, item) + evidenceHasInventoryIdentity(input.inventory, item), ) && evidence.impact.totalWouldDeny === 0 && evidence.negativeSmoke.environment === input.environment && readinessMatchesPromotion(input, evidence); export const validateAuthorizationPromotionGate = ( - input: AuthorizationPromotionGateInput + input: AuthorizationPromotionGateInput, ): NonNullable => { const { inventory, rollout } = input; validateAuthorizationRolloutContract(rollout, { - entrypointKeys: new Set( - inventory.entries.map(({ entrypointKey }) => entrypointKey) - ), + entrypointKeys: new Set(inventory.entries.map(({ entrypointKey }) => entrypointKey)), inventoryHash: inventory.inventoryHash, nowEpochMs: input.nowEpochMs, }); if (rollout.mode === 'report_only') { if (input.environment === 'production') { - fail( - 'production authorization promotion rejects report-only configuration' - ); + fail('production authorization promotion rejects report-only configuration'); } return { environment: input.environment, @@ -417,12 +372,8 @@ export const validateAuthorizationPromotionGate = ( status: 'observing', }; } - if ( - !authorizationEvidenceMatches(input, requireAuthorizationEvidence(input)) - ) { - fail( - 'authorization promotion evidence is missing, stale, mismatched, or unresolved' - ); + if (!authorizationEvidenceMatches(input, requireAuthorizationEvidence(input))) { + fail('authorization promotion evidence is missing, stale, mismatched, or unresolved'); } return { environment: input.environment, @@ -448,16 +399,11 @@ const INFRASTRUCTURE_PHASES = { const GIT_EXECUTABLE = '/usr/bin/git'; -const readJson = >( - schema: DocumentSchema, - filePath: string -) => +const readJson = >(schema: DocumentSchema, filePath: string) => Effect.gen(function* readJsonEffect() { const fileSystem = yield* FileSystem.FileSystem; const source = yield* fileSystem.readFileString(filePath); - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))( - source - ); + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(source); }); const requireString = (value: string | undefined, area: string): string => { @@ -475,9 +421,7 @@ const toEnvironmentSegment = (value: string): string => const normalizeChangedPath = (changedPath: string): string => { const normalized = changedPath.replaceAll('\\', '/').replace(/^\.\//u, ''); - return normalized.startsWith('app/') - ? normalized.slice('app/'.length) - : normalized; + return normalized.startsWith('app/') ? normalized.slice('app/'.length) : normalized; }; const isWithin = (changedPath: string, ownerPath: string): boolean => @@ -485,9 +429,7 @@ const isWithin = (changedPath: string, ownerPath: string): boolean => const parseStageSetups = (zeropsSource: string): ReadonlySet => { const setups = new Set(); - for (const match of zeropsSource.matchAll( - /^\s*-\s+setup:\s*['"]?(?[^'"\s]+)['"]?\s*$/gmu - )) { + for (const match of zeropsSource.matchAll(/^\s*-\s+setup:\s*['"]?(?[^'"\s]+)['"]?\s*$/gmu)) { const setup = match.groups?.setup; if (setup !== undefined && setup.length > 0) { setups.add(setup); @@ -499,16 +441,12 @@ const parseStageSetups = (zeropsSource: string): ReadonlySet => { type TopologyOwner = typeof TopologyOwnerSchema.Type; type ReferenceVertical = NonNullable[number]; -const indexOwners = ( - ownerEntries: readonly TopologyOwner[] -): ReadonlyMap => { +const indexOwners = (ownerEntries: readonly TopologyOwner[]): ReadonlyMap => { const ownersById = new Map(); for (const owner of ownerEntries) { const ownerId = requireString(owner.id, 'ownership owner.id'); if (ownersById.has(ownerId)) { - fail( - `topology/ownership.json contains duplicate owner identity "${ownerId}"` - ); + fail(`topology/ownership.json contains duplicate owner identity "${ownerId}"`); } ownersById.set(ownerId, owner); } @@ -517,29 +455,18 @@ const indexOwners = ( const readShellUnit = ( topology: ReferenceTopology, - ownersById: ReadonlyMap + ownersById: ReadonlyMap, ): Omit => { - const shellId = requireString( - topology.shell?.id, - 'reference topology shell.id' - ); - const shellPackage = requireString( - topology.shell?.package, - 'reference topology shell.package' - ); + const shellId = requireString(topology.shell?.id, 'reference topology shell.id'); + const shellPackage = requireString(topology.shell?.package, 'reference topology shell.package'); const shellOwner = ownersById.get(shellId); if (shellOwner === undefined) { - return fail( - `topology delivery unit "${shellId}" is missing from topology/ownership.json` - ); + return fail(`topology delivery unit "${shellId}" is missing from topology/ownership.json`); } - const shellPath = requireString( - shellOwner.path, - `ownership owner ${shellId}.path` - ); + const shellPath = requireString(shellOwner.path, `ownership owner ${shellId}.path`); if (shellOwner.package !== shellPackage) { fail( - `topology and ownership disagree for "${shellId}": topology package "${shellPackage}" versus ownership package "${String(shellOwner.package)}"` + `topology and ownership disagree for "${shellId}": topology package "${shellPackage}" versus ownership package "${String(shellOwner.package)}"`, ); } return { @@ -552,19 +479,12 @@ const readShellUnit = ( }; }; -const collectVerticalIds = ( - verticals: readonly ReferenceVertical[] -): ReadonlySet => { +const collectVerticalIds = (verticals: readonly ReferenceVertical[]): ReadonlySet => { const verticalIds = new Set(); for (const vertical of verticals) { - const verticalId = requireString( - vertical.id, - 'reference topology vertical.id' - ); + const verticalId = requireString(vertical.id, 'reference topology vertical.id'); if (verticalIds.has(verticalId)) { - fail( - `reference topology contains duplicate vertical identity "${verticalId}"` - ); + fail(`reference topology contains duplicate vertical identity "${verticalId}"`); } verticalIds.add(verticalId); } @@ -573,37 +493,24 @@ const collectVerticalIds = ( const validateSharedPackages = ( sharedPackages: readonly TopologyOwner[], - ownersById: ReadonlyMap + ownersById: ReadonlyMap, ): ReadonlySet => { const sharedPackageIds = new Set(); for (const sharedPackage of sharedPackages) { - const id = requireString( - sharedPackage.id, - 'reference topology shared package.id' - ); - const packageName = requireString( - sharedPackage.package, - `reference topology shared package ${id}.package` - ); - const ownerPath = requireString( - sharedPackage.path, - `reference topology shared package ${id}.path` - ); + const id = requireString(sharedPackage.id, 'reference topology shared package.id'); + const packageName = requireString(sharedPackage.package, `reference topology shared package ${id}.package`); + const ownerPath = requireString(sharedPackage.path, `reference topology shared package ${id}.path`); if (sharedPackageIds.has(id)) { - fail( - `reference topology contains duplicate shared package identity "${id}"` - ); + fail(`reference topology contains duplicate shared package identity "${id}"`); } sharedPackageIds.add(id); const owner = ownersById.get(id); if (owner === undefined) { - return fail( - `topology shared package "${id}" is missing from topology/ownership.json` - ); + return fail(`topology shared package "${id}" is missing from topology/ownership.json`); } if (owner.package !== packageName || owner.path !== ownerPath) { fail( - `topology and ownership disagree for shared package "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"` + `topology and ownership disagree for shared package "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"`, ); } } @@ -613,13 +520,11 @@ const validateSharedPackages = ( const validateDistinctTopologyIds = ( shellId: string, verticalIds: ReadonlySet, - sharedPackageIds: ReadonlySet + sharedPackageIds: ReadonlySet, ): void => { for (const id of [shellId, ...verticalIds]) { if (sharedPackageIds.has(id)) { - fail( - `reference topology reuses delivery identity "${id}" for a shared package` - ); + fail(`reference topology reuses delivery identity "${id}" for a shared package`); } } }; @@ -627,19 +532,15 @@ const validateDistinctTopologyIds = ( const verticalDependencies = ( vertical: ReferenceVertical, id: string, - verticalIds: ReadonlySet + verticalIds: ReadonlySet, ): readonly string[] => { const dependencies = [ ...(vertical.moduleFederation?.verticalRefs ?? []), - ...(vertical.moduleFederation?.remotes ?? []).flatMap((remote) => - remote.id === undefined ? [] : [remote.id] - ), + ...(vertical.moduleFederation?.remotes ?? []).flatMap((remote) => (remote.id === undefined ? [] : [remote.id])), ]; for (const dependency of dependencies) { if (!verticalIds.has(dependency)) { - fail( - `topology delivery unit "${id}" references unknown provider "${dependency}"` - ); + fail(`topology delivery unit "${id}" references unknown provider "${dependency}"`); } } return dependencies; @@ -648,28 +549,20 @@ const verticalDependencies = ( const buildVerticalUnits = ( verticals: readonly ReferenceVertical[], verticalIds: ReadonlySet, - ownersById: ReadonlyMap + ownersById: ReadonlyMap, ): readonly TopologyUnit[] => { const units: TopologyUnit[] = []; for (const vertical of verticals) { const id = requireString(vertical.id, 'reference topology vertical.id'); - const packageName = requireString( - vertical.package, - `reference topology vertical ${id}.package` - ); - const ownerPath = requireString( - vertical.path, - `reference topology vertical ${id}.path` - ); + const packageName = requireString(vertical.package, `reference topology vertical ${id}.package`); + const ownerPath = requireString(vertical.path, `reference topology vertical ${id}.path`); const owner = ownersById.get(id); if (owner === undefined) { - return fail( - `topology delivery unit "${id}" is missing from topology/ownership.json` - ); + return fail(`topology delivery unit "${id}" is missing from topology/ownership.json`); } if (owner.package !== packageName || owner.path !== ownerPath) { fail( - `topology and ownership disagree for "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"` + `topology and ownership disagree for "${id}": expected package "${packageName}" at "${ownerPath}", found package "${String(owner.package)}" at "${String(owner.path)}"`, ); } const dependencies = verticalDependencies(vertical, id, verticalIds); @@ -690,22 +583,17 @@ const addShellUnit = ( units: readonly TopologyUnit[], shell: Omit, shellDependencies: readonly string[], - verticalIds: ReadonlySet + verticalIds: ReadonlySet, ): readonly TopologyUnit[] => { for (const dependency of shellDependencies) { if (!verticalIds.has(dependency)) { - fail( - `topology shell "${shell.id}" references unknown provider "${dependency}"` - ); + fail(`topology shell "${shell.id}" references unknown provider "${dependency}"`); } } return [ ...units, { - dependencies: EffectArray.sort( - [...new Set(shellDependencies)], - Order.String - ), + dependencies: EffectArray.sort([...new Set(shellDependencies)], Order.String), ...shell, }, ]; @@ -713,31 +601,21 @@ const addShellUnit = ( const validateOwnershipCoverage = ( ownerEntries: readonly TopologyOwner[], - topologyOwnerIds: ReadonlySet + topologyOwnerIds: ReadonlySet, ): void => { for (const owner of ownerEntries) { const id = requireString(owner.id, 'ownership owner.id'); const ownerPath = requireString(owner.path, `ownership owner ${id}.path`); - if ( - /^(?:apps|packages|verticals)\//u.test(ownerPath) && - !topologyOwnerIds.has(id) - ) { - fail( - `ownership entry "${id}" at "${ownerPath}" has no matching topology identity` - ); + if (/^(?:apps|packages|verticals)\//u.test(ownerPath) && !topologyOwnerIds.has(id)) { + fail(`ownership entry "${id}" at "${ownerPath}" has no matching topology identity`); } } }; -const validateStageSetupCoverage = ( - units: readonly TopologyUnit[], - stageSetups: ReadonlySet -): void => { +const validateStageSetupCoverage = (units: readonly TopologyUnit[], stageSetups: ReadonlySet): void => { for (const phase of [...Object.values(INFRASTRUCTURE_PHASES), ...units]) { if (!stageSetups.has(phase.stageSetup)) { - fail( - `topology delivery unit "${phase.id}" has unsupported stage setup "${phase.stageSetup}" in zerops.yaml` - ); + fail(`topology delivery unit "${phase.id}" has unsupported stage setup "${phase.stageSetup}" in zerops.yaml`); } } }; @@ -745,35 +623,27 @@ const validateStageSetupCoverage = ( const buildTopologyUnits = ( topology: ReferenceTopology, ownership: Ownership, - stageSetups: ReadonlySet + stageSetups: ReadonlySet, ): readonly TopologyUnit[] => { const ownerEntries = ownership.owners ?? []; const ownersById = indexOwners(ownerEntries); const shell = readShellUnit(topology, ownersById); const verticals = topology.verticals ?? []; const verticalIds = collectVerticalIds(verticals); - const sharedPackageIds = validateSharedPackages( - topology.sharedPackages ?? [], - ownersById - ); + const sharedPackageIds = validateSharedPackages(topology.sharedPackages ?? [], ownersById); validateDistinctTopologyIds(shell.id, verticalIds, sharedPackageIds); const units = addShellUnit( buildVerticalUnits(verticals, verticalIds, ownersById), shell, topology.shell?.verticalRefs ?? [], - verticalIds - ); - validateOwnershipCoverage( - ownerEntries, - new Set([shell.id, ...verticalIds, ...sharedPackageIds]) + verticalIds, ); + validateOwnershipCoverage(ownerEntries, new Set([shell.id, ...verticalIds, ...sharedPackageIds])); validateStageSetupCoverage(units, stageSetups); return units; }; -const orderUnits = ( - units: readonly TopologyUnit[] -): readonly TopologyUnit[] => { +const orderUnits = (units: readonly TopologyUnit[]): readonly TopologyUnit[] => { const unitsById = new Map(units.map((unit) => [unit.id, unit])); const ordered: TopologyUnit[] = []; const visiting = new Set(); @@ -787,9 +657,7 @@ const orderUnits = ( } const unit = unitsById.get(id); if (unit === undefined) { - return fail( - `topology delivery dependencies reference unknown unit "${id}"` - ); + return fail(`topology delivery dependencies reference unknown unit "${id}"`); } visiting.add(id); for (const dependency of unit.dependencies) { @@ -799,60 +667,44 @@ const orderUnits = ( visited.add(id); ordered.push(unit); }; - for (const unit of EffectArray.sortWith( - units, - (candidate) => candidate.id, - Order.String - )) { + for (const unit of EffectArray.sortWith(units, (candidate) => candidate.id, Order.String)) { visit(unit.id); } return ordered; }; -const invalidBaseReason = ( - rootDirectory: string, - baseRevision: string | undefined, - headRevision: string -) => +const invalidBaseReason = (rootDirectory: string, baseRevision: string | undefined, headRevision: string) => Effect.gen(function* invalidBaseReasonEffect() { - if ( - baseRevision === undefined || - baseRevision.length === 0 || - /^0+$/u.test(baseRevision) - ) { + if (baseRevision === undefined || baseRevision.length === 0 || /^0+$/u.test(baseRevision)) { return 'comparison base is unavailable or all-zero'; } const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const revisionExists = yield* spawner .exitCode( - ChildProcess.make( - GIT_EXECUTABLE, - ['cat-file', '-e', `${baseRevision}^{commit}`], - { - cwd: rootDirectory, - stderr: 'ignore', - stdout: 'ignore', - } - ) + ChildProcess.make(GIT_EXECUTABLE, ['cat-file', '-e', `${baseRevision}^{commit}`], { + cwd: rootDirectory, + stderr: 'ignore', + stdout: 'ignore', + }), ) .pipe( Effect.map((exitCode) => exitCode === 0), - Effect.catch(() => Effect.succeed(false)) + Effect.catch(() => Effect.succeed(false)), ); if (!revisionExists) { return `comparison base "${baseRevision}" is unavailable`; } const isAncestor = yield* spawner .exitCode( - ChildProcess.make( - GIT_EXECUTABLE, - ['merge-base', '--is-ancestor', baseRevision, headRevision], - { cwd: rootDirectory, stderr: 'ignore', stdout: 'ignore' } - ) + ChildProcess.make(GIT_EXECUTABLE, ['merge-base', '--is-ancestor', baseRevision, headRevision], { + cwd: rootDirectory, + stderr: 'ignore', + stdout: 'ignore', + }), ) .pipe( Effect.map((exitCode) => exitCode === 0), - Effect.catch(() => Effect.succeed(false)) + Effect.catch(() => Effect.succeed(false)), ); if (!isAncestor) { return `comparison base "${baseRevision}" is not an ancestor of "${headRevision}"`; @@ -860,44 +712,27 @@ const invalidBaseReason = ( return yield* Effect.undefined; }); -const changedPathsFromGit = ( - rootDirectory: string, - baseRevision: string, - headRevision: string -) => +const changedPathsFromGit = (rootDirectory: string, baseRevision: string, headRevision: string) => Effect.gen(function* changedPathsFromGitEffect() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const output = yield* spawner.string( - ChildProcess.make( - GIT_EXECUTABLE, - [ - 'diff', - '--name-only', - '--no-renames', - '-z', - baseRevision, - headRevision, - ], - { cwd: rootDirectory } - ) + ChildProcess.make(GIT_EXECUTABLE, ['diff', '--name-only', '--no-renames', '-z', baseRevision, headRevision], { + cwd: rootDirectory, + }), ); return output.split('\0').filter(Boolean); }); const isMigrationChange = (changedPath: string): boolean => /(?:^|\/)(?:drizzle(?:-auth)?\/|drizzle(?:\.auth)?\.config\.ts$|schema\.ts$|prepare-[^/]+-migration\.mts$|verify-(?:auth-)?db-schema\.mts$)/u.test( - changedPath + changedPath, ) || /^(?:scripts\/run-zerops-migrator\.mjs|scripts\/verify-application-db-schema\.mts|scripts\/postgres\/(?:bootstrap-runtime-role\.mts|bootstrap-spicedb-database\.mts|docker-init-runtime-role\.sh))$/u.test( - changedPath + changedPath, ) || - changedPath === - 'packages/core-runtime/src/install/spicedb-database-config.ts'; + changedPath === 'packages/core-runtime/src/install/spicedb-database-config.ts'; -const isPublicContractChange = ( - ownerPath: string, - changedPath: string -): boolean => { +const isPublicContractChange = (ownerPath: string, changedPath: string): boolean => { const relativePath = changedPath.slice(ownerPath.length + 1); return ( relativePath === 'package.json' || @@ -911,16 +746,13 @@ const isPublicContractChange = ( const isSpiceDbChange = (changedPath: string): boolean => changedPath.startsWith('packages/core-runtime/spicedb/') || changedPath.startsWith('packages/core-runtime/src/permissions/') || - changedPath === - 'packages/core-runtime/src/install/spicedb-database-config.ts' || + changedPath === 'packages/core-runtime/src/install/spicedb-database-config.ts' || changedPath === 'scripts/postgres/bootstrap-spicedb-database.mts' || changedPath === 'scripts/run-zerops-spicedb.sh'; const isAuthorizationRolloutChange = (changedPath: string): boolean => changedPath.startsWith('packages/core-runtime/src/authorization/') || - changedPath.startsWith( - 'packages/core-runtime/src/auth/gateway-assertion-redemption' - ) || + changedPath.startsWith('packages/core-runtime/src/auth/gateway-assertion-redemption') || changedPath.startsWith('scripts/authorization/') || changedPath === 'scripts/check-authorization-readiness.mts' || changedPath === 'scripts/check-module-entrypoint-boundaries.mts' || @@ -941,8 +773,7 @@ const CONSERVATIVE_FULL_DEPLOY_PATHS = new Set([ 'zerops.yaml', ]); const isConservativeFullDeployChange = (changedPath: string): boolean => - CONSERVATIVE_FULL_DEPLOY_PATHS.has(changedPath) || - changedPath.startsWith('topology/'); + CONSERVATIVE_FULL_DEPLOY_PATHS.has(changedPath) || changedPath.startsWith('topology/'); const toPhase = (unit: TopologyUnit): DeploymentPhase => ({ id: unit.id, @@ -954,13 +785,11 @@ const toPhase = (unit: TopologyUnit): DeploymentPhase => ({ const makeComparison = ( options: PlanDeploymentImpactOptions, headRevision: string, - fallbackReason: string | undefined + fallbackReason: string | undefined, ): DeploymentImpactPlan['comparison'] => { const mode = fallbackReason === undefined ? 'diff' : 'full'; if (options.baseRevision === undefined) { - return fallbackReason === undefined - ? { headRevision, mode } - : { headRevision, mode, reason: fallbackReason }; + return fallbackReason === undefined ? { headRevision, mode } : { headRevision, mode, reason: fallbackReason }; } return fallbackReason === undefined ? { baseRevision: options.baseRevision, headRevision, mode } @@ -978,29 +807,19 @@ interface DeploymentImpactState { spicedb: boolean; } -const addAllUnits = ( - impacted: Set, - orderedUnits: readonly TopologyUnit[] -): void => { +const addAllUnits = (impacted: Set, orderedUnits: readonly TopologyUnit[]): void => { for (const unit of orderedUnits) { impacted.add(unit.id); } }; -const addWithConsumers = ( - unitId: string, - impacted: Set, - orderedUnits: readonly TopologyUnit[] -): void => { +const addWithConsumers = (unitId: string, impacted: Set, orderedUnits: readonly TopologyUnit[]): void => { impacted.add(unitId); let changed = true; while (changed) { changed = false; for (const unit of orderedUnits) { - if ( - !impacted.has(unit.id) && - unit.dependencies.some((dependency) => impacted.has(dependency)) - ) { + if (!impacted.has(unit.id) && unit.dependencies.some((dependency) => impacted.has(dependency))) { impacted.add(unit.id); changed = true; } @@ -1013,26 +832,21 @@ const applyOwnedPathImpact = ( ownerEntries: readonly TopologyOwner[], unitsById: ReadonlyMap, orderedUnits: readonly TopologyUnit[], - impacted: Set + impacted: Set, ): void => { if (!/^(?:apps|packages|verticals)\//u.test(changedPath)) { return; } const [owner] = EffectArray.sortWith( - ownerEntries.filter( - (entry) => entry.path !== undefined && isWithin(changedPath, entry.path) - ), + ownerEntries.filter((entry) => entry.path !== undefined && isWithin(changedPath, entry.path)), (entry) => String(entry.path).length, - Order.flip(Order.Number) + Order.flip(Order.Number), ); if (owner === undefined) { const [area] = changedPath.split('/'); fail(`unknown changed path "${changedPath}" in application area "${area}"`); } - const ownerId = requireString( - owner.id, - `owner for changed path ${changedPath}` - ); + const ownerId = requireString(owner.id, `owner for changed path ${changedPath}`); const topologyUnit = unitsById.get(ownerId); if (topologyUnit !== undefined) { impacted.add(ownerId); @@ -1042,9 +856,7 @@ const applyOwnedPathImpact = ( } else if (changedPath.startsWith('packages/')) { addAllUnits(impacted, orderedUnits); } else { - fail( - `changed path "${changedPath}" maps to non-delivery owner "${ownerId}"` - ); + fail(`changed path "${changedPath}" maps to non-delivery owner "${ownerId}"`); } }; @@ -1053,15 +865,9 @@ const applyChangedPathImpact = ( ownerEntries: readonly TopologyOwner[], unitsById: ReadonlyMap, orderedUnits: readonly TopologyUnit[], - state: DeploymentImpactState + state: DeploymentImpactState, ): void => { - applyOwnedPathImpact( - changedPath, - ownerEntries, - unitsById, - orderedUnits, - state.impacted - ); + applyOwnedPathImpact(changedPath, ownerEntries, unitsById, orderedUnits, state.impacted); if (isMigrationChange(changedPath)) { state.migrator = true; } @@ -1085,7 +891,7 @@ const deriveDeploymentImpact = ( changedPaths: readonly string[], fullDeploy: boolean, ownerEntries: readonly TopologyOwner[], - orderedUnits: readonly TopologyUnit[] + orderedUnits: readonly TopologyUnit[], ): DeploymentImpactState => { const state: DeploymentImpactState = { impacted: new Set(), @@ -1098,30 +904,17 @@ const deriveDeploymentImpact = ( } const unitsById = new Map(orderedUnits.map((unit) => [unit.id, unit])); for (const changedPath of changedPaths) { - applyChangedPathImpact( - changedPath, - ownerEntries, - unitsById, - orderedUnits, - state - ); + applyChangedPathImpact(changedPath, ownerEntries, unitsById, orderedUnits, state); } return state; }; -const deploymentComparison = ( - options: PlanDeploymentImpactOptions, - rootDirectory: string -) => +const deploymentComparison = (options: PlanDeploymentImpactOptions, rootDirectory: string) => Effect.gen(function* deploymentComparisonEffect() { const headRevision = options.headRevision ?? 'HEAD'; const fallbackReason = options.changedPaths === undefined - ? yield* invalidBaseReason( - rootDirectory, - options.baseRevision, - headRevision - ) + ? yield* invalidBaseReason(rootDirectory, options.baseRevision, headRevision) : undefined; const fullDeploy = fallbackReason !== undefined; const comparedPaths = @@ -1131,18 +924,15 @@ const deploymentComparison = ( : yield* changedPathsFromGit( rootDirectory, requireString(options.baseRevision, 'base revision'), - headRevision + headRevision, )); - const changedPaths = EffectArray.sort( - [...new Set(comparedPaths.map(normalizeChangedPath))], - Order.String - ); + const changedPaths = EffectArray.sort([...new Set(comparedPaths.map(normalizeChangedPath))], Order.String); return { changedPaths, fallbackReason, fullDeploy, headRevision }; }); const validateWorkerStageSetups = ( workers: readonly { readonly stageSetup: string }[], - stageSetups: ReadonlySet + stageSetups: ReadonlySet, ): void => { for (const delivery of workers) { if (!stageSetups.has(delivery.stageSetup)) { @@ -1151,9 +941,7 @@ const validateWorkerStageSetups = ( } }; -export const planDeploymentImpact = ( - options: PlanDeploymentImpactOptions = {} -) => +export const planDeploymentImpact = (options: PlanDeploymentImpactOptions = {}) => Effect.gen(function* planDeploymentImpactEffect() { const authorization = options.authorizationPromotion === undefined @@ -1161,51 +949,42 @@ export const planDeploymentImpact = ( : validateAuthorizationPromotionGate(options.authorizationPromotion); const pathService = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; - const rootDirectory = - options.rootDirectory ?? - (yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.'))); + const rootDirectory = options.rootDirectory ?? (yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.'))); const topology = yield* readJson( ReferenceTopologySchema, - pathService.join(rootDirectory, 'topology/reference-topology.json') - ); - const ownership = yield* readJson( - OwnershipSchema, - pathService.join(rootDirectory, 'topology/ownership.json') + pathService.join(rootDirectory, 'topology/reference-topology.json'), ); + const ownership = yield* readJson(OwnershipSchema, pathService.join(rootDirectory, 'topology/ownership.json')); const stageSetups = parseStageSetups( - yield* fileSystem.readFileString( - pathService.join(rootDirectory, 'zerops.yaml') - ) - ); - const orderedUnits = orderUnits( - buildTopologyUnits(topology, ownership, stageSetups) + yield* fileSystem.readFileString(pathService.join(rootDirectory, 'zerops.yaml')), ); + const orderedUnits = orderUnits(buildTopologyUnits(topology, ownership, stageSetups)); const workerDeliveries = yield* Effect.all( (topology.verticals ?? []).map((vertical) => outboxWorkerDelivery(rootDirectory, { id: requireString(vertical.id, 'vertical id'), package: requireString(vertical.package, 'vertical package'), path: requireString(vertical.path, 'vertical path'), - }) - ) - ); - const workers = workerDeliveries.filter( - (delivery) => delivery !== undefined + }), + ), ); + const workers = workerDeliveries.filter((delivery) => delivery !== undefined); validateWorkerStageSetups(workers, stageSetups); const shell = orderedUnits.find((unit) => unit.kind === 'shell'); if (shell === undefined) { return fail('reference topology has no Shell delivery unit'); } - const { changedPaths, fallbackReason, fullDeploy, headRevision } = - yield* deploymentComparison(options, rootDirectory); + const { changedPaths, fallbackReason, fullDeploy, headRevision } = yield* deploymentComparison( + options, + rootDirectory, + ); const { impacted, migrator, spicedb } = deriveDeploymentImpact( changedPaths, fullDeploy, ownership.owners ?? [], - orderedUnits + orderedUnits, ); const selectedUnits = orderedUnits.filter((unit) => impacted.has(unit.id)); @@ -1226,7 +1005,7 @@ export const planDeploymentImpact = ( serviceIdEnv: worker.serviceIdEnv, stageSetup: worker.stageSetup, })), - ...selectedUnits.filter((unit) => unit.kind === 'shell').map(toPhase) + ...selectedUnits.filter((unit) => unit.kind === 'shell').map(toPhase), ); const plan: DeploymentImpactPlan = { @@ -1237,9 +1016,7 @@ export const planDeploymentImpact = ( schemaVersion: 1, units: { migrator, - providers: phases - .filter((phase) => phase.kind === 'provider') - .map((phase) => phase.id), + providers: phases.filter((phase) => phase.kind === 'provider').map((phase) => phase.id), shell: impacted.has(shell.id), spicedb, }, @@ -1250,21 +1027,18 @@ export const planDeploymentImpact = ( const loadAuthorizationPromotionGate = ( rootDirectory: string, environment: AuthorizationPromotionGateInput['environment'], - nowEpochMs: number + nowEpochMs: number, ) => Effect.gen(function* loadAuthorizationPromotionGateEffect() { const pathService = yield* Path.Path; - const reportDirectory = pathService.join( - rootDirectory, - '.codex/reports/authorization' - ); + const reportDirectory = pathService.join(rootDirectory, '.codex/reports/authorization'); const rollout = yield* readJson( AuthorizationRolloutContractSchema, - pathService.join(rootDirectory, 'topology/authorization-rollout.json') + pathService.join(rootDirectory, 'topology/authorization-rollout.json'), ); const inventory = yield* readJson( ProtectedEntrypointInventorySchema, - pathService.join(reportDirectory, 'protected-entrypoints.json') + pathService.join(reportDirectory, 'protected-entrypoints.json'), ); if (rollout.mode === 'report_only') { return { environment, inventory, nowEpochMs, rollout }; @@ -1273,17 +1047,17 @@ const loadAuthorizationPromotionGate = ( environment, impact: yield* readJson( AuthorizationImpactReportSchema, - pathService.join(reportDirectory, 'fail-closed-impact.json') + pathService.join(reportDirectory, 'fail-closed-impact.json'), ), inventory, negativeSmoke: yield* readJson( AuthorizationNegativeSmokeEvidenceSchema, - pathService.join(reportDirectory, `negative-smoke.${environment}.json`) + pathService.join(reportDirectory, `negative-smoke.${environment}.json`), ), nowEpochMs, readiness: yield* readJson( AuthorizationReadinessEvidenceSchema, - pathService.join(reportDirectory, 'readiness.json') + pathService.join(reportDirectory, 'readiness.json'), ), rollout, }; @@ -1296,9 +1070,7 @@ const writeGitHubOutputs = (plan: DeploymentImpactPlan, outputPath: string) => Effect.gen(function* writeGitHubOutputsEffect() { const fileSystem = yield* FileSystem.FileSystem; const planJson = yield* Schema.encodeEffect(PlanJsonSchema)(plan); - const providersJson = yield* Schema.encodeEffect(ProvidersJsonSchema)( - plan.units.providers - ); + const providersJson = yield* Schema.encodeEffect(ProvidersJsonSchema)(plan.units.providers); const output = [ `any=${String(plan.any)}`, `migrator=${String(plan.units.migrator)}`, @@ -1312,47 +1084,29 @@ const writeGitHubOutputs = (plan: DeploymentImpactPlan, outputPath: string) => }); const parseAuthorizationNow = (value: string) => - Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString)(value).pipe( - Effect.map(DateTime.toEpochMillis) - ); + Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString)(value).pipe(Effect.map(DateTime.toEpochMillis)); const deploymentImpactCommand = Command.make( 'plan-deployment-impact', { - authorizationEnvironment: Flag.choice('authorization-environment', [ - 'development', - 'production', - 'stage', - ]).pipe(Flag.optional), + authorizationEnvironment: Flag.choice('authorization-environment', ['development', 'production', 'stage']).pipe( + Flag.optional, + ), authorizationNow: Flag.string('authorization-now').pipe(Flag.optional), baseRevision: Flag.string('base').pipe(Flag.optional), changedPaths: Flag.string('changed-path').pipe(Flag.atLeast(0)), headRevision: Flag.string('head').pipe(Flag.optional), }, - ({ - authorizationEnvironment, - authorizationNow, - baseRevision, - changedPaths, - headRevision, - }) => + ({ authorizationEnvironment, authorizationNow, baseRevision, changedPaths, headRevision }) => Effect.gen(function* deploymentImpactCommandEffect() { - const rootDirectory = yield* Config.string('PWD').pipe( - Effect.orElseSucceed(() => '.') - ); + const rootDirectory = yield* Config.string('PWD').pipe(Effect.orElseSucceed(() => '.')); const environment = Option.getOrUndefined(authorizationEnvironment); let authorizationPromotion: AuthorizationPromotionGateInput | undefined; if (environment !== undefined) { const configuredNow = Option.getOrUndefined(authorizationNow); const nowEpochMs = - configuredNow === undefined - ? yield* Clock.currentTimeMillis - : yield* parseAuthorizationNow(configuredNow); - authorizationPromotion = yield* loadAuthorizationPromotionGate( - rootDirectory, - environment, - nowEpochMs - ); + configuredNow === undefined ? yield* Clock.currentTimeMillis : yield* parseAuthorizationNow(configuredNow); + authorizationPromotion = yield* loadAuthorizationPromotionGate(rootDirectory, environment, nowEpochMs); } const options: PlanDeploymentImpactOptions = { baseRevision: Option.getOrUndefined(baseRevision), @@ -1370,15 +1124,13 @@ const deploymentImpactCommand = Command.make( if (Option.isSome(outputPath)) { yield* writeGitHubOutputs(plan, outputPath.value); } - }) + }), ); export const main = Command.run({ version: '1.0.0' })(deploymentImpactCommand); if (import.meta.main) { NodeRuntime.runMain( - Layer.build( - Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer)) - ).pipe(Effect.scoped) + Layer.build(Layer.effectDiscard(main).pipe(Layer.provide(NodeServices.layer))).pipe(Effect.scoped), ); } diff --git a/app/scripts/postgres/bootstrap-runtime-role.mts b/app/scripts/postgres/bootstrap-runtime-role.mts index 4230c46bd..b915676cf 100644 --- a/app/scripts/postgres/bootstrap-runtime-role.mts +++ b/app/scripts/postgres/bootstrap-runtime-role.mts @@ -4,22 +4,17 @@ import type { QueryResult, QueryResultRow } from 'pg'; import { loadDatabaseConnectionPair } from '../../packages/core-runtime/src/db/config.ts'; -class RuntimeRoleBootstrapError extends Schema.TaggedError()( - 'RuntimeRoleBootstrapError', - { - reason: Schema.String, - } -) {} +class RuntimeRoleBootstrapError extends Schema.TaggedError()('RuntimeRoleBootstrapError', { + reason: Schema.String, +}) {} -const quoteLiteral = (value: string): string => - `'${value.replaceAll("'", "''")}'`; -const quoteIdentifier = (value: string): string => - `"${value.replaceAll('"', '""')}"`; +const quoteLiteral = (value: string): string => `'${value.replaceAll("'", "''")}'`; +const quoteIdentifier = (value: string): string => `"${value.replaceAll('"', '""')}"`; const query = ( client: Client, text: string, - values?: unknown[] + values?: unknown[], ): Effect.Effect, RuntimeRoleBootstrapError> => Effect.tryPromise({ catch: (cause) => @@ -29,9 +24,7 @@ const query = ( try: async () => await client.query(text, values), }); -const connectAdmin = ( - connectionString: Redacted.Redacted -): Effect.Effect => +const connectAdmin = (connectionString: Redacted.Redacted): Effect.Effect => Effect.tryPromise({ catch: (cause) => new RuntimeRoleBootstrapError({ @@ -46,9 +39,7 @@ const connectAdmin = ( }, }); -const closeAdmin = ( - client: Client -): Effect.Effect => +const closeAdmin = (client: Client): Effect.Effect => Effect.tryPromise({ catch: (cause) => new RuntimeRoleBootstrapError({ @@ -60,26 +51,23 @@ const closeAdmin = ( const bootstrapRuntimeRole = ( client: Client, database: string, - password: Redacted.Redacted + password: Redacted.Redacted, ): Effect.Effect => Effect.gen(function* bootstrapRuntimeRoleEffect() { yield* query(client, 'begin'); const exists = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_roles where rolname = $1) as exists', - ['ontos_runtime'] + ['ontos_runtime'], ); const passwordLiteral = quoteLiteral(Redacted.value(password)); yield* query( client, exists.rows[0]?.exists ? `alter role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` - : `create role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` - ); - yield* query( - client, - `grant connect on database ${quoteIdentifier(database)} to ontos_runtime` + : `create role ontos_runtime login password ${passwordLiteral} nosuperuser nocreatedb nocreaterole noinherit nobypassrls`, ); + yield* query(client, `grant connect on database ${quoteIdentifier(database)} to ontos_runtime`); yield* Effect.forEach( ['core', 'auth', 'contacts', 'party'], (schema) => @@ -87,44 +75,34 @@ const bootstrapRuntimeRole = ( const schemaExists = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_namespace where nspname = $1) as exists', - [schema] + [schema], ); if (schemaExists.rows[0]?.exists) { + yield* query(client, `grant usage on schema ${schema} to ontos_runtime`); yield* query( client, - `grant usage on schema ${schema} to ontos_runtime` - ); - yield* query( - client, - `grant select, insert, update, delete on all tables in schema ${schema} to ontos_runtime` - ); - yield* query( - client, - `grant usage, select on all sequences in schema ${schema} to ontos_runtime` + `grant select, insert, update, delete on all tables in schema ${schema} to ontos_runtime`, ); + yield* query(client, `grant usage, select on all sequences in schema ${schema} to ontos_runtime`); yield* query( client, - `alter default privileges in schema ${schema} grant select, insert, update, delete on tables to ontos_runtime` + `alter default privileges in schema ${schema} grant select, insert, update, delete on tables to ontos_runtime`, ); yield* query( client, - `alter default privileges in schema ${schema} grant usage, select on sequences to ontos_runtime` + `alter default privileges in schema ${schema} grant usage, select on sequences to ontos_runtime`, ); } }), - { concurrency: 1, discard: true } + { concurrency: 1, discard: true }, ); const role = yield* query<{ rolbypassrls: boolean; rolsuper: boolean }>( client, 'select rolsuper, rolbypassrls from pg_catalog.pg_roles where rolname = $1', - ['ontos_runtime'] + ['ontos_runtime'], ); const [runtimeRole] = role.rows; - if ( - runtimeRole === undefined || - runtimeRole.rolsuper || - runtimeRole.rolbypassrls - ) { + if (runtimeRole === undefined || runtimeRole.rolsuper || runtimeRole.rolbypassrls) { yield* new RuntimeRoleBootstrapError({ reason: 'Runtime role must be non-superuser and must not bypass RLS', }); @@ -138,17 +116,15 @@ const main = Effect.gen(function* mainEffect() { (failure) => new RuntimeRoleBootstrapError({ reason: failure.reason, - }) - ) + }), + ), ); const password = yield* Effect.try({ catch: (cause) => new RuntimeRoleBootstrapError({ reason: `Unable to read the runtime PostgreSQL role credentials: ${String(cause)}`, }), - try: () => - new Client({ connectionString: connections.runtime.connectionString }) - .password, + try: () => new Client({ connectionString: connections.runtime.connectionString }).password, }); if (connections.runtime.user !== 'ontos_runtime') { yield* new RuntimeRoleBootstrapError({ @@ -163,17 +139,10 @@ const main = Effect.gen(function* mainEffect() { : Redacted.make(password); yield* Effect.acquireUseRelease( connectAdmin(Redacted.make(connections.admin.connectionString)), - (client) => - bootstrapRuntimeRole( - client, - connections.admin.database, - redactedPassword - ), - closeAdmin - ); - yield* Effect.sync(() => - console.log('Verified least-privilege PostgreSQL role ontos_runtime') + (client) => bootstrapRuntimeRole(client, connections.admin.database, redactedPassword), + closeAdmin, ); + yield* Effect.sync(() => console.log('Verified least-privilege PostgreSQL role ontos_runtime')); }).pipe(Effect.tapError((failure) => Effect.logError(failure.reason))); const exit = await Effect.runPromiseExit(main); diff --git a/app/scripts/postgres/bootstrap-spicedb-database.mts b/app/scripts/postgres/bootstrap-spicedb-database.mts index b92be5a77..c469de5c9 100644 --- a/app/scripts/postgres/bootstrap-spicedb-database.mts +++ b/app/scripts/postgres/bootstrap-spicedb-database.mts @@ -1,14 +1,5 @@ import { NodeFileSystem } from '@effect/platform-node'; -import { - Config, - ConfigProvider, - Console, - Effect, - Exit, - Match, - Redacted, - Schema, -} from 'effect'; +import { Config, ConfigProvider, Console, Effect, Exit, Match, Redacted, Schema } from 'effect'; import type { FileSystem } from 'effect'; import { Client } from 'pg'; import type { QueryResult, QueryResultRow } from 'pg'; @@ -22,28 +13,21 @@ class SpiceDbDatabaseBootstrapError extends Schema.TaggedError - new SpiceDbDatabaseBootstrapError( - cause === undefined ? { reason } : { cause, reason } - ); +const bootstrapFailure = (reason: string, cause?: unknown): SpiceDbDatabaseBootstrapError => + new SpiceDbDatabaseBootstrapError(cause === undefined ? { reason } : { cause, reason }); -const quoteLiteral = (value: string): string => - `'${value.replaceAll("'", "''")}'`; +const quoteLiteral = (value: string): string => `'${value.replaceAll("'", "''")}'`; const query = ( client: Client, text: string, - values?: unknown[] + values?: unknown[], ): Effect.Effect, SpiceDbDatabaseBootstrapError> => Effect.tryPromise({ - catch: (cause) => - bootstrapFailure('SpiceDB PostgreSQL bootstrap query failed', cause), + catch: (cause) => bootstrapFailure('SpiceDB PostgreSQL bootstrap query failed', cause), try: async () => await client.query(text, values), }); @@ -59,47 +43,23 @@ const loadRootConfiguration = (): Effect.Effect< }).pipe( Effect.catchTag('PlatformError', (failure) => Match.value(failure.reason).pipe( - Match.tag('NotFound', () => - Effect.succeed( - ConfigProvider.fromUnknown({}, { preserveEmptyStrings: true }) - ) - ), - Match.orElse(() => Effect.fail(failure)) - ) + Match.tag('NotFound', () => Effect.succeed(ConfigProvider.fromUnknown({}, { preserveEmptyStrings: true }))), + Match.orElse(() => Effect.fail(failure)), + ), ), - Effect.mapError((cause) => - bootstrapFailure( - `Unable to load the root environment from ${APP_ENV_PATH}`, - cause - ) - ) + Effect.mapError((cause) => bootstrapFailure(`Unable to load the root environment from ${APP_ENV_PATH}`, cause)), ); - const provider = ConfigProvider.orElse( - ConfigProvider.fromEnv({ preserveEmptyStrings: true }), - fileProvider - ); + const provider = ConfigProvider.orElse(ConfigProvider.fromEnv({ preserveEmptyStrings: true }), fileProvider); const [adminUrl, spiceDbUrl] = yield* Effect.all( - [ - Config.redacted('DATABASE_ADMIN_URL').parse(provider), - Config.redacted('SPICEDB_DATABASE_URL').parse(provider), - ], - { concurrency: 1 } + [Config.redacted('DATABASE_ADMIN_URL').parse(provider), Config.redacted('SPICEDB_DATABASE_URL').parse(provider)], + { concurrency: 1 }, ).pipe( - Effect.mapError((cause) => - bootstrapFailure( - 'SpiceDB PostgreSQL bootstrap configuration is invalid', - cause - ) - ) + Effect.mapError((cause) => bootstrapFailure('SpiceDB PostgreSQL bootstrap configuration is invalid', cause)), ); return yield* Effect.try({ - catch: (cause) => - bootstrapFailure( - 'SpiceDB PostgreSQL bootstrap configuration is invalid', - cause - ), + catch: (cause) => bootstrapFailure('SpiceDB PostgreSQL bootstrap configuration is invalid', cause), try: () => parseSpiceDbDatabaseBootstrapConfig({ DATABASE_ADMIN_URL: Redacted.value(adminUrl), @@ -108,15 +68,9 @@ const loadRootConfiguration = (): Effect.Effect< }); }); -const connectAdmin = ( - connectionString: Redacted.Redacted -): Effect.Effect => +const connectAdmin = (connectionString: Redacted.Redacted): Effect.Effect => Effect.tryPromise({ - catch: (cause) => - bootstrapFailure( - 'Unable to connect to the administrative PostgreSQL database', - cause - ), + catch: (cause) => bootstrapFailure('Unable to connect to the administrative PostgreSQL database', cause), try: async () => { const client = new Client({ connectionString: Redacted.value(connectionString), @@ -126,34 +80,28 @@ const connectAdmin = ( }, }); -const closeAdmin = ( - client: Client -): Effect.Effect => +const closeAdmin = (client: Client): Effect.Effect => Effect.tryPromise({ - catch: (cause) => - bootstrapFailure( - 'Unable to close the administrative PostgreSQL connection', - cause - ), + catch: (cause) => bootstrapFailure('Unable to close the administrative PostgreSQL connection', cause), try: async () => await client.end(), }); const bootstrapDatabase = ( client: Client, - configuration: SpiceDbDatabaseBootstrapConfig + configuration: SpiceDbDatabaseBootstrapConfig, ): Effect.Effect => Effect.gen(function* bootstrapDatabaseEffect() { const role = yield* query<{ exists: boolean }>( client, 'select exists(select 1 from pg_catalog.pg_roles where rolname = $1) as exists', - [configuration.user] + [configuration.user], ); const password = quoteLiteral(configuration.password); yield* query( client, (role.rows[0]?.exists ?? false) ? `alter role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` - : `create role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls` + : `create role spicedb login password ${password} nosuperuser nocreatedb nocreaterole noinherit nobypassrls`, ); const database = yield* query<{ owner: string }>( @@ -161,14 +109,12 @@ const bootstrapDatabase = ( `select pg_catalog.pg_get_userbyid(datdba) as owner from pg_catalog.pg_database where datname = $1`, - [configuration.database] + [configuration.database], ); if (database.rows.length === 0) { yield* query(client, 'create database spicedb owner spicedb'); } else if (database.rows[0]?.owner !== configuration.user) { - yield* bootstrapFailure( - 'Existing spicedb database must be owned by the spicedb role' - ); + yield* bootstrapFailure('Existing spicedb database must be owned by the spicedb role'); } }); @@ -177,14 +123,10 @@ const main = Effect.gen(function* mainEffect() { yield* Effect.acquireUseRelease( connectAdmin(Redacted.make(configuration.adminUrl)), (client) => bootstrapDatabase(client, configuration), - closeAdmin - ); - yield* Console.log( - 'Verified least-privilege PostgreSQL database and role for SpiceDB' + closeAdmin, ); + yield* Console.log('Verified least-privilege PostgreSQL database and role for SpiceDB'); }).pipe(Effect.tapError((failure) => Console.error(failure.reason))); -const exit = await Effect.runPromiseExit( - Effect.provide(main, NodeFileSystem.layer) -); +const exit = await Effect.runPromiseExit(Effect.provide(main, NodeFileSystem.layer)); process.exitCode = Exit.isFailure(exit) ? 1 : 0; diff --git a/app/scripts/prepare-dev-module-contract.mts b/app/scripts/prepare-dev-module-contract.mts index b71a9945f..839d256b6 100644 --- a/app/scripts/prepare-dev-module-contract.mts +++ b/app/scripts/prepare-dev-module-contract.mts @@ -10,31 +10,25 @@ class ModuleContractPreparationError extends Schema.TaggedError Effect.gen(function* prepareDevModuleContractProgram() { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; - const workspaceRoot = yield* pathService.fromFileUrl( - new URL('..', import.meta.url) - ); + const workspaceRoot = yield* pathService.fromFileUrl(new URL('..', import.meta.url)); const generated = yield* generateOntosModuleContract({ target: 'dist', vertical, @@ -45,37 +39,18 @@ const prepareDevModuleContractCommand = Command.make( new ModuleContractPreparationError({ cause, reason: `Unable to generate the ${vertical} development module contract`, - }) - ) - ); - const publicDirectory = pathService.join( - workspaceRoot, - 'verticals', - vertical, - '.dev-public' - ); - const contractDirectory = pathService.join( - publicDirectory, - '.well-known' + }), + ), ); + const publicDirectory = pathService.join(workspaceRoot, 'verticals', vertical, '.dev-public'); + const contractDirectory = pathService.join(publicDirectory, '.well-known'); yield* fileSystem.makeDirectory(contractDirectory, { recursive: true }); - yield* fileSystem.copyFile( - generated.path, - pathService.join(contractDirectory, 'ontos-module-manifest.json') - ); + yield* fileSystem.copyFile(generated.path, pathService.join(contractDirectory, 'ontos-module-manifest.json')); const headers = yield* fileSystem.readFileString( - pathService.join( - pathService.dirname(pathService.dirname(generated.path)), - '_headers' - ) - ); - yield* fileSystem.writeFileString( - pathService.join(publicDirectory, '_headers'), - headers - ); - yield* Console.log( - `Prepared the ${vertical} development module contract in ${publicDirectory}` + pathService.join(pathService.dirname(pathService.dirname(generated.path)), '_headers'), ); + yield* fileSystem.writeFileString(pathService.join(publicDirectory, '_headers'), headers); + yield* Console.log(`Prepared the ${vertical} development module contract in ${publicDirectory}`); }).pipe( Effect.mapError((cause) => Schema.is(ModuleContractPreparationError)(cause) @@ -83,9 +58,9 @@ const prepareDevModuleContractCommand = Command.make( : new ModuleContractPreparationError({ cause, reason: `Unable to prepare the ${vertical} development module contract`, - }) - ) - ) + }), + ), + ), ); const NodeServices = loadCoreNodeServices(); @@ -93,11 +68,9 @@ const NodeServices = loadCoreNodeServices(); const exit = await Effect.runPromiseExit( Command.run(prepareDevModuleContractCommand, { version: '1.0.0' }).pipe( Effect.tapError((failure) => - Schema.is(ModuleContractPreparationError)(failure) - ? Console.error(failure.message) - : Effect.void + Schema.is(ModuleContractPreparationError)(failure) ? Console.error(failure.message) : Effect.void, ), - Effect.provide(NodeServices.layer) - ) + Effect.provide(NodeServices.layer), + ), ); process.exitCode = Exit.isFailure(exit) ? 1 : 0; diff --git a/app/scripts/proof-cloudflare-version.mts b/app/scripts/proof-cloudflare-version.mts index 1d7cf1963..d09d05706 100644 --- a/app/scripts/proof-cloudflare-version.mts +++ b/app/scripts/proof-cloudflare-version.mts @@ -2,10 +2,7 @@ import { NodeServices } from '@effect/platform-node'; import { Effect } from 'effect'; -import { - runUltramodernScript, - ultramodernExitCode, -} from './shared/ultramodern-command.mts'; +import { runUltramodernScript, ultramodernExitCode } from './shared/ultramodern-command.mts'; import { ultramodernCommandFailure } from './ultramodern-command-failure.mts'; const exit = await Effect.runPromiseExit( @@ -15,6 +12,6 @@ const exit = await Effect.runPromiseExit( failure: ultramodernCommandFailure, moduleUrl: import.meta.url, nodeExecutable: process.execPath, - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped) + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); process.exitCode = ultramodernExitCode(exit); diff --git a/app/scripts/proof-node-backend-federation.mts b/app/scripts/proof-node-backend-federation.mts index 999914c22..1f3d1741a 100644 --- a/app/scripts/proof-node-backend-federation.mts +++ b/app/scripts/proof-node-backend-federation.mts @@ -1,23 +1,13 @@ #!/usr/bin/env node import { NodeServices } from '@effect/platform-node'; -import { - Config, - Console, - Effect, - Exit, - Option, - Path, - Predicate, - Schema, - Stdio, -} from 'effect'; +import { Config, Console, Effect, Exit, Option, Path, Predicate, Schema, Stdio } from 'effect'; import { ChildProcessSpawner } from 'effect/unstable/process'; import { ultramodernLaunch } from './shared/ultramodern-launch.mts'; class BackendFederationProofLaunchError extends Schema.TaggedError()( 'BackendFederationProofLaunchError', - { cause: Schema.Defect(), message: Schema.String } + { cause: Schema.Defect(), message: Schema.String }, ) {} const program = Effect.gen(function* backendFederationProofProgram() { @@ -26,23 +16,14 @@ const program = Effect.gen(function* backendFederationProofProgram() { const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const createBin = yield* Config.string('ULTRAMODERN_CREATE_BIN').pipe( Config.option, - Effect.map(Option.filter((value) => value.length > 0)) + Effect.map(Option.filter((value) => value.length > 0)), ); const forwardedArgs = yield* stdio.args; const workspaceRoot = yield* Config.string('ULTRAMODERN_WORKSPACE_ROOT').pipe( - Config.withDefault(path.resolve(import.meta.dirname, '..')) - ); - const ultramodernArgs = [ - 'ultramodern', - 'backend-federation-proof', - ...forwardedArgs, - ]; - const launch = ultramodernLaunch( - createBin, - ultramodernArgs, - workspaceRoot, - path.sep + Config.withDefault(path.resolve(import.meta.dirname, '..')), ); + const ultramodernArgs = ['ultramodern', 'backend-federation-proof', ...forwardedArgs]; + const launch = ultramodernLaunch(createBin, ultramodernArgs, workspaceRoot, path.sep); return yield* processSpawner.exitCode(launch.command).pipe( Effect.matchEffect({ onFailure: (cause) => { @@ -50,20 +31,18 @@ const program = Effect.gen(function* backendFederationProofProgram() { return Effect.succeed(1); } const launchCause = cause.reason.cause; - const causeMessage = Predicate.isError(launchCause) - ? launchCause.message - : cause.message; + const causeMessage = Predicate.isError(launchCause) ? launchCause.message : cause.message; return Effect.fail( new BackendFederationProofLaunchError({ cause, message: `Failed to launch ${launch.target} for UltraModern command "${ultramodernArgs .slice(1) .join(' ')}": ${causeMessage}`, - }) + }), ); }, onSuccess: (status) => Effect.succeed(Number(status)), - }) + }), ); }); @@ -71,8 +50,8 @@ const exit = await Effect.runPromiseExit( program.pipe( Effect.tapError((error) => Console.error(error.message)), Effect.provide(NodeServices.layer), - Effect.scoped - ) + Effect.scoped, + ), ); process.exitCode = Exit.match(exit, { diff --git a/app/scripts/proof-workerd-ssr.mts b/app/scripts/proof-workerd-ssr.mts index f93dbc555..15e5dd330 100644 --- a/app/scripts/proof-workerd-ssr.mts +++ b/app/scripts/proof-workerd-ssr.mts @@ -5,36 +5,16 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http'; import path from 'node:path'; import { NodeFileSystem } from '@effect/platform-node'; -import { - Array as EffectArray, - Config, - Effect, - Exit, - FileSystem, - ManagedRuntime, - Option, - Order, - Schema, -} from 'effect'; +import { Array as EffectArray, Config, Effect, Exit, FileSystem, ManagedRuntime, Option, Order, Schema } from 'effect'; import type { PlatformError } from 'effect/PlatformError'; import type { Scope } from 'effect/Scope'; -import { - Headers as MiniflareHeaders, - Log, - LogLevel, - Miniflare, - Response as MiniflareResponse, -} from 'miniflare'; -import type { - Request as MiniflareRequest, - RequestInit as MiniflareRequestInit, -} from 'miniflare'; +import { Headers as MiniflareHeaders, Log, LogLevel, Miniflare, Response as MiniflareResponse } from 'miniflare'; +import type { Request as MiniflareRequest, RequestInit as MiniflareRequestInit } from 'miniflare'; const DISTRIBUTED_SSR_FRAGMENT_REQUEST_HEADER = 'x-modern-js-fragment-request'; const APPLICATION_JSON_CONTENT_TYPE = 'application/json'; const CONTENT_TYPE_HEADER = 'content-type'; -const DEGRADED_BOUNDARY_MARKER = - 'data-modern-distributed-ssr-status="degraded"'; +const DEGRADED_BOUNDARY_MARKER = 'data-modern-distributed-ssr-status="degraded"'; const DISTRIBUTED_SSR_REQUIRED_HEADERS = [ 'x-modern-distributed-ssr-boundary-id', 'x-modern-distributed-ssr-expose', @@ -60,17 +40,13 @@ const WranglerSchema = Schema.Struct({ binding: Schema.optionalKey(Schema.String), directory: Schema.optionalKey(Schema.String), run_worker_first: Schema.optionalKey(Schema.Boolean), - }) + }), ), compatibility_date: Schema.optionalKey(Schema.String), compatibility_flags: Schema.optionalKey(Schema.Array(Schema.String)), main: Schema.optionalKey(Schema.String), name: Schema.String, - services: Schema.optionalKey( - Schema.Array( - Schema.Struct({ binding: Schema.String, service: Schema.String }) - ) - ), + services: Schema.optionalKey(Schema.Array(Schema.Struct({ binding: Schema.String, service: Schema.String }))), vars: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), }); const ArtifactSchema = Schema.Struct({ @@ -103,26 +79,18 @@ const ExecutionEnvelopeSchema = Schema.Struct({ target: Schema.String, }); const RawAppSchema = Schema.Struct({ - api: Schema.optionalKey( - Schema.Struct({ prefix: Schema.optionalKey(Schema.String) }) - ), - deliveryUnit: Schema.optionalKey( - Schema.Struct({ unitId: Schema.optionalKey(UnitIdSchema) }) - ), + api: Schema.optionalKey(Schema.Struct({ prefix: Schema.optionalKey(Schema.String) })), + deliveryUnit: Schema.optionalKey(Schema.Struct({ unitId: Schema.optionalKey(UnitIdSchema) })), deploy: Schema.optionalKey( Schema.Struct({ cloudflare: Schema.optionalKey( Schema.Struct({ - distributedSsrProofRoutes: Schema.optionalKey( - Schema.Array(Schema.String) - ), + distributedSsrProofRoutes: Schema.optionalKey(Schema.Array(Schema.String)), jsonSmokeChecks: Schema.optionalKey(Schema.Array(SmokeCheckSchema)), - routes: Schema.optionalKey( - Schema.Struct({ ssr: Schema.optionalKey(Schema.String) }) - ), - }) + routes: Schema.optionalKey(Schema.Struct({ ssr: Schema.optionalKey(Schema.String) })), + }), ), - }) + }), ), id: AppIdSchema, kind: Schema.optionalKey(Schema.String), @@ -130,25 +98,17 @@ const RawAppSchema = Schema.Struct({ Schema.Struct({ name: Schema.optionalKey(Schema.String), verticalRefs: Schema.optionalKey(Schema.Array(Schema.String)), - }) + }), ), path: Schema.optionalKey(Schema.String), port: Schema.Number, }); const CompactConfigSchema = Schema.Struct({ - topology: Schema.optionalKey( - Schema.Struct({ apps: Schema.optionalKey(Schema.Array(RawAppSchema)) }) - ), + topology: Schema.optionalKey(Schema.Struct({ apps: Schema.optionalKey(Schema.Array(RawAppSchema)) })), }); -const containsApiReleaseMarker = Schema.is( - Schema.Struct({ marker: ApiReleaseMarkerSchema }) -); -const isJsonScalar = Schema.is( - Schema.Union([Schema.Null, Schema.Boolean, Schema.Number, Schema.String]) -); -const findReleaseMarkers = ( - value: Schema.Json -): readonly ApiReleaseMarker[] => { +const containsApiReleaseMarker = Schema.is(Schema.Struct({ marker: ApiReleaseMarkerSchema })); +const isJsonScalar = Schema.is(Schema.Union([Schema.Null, Schema.Boolean, Schema.Number, Schema.String])); +const findReleaseMarkers = (value: Schema.Json): readonly ApiReleaseMarker[] => { if (isJsonScalar(value)) { return []; } @@ -163,19 +123,17 @@ const ServiceBindingFaultCommandSchema = Schema.Struct({ }); const FragmentPropsSchema = Schema.Record( Schema.String, - Schema.Union([Schema.Null, Schema.Boolean, Schema.Number, Schema.String]) + Schema.Union([Schema.Null, Schema.Boolean, Schema.Number, Schema.String]), ); const ServiceBindingFaultResponseSchema = Schema.fromJsonString( - Schema.Struct({ failed: Schema.Boolean, service: Schema.String }) -); -const TargetUrlsSchema = Schema.fromJsonString( - Schema.Record(Schema.String, Schema.String) + Schema.Struct({ failed: Schema.Boolean, service: Schema.String }), ); +const TargetUrlsSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.String)); -export class WorkerdProofError extends Schema.TaggedError()( - 'WorkerdProofError', - { cause: Schema.optionalKey(Schema.Defect()), message: Schema.String } -) {} +export class WorkerdProofError extends Schema.TaggedError()('WorkerdProofError', { + cause: Schema.optionalKey(Schema.Defect()), + message: Schema.String, +}) {} type SmokeCheck = typeof SmokeCheckSchema.Type; type Wrangler = typeof WranglerSchema.Type; @@ -184,20 +142,12 @@ type ReleaseEnvelopeIdentity = typeof ReleaseEnvelopeIdentitySchema.Type; type ApiReleaseMarker = typeof ApiReleaseMarkerSchema.Type; type JsonObject = typeof FragmentPropsSchema.Type; type ProofFailure = PlatformError | WorkerdProofError; -type ProofEffect = Effect.Effect< - Value, - ProofFailure, - FileSystem.FileSystem ->; -type ScopedProofEffect = Effect.Effect< - Value, - ProofFailure, - FileSystem.FileSystem | Scope ->; +type ProofEffect = Effect.Effect; +type ScopedProofEffect = Effect.Effect; // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Miniflare requires this foreign SDK service-binding callback. type ServiceBindingHandler = ( request: MiniflareRequest, - miniflare: Miniflare + miniflare: Miniflare, ) => MiniflareResponse | Promise; type ServiceBindings = Record; @@ -364,45 +314,31 @@ const proofError = (message: string, cause?: unknown) => { } return new WorkerdProofError({ cause, message }); }; -const ensure = ( - condition: boolean, - message: string -): Effect.Effect => +const ensure = (condition: boolean, message: string): Effect.Effect => condition ? Effect.void : Effect.fail(proofError(message)); -const sha256 = (bytes: Uint8Array) => - crypto.createHash('sha256').update(bytes).digest('hex'); +const sha256 = (bytes: Uint8Array) => crypto.createHash('sha256').update(bytes).digest('hex'); const count = (source: string, value: string) => source.split(value).length - 1; const normalizePath = (value: string) => value.replaceAll('\\', '/'); const adapterRuntime = ManagedRuntime.make(NodeFileSystem.layer); -const encodeJson = ( - value: Value -): Effect.Effect => +const encodeJson = (value: Value): Effect.Effect => Schema.encodeEffect(JsonTextSchema)(value).pipe( - Effect.mapError((cause) => proofError('Could not encode JSON', cause)) + Effect.mapError((cause) => proofError('Could not encode JSON', cause)), ); -const readJsonDocument = < - DocumentSchema extends Schema.ConstraintDecoder, ->( +const readJsonDocument = >( absolutePath: string, - schema: DocumentSchema + schema: DocumentSchema, ): ProofEffect => Effect.gen(function* readJsonDocumentEffect() { const fileSystem = yield* FileSystem.FileSystem; const source = yield* fileSystem.readFileString(absolutePath); - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))( - source - ).pipe( - Effect.mapError((cause) => - proofError(`Invalid JSON document ${absolutePath}`, cause) - ) + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(source).pipe( + Effect.mapError((cause) => proofError(`Invalid JSON document ${absolutePath}`, cause)), ); }); -const collectJavaScriptFiles = ( - absoluteDirectory: string -): ProofEffect => +const collectJavaScriptFiles = (absoluteDirectory: string): ProofEffect => Effect.gen(function* collectJavaScriptFilesEffect() { const fileSystem = yield* FileSystem.FileSystem; if (!(yield* fileSystem.exists(absoluteDirectory))) { @@ -418,19 +354,14 @@ const collectJavaScriptFiles = ( if (info.type === 'Directory') { return yield* collectJavaScriptFiles(absolutePath); } - return info.type === 'File' && /\.(?:c|m)?js$/u.test(name) - ? [absolutePath] - : []; + return info.type === 'File' && /\.(?:c|m)?js$/u.test(name) ? [absolutePath] : []; }), - { concurrency: 1 } + { concurrency: 1 }, ); return EffectArray.sort(nested.flat(), Order.String); }); -const createWorkerModules = ( - outputRoot: string, - main: string -): ProofEffect => +const createWorkerModules = (outputRoot: string, main: string): ProofEffect => Effect.gen(function* createWorkerModulesEffect() { const entryPath = path.resolve(outputRoot, main); const collected = yield* Effect.all( @@ -438,10 +369,10 @@ const createWorkerModules = ( collectJavaScriptFiles(path.join(outputRoot, 'server')), collectJavaScriptFiles(path.join(outputRoot, 'worker')), ], - { concurrency: 2 } + { concurrency: 2 }, ); const modulePaths = [entryPath, ...collected.flat()].filter( - (modulePath, index, paths) => paths.indexOf(modulePath) === index + (modulePath, index, paths) => paths.indexOf(modulePath) === index, ); return modulePaths.map((modulePath): WorkerModule => ({ path: modulePath, @@ -452,85 +383,45 @@ const createWorkerModules = ( const readExecutionEnvelope = ( appId: string, outputRoot: string, - expectedUnitId: string | undefined + expectedUnitId: string | undefined, ): ProofEffect<{ readonly envelope: ExecutionEnvelope; readonly envelopePath: string; }> => Effect.gen(function* readExecutionEnvelopeEffect() { const fileSystem = yield* FileSystem.FileSystem; - const envelopePath = path.join( - outputRoot, - 'release/microvertical-release-envelope.json' - ); - yield* ensure( - yield* fileSystem.exists(envelopePath), - `${appId} executed .output release envelope is missing` - ); - const envelope = yield* readJsonDocument( - envelopePath, - ExecutionEnvelopeSchema - ); - yield* ensure( - envelope.schemaVersion === 3, - `${appId} executed envelope schema must be 3` - ); - yield* ensure( - envelope.target === 'cloudflare', - `${appId} executed envelope must target cloudflare` - ); - yield* ensure( - expectedUnitId !== undefined && - expectedUnitId.length > 0 && - envelope.identity.unitId === expectedUnitId, - `${appId} executed envelope unit identity is invalid` - ); - yield* ensure( - /^[a-f\d]{64}$/u.test(envelope.envelopeDigest), - `${appId} executed envelope digest is invalid` - ); + const envelopePath = path.join(outputRoot, 'release/microvertical-release-envelope.json'); + yield* ensure(yield* fileSystem.exists(envelopePath), `${appId} executed .output release envelope is missing`); + const envelope = yield* readJsonDocument(envelopePath, ExecutionEnvelopeSchema); + yield* ensure(envelope.schemaVersion === 3, `${appId} executed envelope schema must be 3`); + yield* ensure(envelope.target === 'cloudflare', `${appId} executed envelope must target cloudflare`); yield* ensure( - envelope.artifacts.length > 0, - `${appId} executed envelope has no artifacts` + expectedUnitId !== undefined && expectedUnitId.length > 0 && envelope.identity.unitId === expectedUnitId, + `${appId} executed envelope unit identity is invalid`, ); + yield* ensure(/^[a-f\d]{64}$/u.test(envelope.envelopeDigest), `${appId} executed envelope digest is invalid`); + yield* ensure(envelope.artifacts.length > 0, `${appId} executed envelope has no artifacts`); return { envelope, envelopePath }; }); -const bindExecutedModule = ( - app: App, - envelope: ExecutionEnvelope, - module: WorkerModule -): ProofEffect => +const bindExecutedModule = (app: App, envelope: ExecutionEnvelope, module: WorkerModule): ProofEffect => Effect.gen(function* bindExecutedModuleEffect() { const fileSystem = yield* FileSystem.FileSystem; - const logicalPath = normalizePath( - path.relative(app.outputRoot, module.path) - ); + const logicalPath = normalizePath(path.relative(app.outputRoot, module.path)); yield* ensure( - logicalPath.length > 0 && - !logicalPath.startsWith('../') && - !path.posix.isAbsolute(logicalPath), - `${app.id} selected module escapes .output: ${logicalPath}` - ); - const artifact = envelope.artifacts.find( - (candidate) => candidate.logicalPath === logicalPath + logicalPath.length > 0 && !logicalPath.startsWith('../') && !path.posix.isAbsolute(logicalPath), + `${app.id} selected module escapes .output: ${logicalPath}`, ); + const artifact = envelope.artifacts.find((candidate) => candidate.logicalPath === logicalPath); if (artifact === undefined) { - return yield* Effect.fail( - proofError( - `${app.id} selected module ${logicalPath} is not envelope-bound` - ) - ); + return yield* Effect.fail(proofError(`${app.id} selected module ${logicalPath} is not envelope-bound`)); } - yield* ensure( - artifact.kind === 'file', - `${app.id} selected module ${logicalPath} is bound to a non-file artifact` - ); + yield* ensure(artifact.kind === 'file', `${app.id} selected module ${logicalPath} is bound to a non-file artifact`); const bytes = yield* fileSystem.readFile(module.path); const digest = sha256(bytes); yield* ensure( artifact.byteLength === bytes.byteLength && artifact.sha256 === digest, - `${app.id} selected module ${logicalPath} differs from its envelope artifact` + `${app.id} selected module ${logicalPath} differs from its envelope artifact`, ); return { byteLength: bytes.byteLength, @@ -541,29 +432,18 @@ const bindExecutedModule = ( }; }); -const resolveAppPath = ( - id: string, - kind: App['kind'], - configuredPath: string | undefined -) => { +const resolveAppPath = (id: string, kind: App['kind'], configuredPath: string | undefined) => { if (configuredPath !== undefined) { return normalizePath(configuredPath); } return kind === 'shell' ? 'apps/shell-super-app' : `verticals/${id}`; }; -const resolveProofRoutes = ( - configuredRoutes: readonly string[], - configuredSsrRoute: string | undefined -) => { - const proofRoutes = [ - ...new Set(configuredRoutes.filter((route) => route.startsWith('/'))), - ]; +const resolveProofRoutes = (configuredRoutes: readonly string[], configuredSsrRoute: string | undefined) => { + const proofRoutes = [...new Set(configuredRoutes.filter((route) => route.startsWith('/')))]; if (proofRoutes.length > 0) { return proofRoutes; } - return [ - configuredSsrRoute?.startsWith('/') === true ? configuredSsrRoute : '/', - ]; + return [configuredSsrRoute?.startsWith('/') === true ? configuredSsrRoute : '/']; }; const deriveAppConfiguration = (rawApp: typeof RawAppSchema.Type) => { @@ -573,10 +453,7 @@ const deriveAppConfiguration = (rawApp: typeof RawAppSchema.Type) => { id: rawApp.id, jsonSmokeChecks: cloudflare?.jsonSmokeChecks ?? [], port: rawApp.port, - proofRoutes: resolveProofRoutes( - cloudflare?.distributedSsrProofRoutes ?? [], - cloudflare?.routes?.ssr - ), + proofRoutes: resolveProofRoutes(cloudflare?.distributedSsrProofRoutes ?? [], cloudflare?.routes?.ssr), verticalRefs: rawApp.moduleFederation?.verticalRefs ?? [], }; }; @@ -586,32 +463,24 @@ const loadApps = (workspaceRoot: string): ProofEffect => const fileSystem = yield* FileSystem.FileSystem; const compactConfig = yield* readJsonDocument( path.join(workspaceRoot, '.modernjs/ultramodern.json'), - CompactConfigSchema + CompactConfigSchema, ); return yield* Effect.forEach( compactConfig.topology?.apps ?? [], (rawApp) => Effect.gen(function* loadAppEffect() { - const kind: App['kind'] = - rawApp.kind === 'vertical' ? 'vertical' : 'shell'; + const kind: App['kind'] = rawApp.kind === 'vertical' ? 'vertical' : 'shell'; const appPath = resolveAppPath(rawApp.id, kind, rawApp.path); const outputRoot = path.join(workspaceRoot, appPath, '.output'); const wranglerPath = path.join(outputRoot, 'wrangler.json'); yield* ensure( yield* fileSystem.exists(wranglerPath), - `${rawApp.id} Cloudflare output is missing; run pnpm cloudflare:build first` - ); - const wrangler = yield* readJsonDocument( - wranglerPath, - WranglerSchema + `${rawApp.id} Cloudflare output is missing; run pnpm cloudflare:build first`, ); + const wrangler = yield* readJsonDocument(wranglerPath, WranglerSchema); const executedEnvelope = kind === 'vertical' - ? yield* readExecutionEnvelope( - rawApp.id, - outputRoot, - rawApp.deliveryUnit?.unitId - ) + ? yield* readExecutionEnvelope(rawApp.id, outputRoot, rawApp.deliveryUnit?.unitId) : undefined; return { ...deriveAppConfiguration(rawApp), @@ -622,22 +491,20 @@ const loadApps = (workspaceRoot: string): ProofEffect => wrangler, }; }), - { concurrency: 1 } + { concurrency: 1 }, ); }); const workerName = (app: App): Effect.Effect => app.wrangler.name.length > 0 ? Effect.succeed(app.wrangler.name) - : Effect.fail( - proofError(`${app.id} wrangler output must define a worker name`) - ); + : Effect.fail(proofError(`${app.id} wrangler output must define a worker name`)); const createWorkerConfiguration = ( app: App, workspaceRoot: string, outboundService: ServiceBindingHandler, - serviceBindings: ServiceBindings + serviceBindings: ServiceBindings, ): ProofEffect => Effect.gen(function* createWorkerConfigurationEffect() { const fileSystem = yield* FileSystem.FileSystem; @@ -652,51 +519,38 @@ const createWorkerConfiguration = ( const bytes = yield* fileSystem.readFile(module.path); return { byteLength: bytes.byteLength, - logicalPath: normalizePath( - path.relative(app.outputRoot, module.path) - ), + logicalPath: normalizePath(path.relative(app.outputRoot, module.path)), runtime: 'workerd', sha256: sha256(bytes), type: module.type, }; }) : bindExecutedModule(app, app.envelope, module), - { concurrency: 1 } - ); - const mainLogicalPath = normalizePath( - path.relative(app.outputRoot, path.resolve(app.outputRoot, main)) + { concurrency: 1 }, ); + const mainLogicalPath = normalizePath(path.relative(app.outputRoot, path.resolve(app.outputRoot, main))); yield* ensure( boundModules.some((module) => module.logicalPath === mainLogicalPath), - `${app.id} Miniflare main ${mainLogicalPath} is not in the selected module set` + `${app.id} Miniflare main ${mainLogicalPath} is not in the selected module set`, ); - const validateSelectedSurfaces = Effect.gen( - function* validateSelectedSurfacesEffect() { - const apiBackend = app.envelope?.surfaces.apiBackend ?? []; - const ssr = app.envelope?.surfaces.ssr ?? []; - const selectedPaths = new Set( - boundModules.map((module) => module.logicalPath) - ); - yield* ensure( - app.kind !== 'vertical' || - (apiBackend.length > 0 && - apiBackend.every((logicalPath) => - selectedPaths.has(logicalPath) - )), - `${app.id} BFF worker surface is not selected by Miniflare` - ); - yield* ensure( - app.kind !== 'vertical' || - (ssr.includes(mainLogicalPath) && - boundModules.every((module) => - [...ssr, ...apiBackend].includes(module.logicalPath) - )), - `${app.id} Miniflare main/SSR modules are not envelope-bound SSR surfaces` - ); + const validateSelectedSurfaces = Effect.gen(function* validateSelectedSurfacesEffect() { + const apiBackend = app.envelope?.surfaces.apiBackend ?? []; + const ssr = app.envelope?.surfaces.ssr ?? []; + const selectedPaths = new Set(boundModules.map((module) => module.logicalPath)); + yield* ensure( + app.kind !== 'vertical' || + (apiBackend.length > 0 && apiBackend.every((logicalPath) => selectedPaths.has(logicalPath))), + `${app.id} BFF worker surface is not selected by Miniflare`, + ); + yield* ensure( + app.kind !== 'vertical' || + (ssr.includes(mainLogicalPath) && + boundModules.every((module) => [...ssr, ...apiBackend].includes(module.logicalPath))), + `${app.id} Miniflare main/SSR modules are not envelope-bound SSR surfaces`, + ); - return { apiBackend }; - } - ); + return { apiBackend }; + }); const { apiBackend } = yield* validateSelectedSurfaces; const name = yield* workerName(app); const options: ProofWorkerOptions = { @@ -711,8 +565,7 @@ const createWorkerConfiguration = ( }, bindings: { ...app.wrangler.vars, - DATABASE_URL: - 'postgresql://workerd-proof:workerd-proof@127.0.0.1:5432/workerd-proof', + DATABASE_URL: 'postgresql://workerd-proof:workerd-proof@127.0.0.1:5432/workerd-proof', SPICEDB_ENDPOINT: '127.0.0.1:50051', SPICEDB_INSECURE: 'true', SPICEDB_PRESHARED_KEY: 'workerd-proof', @@ -731,55 +584,38 @@ const createWorkerConfiguration = ( appId: app.id, envelopeDigest: app.envelope?.envelopeDigest ?? null, envelopePath: - app.envelopePath === undefined - ? null - : normalizePath(path.relative(workspaceRoot, app.envelopePath)), + app.envelopePath === undefined ? null : normalizePath(path.relative(workspaceRoot, app.envelopePath)), identity: app.envelope?.identity ?? null, main: mainLogicalPath, modules: boundModules, - modulesRoot: normalizePath( - path.relative(workspaceRoot, app.outputRoot) - ), + modulesRoot: normalizePath(path.relative(workspaceRoot, app.outputRoot)), worker: name, }, options, }; }); -const responseEvidence = ( - app: App, - response: MiniflareResponse -): Effect.Effect => +const responseEvidence = (app: App, response: MiniflareResponse): Effect.Effect => Effect.gen(function* responseEvidenceEffect() { const arrayBuffer = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${app.id} API response body could not be read`, cause), + catch: (cause) => proofError(`${app.id} API response body could not be read`, cause), try: async () => await response.arrayBuffer(), }); const bytes = Buffer.from(arrayBuffer); const source = bytes.toString('utf-8'); - const body = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(Schema.Json) - )(source).pipe( - Effect.mapError((cause) => - proofError(`${app.id} API response is not valid JSON`, cause) - ) + const body = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))(source).pipe( + Effect.mapError((cause) => proofError(`${app.id} API response is not valid JSON`, cause)), ); const marker = findReleaseMarkers(body).find( (candidate) => candidate.appId === app.id && candidate.build === app.envelope?.identity.buildMarker && - candidate.version === app.envelope.identity.releaseVersion + candidate.version === app.envelope.identity.releaseVersion, ); if (marker === undefined) { - return yield* proofError( - `${app.id} API response is not tied to its executed release identity` - ); + return yield* proofError(`${app.id} API response is not tied to its executed release identity`); } - yield* ensure( - response.ok, - `${app.id} API response returned HTTP ${response.status}` - ); + yield* ensure(response.ok, `${app.id} API response returned HTTP ${response.status}`); return { bodyBase64: bytes.toString('base64'), byteLength: bytes.byteLength, @@ -797,21 +633,16 @@ const encodeSmokeCheckIdentity = Schema.encodeEffect( id: Schema.optional(Schema.String), method: Schema.String, route: Schema.String, - }) - ) + }), + ), ); -const resolveApiSmokeChecks = ( - app: App, - shell: App -): Effect.Effect => +const resolveApiSmokeChecks = (app: App, shell: App): Effect.Effect => Effect.gen(function* resolveApiSmokeChecksEffect() { const shellChecks = app.apiPrefix?.startsWith('/') === true ? shell.jsonSmokeChecks.filter( - (check) => - check.route === app.apiPrefix || - check.route.startsWith(`${app.apiPrefix}/`) + (check) => check.route === app.apiPrefix || check.route.startsWith(`${app.apiPrefix}/`), ) : []; const uniqueChecks = new Map(); @@ -822,11 +653,7 @@ const resolveApiSmokeChecks = ( id: check.id, method: (check.method ?? 'GET').toUpperCase(), route: check.route, - }).pipe( - Effect.mapError((cause) => - proofError(`${app.id} smoke identity could not be encoded`, cause) - ) - ); + }).pipe(Effect.mapError((cause) => proofError(`${app.id} smoke identity could not be encoded`, cause))); if (!uniqueChecks.has(key)) { uniqueChecks.set(key, check); } @@ -841,7 +668,7 @@ const runApiCheck = ( check: SmokeCheck, miniflare: Miniflare, shellWorkerName: string, - targetEvidence: ExecutionEvidence + targetEvidence: ExecutionEvidence, ): Effect.Effect => Effect.gen(function* runApiCheckEffect() { const method = (check.method ?? 'GET').toUpperCase(); @@ -850,9 +677,7 @@ const runApiCheck = ( check.body === undefined ? undefined : yield* Schema.encodeEffect(JsonTextSchema)(check.body).pipe( - Effect.mapError((cause) => - proofError(`${app.id} smoke body is invalid`, cause) - ) + Effect.mapError((cause) => proofError(`${app.id} smoke body is invalid`, cause)), ); if (body !== undefined) { headers.set(CONTENT_TYPE_HEADER, APPLICATION_JSON_CONTENT_TYPE); @@ -862,33 +687,20 @@ const runApiCheck = ( init.body = body; } const target = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${app.id} worker could not be resolved`, cause), + catch: (cause) => proofError(`${app.id} worker could not be resolved`, cause), try: async () => await miniflare.getWorker(appWorkerName), }); const directResponse = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${app.id} direct API request failed`, cause), - try: async () => - await target.fetch( - `https://${appWorkerName}.invalid${check.route}`, - init - ), + catch: (cause) => proofError(`${app.id} direct API request failed`, cause), + try: async () => await target.fetch(`https://${appWorkerName}.invalid${check.route}`, init), }); const direct = yield* responseEvidence(app, directResponse); const shellResponse = yield* Effect.tryPromise({ catch: (cause) => proofError(`${app.id} Shell API request failed`, cause), - try: async () => - await miniflare.dispatchFetch( - `https://${shellWorkerName}.invalid${check.route}`, - init - ), + try: async () => await miniflare.dispatchFetch(`https://${shellWorkerName}.invalid${check.route}`, init), }); const throughShell = yield* responseEvidence(app, shellResponse); - yield* ensure( - direct.sha256 === throughShell.sha256, - `${app.id} direct and service-binding API responses differ` - ); + yield* ensure(direct.sha256 === throughShell.sha256, `${app.id} direct and service-binding API responses differ`); return { appId: app.id, binding, @@ -909,38 +721,22 @@ const runAppApiProofs = ( app: App, miniflare: Miniflare, shell: App, - executionByAppId: ReadonlyMap + executionByAppId: ReadonlyMap, ): Effect.Effect => Effect.gen(function* runAppApiProofsEffect() { const checks = yield* resolveApiSmokeChecks(app, shell); - yield* ensure( - checks.length > 0, - `${app.id} has no real Cloudflare API smoke check` - ); + yield* ensure(checks.length > 0, `${app.id} has no real Cloudflare API smoke check`); const appWorkerName = yield* workerName(app); const shellWorkerName = yield* workerName(shell); - const binding = (shell.wrangler.services ?? []).find( - (candidate) => candidate.service === appWorkerName - ); + const binding = (shell.wrangler.services ?? []).find((candidate) => candidate.service === appWorkerName); const targetEvidence = executionByAppId.get(app.id); if (binding === undefined || targetEvidence === undefined) { - return yield* Effect.fail( - proofError(`${app.id} service-binding evidence is missing`) - ); + return yield* Effect.fail(proofError(`${app.id} service-binding evidence is missing`)); } return yield* Effect.forEach( checks, - (check) => - runApiCheck( - app, - appWorkerName, - binding.binding, - check, - miniflare, - shellWorkerName, - targetEvidence - ), - { concurrency: 1 } + (check) => runApiCheck(app, appWorkerName, binding.binding, check, miniflare, shellWorkerName, targetEvidence), + { concurrency: 1 }, ); }); @@ -948,33 +744,25 @@ const runApiProofs = ( apps: readonly App[], miniflare: Miniflare, shell: App, - executionByAppId: ReadonlyMap + executionByAppId: ReadonlyMap, ): Effect.Effect => Effect.forEach( apps.filter((candidate) => candidate.kind === 'vertical'), (app) => runAppApiProofs(app, miniflare, shell, executionByAppId), - { concurrency: 1 } + { concurrency: 1 }, ).pipe(Effect.map((nested) => nested.flat())); -const readRequestBody = ( - request: IncomingMessage -): Effect.Effect, WorkerdProofError> => +const readRequestBody = (request: IncomingMessage): Effect.Effect, WorkerdProofError> => Effect.callback((resume) => { const chunks: Buffer[] = []; const onData = (chunk: Buffer | string) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }; const onEnd = () => { - resume( - Effect.succeed( - chunks.length > 0 ? Option.some(Buffer.concat(chunks)) : Option.none() - ) - ); + resume(Effect.succeed(chunks.length > 0 ? Option.some(Buffer.concat(chunks)) : Option.none())); }; const onError = (cause: Error) => { - resume( - Effect.fail(proofError('Could not read incoming proof request', cause)) - ); + resume(Effect.fail(proofError('Could not read incoming proof request', cause))); }; request.on('data', onData); request.once('end', onEnd); @@ -985,15 +773,10 @@ const readRequestBody = ( request.off('error', onError); }); }); -const listen = ( - server: Server, - port: number -): Effect.Effect => +const listen = (server: Server, port: number): Effect.Effect => Effect.callback((resume) => { const onError = (cause: Error) => { - resume( - Effect.fail(proofError(`Could not listen on proof port ${port}`, cause)) - ); + resume(Effect.fail(proofError(`Could not listen on proof port ${port}`, cause))); }; server.once('error', onError); server.listen(port, '127.0.0.1', () => { @@ -1029,27 +812,17 @@ const handleTargetRequest = ( runtime: Miniflare, failedServices: Set, incoming: IncomingMessage, - outgoing: ServerResponse + outgoing: ServerResponse, ): Effect.Effect => Effect.gen(function* handleTargetRequestEffect() { const body = Option.getOrUndefined(yield* readRequestBody(incoming)); - if ( - incoming.method === 'POST' && - incoming.url === '/_ultramodern-proof/service-binding-fault' - ) { - const command = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(ServiceBindingFaultCommandSchema) - )(body?.toString('utf-8') ?? '{}').pipe( - Effect.mapError((cause) => - proofError('Invalid service-binding fault command', cause) - ) - ); + if (incoming.method === 'POST' && incoming.url === '/_ultramodern-proof/service-binding-fault') { + const command = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ServiceBindingFaultCommandSchema))( + body?.toString('utf-8') ?? '{}', + ).pipe(Effect.mapError((cause) => proofError('Invalid service-binding fault command', cause))); const targetApp = yield* Effect.fromOption( - Option.fromNullishOr( - apps.find((candidate) => candidate.id === command.appId) - ), - () => - proofError(`Unknown service-binding fault target ${command.appId}`) + Option.fromNullishOr(apps.find((candidate) => candidate.id === command.appId)), + () => proofError(`Unknown service-binding fault target ${command.appId}`), ); const service = yield* workerName(targetApp); if (command.failed) { @@ -1057,16 +830,10 @@ const handleTargetRequest = ( } else { failedServices.delete(service); } - const encoded = yield* Schema.encodeEffect( - ServiceBindingFaultResponseSchema - )({ + const encoded = yield* Schema.encodeEffect(ServiceBindingFaultResponseSchema)({ failed: failedServices.has(service), service, - }).pipe( - Effect.mapError((cause) => - proofError('Could not encode fault response', cause) - ) - ); + }).pipe(Effect.mapError((cause) => proofError('Could not encode fault response', cause))); yield* Effect.sync(() => { outgoing.writeHead(200, { [CONTENT_TYPE_HEADER]: APPLICATION_JSON_CONTENT_TYPE, @@ -1084,21 +851,14 @@ const handleTargetRequest = ( } const response = yield* Effect.tryPromise({ catch: (cause) => proofError(`${app.id} target dispatch failed`, cause), - try: async () => - await runtime.dispatchFetch( - `https://${name}.invalid${incoming.url ?? '/'}`, - init - ), + try: async () => await runtime.dispatchFetch(`https://${name}.invalid${incoming.url ?? '/'}`, init), }); const bytes = yield* Effect.tryPromise({ catch: (cause) => proofError('Could not read Worker response', cause), try: async () => await response.arrayBuffer(), }); yield* Effect.sync(() => { - outgoing.writeHead( - response.status, - Object.fromEntries(response.headers.entries()) - ); + outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries())); outgoing.end(Buffer.from(bytes)); }); } @@ -1111,30 +871,16 @@ const handleTargetRequest = ( [CONTENT_TYPE_HEADER]: 'text/plain; charset=utf-8', }); outgoing.end('Workerd proof request failed'); - }) - ) - ) - ) + }), + ), + ), + ), ); const createTargetRequestListener = - ( - apps: readonly App[], - app: App, - runtime: Miniflare, - failedServices: Set - ) => + (apps: readonly App[], app: App, runtime: Miniflare, failedServices: Set) => (incoming: IncomingMessage, outgoing: ServerResponse): void => { - adapterRuntime.runCallback( - handleTargetRequest( - apps, - app, - runtime, - failedServices, - incoming, - outgoing - ) - ); + adapterRuntime.runCallback(handleTargetRequest(apps, app, runtime, failedServices, incoming, outgoing)); }; const startTargetServer = ( @@ -1142,17 +888,15 @@ const startTargetServer = ( app: App, configuration: ProofWorkerOptions | undefined, failedServices: Set, - miniflare: Miniflare + miniflare: Miniflare, ): Effect.Effect => Effect.gen(function* startTargetServerEffect() { yield* ensure( Number.isInteger(app.port) && app.port > 0, - `${app.id} requires a configured local port for all-workerd browser proof` + `${app.id} requires a configured local port for all-workerd browser proof`, ); if (configuration === undefined) { - return yield* Effect.fail( - proofError(`${app.id} Worker configuration is missing`) - ); + return yield* Effect.fail(proofError(`${app.id} Worker configuration is missing`)); } const runtime = app.kind === 'vertical' @@ -1161,9 +905,7 @@ const startTargetServer = ( workers: [configuration], }) : miniflare; - const server = http.createServer( - createTargetRequestListener(apps, app, runtime, failedServices) - ); + const server = http.createServer(createTargetRequestListener(apps, app, runtime, failedServices)); yield* listen(server, app.port); return { app, @@ -1172,9 +914,7 @@ const startTargetServer = ( }; }); -const disposeTargetRuntime = ( - runtime: Option.Option -): Effect.Effect => { +const disposeTargetRuntime = (runtime: Option.Option): Effect.Effect => { if (Option.isNone(runtime)) { return Effect.void; } @@ -1188,61 +928,37 @@ const startWorkerdTargetServers = ( apps: readonly App[], miniflare: Miniflare, failedServices: Set, - workerConfigurations: readonly ProofWorkerOptions[] + workerConfigurations: readonly ProofWorkerOptions[], ): Effect.Effect => Effect.gen(function* startWorkerdTargetServersEffect() { const started = yield* Effect.forEach( apps, - (app, index) => - startTargetServer( - apps, - app, - workerConfigurations[index], - failedServices, - miniflare - ), - { concurrency: 1 } - ); - const targetUrls = Object.fromEntries( - started.map(({ app }) => [app.id, `http://127.0.0.1:${app.port}`]) - ); - const runtimeDisposals = started.map(({ runtime }) => - disposeTargetRuntime(runtime) - ); - const stop = Effect.all( - [ - ...started.map(({ server }) => closeServer(server)), - ...runtimeDisposals, - ], - { concurrency: 'unbounded' } - ).pipe(Effect.asVoid); + (app, index) => startTargetServer(apps, app, workerConfigurations[index], failedServices, miniflare), + { concurrency: 1 }, + ); + const targetUrls = Object.fromEntries(started.map(({ app }) => [app.id, `http://127.0.0.1:${app.port}`])); + const runtimeDisposals = started.map(({ runtime }) => disposeTargetRuntime(runtime)); + const stop = Effect.all([...started.map(({ server }) => closeServer(server)), ...runtimeDisposals], { + concurrency: 'unbounded', + }).pipe(Effect.asVoid); return { stop, targetUrls }; }); const readAttribute = (tag: string, name: string) => { const escapedName = name.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`); - const match = new RegExp( - `\\s${escapedName}=(?:"([^"]*)"|'([^']*)')`, - 'u' - ).exec(tag); + const match = new RegExp(`\\s${escapedName}=(?:"([^"]*)"|'([^']*)')`, 'u').exec(tag); return match?.[1] ?? match?.[2]; }; -const collectDistributedBoundaries = ( - html: string -): Effect.Effect => +const collectDistributedBoundaries = (html: string): Effect.Effect => Effect.forEach( - html.matchAll( - /<[a-z][^>]*data-modern-distributed-ssr-boundary=(?:"[^"]+"|'[^']+')[^>]*>/giu - ), + html.matchAll(/<[a-z][^>]*data-modern-distributed-ssr-boundary=(?:"[^"]+"|'[^']+')[^>]*>/giu), (match) => Effect.gen(function* collectBoundaryEffect() { const [tag] = match; const key = readAttribute(tag, 'data-modern-distributed-ssr-boundary'); const separator = key?.indexOf('::') ?? -1; if (key === undefined || separator <= 0) { - return yield* Effect.fail( - proofError(`Invalid distributed SSR boundary key ${key}`) - ); + return yield* Effect.fail(proofError(`Invalid distributed SSR boundary key ${key}`)); } return { buildMarker: readAttribute(tag, 'data-modern-distributed-ssr-build'), @@ -1253,32 +969,26 @@ const collectDistributedBoundaries = ( status: readAttribute(tag, 'data-modern-distributed-ssr-status'), }; }), - { concurrency: 1 } + { concurrency: 1 }, ); const collectStylesheetHrefs = (html: string): readonly string[] => [...html.matchAll(/]*>/giu)] - .filter( - (match) => - readAttribute(match[0], 'rel')?.split(/\s+/u).includes('stylesheet') === - true - ) + .filter((match) => readAttribute(match[0], 'rel')?.split(/\s+/u).includes('stylesheet') === true) .map((match) => readAttribute(match[0], 'href')) .filter((href): href is string => href !== undefined); const isDistributedSsrFragmentRequest = (request: MiniflareRequest) => request.headers.get(DISTRIBUTED_SSR_FRAGMENT_REQUEST_HEADER) === '1'; const readRequiredFragmentHeader = ( request: MiniflareRequest, - header: string + header: string, ): Effect.Effect => { const value = request.headers.get(header); return value === null || value.length === 0 - ? Effect.fail( - proofError(`Distributed SSR fragment request is missing ${header}`) - ) + ? Effect.fail(proofError(`Distributed SSR fragment request is missing ${header}`)) : Effect.succeed(value); }; const decodeDistributedSsrFragmentRequest = ( - request: MiniflareRequest + request: MiniflareRequest, ): Effect.Effect< { readonly boundaryId: string; @@ -1292,23 +1002,18 @@ const decodeDistributedSsrFragmentRequest = ( Effect.gen(function* decodeDistributedSsrFragmentRequestEffect() { yield* ensure( isDistributedSsrFragmentRequest(request), - 'Distributed SSR fragment request is missing its request marker' + 'Distributed SSR fragment request is missing its request marker', ); yield* ensure( request.method === 'GET', - `Distributed SSR fragment request must use GET, received ${request.method}` + `Distributed SSR fragment request must use GET, received ${request.method}`, ); const values = yield* Effect.forEach( DISTRIBUTED_SSR_REQUIRED_HEADERS, (header) => readRequiredFragmentHeader(request, header), - { concurrency: 5 } - ); - const headers = new Map( - DISTRIBUTED_SSR_REQUIRED_HEADERS.map((header, index) => [ - header, - values[index], - ]) + { concurrency: 5 }, ); + const headers = new Map(DISTRIBUTED_SSR_REQUIRED_HEADERS.map((header, index) => [header, values[index]])); const propsSource = headers.get('x-modern-distributed-ssr-props'); const sourceUrl = headers.get('x-modern-distributed-ssr-source-url'); const boundaryId = headers.get('x-modern-distributed-ssr-boundary-id'); @@ -1321,21 +1026,12 @@ const decodeDistributedSsrFragmentRequest = ( expose === undefined || remote === undefined ) { - return yield* Effect.fail( - proofError('Distributed SSR fragment headers are incomplete') - ); + return yield* Effect.fail(proofError('Distributed SSR fragment headers are incomplete')); } - const props = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(FragmentPropsSchema) - )(decodeURIComponent(propsSource)).pipe( - Effect.mapError((cause) => - proofError('Fragment props must be an object', cause) - ) - ); - yield* ensure( - URL.canParse(sourceUrl), - 'Distributed SSR fragment source URL must be absolute' - ); + const props = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(FragmentPropsSchema))( + decodeURIComponent(propsSource), + ).pipe(Effect.mapError((cause) => proofError('Fragment props must be an object', cause))); + yield* ensure(URL.canParse(sourceUrl), 'Distributed SSR fragment source URL must be absolute'); return { boundaryId, expose, props, remote, sourceUrl }; }); @@ -1346,14 +1042,12 @@ const handleServiceBinding = ( fragmentBindingRequests: FragmentBindingRequest[], miniflare: Miniflare, request: MiniflareRequest, - service: ServiceReference + service: ServiceReference, ): Effect.Effect => Effect.gen(function* serviceBindingEffect() { if (failedServices.has(service.service)) { return yield* Effect.fail( - proofError( - `Injected unavailable service binding ${service.binding} -> ${service.service}` - ) + proofError(`Injected unavailable service binding ${service.binding} -> ${service.service}`), ); } const requestUrl = new URL(request.url); @@ -1388,13 +1082,11 @@ const handleServiceBinding = ( apiBindingRequests.push(apiBindingRequest); } const target = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`Could not resolve service ${service.service}`, cause), + catch: (cause) => proofError(`Could not resolve service ${service.service}`, cause), try: async () => await miniflare.getWorker(service.service), }); const response = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`Service ${service.service} request failed`, cause), + catch: (cause) => proofError(`Service ${service.service} request failed`, cause), try: async () => await target.fetch(request), }); if (apiBindingRequest !== undefined) { @@ -1412,7 +1104,7 @@ const createServiceBindingHandler = apiBindingRequests: ApiBindingRequest[], failedServices: Set, fragmentBindingRequests: FragmentBindingRequest[], - service: ServiceReference + service: ServiceReference, ): ServiceBindingHandler => (request, miniflare): MiniflareResponse | Promise => adapterRuntime.runPromise( @@ -1423,56 +1115,46 @@ const createServiceBindingHandler = fragmentBindingRequests, miniflare, request, - service - ) + service, + ), ); const createServiceBindings = ( caller: App, apiBindingRequests: ApiBindingRequest[], failedServices: Set, - fragmentBindingRequests: FragmentBindingRequest[] + fragmentBindingRequests: FragmentBindingRequest[], ): ServiceBindings => Object.fromEntries( (caller.wrangler.services ?? []).map((service) => [ service.binding, - createServiceBindingHandler( - caller, - apiBindingRequests, - failedServices, - fragmentBindingRequests, - service - ), - ]) + createServiceBindingHandler(caller, apiBindingRequests, failedServices, fragmentBindingRequests, service), + ]), ); -const waitForTerminationSignal: Effect.Effect = Effect.callback( - (resume) => { - const done = () => { - resume(Effect.void); - }; - process.once('SIGINT', done); - process.once('SIGTERM', done); - return Effect.sync(() => { - process.off('SIGINT', done); - process.off('SIGTERM', done); - }); - } -); +const waitForTerminationSignal: Effect.Effect = Effect.callback((resume) => { + const done = () => { + resume(Effect.void); + }; + process.once('SIGINT', done); + process.once('SIGTERM', done); + return Effect.sync(() => { + process.off('SIGINT', done); + process.off('SIGTERM', done); + }); +}); const writeReport = ( reportPath: string, apiProofs: readonly ApiProof[], executions: readonly ExecutionEvidence[], proofs: readonly ShellProof[], - remoteProofs: readonly RemoteProof[] + remoteProofs: readonly RemoteProof[], ): ProofEffect => Effect.gen(function* writeReportEffect() { const fileSystem = yield* FileSystem.FileSystem; const routes = proofs.flatMap((proof) => { - const decoded = Schema.decodeUnknownOption( - Schema.Struct({ route: Schema.String }) - )(proof); + const decoded = Schema.decodeUnknownOption(Schema.Struct({ route: Schema.String }))(proof); return Option.match(decoded, { onNone: () => [], onSome: ({ route }) => [route], @@ -1510,42 +1192,33 @@ const proveBoundary = ( renderedRemoteIds: Set, route: string, routeFragmentBindingRequests: readonly FragmentBindingRequest[], - shell: App + shell: App, ): Effect.Effect => Effect.gen(function* proveBoundaryEffect() { renderedRemoteIds.add(boundary.remote); - yield* ensure( - boundary.status === 'ready', - `${shell.id} did not mark ${boundary.key} as ready for ${route}` - ); + yield* ensure(boundary.status === 'ready', `${shell.id} did not mark ${boundary.key} as ready for ${route}`); yield* ensure( boundary.buildMarker !== undefined && boundary.buildMarker.length > 0, - `${shell.id} ${boundary.key} is missing immutable build provenance` + `${shell.id} ${boundary.key} is missing immutable build provenance`, ); yield* ensure( /^[a-f\d]{64}$/u.test(boundary.digest ?? ''), - `${shell.id} ${boundary.key} is missing a verified SHA-256 digest` + `${shell.id} ${boundary.key} is missing a verified SHA-256 digest`, ); - const remote = yield* Effect.fromOption( - Option.fromNullishOr(apps.find((app) => app.id === boundary.remote)), - () => proofError(`${shell.id} rendered unknown remote ${boundary.remote}`) + const remote = yield* Effect.fromOption(Option.fromNullishOr(apps.find((app) => app.id === boundary.remote)), () => + proofError(`${shell.id} rendered unknown remote ${boundary.remote}`), ); const remoteWorkerName = yield* workerName(remote); const requests = routeFragmentBindingRequests.filter( (request) => request.service === remoteWorkerName && request.remote === boundary.remote && - request.expose === boundary.expose + request.expose === boundary.expose, ); - const renderedCount = boundaries.filter( - (candidate) => candidate.key === boundary.key - ).length; + const renderedCount = boundaries.filter((candidate) => candidate.key === boundary.key).length; yield* ensure( - requests.length === renderedCount && - requests.every((request) => - request.pathname.includes('/_mf/fragment/') - ), - `${shell.id} must compose each ${boundary.key} occurrence through its remote service binding` + requests.length === renderedCount && requests.every((request) => request.pathname.includes('/_mf/fragment/')), + `${shell.id} must compose each ${boundary.key} occurrence through its remote service binding`, ); }); @@ -1554,19 +1227,18 @@ const routeHtml = ( appId: string, route: string, outboundRequests: readonly OutboundRequest[], - outboundStart: number + outboundStart: number, ) => Effect.gen(function* routeHtmlEffect() { const html = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${appId} route ${route} body failed`, cause), + catch: (cause) => proofError(`${appId} route ${route} body failed`, cause), try: async () => await response.text(), }); const routeOutboundRequests = outboundRequests.slice(outboundStart); const outboundEvidence = yield* encodeJson(routeOutboundRequests); yield* ensure( response.status === 200, - `${appId} returned HTTP ${response.status} for ${route} in workerd; outbound requests: ${outboundEvidence}; response: ${html.slice(0, 500)} ... ${html.slice(-1000)}` + `${appId} returned HTTP ${response.status} for ${route} in workerd; outbound requests: ${outboundEvidence}; response: ${html.slice(0, 500)} ... ${html.slice(-1000)}`, ); return { html, routeOutboundRequests }; }); @@ -1577,7 +1249,7 @@ const proveShellRoute = ( route: string, shell: App, shellWorkerName: string, - state: ShellProofState + state: ShellProofState, ): Effect.Effect => Effect.gen(function* proveShellRouteEffect() { const apiStart = state.apiBindingRequests.length; @@ -1586,52 +1258,38 @@ const proveShellRoute = ( const response = yield* Effect.tryPromise({ catch: (cause) => proofError(`${shell.id} route ${route} failed`, cause), try: async () => - await miniflare.dispatchFetch( - `https://${shellWorkerName}.invalid${route}`, - { - headers: { accept: 'text/html' }, - } - ), + await miniflare.dispatchFetch(`https://${shellWorkerName}.invalid${route}`, { + headers: { accept: 'text/html' }, + }), }); const { html, routeOutboundRequests } = yield* routeHtml( response, shell.id, route, state.outboundRequests, - outboundStart + outboundStart, ); yield* ensure( !html.includes(DEGRADED_BOUNDARY_MARKER), - `${shell.id} rendered a degraded MicroVertical fallback for ${route} in workerd` + `${shell.id} rendered a degraded MicroVertical fallback for ${route} in workerd`, ); const boundaries = yield* collectDistributedBoundaries(html); const routeApiBindingRequests = state.apiBindingRequests.slice(apiStart); - const routeFragmentBindingRequests = - state.fragmentBindingRequests.slice(fragmentStart); + const routeFragmentBindingRequests = state.fragmentBindingRequests.slice(fragmentStart); yield* Effect.forEach( boundaries, (boundary) => - proveBoundary( - apps, - boundaries, - boundary, - state.renderedRemoteIds, - route, - routeFragmentBindingRequests, - shell - ), - { concurrency: 1 } + proveBoundary(apps, boundaries, boundary, state.renderedRemoteIds, route, routeFragmentBindingRequests, shell), + { concurrency: 1 }, ); const stylesheetHrefs = collectStylesheetHrefs(html); yield* ensure( new Set(stylesheetHrefs).size === stylesheetHrefs.length, - `${shell.id} rendered duplicate distributed SSR stylesheets for ${route}` + `${shell.id} rendered duplicate distributed SSR stylesheets for ${route}`, ); yield* ensure( - !routeOutboundRequests.some(({ url }) => - /(?:remoteEntry|\.m?js(?:\?|$))/u.test(url) - ), - `${shell.id} attempted to fetch remote JavaScript during ${route} server composition` + !routeOutboundRequests.some(({ url }) => /(?:remoteEntry|\.m?js(?:\?|$))/u.test(url)), + `${shell.id} attempted to fetch remote JavaScript during ${route} server composition`, ); state.proofs.push({ apiBindingRequests: routeApiBindingRequests, @@ -1650,7 +1308,7 @@ const proveShellRoute = ( const proveRemote = ( miniflare: Miniflare, remote: App, - state: ShellProofState + state: ShellProofState, ): Effect.Effect => Effect.gen(function* proveRemoteEffect() { if (!state.renderedRemoteIds.has(remote.id)) { @@ -1658,13 +1316,11 @@ const proveRemote = ( const outboundStart = state.outboundRequests.length; const remoteWorkerName = yield* workerName(remote); const target = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${remote.id} Worker could not be resolved`, cause), + catch: (cause) => proofError(`${remote.id} Worker could not be resolved`, cause), try: async () => await miniflare.getWorker(remoteWorkerName), }); const response = yield* Effect.tryPromise({ - catch: (cause) => - proofError(`${remote.id} route ${route} failed`, cause), + catch: (cause) => proofError(`${remote.id} route ${route} failed`, cause), try: async () => await target.fetch(`https://${remoteWorkerName}.invalid${route}`, { headers: { accept: 'text/html' }, @@ -1675,22 +1331,19 @@ const proveRemote = ( remote.id, route, state.outboundRequests, - outboundStart + outboundStart, ); yield* ensure( - response.headers.get(CONTENT_TYPE_HEADER)?.includes('text/html') === - true, - `${remote.id} did not return HTML for ${route} in workerd` + response.headers.get(CONTENT_TYPE_HEADER)?.includes('text/html') === true, + `${remote.id} did not return HTML for ${route} in workerd`, ); yield* ensure( !html.includes(DEGRADED_BOUNDARY_MARKER), - `${remote.id} rendered a degraded distributed SSR boundary for ${route} in workerd` + `${remote.id} rendered a degraded distributed SSR boundary for ${route} in workerd`, ); yield* ensure( - !routeOutboundRequests.some(({ url }) => - /(?:remoteEntry|\.m?js(?:\?|$))/u.test(url) - ), - `${remote.id} attempted to fetch remote JavaScript during ${route} server rendering` + !routeOutboundRequests.some(({ url }) => /(?:remoteEntry|\.m?js(?:\?|$))/u.test(url)), + `${remote.id} attempted to fetch remote JavaScript during ${route} server rendering`, ); state.remoteProofs.push({ appId: remote.id, @@ -1707,7 +1360,7 @@ const runShellProof = ( apps: readonly App[], shell: App, keepWorkerd: boolean, - reportPath: string + reportPath: string, ): ScopedProofEffect<{ readonly apiProofs: readonly ApiProof[]; readonly executions: readonly ExecutionEvidence[]; @@ -1718,14 +1371,8 @@ const runShellProof = ( const remotes = shell.verticalRefs .map((ref) => apps.find((app) => app.id === ref)) .filter((remote): remote is App => remote !== undefined); - yield* ensure( - remotes.length === shell.verticalRefs.length, - `${shell.id} references a missing MicroVertical` - ); - yield* ensure( - remotes.length > 0, - `${shell.id} has no MicroVerticals to prove` - ); + yield* ensure(remotes.length === shell.verticalRefs.length, `${shell.id} references a missing MicroVertical`); + yield* ensure(remotes.length > 0, `${shell.id} has no MicroVerticals to prove`); const failedServices = new Set(); const state: ShellProofState = { apiBindingRequests: [], @@ -1742,21 +1389,12 @@ const runShellProof = ( app, process.cwd(), createOutboundService(app, state.outboundRequests), - createServiceBindings( - app, - state.apiBindingRequests, - failedServices, - state.fragmentBindingRequests - ) + createServiceBindings(app, state.apiBindingRequests, failedServices, state.fragmentBindingRequests), ), - { concurrency: 1 } - ); - const executions = workerConfigurations.map( - ({ executionEvidence }) => executionEvidence - ); - const executionByAppId = new Map( - apps.map((app, index) => [app.id, executions[index]]) + { concurrency: 1 }, ); + const executions = workerConfigurations.map(({ executionEvidence }) => executionEvidence); + const executionByAppId = new Map(apps.map((app, index) => [app.id, executions[index]])); const workers = workerConfigurations.map(({ options }) => options); const miniflare = new Miniflare({ log: new Log(LogLevel.ERROR), workers }); const shellWorkerName = yield* workerName(shell); @@ -1764,52 +1402,25 @@ const runShellProof = ( Effect.tryPromise({ catch: () => proofError('Could not dispose Workerd runtime'), try: async () => await miniflare.dispose(), - }).pipe(Effect.ignore) + }).pipe(Effect.ignore), ); yield* Effect.forEach( shell.proofRoutes, - (route) => - proveShellRoute(apps, miniflare, route, shell, shellWorkerName, state), - { concurrency: 1 } - ); - yield* Effect.forEach( - remotes, - (remote) => proveRemote(miniflare, remote, state), - { - concurrency: 1, - } - ); - const apiProofs = yield* runApiProofs( - apps, - miniflare, - shell, - executionByAppId + (route) => proveShellRoute(apps, miniflare, route, shell, shellWorkerName, state), + { concurrency: 1 }, ); + yield* Effect.forEach(remotes, (remote) => proveRemote(miniflare, remote, state), { + concurrency: 1, + }); + const apiProofs = yield* runApiProofs(apps, miniflare, shell, executionByAppId); if (keepWorkerd) { - yield* writeReport( - reportPath, - apiProofs, - executions, - state.proofs, - state.remoteProofs - ); - const targetServers = yield* startWorkerdTargetServers( - apps, - miniflare, - failedServices, - workers - ); - const encodedTargetUrls = yield* Schema.encodeEffect(TargetUrlsSchema)( - targetServers.targetUrls - ).pipe( - Effect.mapError((cause) => - proofError('Could not encode Workerd target URLs', cause) - ) + yield* writeReport(reportPath, apiProofs, executions, state.proofs, state.remoteProofs); + const targetServers = yield* startWorkerdTargetServers(apps, miniflare, failedServices, workers); + const encodedTargetUrls = yield* Schema.encodeEffect(TargetUrlsSchema)(targetServers.targetUrls).pipe( + Effect.mapError((cause) => proofError('Could not encode Workerd target URLs', cause)), ); yield* Effect.log(`WORKERD_TARGET_URLS=${encodedTargetUrls}`); - yield* Effect.log( - `WORKERD_URL=${targetServers.targetUrls[shell.id] ?? ''}` - ); + yield* Effect.log(`WORKERD_URL=${targetServers.targetUrls[shell.id] ?? ''}`); yield* waitForTerminationSignal.pipe(Effect.ensuring(targetServers.stop)); } return { @@ -1822,46 +1433,27 @@ const runShellProof = ( const main = Effect.gen(function* mainEffect() { const workspaceRoot = process.cwd(); - const reportPath = path.join( - workspaceRoot, - '.codex/reports/cloudflare-workerd-ssr/composition-proof.json' - ); - const keepWorkerd = yield* Config.boolean('ULTRAMODERN_KEEP_WORKERD').pipe( - Config.withDefault(false) - ); + const reportPath = path.join(workspaceRoot, '.codex/reports/cloudflare-workerd-ssr/composition-proof.json'); + const keepWorkerd = yield* Config.boolean('ULTRAMODERN_KEEP_WORKERD').pipe(Config.withDefault(false)); const apps = yield* loadApps(workspaceRoot); const shells = apps.filter((app) => app.kind === 'shell'); - yield* ensure( - shells.length > 0, - 'Workerd SSR proof requires at least one shell' - ); + yield* ensure(shells.length > 0, 'Workerd SSR proof requires at least one shell'); if (keepWorkerd) { - yield* ensure( - shells.length === 1, - 'Browser workerd proof requires exactly one shell' - ); + yield* ensure(shells.length === 1, 'Browser workerd proof requires exactly one shell'); } - const results = yield* Effect.forEach( - shells, - (shell) => runShellProof(apps, shell, keepWorkerd, reportPath), - { concurrency: 1 } - ); + const results = yield* Effect.forEach(shells, (shell) => runShellProof(apps, shell, keepWorkerd, reportPath), { + concurrency: 1, + }); const apiProofs = results.flatMap((result) => result.apiProofs); const executions = results.flatMap((result) => result.executions); const proofs = results.flatMap((result) => result.proofs); const remoteProofs = results.flatMap((result) => result.remoteProofs); yield* writeReport(reportPath, apiProofs, executions, proofs, remoteProofs); - yield* Effect.log( - `Workerd SSR composition proof passed for ${shells.length} shell(s): ${reportPath}` - ); + yield* Effect.log(`Workerd SSR composition proof passed for ${shells.length} shell(s): ${reportPath}`); }).pipe(Effect.scoped); -const loggedMain = main.pipe( - Effect.tapCause((cause) => Effect.logError(cause)) -); -const exit = await Effect.runPromiseExit( - Effect.provide(loggedMain, NodeFileSystem.layer) -); +const loggedMain = main.pipe(Effect.tapCause((cause) => Effect.logError(cause))); +const exit = await Effect.runPromiseExit(Effect.provide(loggedMain, NodeFileSystem.layer)); if (Exit.isFailure(exit)) { process.exitCode = 1; } diff --git a/app/scripts/provision-current-action-authorization.mts b/app/scripts/provision-current-action-authorization.mts index a6dfa827f..d988a3e74 100644 --- a/app/scripts/provision-current-action-authorization.mts +++ b/app/scripts/provision-current-action-authorization.mts @@ -42,7 +42,7 @@ const TopologySchema = Schema.Struct({ id: Schema.String, package: Schema.String, path: Schema.String, - }) + }), ), }); @@ -52,7 +52,7 @@ const OwnershipSchema = Schema.Struct({ id: Schema.String, package: Schema.String, path: Schema.String, - }) + }), ), }); @@ -67,12 +67,10 @@ export interface ActionAuthorizationProvisioningTarget { const failure = ( code: ActionAuthorizationProvisioningError['code'], reason: string, - cause?: unknown + cause?: unknown, ): ActionAuthorizationProvisioningError => { const error = new ActionAuthorizationProvisioningError({ code, reason }); - return cause === undefined - ? error - : Object.defineProperty(error, 'cause', { value: cause }); + return cause === undefined ? error : Object.defineProperty(error, 'cause', { value: cause }); }; const isLoopbackSpiceDb = (configuration: SpiceDbConfigValue): boolean => { @@ -89,14 +87,10 @@ const isLoopbackSpiceDb = (configuration: SpiceDbConfigValue): boolean => { }; export const selectActionAuthorizationProvisioningTarget = ( - configuration: SpiceDbConfigValue -): Effect.Effect< - ActionAuthorizationProvisioningTarget, - ActionAuthorizationProvisioningError -> => { + configuration: SpiceDbConfigValue, +): Effect.Effect => { if ( - (configuration.deploymentEnvironment === undefined || - configuration.deploymentEnvironment === 'development') && + (configuration.deploymentEnvironment === undefined || configuration.deploymentEnvironment === 'development') && isLoopbackSpiceDb(configuration) ) { return Effect.succeed({ @@ -116,30 +110,25 @@ export const selectActionAuthorizationProvisioningTarget = ( configuration.insecureLocal ) { const contexts = EffectArray.sortWith( - [STAGE_CONTEXTS.techsio, STAGE_CONTEXTS.siampark].map( - ({ principalId, tenantId }) => ({ - principalId, - tenantId, - }) - ), + [STAGE_CONTEXTS.techsio, STAGE_CONTEXTS.siampark].map(({ principalId, tenantId }) => ({ + principalId, + tenantId, + })), ({ tenantId }) => tenantId, - Order.String + Order.String, ); return Effect.succeed({ configuration, contexts, environment: 'stage' }); } return Effect.fail( failure( 'action_authorization_configuration_invalid', - 'Current Action authorization can run only against fixed development or stage SpiceDB' - ) + 'Current Action authorization can run only against fixed development or stage SpiceDB', + ), ); }; const discoveryFailure = (): ActionAuthorizationProvisioningError => - failure( - 'action_authorization_discovery_failed', - 'The complete current Action set could not be derived safely' - ); + failure('action_authorization_discovery_failed', 'The complete current Action set could not be derived safely'); const decodeRepositoryInventory = (workspaceRoot: string) => Effect.gen(function* decodeRepositoryInventoryEffect() { @@ -147,35 +136,23 @@ const decodeRepositoryInventory = (workspaceRoot: string) => const path = yield* Path.Path; const [topologySource, ownershipSource] = yield* Effect.all( [ - fileSystem.readFileString( - path.join(workspaceRoot, 'topology/reference-topology.json'), - 'utf-8' - ), - fileSystem.readFileString( - path.join(workspaceRoot, 'topology/ownership.json'), - 'utf-8' - ), + fileSystem.readFileString(path.join(workspaceRoot, 'topology/reference-topology.json'), 'utf-8'), + fileSystem.readFileString(path.join(workspaceRoot, 'topology/ownership.json'), 'utf-8'), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ).pipe(Effect.mapError(discoveryFailure)); - const ownership = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(OwnershipSchema), - { - onExcessProperty: 'preserve', - } - )(ownershipSource).pipe(Effect.mapError(discoveryFailure)); - const topology = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(TopologySchema), - { - onExcessProperty: 'preserve', - } - )(topologySource).pipe(Effect.mapError(discoveryFailure)); + const ownership = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OwnershipSchema), { + onExcessProperty: 'preserve', + })(ownershipSource).pipe(Effect.mapError(discoveryFailure)); + const topology = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(TopologySchema), { + onExcessProperty: 'preserve', + })(topologySource).pipe(Effect.mapError(discoveryFailure)); return { ownership, topology }; }); export const discoverCurrentActions = ( workspaceRoot: string, - deriveContract: DeriveContract = deriveOntosModuleDeploymentContract + deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, ): Effect.Effect< readonly ActionAuthorizationProvisioningAction[], ActionAuthorizationProvisioningError, @@ -183,29 +160,23 @@ export const discoverCurrentActions = ( > => Effect.gen(function* discoverCurrentActionsEffectGenerator() { const path = yield* Path.Path; - const { ownership, topology } = - yield* decodeRepositoryInventory(workspaceRoot); + const { ownership, topology } = yield* decodeRepositoryInventory(workspaceRoot); if (topology.verticals.length === 0 || coreActionCatalog.length === 0) { return yield* discoveryFailure(); } const ownerKeys = new Set( ownership.owners.map( - ({ id, package: packageName, path: ownerPath }) => - `${id}\u0000${packageName}\u0000${ownerPath}` - ) - ); - const verticals = EffectArray.sortWith( - topology.verticals, - ({ id }) => id, - Order.String + ({ id, package: packageName, path: ownerPath }) => `${id}\u0000${packageName}\u0000${ownerPath}`, + ), ); + const verticals = EffectArray.sortWith(topology.verticals, ({ id }) => id, Order.String); if ( new Set(verticals.map(({ id }) => id)).size !== verticals.length || verticals.some( ({ id, package: packageName, path: ownerPath }) => !ownerKeys.has(`${id}\u0000${packageName}\u0000${ownerPath}`) || path.dirname(ownerPath) !== 'verticals' || - path.basename(ownerPath) !== id + path.basename(ownerPath) !== id, ) ) { return yield* discoveryFailure(); @@ -215,35 +186,29 @@ export const discoverCurrentActions = ( ({ id }) => deriveContract({ vertical: id, workspaceRoot }).pipe( Effect.mapError(discoveryFailure), - Effect.map((contract) => ({ contract, id })) + Effect.map((contract) => ({ contract, id })), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); - const collectVerticalActions = Effect.gen( - function* collectVerticalActionsEffect() { - const verticalActions: ActionAuthorizationProvisioningAction[] = []; - for (const { contract, id } of contracts) { - if ( - contract.deployment.appId !== id || - contract.manifest.publicSurface.actions.length === 0 - ) { + const collectVerticalActions = Effect.gen(function* collectVerticalActionsEffect() { + const verticalActions: ActionAuthorizationProvisioningAction[] = []; + for (const { contract, id } of contracts) { + if (contract.deployment.appId !== id || contract.manifest.publicSurface.actions.length === 0) { + return yield* discoveryFailure(); + } + for (const { actionKey, entrypoint } of contract.manifest.publicSurface.actions) { + if (entrypoint?.authorization.kind !== 'action_execution') { return yield* discoveryFailure(); } - for (const { actionKey, entrypoint } of contract.manifest - .publicSurface.actions) { - if (entrypoint?.authorization.kind !== 'action_execution') { - return yield* discoveryFailure(); - } - verticalActions.push({ - actionKey, - provisioning: entrypoint.authorization.provisioning, - }); - } + verticalActions.push({ + actionKey, + provisioning: entrypoint.authorization.provisioning, + }); } - - return { verticalActions }; } - ); + + return { verticalActions }; + }); const { verticalActions } = yield* collectVerticalActions; const coreActions: ActionAuthorizationProvisioningAction[] = []; for (const { actionKey, entrypoint } of coreActionCatalog) { @@ -258,13 +223,10 @@ export const discoverCurrentActions = ( const actions = EffectArray.sortWith( [...coreActions, ...verticalActions], ({ actionKey }) => actionKey, - Order.String + Order.String, ); const actionKeys = actions.map(({ actionKey }) => actionKey); - if ( - actionKeys.length === 0 || - new Set(actionKeys).size !== actionKeys.length - ) { + if (actionKeys.length === 0 || new Set(actionKeys).size !== actionKeys.length) { return yield* discoveryFailure(); } return actions; @@ -272,27 +234,21 @@ export const discoverCurrentActions = ( export const discoverCurrentActionKeys = ( workspaceRoot: string, - deriveContract: DeriveContract = deriveOntosModuleDeploymentContract -): Effect.Effect< - readonly string[], - ActionAuthorizationProvisioningError, - NodeServices.NodeServices -> => + deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, +): Effect.Effect => discoverCurrentActions(workspaceRoot, deriveContract).pipe( - Effect.map((actions) => actions.map(({ actionKey }) => actionKey)) + Effect.map((actions) => actions.map(({ actionKey }) => actionKey)), ); interface CloseableProvisioningClient extends ActionAuthorizationProvisioningClient { readonly close: () => void; } -const provisioningServiceFailure = ( - cause?: unknown -): ActionAuthorizationProvisioningError => +const provisioningServiceFailure = (cause?: unknown): ActionAuthorizationProvisioningError => failure( 'action_authorization_service_unavailable', 'The authorization service could not provision current Action rules safely', - cause + cause, ); const callProvisioningClient = (operation: () => PromiseLike) => @@ -301,37 +257,22 @@ const callProvisioningClient = (operation: () => PromiseLike) => duration: Duration.seconds(30), orElse: () => Effect.fail( - provisioningServiceFailure( - new Cause.TimeoutError( - 'SpiceDB authorization provisioning request timed out' - ) - ) + provisioningServiceFailure(new Cause.TimeoutError('SpiceDB authorization provisioning request timed out')), ), - }) + }), ); -const createProvisioningClient = ( - configuration: SpiceDbConfigValue -): CloseableProvisioningClient => { - const client = v1.NewClient( - configuration.preSharedKey, - configuration.endpoint, - spiceDbClientSecurity(configuration) - ); +const createProvisioningClient = (configuration: SpiceDbConfigValue): CloseableProvisioningClient => { + const client = v1.NewClient(configuration.preSharedKey, configuration.endpoint, spiceDbClientSecurity(configuration)); return { checkPermission: (request) => - callProvisioningClient( - client.promises.checkPermission.bind(client.promises, request) - ).pipe(Effect.map(Option.fromNullishOr)), + callProvisioningClient(client.promises.checkPermission.bind(client.promises, request)).pipe( + Effect.map(Option.fromNullishOr), + ), close: () => client.close(), writeRelationships: (request) => - callProvisioningClient( - client.promises.writeRelationships.bind(client.promises, request) - ), - writeSchema: (request) => - callProvisioningClient( - client.promises.writeSchema.bind(client.promises, request) - ), + callProvisioningClient(client.promises.writeRelationships.bind(client.promises, request)), + writeSchema: (request) => callProvisioningClient(client.promises.writeSchema.bind(client.promises, request)), }; }; @@ -341,16 +282,16 @@ const acquireProvisioningClient = (configuration: SpiceDbConfigValue) => catch: () => failure( 'action_authorization_service_unavailable', - 'The authorization provisioning client could not be created' + 'The authorization provisioning client could not be created', ), try: () => createProvisioningClient(configuration), }), - (client) => Effect.sync(() => client.close()) + (client) => Effect.sync(() => client.close()), ); const runCurrentActionAuthorizationProvisioningWithServices = ( workspaceRoot: string, - commandArguments: readonly string[] = [] + commandArguments: readonly string[] = [], ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage'; @@ -362,19 +303,15 @@ const runCurrentActionAuthorizationProvisioningWithServices = ( if (commandArguments.length > 0) { return yield* failure( 'action_authorization_configuration_invalid', - 'Current Action authorization provisioning accepts no command-line arguments' + 'Current Action authorization provisioning accepts no command-line arguments', ); } const configuration = yield* loadSpiceDbConfig().pipe( Effect.mapError(() => - failure( - 'action_authorization_configuration_invalid', - 'The SpiceDB provisioning configuration is invalid' - ) - ) + failure('action_authorization_configuration_invalid', 'The SpiceDB provisioning configuration is invalid'), + ), ); - const target = - yield* selectActionAuthorizationProvisioningTarget(configuration); + const target = yield* selectActionAuthorizationProvisioningTarget(configuration); const actions = yield* discoverCurrentActions(workspaceRoot); const client = yield* acquireProvisioningClient(target.configuration); const result = yield* provisionActionAuthorization(client, { @@ -386,16 +323,14 @@ const runCurrentActionAuthorizationProvisioningWithServices = ( export function runCurrentActionAuthorizationProvisioning( workspaceRoot: string, - commandArguments: readonly [string, ...string[]] + commandArguments: readonly [string, ...string[]], ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage'; }, ActionAuthorizationProvisioningError >; -export function runCurrentActionAuthorizationProvisioning( - workspaceRoot: string -): Effect.Effect< +export function runCurrentActionAuthorizationProvisioning(workspaceRoot: string): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage'; }, @@ -404,7 +339,7 @@ export function runCurrentActionAuthorizationProvisioning( >; export function runCurrentActionAuthorizationProvisioning( workspaceRoot: string, - commandArguments: readonly string[] = [] + commandArguments: readonly string[] = [], ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage'; @@ -412,49 +347,33 @@ export function runCurrentActionAuthorizationProvisioning( ActionAuthorizationProvisioningError, NodeServices.NodeServices > { - return runCurrentActionAuthorizationProvisioningWithServices( - workspaceRoot, - commandArguments - ); + return runCurrentActionAuthorizationProvisioningWithServices(workspaceRoot, commandArguments); } -export const formatActionAuthorizationProvisioningFailure = ( - cause: unknown -): string => +export const formatActionAuthorizationProvisioningFailure = (cause: unknown): string => Schema.is(ActionAuthorizationProvisioningError)(cause) ? `${cause.code}: ${cause.reason}` : 'action_authorization_service_unavailable: Unexpected Action authorization provisioning failure'; -const command = Command.make( - 'authorization-provision-current-actions', - {}, - () => - Effect.gen(function* provisionCurrentActionsCommand() { - const path = yield* Path.Path; - const workspaceRoot = path.resolve(import.meta.dirname, '..'); - const result = - yield* runCurrentActionAuthorizationProvisioningWithServices( - workspaceRoot - ).pipe( - Effect.tapError((cause) => - Console.error(formatActionAuthorizationProvisioningFailure(cause)) - ) - ); - yield* Console.log( - `Provisioned ${result.grantCount} explicit Action grants for ${result.actionCount} Actions across ${result.tenantCount} ${result.environment} Tenant(s).` - ); - }) +const command = Command.make('authorization-provision-current-actions', {}, () => + Effect.gen(function* provisionCurrentActionsCommand() { + const path = yield* Path.Path; + const workspaceRoot = path.resolve(import.meta.dirname, '..'); + const result = yield* runCurrentActionAuthorizationProvisioningWithServices(workspaceRoot).pipe( + Effect.tapError((cause) => Console.error(formatActionAuthorizationProvisioningFailure(cause))), + ); + yield* Console.log( + `Provisioned ${result.grantCount} explicit Action grants for ${result.actionCount} Actions across ${result.tenantCount} ${result.environment} Tenant(s).`, + ); + }), ); -if ( - process.argv[1] !== undefined && - import.meta.url === pathToFileURL(process.argv[1]).href -) { +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) { NodeRuntime.runMain( Layer.effectDiscard(Command.run(command, { version: '0.1.0' })).pipe( Layer.provide(NodeServices.layer), - Layer.launch + Layer.launch, ), - { disableErrorReporting: true } + { disableErrorReporting: true }, ); } diff --git a/app/scripts/published-outbox-contracts.mts b/app/scripts/published-outbox-contracts.mts index 1ee7138a2..34a87653d 100644 --- a/app/scripts/published-outbox-contracts.mts +++ b/app/scripts/published-outbox-contracts.mts @@ -13,19 +13,15 @@ export interface PublishedOutboxPackage { readonly name?: string; } -const outboxExportPattern = - /^\.\/outbox\/(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u; -const resourceExportPattern = - /^\.\/resources\/(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u; +const outboxExportPattern = /^\.\/outbox\/(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u; +const resourceExportPattern = /^\.\/resources\/(?[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u; const RESOURCE_HEADER = '// @generated by OntOS Codesmith Resource v1'; const GENERATED_CLIENT_HEADERS = [ '// @generated by OntOS Codesmith Governed Contribution v1\n', '// @generated by OntOS Codesmith module-api v1\n', ] as const; -const COMMAND_CLIENT_HEADER = - '// @generated by OntOS Codesmith MicroVertical Command Client v1\n'; -const ACTION_GATEWAY_HEADER = - '// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n'; +const COMMAND_CLIENT_HEADER = '// @generated by OntOS Codesmith MicroVertical Command Client v1\n'; +const ACTION_GATEWAY_HEADER = '// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n'; const sortLexically = (values: readonly string[]): readonly string[] => { const sorted: string[] = []; @@ -44,7 +40,7 @@ const packageMatchesManifest = ( packageJson: PublishedOutboxPackage, packageName: string, appId: string, - moduleId: string + moduleId: string, ): boolean => packageJson.name === packageName && packageJson.modernjs?.appId === appId && @@ -57,32 +53,21 @@ export const resolvePublishedContractModuleId = (input: { readonly expectedAppId: string; readonly manifestSource: string; }): string => { - const moduleIds = [ - ...input.manifestSource.matchAll( - /^\/\/ @ontos-module-id (?[^\s]+)$/gmu - ), - ].map((match) => match.groups?.moduleId); - const appIds = [ - ...input.manifestSource.matchAll( - /^\/\/ @ontos-deployment-app-id (?[^\s]+)$/gmu - ), - ].map((match) => match.groups?.appId); + const moduleIds = [...input.manifestSource.matchAll(/^\/\/ @ontos-module-id (?[^\s]+)$/gmu)].map( + (match) => match.groups?.moduleId, + ); + const appIds = [...input.manifestSource.matchAll(/^\/\/ @ontos-deployment-app-id (?[^\s]+)$/gmu)].map( + (match) => match.groups?.appId, + ); const [moduleId] = moduleIds; assertCondition( - input.manifestSource.startsWith( - '// @generated by OntOS Codesmith Module Contract v1\n' - ) && + input.manifestSource.startsWith('// @generated by OntOS Codesmith Module Contract v1\n') && moduleIds.length === 1 && moduleId !== undefined && appIds.length === 1 && appIds[0] === input.expectedAppId && - packageMatchesManifest( - input.dependencyPackageJson, - input.dependencyPackageName, - input.expectedAppId, - moduleId - ), - `${input.dependencyPackageName} package and generated manifest ownership disagree` + packageMatchesManifest(input.dependencyPackageJson, input.dependencyPackageName, input.expectedAppId, moduleId), + `${input.dependencyPackageName} package and generated manifest ownership disagree`, ); return moduleId; }; @@ -93,55 +78,37 @@ export const assertPublishedOutboxContractSource = (input: { readonly specifier: string; }): void => { assertCondition( - input.source.startsWith( - '// @generated by OntOS Codesmith Outbox Message Contract v1\n' - ) && + input.source.startsWith('// @generated by OntOS Codesmith Outbox Message Contract v1\n') && input.source.includes(`// @ontos-outbox-producer ${input.moduleId}\n`) && input.source.includes('// @ontos-outbox-topic ') && input.source.includes('export const OutboxPayloadSchema =') && - input.source.includes( - `export const outboxProducerModuleKey = '${input.moduleId}' as const;` - ) && - !/(?:src\/actions|create[A-Za-z0-9]+Message|handler|repository|transport)/u.test( - input.source - ), - `${input.specifier} must remain a generated schema-only Outbox contract` + input.source.includes(`export const outboxProducerModuleKey = '${input.moduleId}' as const;`) && + !/(?:src\/actions|create[A-Za-z0-9]+Message|handler|repository|transport)/u.test(input.source), + `${input.specifier} must remain a generated schema-only Outbox contract`, ); }; -export const publishedOutboxContractExports = ( - packageJson: PublishedOutboxPackage -): readonly string[] => +export const publishedOutboxContractExports = (packageJson: PublishedOutboxPackage): readonly string[] => sortLexically( Object.entries(packageJson.exports ?? {}) .filter(([exportKey, target]) => { const match = outboxExportPattern.exec(exportKey); - return ( - match !== null && - target === `./shared/outbox/${match.groups?.slug ?? ''}.ts` - ); + return match !== null && target === `./shared/outbox/${match.groups?.slug ?? ''}.ts`; }) - .map(([exportKey]) => exportKey) + .map(([exportKey]) => exportKey), ); -export const publishedResourceRefContractExports = ( - packageJson: PublishedOutboxPackage -): readonly string[] => +export const publishedResourceRefContractExports = (packageJson: PublishedOutboxPackage): readonly string[] => sortLexically( Object.entries(packageJson.exports ?? {}) .filter(([exportKey, target]) => { const match = resourceExportPattern.exec(exportKey); - return ( - match !== null && - target === `./shared/resources/${match.groups?.slug ?? ''}.ts` - ); + return match !== null && target === `./shared/resources/${match.groups?.slug ?? ''}.ts`; }) - .map(([exportKey]) => exportKey) + .map(([exportKey]) => exportKey), ); -const publishedEffectClientContractExports = ( - packageJson: PublishedOutboxPackage -): readonly string[] => { +const publishedEffectClientContractExports = (packageJson: PublishedOutboxPackage): readonly string[] => { const appId = packageJson.modernjs?.appId; return appId !== undefined && packageJson.modernjs?.apiRuntime === 'effect' && @@ -153,9 +120,7 @@ const publishedEffectClientContractExports = ( const importedModuleSpecifiers = (source: string): readonly string[] => [ - ...source.matchAll( - /\b(?:import|export)\s+(?:type\s+)?[^;'"`]+?\s+from\s*['"](?[^'"]+)['"]/gu - ), + ...source.matchAll(/\b(?:import|export)\s+(?:type\s+)?[^;'"`]+?\s+from\s*['"](?[^'"]+)['"]/gu), ...source.matchAll(/\bimport\s*['"](?[^'"]+)['"]/gu), ...source.matchAll(/\bimport\s*\(\s*['"](?[^'"]+)['"]/gu), ...source.matchAll(/\brequire\s*\(\s*['"](?[^'"]+)['"]/gu), @@ -170,17 +135,11 @@ const hasResourceRefDeclarations = (input: { }): boolean => { const resourceType = `${input.moduleId}.${input.slug}`; return ( - /export const [A-Z][A-Za-z0-9]*RefSchema = Schema\.Struct\(/u.test( - input.source - ) && - /export type [A-Z][A-Za-z0-9]*Ref = typeof [A-Z][A-Za-z0-9]*RefSchema\.Type;/u.test( - input.source - ) && + /export const [A-Z][A-Za-z0-9]*RefSchema = Schema\.Struct\(/u.test(input.source) && + /export type [A-Z][A-Za-z0-9]*Ref = typeof [A-Z][A-Za-z0-9]*RefSchema\.Type;/u.test(input.source) && input.source.includes(`moduleId: Schema.Literal('${input.moduleId}')`) && input.source.includes(`resourceType: Schema.Literal('${resourceType}')`) && - /export const [a-z][A-Za-z0-9]*ResourceDescriptor = \{/u.test( - input.source - ) && + /export const [a-z][A-Za-z0-9]*ResourceDescriptor = \{/u.test(input.source) && input.source.includes(`key: '${resourceType}'`) && input.source.includes(`owningModuleId: '${input.moduleId}'`) && input.source.includes('satisfies OntosResourceType') @@ -196,17 +155,11 @@ const isGeneratedSchemaOnlyResourceRef = (input: { const imports = importedModuleSpecifiers(input.source); return ( input.source.startsWith(expectedHeader) && - /^import type \{ OntosResourceType \} from '@app\/core-runtime';$/mu.test( - input.source - ) && + /^import type \{ OntosResourceType \} from '@app\/core-runtime';$/mu.test(input.source) && /^import \{ Schema \} from 'effect';$/mu.test(input.source) && imports.length === 2 && - imports.every( - (specifier) => specifier === '@app/core-runtime' || specifier === 'effect' - ) && - !/\b(?:import\s*\(|require\s*\(|async\b|function\b|class\b|fetch\s*\(|new\s+|process\.)/u.test( - input.source - ) && + imports.every((specifier) => specifier === '@app/core-runtime' || specifier === 'effect') && + !/\b(?:import\s*\(|require\s*\(|async\b|function\b|class\b|fetch\s*\(|new\s+|process\.)/u.test(input.source) && !input.source.includes('=>') && hasResourceRefDeclarations(input) ); @@ -224,22 +177,18 @@ const isAllowedEffectClientAggregateImport = (specifier: string): boolean => specifier === '@modern-js/plugin-bff/effect-client' || specifier === 'effect' || specifier.startsWith('effect/') || - /^\.\/(?:action-gateway|[a-z][a-z0-9]*(?:-[a-z0-9]+)*-client)\.ts$/u.test( - specifier - ); + /^\.\/(?:action-gateway|[a-z][a-z0-9]*(?:-[a-z0-9]+)*-client)\.ts$/u.test(specifier); const isGeneratedEffectClientLeaf = (source: string, appId: string): boolean => GENERATED_CLIENT_HEADERS.some((header) => source.startsWith(header)) || - source.startsWith( - `${COMMAND_CLIENT_HEADER}// @ontos-command-client-owner ${appId}\n` - ); + source.startsWith(`${COMMAND_CLIENT_HEADER}// @ontos-command-client-owner ${appId}\n`); const hasValidActionGateway = ( imports: readonly string[], input: { readonly appId: string; readonly readOwnerSource: (path: string) => string; - } + }, ): boolean => { if (imports.includes('./action-gateway.ts')) { const gateway = input.readOwnerSource('./src/api/action-gateway.ts'); @@ -271,7 +220,7 @@ const isGeneratedPublicEffectClient = (input: { !input.source.includes('makeEffectHttpApiClient') || !new RegExp( `(?:interface|type) ${pascalOwner}Client\\b|(?:const ${camelOwner}Client|create${pascalOwner}Client)\\b`, - 'u' + 'u', ).test(input.source) ) { return false; @@ -280,18 +229,12 @@ const isGeneratedPublicEffectClient = (input: { return false; } - const localClients = [ - ...new Set( - imports.filter((specifier) => /^\.\/.+-client\.ts$/u.test(specifier)) - ), - ]; + const localClients = [...new Set(imports.filter((specifier) => /^\.\/.+-client\.ts$/u.test(specifier)))]; if (localClients.length === 0) { return false; } for (const localClient of localClients) { - const localSource = input.readOwnerSource( - `./src/api/${localClient.slice(2)}` - ); + const localSource = input.readOwnerSource(`./src/api/${localClient.slice(2)}`); if (!isGeneratedEffectClientLeaf(localSource, input.appId)) { return false; } @@ -314,7 +257,7 @@ const resourceExportSlug = (exportKey: string): string | undefined => const assertResourceRefUsage = ( input: PublishedContractUsageInput, dependencySpecifiers: readonly string[], - resourceExports: readonly string[] + resourceExports: readonly string[], ): void => { const moduleId = input.dependencyPackageJson.modernjs?.ontosModule?.moduleId; for (const resourceExport of resourceExports) { @@ -333,7 +276,7 @@ const assertResourceRefUsage = ( slug, source: input.readExportSource(target), }), - `${specifier} must remain a generated schema-only ResourceRef contract` + `${specifier} must remain a generated schema-only ResourceRef contract`, ); } }; @@ -341,7 +284,7 @@ const assertResourceRefUsage = ( const assertEffectClientUsage = ( input: PublishedContractUsageInput, dependencySpecifiers: readonly string[], - effectClientExports: readonly string[] + effectClientExports: readonly string[], ): void => { if (effectClientExports.length === 1) { const specifier = `${input.dependencyPackageName}/api/client`; @@ -357,53 +300,38 @@ const assertEffectClientUsage = ( readOwnerSource: input.readExportSource, source: input.readExportSource(target), }), - `${specifier} must remain a generated public Effect client aggregate` + `${specifier} must remain a generated public Effect client aggregate`, ); } } }; -export const assertPublishedCrossMicroVerticalContractUsage = ( - input: PublishedContractUsageInput -): void => { +export const assertPublishedCrossMicroVerticalContractUsage = (input: PublishedContractUsageInput): void => { const dependencySpecifiers = input.moduleSpecifiers.filter( - (specifier) => - specifier === input.dependencyPackageName || - specifier.startsWith(`${input.dependencyPackageName}/`) + (specifier) => specifier === input.dependencyPackageName || specifier.startsWith(`${input.dependencyPackageName}/`), ); assertCondition( dependencySpecifiers.length > 0, - `${input.dependencyPackageName} is an unused cross-MicroVertical dependency` + `${input.dependencyPackageName} is an unused cross-MicroVertical dependency`, ); assertCondition( input.dependencyDeclared, - `consumer must declare ${input.dependencyPackageName} as a workspace dependency` - ); - assertCondition( - input.projectReferenceDeclared, - `consumer must project-reference ${input.dependencyPackageName}` + `consumer must declare ${input.dependencyPackageName} as a workspace dependency`, ); + assertCondition(input.projectReferenceDeclared, `consumer must project-reference ${input.dependencyPackageName}`); - const outboxExports = publishedOutboxContractExports( - input.dependencyPackageJson - ); - const resourceExports = publishedResourceRefContractExports( - input.dependencyPackageJson - ); - const effectClientExports = publishedEffectClientContractExports( - input.dependencyPackageJson - ); + const outboxExports = publishedOutboxContractExports(input.dependencyPackageJson); + const resourceExports = publishedResourceRefContractExports(input.dependencyPackageJson); + const effectClientExports = publishedEffectClientContractExports(input.dependencyPackageJson); const allowedSpecifiers = new Set( [...outboxExports, ...resourceExports, ...effectClientExports].map( - (exportKey) => `${input.dependencyPackageName}${exportKey.slice(1)}` - ) - ); - const forbiddenSpecifier = dependencySpecifiers.find( - (specifier) => !allowedSpecifiers.has(specifier) + (exportKey) => `${input.dependencyPackageName}${exportKey.slice(1)}`, + ), ); + const forbiddenSpecifier = dependencySpecifiers.find((specifier) => !allowedSpecifiers.has(specifier)); assertCondition( forbiddenSpecifier === undefined, - `${forbiddenSpecifier} is not a published schema-only contract subpath` + `${forbiddenSpecifier} is not a published schema-only contract subpath`, ); assertResourceRefUsage(input, dependencySpecifiers, resourceExports); @@ -415,32 +343,24 @@ export const assertPublishedOutboxDependencyUsage = (input: { readonly dependencyPackageName: string; readonly moduleSpecifiers: readonly string[]; }): void => { - const contractExports = publishedOutboxContractExports( - input.dependencyPackageJson - ); + const contractExports = publishedOutboxContractExports(input.dependencyPackageJson); assertCondition( contractExports.length > 0, - `${input.dependencyPackageName} is not a published schema-only Outbox contract dependency` + `${input.dependencyPackageName} is not a published schema-only Outbox contract dependency`, ); const dependencySpecifiers = input.moduleSpecifiers.filter( - (specifier) => - specifier === input.dependencyPackageName || - specifier.startsWith(`${input.dependencyPackageName}/`) + (specifier) => specifier === input.dependencyPackageName || specifier.startsWith(`${input.dependencyPackageName}/`), ); const allowedSpecifiers = new Set( - contractExports.map( - (exportKey) => `${input.dependencyPackageName}${exportKey.slice(1)}` - ) + contractExports.map((exportKey) => `${input.dependencyPackageName}${exportKey.slice(1)}`), ); assertCondition( dependencySpecifiers.length > 0, - `${input.dependencyPackageName} is an unused cross-MicroVertical dependency` - ); - const forbiddenSpecifier = dependencySpecifiers.find( - (specifier) => !allowedSpecifiers.has(specifier) + `${input.dependencyPackageName} is an unused cross-MicroVertical dependency`, ); + const forbiddenSpecifier = dependencySpecifiers.find((specifier) => !allowedSpecifiers.has(specifier)); assertCondition( forbiddenSpecifier === undefined, - `${forbiddenSpecifier} is not a published schema-only Outbox contract subpath` + `${forbiddenSpecifier} is not a published schema-only Outbox contract subpath`, ); }; diff --git a/app/scripts/quality-audit-gate.mts b/app/scripts/quality-audit-gate.mts index 79a8c805c..31bd5086f 100644 --- a/app/scripts/quality-audit-gate.mts +++ b/app/scripts/quality-audit-gate.mts @@ -6,10 +6,7 @@ import { runQualityCli } from './quality-cli-lifecycle.mts'; const FALLOW_FILES = 'fallow-files'; const FALLOW_HEALTH = 'fallow-health'; -const Count = Schema.Finite.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(0) -); +const Count = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)); const PositiveCount = Count.check(Schema.isGreaterThan(0)); const Counts = Schema.Record(Schema.String, Count); const base = { @@ -29,9 +26,7 @@ const ResultSchema = Schema.Union([ nativeFindingCounts: Counts, processed: PositiveCount, total: PositiveCount, - workspaces: Schema.Array(Schema.NonEmptyString).check( - Schema.isMinLength(1) - ), + workspaces: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(1)), }), name: Schema.Literal('knip'), }), @@ -72,14 +67,13 @@ const Summary = Schema.fromJsonString( Schema.Struct({ results: Schema.Array(ResultSchema), status: Schema.Literal('reported'), - }) + }), ); class QualityAuditGateError extends Data.TaggedError('QualityAuditGateError')<{ message: string; }> {} -const reject = (message: string) => - Effect.fail(new QualityAuditGateError({ message })); +const reject = (message: string) => Effect.fail(new QualityAuditGateError({ message })); const sum = (counts: Readonly>) => Object.values(counts).reduce((total, count) => total + count, 0); @@ -95,8 +89,7 @@ const consistent = (entry: typeof ResultSchema.Type) => keys.every( (key) => knip.nativeFindingCounts[key] === - (knip.findingCounts[key] ?? 0) + - (key === 'unlisted' ? knip.modeledUsages : 0) + (knip.findingCounts[key] ?? 0) + (key === 'unlisted' ? knip.modeledUsages : 0), ) && sum(knip.findingCounts) === findings && sum(knip.nativeFindingCounts) === findings + knip.modeledUsages && @@ -118,74 +111,55 @@ const consistent = (entry: typeof ResultSchema.Type) => health.weightedFindings <= health.analyzedFunctions ); }), - Match.orElse( - (result) => result.coverage.tokenEligibleFiles === result.files - ) + Match.orElse((result) => result.coverage.tokenEligibleFiles === result.files), ); -export const validateQualityAuditSummary = Effect.fn( - 'qualityAuditGate.validate' -)(function* validateQualityAuditSummaryEffect(source: string) { - const summary = yield* Schema.decodeEffect(Summary)(source).pipe( - Effect.mapError( - (cause) => - new QualityAuditGateError({ - message: `Malformed audit summary: ${String(cause)}`, - }) - ) - ); - // The schema admits exactly six names; cardinality plus uniqueness requires all of them. - if ( - summary.results.length !== 6 || - new Set(summary.results.map(({ name }) => name)).size !== 6 - ) { - return yield* reject( - 'Audit gate requires all six unique analyzer results; run the full audit' +export const validateQualityAuditSummary = Effect.fn('qualityAuditGate.validate')( + function* validateQualityAuditSummaryEffect(source: string) { + const summary = yield* Schema.decodeEffect(Summary)(source).pipe( + Effect.mapError( + (cause) => + new QualityAuditGateError({ + message: `Malformed audit summary: ${String(cause)}`, + }), + ), ); - } - for (const result of summary.results) { - if (!consistent(result)) { - return yield* reject( - `${result.name}: inconsistent audit counts or incomplete analysis` - ); + // The schema admits exactly six names; cardinality plus uniqueness requires all of them. + if (summary.results.length !== 6 || new Set(summary.results.map(({ name }) => name)).size !== 6) { + return yield* reject('Audit gate requires all six unique analyzer results; run the full audit'); } - } - const discovery = summary.results.find(({ name }) => name === FALLOW_FILES); - const health = summary.results.find(({ name }) => name === FALLOW_HEALTH); - if (discovery?.files !== health?.files) { - return yield* reject('Fallow discovery and health coverage disagree'); - } - const findings = summary.results.filter( - (result) => !result.advisory && result.findings > 0 - ); - const details = findings - .map(({ findings: count, name }) => `${name}=${count}`) - .join(', '); - if (findings.length > 0) { - return yield* reject(`Quality audit gate failed: ${details}`); - } - return yield* Effect.void; -}); + for (const result of summary.results) { + if (!consistent(result)) { + return yield* reject(`${result.name}: inconsistent audit counts or incomplete analysis`); + } + } + const discovery = summary.results.find(({ name }) => name === FALLOW_FILES); + const health = summary.results.find(({ name }) => name === FALLOW_HEALTH); + if (discovery?.files !== health?.files) { + return yield* reject('Fallow discovery and health coverage disagree'); + } + const findings = summary.results.filter((result) => !result.advisory && result.findings > 0); + const details = findings.map(({ findings: count, name }) => `${name}=${count}`).join(', '); + if (findings.length > 0) { + return yield* reject(`Quality audit gate failed: ${details}`); + } + return yield* Effect.void; + }, +); const cli = Command.make( 'quality-audit-gate', { - summary: Flag.string('summary').pipe( - Flag.withDefault('.codex/reports/quality-audit/summary.json') - ), + summary: Flag.string('summary').pipe(Flag.withDefault('.codex/reports/quality-audit/summary.json')), }, ({ summary }) => Effect.gen(function* qualityAuditGateCommand() { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* path.fromFileUrl(new URL('..', import.meta.url)); - yield* validateQualityAuditSummary( - yield* fs.readFileString(path.resolve(root, summary)) - ); - yield* Console.log( - 'Quality audit gate passed (semantic similarity remains advisory)' - ); - }) + yield* validateQualityAuditSummary(yield* fs.readFileString(path.resolve(root, summary))); + yield* Console.log('Quality audit gate passed (semantic similarity remains advisory)'); + }), ); if (Schema.is(Schema.Struct({ main: Schema.Literal(true) }))(import.meta)) { diff --git a/app/scripts/quality-audit.mts b/app/scripts/quality-audit.mts index 68b58a8be..a30db79e2 100644 --- a/app/scripts/quality-audit.mts +++ b/app/scripts/quality-audit.mts @@ -1,27 +1,12 @@ #!/usr/bin/env node import nodePath from 'node:path'; -import { - Array as EffectArray, - Clock, - Console, - Effect, - FileSystem, - Order, - Path, - Result, - Schema, - Stream, -} from 'effect'; +import { Array as EffectArray, Clock, Console, Effect, FileSystem, Order, Path, Result, Schema, Stream } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import { importCloneEvidence } from '../quality-audit/import-clone-evidence.mts'; -import { - buildKnipModel, - KnipConfigSchema, - KnipModelEvidenceSchema, -} from '../quality-audit/knip-model.mts'; +import { buildKnipModel, KnipConfigSchema, KnipModelEvidenceSchema } from '../quality-audit/knip-model.mts'; import type { KnipModelEvidence } from '../quality-audit/knip-model.mts'; import { runQualityCli } from './quality-cli-lifecycle.mts'; @@ -47,10 +32,7 @@ const SOURCE_GROUPS = { } as const; const ToolSchema = Schema.Literals(['all', 'knip', 'jscpd', 'fallow']); type AuditTool = typeof ToolSchema.Type; -const CountSchema = Schema.Finite.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(0) -); +const CountSchema = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)); const ScopeSchema = Schema.Struct({ exclude: Schema.Array(Schema.String), patterns: Schema.Array(Schema.String), @@ -68,7 +50,7 @@ const KnipIssueSchema = Schema.Struct({ col: Schema.optionalKey(CountSchema), line: Schema.optionalKey(CountSchema), name: Schema.String, - }) + }), ), unresolved: Schema.Array(Schema.Unknown), }); @@ -85,7 +67,7 @@ const JscpdSchema = Schema.Struct({ lines: CountSchema, secondFile: Schema.Struct({ name: Schema.String, start: CountSchema }), tokens: CountSchema, - }) + }), ), statistics: Schema.Struct({ total: Schema.Struct({ clones: CountSchema, sources: CountSchema }), @@ -109,11 +91,11 @@ const FallowClonesSchema = Schema.Struct({ end_line: CountSchema, file: Schema.String, start_line: CountSchema, - }) + }), ).check(Schema.isMinLength(2)), line_count: CountSchema, token_count: CountSchema, - }) + }), ), kind: Schema.Literal('dupes'), schema_version: Schema.Literal(9), @@ -130,13 +112,13 @@ const FallowHealthSchema = Schema.Struct({ kind: Schema.String, metric: Schema.Literals(['cyclomatic', 'cognitive']), weight: CountSchema, - }) + }), ), cyclomatic: CountSchema, line: CountSchema, name: Schema.String, path: Schema.String, - }) + }), ), kind: Schema.Literal('health'), schema_version: Schema.Literal(11), @@ -152,12 +134,9 @@ const FallowHealthSchema = Schema.Struct({ workspace_diagnostics: Schema.optionalKey(Schema.Array(DiagnosticSchema)), }); -class QualityAuditError extends Schema.TaggedError()( - 'QualityAuditError', - { - reason: Schema.String, - } -) { +class QualityAuditError extends Schema.TaggedError()('QualityAuditError', { + reason: Schema.String, +}) { override get message(): string { return this.reason; } @@ -194,11 +173,7 @@ interface AuditResult { readonly status: string; } -const errorResult = ( - name: string, - directory: string, - diagnostic: string -): AuditResult => ({ +const errorResult = (name: string, directory: string, diagnostic: string): AuditResult => ({ coverage: {}, diagnostic, directory, @@ -208,33 +183,22 @@ const errorResult = ( status: 'error', }); -const failure = (reason: string): QualityAuditError => - new QualityAuditError({ reason }); +const failure = (reason: string): QualityAuditError => new QualityAuditError({ reason }); const jsonCodec = Schema.fromJsonString(Schema.Unknown, { space: 2 }); -const writeJson = Effect.fn('qualityAudit.writeJson')(function* writeJsonEffect< - A, ->(file: string, value: A) { +const writeJson = Effect.fn('qualityAudit.writeJson')(function* writeJsonEffect(file: string, value: A) { const fs = yield* FileSystem.FileSystem; const source = yield* Schema.encodeEffect(jsonCodec)(value); yield* fs.writeFileString(file, `${source}\n`); }); -const decodeReport = ( - schema: S, - source: string, - subject = 'analyzer report' -) => +const decodeReport = (schema: S, source: string, subject = 'analyzer report') => Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(source).pipe( - Effect.mapError((issue) => - failure(`Malformed ${subject}: ${String(issue)}`) - ) + Effect.mapError((issue) => failure(`Malformed ${subject}: ${String(issue)}`)), ); const nonempty = (count: number, label: string) => - count > 0 - ? Effect.void - : Effect.fail(failure(`${label}: analysis contains no files`)); + count > 0 ? Effect.void : Effect.fail(failure(`${label}: analysis contains no files`)); const sourceGroup = (file: string) => { if (`/${file}`.includes('/fixtures/')) { @@ -249,42 +213,34 @@ const sourceGroup = (file: string) => { return SOURCE_GROUPS.runtime; }; -const validateKnip = Effect.fn('qualityAudit.validateKnip')( - function* validateKnipEffect(name: string, source: string) { - const records = source.trim().split('\n'); - if (records.length !== 2) { - return yield* failure('Knip must emit findings and coverage records'); - } - const [, coverage] = yield* Effect.all( - [ - decodeReport(KnipSchema, records[0] ?? ''), - decodeReport(KnipCoverageSchema, records[1] ?? ''), - ], - { concurrency: 'unbounded' } - ); - yield* nonempty(coverage.coverage.processed, name); - yield* nonempty(coverage.coverage.total, name); - yield* nonempty(coverage.workspaces.length, 'Knip workspaces'); - return { - coverage: { - findingCounts: coverage.findingCounts, - processed: coverage.coverage.processed, - total: coverage.coverage.total, - workspaces: coverage.workspaces, - }, - files: coverage.coverage.processed, - findings: Object.values(coverage.findingCounts).reduce( - (sum, count) => sum + count, - 0 - ), - }; +const validateKnip = Effect.fn('qualityAudit.validateKnip')(function* validateKnipEffect(name: string, source: string) { + const records = source.trim().split('\n'); + if (records.length !== 2) { + return yield* failure('Knip must emit findings and coverage records'); } -); + const [, coverage] = yield* Effect.all( + [decodeReport(KnipSchema, records[0] ?? ''), decodeReport(KnipCoverageSchema, records[1] ?? '')], + { concurrency: 'unbounded' }, + ); + yield* nonempty(coverage.coverage.processed, name); + yield* nonempty(coverage.coverage.total, name); + yield* nonempty(coverage.workspaces.length, 'Knip workspaces'); + return { + coverage: { + findingCounts: coverage.findingCounts, + processed: coverage.coverage.processed, + total: coverage.coverage.total, + workspaces: coverage.workspaces, + }, + files: coverage.coverage.processed, + findings: Object.values(coverage.findingCounts).reduce((sum, count) => sum + count, 0), + }; +}); const modeledUsage = ( file: string, issue: (typeof KnipIssueSchema.Type.unlisted)[number], - evidence: readonly KnipModelEvidence[] + evidence: readonly KnipModelEvidence[], ) => evidence.find((entry) => { if ( @@ -296,654 +252,504 @@ const modeledUsage = ( return false; } if (entry.kind === 'resolver') { - return ( - entry.line === issue.line && - entry.column === issue.col && - entry.owningManifest !== undefined - ); + return entry.line === issue.line && entry.column === issue.col && entry.owningManifest !== undefined; } - return ( - entry.kind === 'compiler-option' && - issue.line === undefined && - issue.col === undefined - ); + return entry.kind === 'compiler-option' && issue.line === undefined && issue.col === undefined; }); -const calibrateKnip = Effect.fn('qualityAudit.calibrateKnip')( - function* calibrateKnipEffect(source: string, directory: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const report = yield* decodeReport( - KnipSchema, - source.trim().split('\n')[0] ?? '' - ); - const evidence = yield* decodeReport( - Schema.Array(KnipModelEvidenceSchema), - yield* fs.readFileString( - path.join(path.dirname(directory), 'knip-model.json') - ) - ); - const modeled = report.issues.flatMap((record) => - record.unlisted.flatMap((issue) => { - const consumer = modeledUsage(record.file, issue, evidence); - return consumer === undefined - ? [] - : [{ category: 'unlisted', consumer, file: record.file, issue }]; - }) - ); - yield* writeJson(path.join(directory, 'modeled-usages.json'), modeled); - return modeled.length; +const calibrateKnip = Effect.fn('qualityAudit.calibrateKnip')(function* calibrateKnipEffect( + source: string, + directory: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const report = yield* decodeReport(KnipSchema, source.trim().split('\n')[0] ?? ''); + const evidence = yield* decodeReport( + Schema.Array(KnipModelEvidenceSchema), + yield* fs.readFileString(path.join(path.dirname(directory), 'knip-model.json')), + ); + const modeled = report.issues.flatMap((record) => + record.unlisted.flatMap((issue) => { + const consumer = modeledUsage(record.file, issue, evidence); + return consumer === undefined ? [] : [{ category: 'unlisted', consumer, file: record.file, issue }]; + }), + ); + yield* writeJson(path.join(directory, 'modeled-usages.json'), modeled); + return modeled.length; +}); + +const evaluateKnip = Effect.fn('qualityAudit.evaluateKnip')(function* evaluateKnipEffect( + report: string, + directory: string, +) { + const validated = yield* validateKnip('knip', report); + const modeledUsages = yield* calibrateKnip(report, directory); + const nativeFindingCounts = validated.coverage.findingCounts; + const { unlisted: nativeUnlisted = 0 } = nativeFindingCounts; + const unlisted = nativeUnlisted - modeledUsages; + if (unlisted < 0) { + return yield* failure('Knip modeled usages exceed raw unlisted count'); } -); + return { + ...validated, + coverage: { + ...validated.coverage, + findingCounts: { ...nativeFindingCounts, unlisted }, + modeledUsages, + nativeFindingCounts, + }, + findings: validated.findings - modeledUsages, + }; +}); -const evaluateKnip = Effect.fn('qualityAudit.evaluateKnip')( - function* evaluateKnipEffect(report: string, directory: string) { - const validated = yield* validateKnip('knip', report); - const modeledUsages = yield* calibrateKnip(report, directory); - const nativeFindingCounts = validated.coverage.findingCounts; - const { unlisted: nativeUnlisted = 0 } = nativeFindingCounts; - const unlisted = nativeUnlisted - modeledUsages; - if (unlisted < 0) { - return yield* failure('Knip modeled usages exceed raw unlisted count'); - } - return { - ...validated, - coverage: { - ...validated.coverage, - findingCounts: { ...nativeFindingCounts, unlisted }, - modeledUsages, - nativeFindingCounts, - }, - findings: validated.findings - modeledUsages, - }; +const validateJscpd = Effect.fn('qualityAudit.validateJscpd')(function* validateJscpdEffect( + name: string, + source: string, +) { + const report = yield* decodeReport(JscpdSchema, source); + yield* nonempty(report.statistics.total.sources, name); + if (report.statistics.total.clones !== report.duplicates.length) { + return yield* failure('JSCPD clone count disagrees with report'); } -); + return { + coverage: { tokenEligibleFiles: report.statistics.total.sources }, + files: report.statistics.total.sources, + findings: report.duplicates.length, + }; +}); -const validateJscpd = Effect.fn('qualityAudit.validateJscpd')( - function* validateJscpdEffect(name: string, source: string) { - const report = yield* decodeReport(JscpdSchema, source); - yield* nonempty(report.statistics.total.sources, name); - if (report.statistics.total.clones !== report.duplicates.length) { - return yield* failure('JSCPD clone count disagrees with report'); - } - return { - coverage: { tokenEligibleFiles: report.statistics.total.sources }, - files: report.statistics.total.sources, - findings: report.duplicates.length, - }; +const validateDiscovery = Effect.fn('qualityAudit.validateDiscovery')(function* validateDiscoveryEffect( + name: string, + source: string, +) { + const report = yield* decodeReport(FallowFilesSchema, source); + yield* nonempty(report.file_count, name); + if (report.file_count !== report.files.length) { + return yield* failure('Fallow discovery count disagrees with file list'); } -); + return { + coverage: { discoveredFiles: report.file_count }, + files: report.file_count, + findings: 0, + }; +}); -const validateDiscovery = Effect.fn('qualityAudit.validateDiscovery')( - function* validateDiscoveryEffect(name: string, source: string) { - const report = yield* decodeReport(FallowFilesSchema, source); - yield* nonempty(report.file_count, name); - if (report.file_count !== report.files.length) { - return yield* failure('Fallow discovery count disagrees with file list'); - } - return { - coverage: { discoveredFiles: report.file_count }, - files: report.file_count, - findings: 0, - }; +const validateClones = Effect.fn('qualityAudit.validateClones')(function* validateClonesEffect( + name: string, + source: string, +) { + const report = yield* decodeReport(FallowClonesSchema, source); + yield* nonempty(report.stats.total_files, name); + if ((report.workspace_diagnostics?.length ?? 0) > 0) { + return yield* failure('Fallow reports incomplete workspace discovery; inspect raw report'); } -); + if (report.stats.clone_groups !== report.clone_groups.length) { + return yield* failure('Fallow clone count disagrees with report'); + } + return { + coverage: { tokenEligibleFiles: report.stats.total_files }, + files: report.stats.total_files, + findings: report.clone_groups.length, + }; +}); -const validateClones = Effect.fn('qualityAudit.validateClones')( - function* validateClonesEffect(name: string, source: string) { - const report = yield* decodeReport(FallowClonesSchema, source); - yield* nonempty(report.stats.total_files, name); - if ((report.workspace_diagnostics?.length ?? 0) > 0) { - return yield* failure( - 'Fallow reports incomplete workspace discovery; inspect raw report' - ); - } - if (report.stats.clone_groups !== report.clone_groups.length) { - return yield* failure('Fallow clone count disagrees with report'); - } +const validateHealth = Effect.fn('qualityAudit.validateHealth')(function* validateHealthEffect( + name: string, + source: string, +) { + const report = yield* decodeReport(FallowHealthSchema, source); + yield* nonempty(report.summary.files_analyzed, name); + yield* nonempty(report.summary.functions_analyzed, 'Fallow functions'); + if ((report.workspace_diagnostics?.length ?? 0) > 0) { + return yield* failure('Fallow reports incomplete workspace discovery; inspect raw report'); + } + if (report.summary.functions_above_threshold !== report.findings.length) { + return yield* failure('Fallow complexity count disagrees with report'); + } + const complexity = report.findings.map((finding) => { + const cyclomatic = + 1 + + finding.contributions + .filter((entry) => entry.metric === 'cyclomatic') + .reduce((sum, entry) => sum + entry.weight, 0); + const cognitive = finding.contributions + .filter((entry) => entry.metric === 'cognitive') + .reduce((sum, entry) => sum + entry.weight, 0); + const hookDensityWeight = finding.contributions + .filter((entry) => entry.metric === 'cognitive' && entry.kind === 'hook-density') + .reduce((sum, entry) => sum + entry.weight, 0); + const propCountWeight = finding.contributions + .filter((entry) => entry.metric === 'cognitive' && entry.kind === 'prop-count') + .reduce((sum, entry) => sum + entry.weight, 0); + const controlFlowCognitive = cognitive - hookDensityWeight - propCountWeight; return { - coverage: { tokenEligibleFiles: report.stats.total_files }, - files: report.stats.total_files, - findings: report.clone_groups.length, + controlFlowCognitive, + cyclomatic: finding.cyclomatic, + exceedsControlFlowLimits: + finding.cyclomatic > COMPLEXITY_LIMITS.cyclomatic || controlFlowCognitive > COMPLEXITY_LIMITS.cognitive, + hookDensityWeight, + line: finding.line, + name: finding.name, + path: finding.path, + propCountWeight, + reconstructed: cyclomatic === finding.cyclomatic && cognitive === finding.cognitive, + weightedCognitive: finding.cognitive, }; + }); + if (complexity.some((finding) => !finding.reconstructed)) { + return yield* failure('Fallow complexity contributions disagree with function metrics'); } -); + if ( + report.findings.some( + (finding) => + finding.cyclomatic <= COMPLEXITY_LIMITS.cyclomatic && finding.cognitive <= COMPLEXITY_LIMITS.cognitive, + ) + ) { + return yield* failure('Fallow reported a function below both configured thresholds'); + } + const controlFlowFindings = complexity.filter((finding) => finding.exceedsControlFlowLimits).length; + return { + complexity, + coverage: { + analyzedFiles: report.summary.files_analyzed, + analyzedFunctions: report.summary.functions_analyzed, + controlFlowFindings, + uiOnlyFindings: complexity.length - controlFlowFindings, + weightedFindings: complexity.length, + }, + files: report.summary.files_analyzed, + findings: controlFlowFindings, + }; +}); -const validateHealth = Effect.fn('qualityAudit.validateHealth')( - function* validateHealthEffect(name: string, source: string) { - const report = yield* decodeReport(FallowHealthSchema, source); - yield* nonempty(report.summary.files_analyzed, name); - yield* nonempty(report.summary.functions_analyzed, 'Fallow functions'); - if ((report.workspace_diagnostics?.length ?? 0) > 0) { - return yield* failure( - 'Fallow reports incomplete workspace discovery; inspect raw report' - ); +export const validateReport = Effect.fn('qualityAudit.validateReport')(function* validateReportEffect( + name: string, + source: string, +) { + switch (name) { + case 'knip': { + return yield* validateKnip(name, source); } - if (report.summary.functions_above_threshold !== report.findings.length) { - return yield* failure('Fallow complexity count disagrees with report'); + case 'jscpd': { + return yield* validateJscpd(name, source); } - const complexity = report.findings.map((finding) => { - const cyclomatic = - 1 + - finding.contributions - .filter((entry) => entry.metric === 'cyclomatic') - .reduce((sum, entry) => sum + entry.weight, 0); - const cognitive = finding.contributions - .filter((entry) => entry.metric === 'cognitive') - .reduce((sum, entry) => sum + entry.weight, 0); - const hookDensityWeight = finding.contributions - .filter( - (entry) => - entry.metric === 'cognitive' && entry.kind === 'hook-density' - ) - .reduce((sum, entry) => sum + entry.weight, 0); - const propCountWeight = finding.contributions - .filter( - (entry) => entry.metric === 'cognitive' && entry.kind === 'prop-count' - ) - .reduce((sum, entry) => sum + entry.weight, 0); - const controlFlowCognitive = - cognitive - hookDensityWeight - propCountWeight; - return { - controlFlowCognitive, - cyclomatic: finding.cyclomatic, - exceedsControlFlowLimits: - finding.cyclomatic > COMPLEXITY_LIMITS.cyclomatic || - controlFlowCognitive > COMPLEXITY_LIMITS.cognitive, - hookDensityWeight, - line: finding.line, - name: finding.name, - path: finding.path, - propCountWeight, - reconstructed: - cyclomatic === finding.cyclomatic && cognitive === finding.cognitive, - weightedCognitive: finding.cognitive, - }; - }); - if (complexity.some((finding) => !finding.reconstructed)) { - return yield* failure( - 'Fallow complexity contributions disagree with function metrics' - ); + case FALLOW_FILES: { + return yield* validateDiscovery(name, source); } - if ( - report.findings.some( - (finding) => - finding.cyclomatic <= COMPLEXITY_LIMITS.cyclomatic && - finding.cognitive <= COMPLEXITY_LIMITS.cognitive - ) - ) { - return yield* failure( - 'Fallow reported a function below both configured thresholds' - ); + case 'fallow-clones': + case FALLOW_SIMILARITY: { + return yield* validateClones(name, source); } - const controlFlowFindings = complexity.filter( - (finding) => finding.exceedsControlFlowLimits - ).length; - return { - complexity, - coverage: { - analyzedFiles: report.summary.files_analyzed, - analyzedFunctions: report.summary.functions_analyzed, - controlFlowFindings, - uiOnlyFindings: complexity.length - controlFlowFindings, - weightedFindings: complexity.length, - }, - files: report.summary.files_analyzed, - findings: controlFlowFindings, - }; - } -); - -export const validateReport = Effect.fn('qualityAudit.validateReport')( - function* validateReportEffect(name: string, source: string) { - switch (name) { - case 'knip': { - return yield* validateKnip(name, source); - } - case 'jscpd': { - return yield* validateJscpd(name, source); - } - case FALLOW_FILES: { - return yield* validateDiscovery(name, source); - } - case 'fallow-clones': - case FALLOW_SIMILARITY: { - return yield* validateClones(name, source); - } - case FALLOW_HEALTH: { - return yield* validateHealth(name, source); - } - default: { - return yield* failure(`Unknown analyzer report: ${name}`); - } + case FALLOW_HEALTH: { + return yield* validateHealth(name, source); } - } -); - -const collectSourceFiles = Effect.fn('qualityAudit.collectSourceFiles')( - function* collectSourceFilesEffect(root: string, output: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const source = yield* fs.readFileString( - path.join(root, 'quality-audit/scope.json') - ); - const scope = yield* decodeReport( - ScopeSchema, - source, - 'quality-audit/scope.json' - ); - const [canonicalRoot, canonicalOutput] = yield* Effect.all([ - fs.realPath(root), - fs.realPath(output), - ]); - const outputPaths = [ - path.relative(root, output), - path.relative(canonicalRoot, canonicalOutput), - ]; - if ( - outputPaths.some((directory) => - scope.patterns.some((pattern) => - nodePath.matchesGlob( - path.join(directory, 'knip-consumers.mts'), - pattern - ) - ) - ) - ) { - return yield* failure( - 'Choose an output directory outside configured source roots, such as .codex/reports/quality-audit' - ); + default: { + return yield* failure(`Unknown analyzer report: ${name}`); } - const groups = yield* Effect.forEach( - scope.patterns, - (pattern) => - fs.glob(pattern, { - exclude: scope.exclude, - root, - }), - { concurrency: 'unbounded' } - ); - const files = EffectArray.sort([...new Set(groups.flat())], Order.String); - yield* nonempty(files.length, 'Source inventory'); - return files; } -); +}); -const readSourceProvenance = Effect.fn('qualityAudit.readSourceProvenance')( - function* readProvenance(root: string) { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const git = (args: readonly string[]) => - spawner - .string(ChildProcess.make('git', args, { cwd: root })) - .pipe(Effect.timeout('10 seconds'), Effect.result); - const [revision, status] = yield* Effect.all( - [ - git(['rev-parse', 'HEAD']), - git(['status', '--porcelain=v1', '--untracked-files=all']), - ], - { concurrency: 'unbounded' } +const collectSourceFiles = Effect.fn('qualityAudit.collectSourceFiles')(function* collectSourceFilesEffect( + root: string, + output: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const source = yield* fs.readFileString(path.join(root, 'quality-audit/scope.json')); + const scope = yield* decodeReport(ScopeSchema, source, 'quality-audit/scope.json'); + const [canonicalRoot, canonicalOutput] = yield* Effect.all([fs.realPath(root), fs.realPath(output)]); + const outputPaths = [path.relative(root, output), path.relative(canonicalRoot, canonicalOutput)]; + if ( + outputPaths.some((directory) => + scope.patterns.some((pattern) => nodePath.matchesGlob(path.join(directory, 'knip-consumers.mts'), pattern)), + ) + ) { + return yield* failure( + 'Choose an output directory outside configured source roots, such as .codex/reports/quality-audit', ); - return { - sourceRevision: Result.isSuccess(revision) - ? revision.success.trim() - : 'unavailable (no Git HEAD)', - sourceState: Result.match(status, { - onFailure: () => 'unavailable', - onSuccess: (output) => - output.trim().length > 0 ? 'modified' : 'clean', + } + const groups = yield* Effect.forEach( + scope.patterns, + (pattern) => + fs.glob(pattern, { + exclude: scope.exclude, + root, }), - workingTreeChanges: Result.isSuccess(status) - ? status.success.trimEnd().split('\n').filter(Boolean) - : [], - }; + { concurrency: 'unbounded' }, + ); + const files = EffectArray.sort([...new Set(groups.flat())], Order.String); + yield* nonempty(files.length, 'Source inventory'); + return files; +}); + +const readSourceProvenance = Effect.fn('qualityAudit.readSourceProvenance')(function* readProvenance(root: string) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const git = (args: readonly string[]) => + spawner.string(ChildProcess.make('git', args, { cwd: root })).pipe(Effect.timeout('10 seconds'), Effect.result); + const [revision, status] = yield* Effect.all( + [git(['rev-parse', 'HEAD']), git(['status', '--porcelain=v1', '--untracked-files=all'])], + { concurrency: 'unbounded' }, + ); + return { + sourceRevision: Result.isSuccess(revision) ? revision.success.trim() : 'unavailable (no Git HEAD)', + sourceState: Result.match(status, { + onFailure: () => 'unavailable', + onSuccess: (output) => (output.trim().length > 0 ? 'modified' : 'clean'), + }), + workingTreeChanges: Result.isSuccess(status) ? status.success.trimEnd().split('\n').filter(Boolean) : [], + }; +}); + +const snapshotConfiguration = Effect.fn('qualityAudit.snapshotConfiguration')(function* snapshotConfigurationEffect( + root: string, + directory: string, + tool: AuditTool, + consumerPath: string | undefined, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const selected = ['knip', 'jscpd', 'fallow'].filter((name) => tool === 'all' || tool === name); + const configs = [ + 'scope.json', + ...selected.map((name) => `${name}.json`), + ...(selected.includes('knip') ? ['knip-reporter.mts'] : []), + ]; + yield* fs.makeDirectory(path.join(directory, 'configs')); + yield* Effect.forEach( + configs, + (name) => fs.copyFile(path.join(root, 'quality-audit', name), path.join(directory, 'configs', name)), + { concurrency: 'unbounded' }, + ); + if (selected.includes('knip')) { + const target = path.join(directory, 'configs/knip.json'); + yield* fs.copyFile(target, path.join(directory, 'configs/knip-base.json')); + const source = yield* fs.readFileString(target); + const config = yield* decodeReport(KnipConfigSchema, source, target); + const model = yield* buildKnipModel(root, config, consumerPath); + if (model.consumerSource !== undefined && consumerPath !== undefined) { + yield* fs.writeFileString(consumerPath, model.consumerSource); + yield* fs.copyFile(consumerPath, path.join(directory, 'knip-consumers.mts')); + } + yield* writeJson(target, model.config); + yield* writeJson(path.join(directory, 'knip-model.json'), model.evidence); } -); + if (selected.includes('fallow')) { + const target = path.join(directory, 'configs/fallow.json'); + const source = yield* fs.readFileString(target); + const config = yield* decodeReport(Schema.Record(Schema.String, Schema.Json), source, target); + const ignores = yield* decodeReport(Schema.Struct({ ignorePatterns: Schema.Array(Schema.String) }), source, target); + const relativeOutput = path.relative(root, path.dirname(directory)); + const outputIsInsideRoot = + relativeOutput !== '..' && !relativeOutput.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeOutput); + yield* writeJson(target, { + ...config, + ignorePatterns: outputIsInsideRoot ? [...ignores.ignorePatterns, `${relativeOutput}/**`] : ignores.ignorePatterns, + }); + } + return configs; +}); + +const verifyFallowCounts = (results: readonly AuditResult[]) => { + const discovery = results.find((result) => result.name === FALLOW_FILES); + const health = results.find((result) => result.name === FALLOW_HEALTH); + return discovery?.status === 'reported' && health?.status === 'reported' && discovery.files !== health.files + ? Effect.fail(failure('Fallow discovery and complexity file counts disagree')) + : Effect.void; +}; -const snapshotConfiguration = Effect.fn('qualityAudit.snapshotConfiguration')( - function* snapshotConfigurationEffect( - root: string, +const reconcileFallowCoverage = Effect.fn('qualityAudit.reconcileFallowCoverage')( + function* reconcileFallowCoverageEffect( directory: string, - tool: AuditTool, - consumerPath: string | undefined + files: readonly string[], + results: readonly AuditResult[], + expectedWorkspaces: readonly string[], ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const selected = ['knip', 'jscpd', 'fallow'].filter( - (name) => tool === 'all' || tool === name - ); - const configs = [ - 'scope.json', - ...selected.map((name) => `${name}.json`), - ...(selected.includes('knip') ? ['knip-reporter.mts'] : []), - ]; - yield* fs.makeDirectory(path.join(directory, 'configs')); - yield* Effect.forEach( - configs, - (name) => - fs.copyFile( - path.join(root, 'quality-audit', name), - path.join(directory, 'configs', name) - ), - { concurrency: 'unbounded' } - ); - if (selected.includes('knip')) { - const target = path.join(directory, 'configs/knip.json'); - yield* fs.copyFile( - target, - path.join(directory, 'configs/knip-base.json') + const fallow = results.find((result) => result.name === FALLOW_FILES && result.status === 'reported'); + if (fallow !== undefined) { + const report = yield* decodeReport( + FallowFilesSchema, + yield* fs.readFileString(path.join(fallow.directory, 'report.json')), ); - const source = yield* fs.readFileString(target); - const config = yield* decodeReport(KnipConfigSchema, source, target); - const model = yield* buildKnipModel(root, config, consumerPath); - if (model.consumerSource !== undefined && consumerPath !== undefined) { - yield* fs.writeFileString(consumerPath, model.consumerSource); - yield* fs.copyFile( - consumerPath, - path.join(directory, 'knip-consumers.mts') - ); - } - yield* writeJson(target, model.config); - yield* writeJson(path.join(directory, 'knip-model.json'), model.evidence); - } - if (selected.includes('fallow')) { - const target = path.join(directory, 'configs/fallow.json'); - const source = yield* fs.readFileString(target); - const config = yield* decodeReport( - Schema.Record(Schema.String, Schema.Json), - source, - target - ); - const ignores = yield* decodeReport( - Schema.Struct({ ignorePatterns: Schema.Array(Schema.String) }), - source, - target - ); - const relativeOutput = path.relative(root, path.dirname(directory)); - const outputIsInsideRoot = - relativeOutput !== '..' && - !relativeOutput.startsWith(`..${path.sep}`) && - !path.isAbsolute(relativeOutput); - yield* writeJson(target, { - ...config, - ignorePatterns: outputIsInsideRoot - ? [...ignores.ignorePatterns, `${relativeOutput}/**`] - : ignores.ignorePatterns, + const discovered = new Set(report.files); + const intended = new Set(files); + const missing = files.filter((file) => !discovered.has(file)); + const extra = report.files.filter((file) => !intended.has(file)); + yield* writeJson(path.join(directory, 'coverage.json'), { + expectedWorkspaces, + extra, + intendedSources: files.length, + missing, + note: 'Fallow additionally discovers CSS; clone statistics count only token-eligible files.', }); + const counts = yield* verifyFallowCounts(results).pipe(Effect.result); + if (missing.length > 0 || extra.some((file) => !file.endsWith('.css'))) { + const diagnostic = [ + 'Fallow discovery differs from source inventory; inspect coverage.json', + Result.isFailure(counts) ? String(counts.failure) : '', + ] + .filter(Boolean) + .join('; '); + return yield* failure(diagnostic); + } + if (Result.isFailure(counts)) { + return yield* counts.failure; + } } - return configs; - } + return yield* Effect.void; + }, ); -const verifyFallowCounts = (results: readonly AuditResult[]) => { - const discovery = results.find((result) => result.name === FALLOW_FILES); - const health = results.find((result) => result.name === FALLOW_HEALTH); - return discovery?.status === 'reported' && - health?.status === 'reported' && - discovery.files !== health.files - ? Effect.fail( - failure('Fallow discovery and complexity file counts disagree') - ) - : Effect.void; -}; - -const reconcileFallowCoverage = Effect.fn( - 'qualityAudit.reconcileFallowCoverage' -)(function* reconcileFallowCoverageEffect( +const reconcileCoverage = Effect.fn('qualityAudit.reconcileCoverage')(function* reconcileCoverageEffect( + root: string, directory: string, files: readonly string[], results: readonly AuditResult[], - expectedWorkspaces: readonly string[] ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const fallow = results.find( - (result) => result.name === FALLOW_FILES && result.status === 'reported' - ); - if (fallow !== undefined) { - const report = yield* decodeReport( - FallowFilesSchema, - yield* fs.readFileString(path.join(fallow.directory, 'report.json')) + const canonicalRoot = yield* fs.realPath(root); + const expectedManifests = yield* fs.glob('{apps,verticals,packages}/*/package.json', { + exclude: ['**/node_modules/**'], + root, + }); + const expectedWorkspaces = ['.', ...expectedManifests.map((file) => path.dirname(file))]; + const knip = results.find((result) => result.name === 'knip' && result.status === 'reported'); + if (knip !== undefined) { + const observed = (knip.coverage.workspaces ?? []).map( + (workspace) => path.relative(canonicalRoot, workspace) || '.', ); - const discovered = new Set(report.files); - const intended = new Set(files); - const missing = files.filter((file) => !discovered.has(file)); - const extra = report.files.filter((file) => !intended.has(file)); - yield* writeJson(path.join(directory, 'coverage.json'), { - expectedWorkspaces, - extra, - intendedSources: files.length, - missing, - note: 'Fallow additionally discovers CSS; clone statistics count only token-eligible files.', - }); - const counts = yield* verifyFallowCounts(results).pipe(Effect.result); - if (missing.length > 0 || extra.some((file) => !file.endsWith('.css'))) { - const diagnostic = [ - 'Fallow discovery differs from source inventory; inspect coverage.json', - Result.isFailure(counts) ? String(counts.failure) : '', - ] - .filter(Boolean) - .join('; '); - return yield* failure(diagnostic); - } - if (Result.isFailure(counts)) { - return yield* counts.failure; + if ( + observed.length !== expectedWorkspaces.length || + expectedWorkspaces.some((workspace) => !observed.includes(workspace)) + ) { + return yield* failure( + `Knip workspace coverage mismatch: expected ${expectedWorkspaces.join(', ')}, observed ${observed.join(', ')}`, + ); } } - return yield* Effect.void; + return yield* reconcileFallowCoverage(directory, files, results, expectedWorkspaces); }); -const reconcileCoverage = Effect.fn('qualityAudit.reconcileCoverage')( - function* reconcileCoverageEffect( - root: string, - directory: string, - files: readonly string[], - results: readonly AuditResult[] - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const canonicalRoot = yield* fs.realPath(root); - const expectedManifests = yield* fs.glob( - '{apps,verticals,packages}/*/package.json', - { - exclude: ['**/node_modules/**'], - root, - } - ); - const expectedWorkspaces = [ - '.', - ...expectedManifests.map((file) => path.dirname(file)), - ]; - const knip = results.find( - (result) => result.name === 'knip' && result.status === 'reported' - ); - if (knip !== undefined) { - const observed = (knip.coverage.workspaces ?? []).map( - (workspace) => path.relative(canonicalRoot, workspace) || '.' - ); - if ( - observed.length !== expectedWorkspaces.length || - expectedWorkspaces.some((workspace) => !observed.includes(workspace)) - ) { - return yield* failure( - `Knip workspace coverage mismatch: expected ${expectedWorkspaces.join(', ')}, observed ${observed.join(', ')}` - ); - } +const executeStep = Effect.fn('qualityAudit.executeStep')(function* executeStepEffect( + root: string, + directory: string, + step: AuditStep, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + yield* fs.makeDirectory(directory, { recursive: true }); + const stdoutPath = path.join(directory, 'stdout.txt'); + const stderrPath = path.join(directory, 'stderr.txt'); + yield* Effect.all([fs.writeFileString(stdoutPath, ''), fs.writeFileString(stderrPath, '')], { + concurrency: 'unbounded', + }); + const startedAt = yield* Clock.currentTimeMillis; + const binary = path.join(root, 'node_modules', step.tool, TOOL_BINS[step.tool]); + const command = [process.execPath, binary, ...step.args]; + const execution = yield* Effect.gen(function* launchAnalyzer() { + if (!(yield* fs.exists(binary))) { + return yield* failure(`Missing pinned local binary: ${binary}`); } - return yield* reconcileFallowCoverage( - directory, - files, - results, - expectedWorkspaces + const installed = yield* decodeReport( + Schema.Struct({ version: Schema.String }), + yield* fs.readFileString(path.join(root, 'node_modules', step.tool, 'package.json')), ); - } -); - -const executeStep = Effect.fn('qualityAudit.executeStep')( - function* executeStepEffect( - root: string, - directory: string, - step: AuditStep - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - yield* fs.makeDirectory(directory, { recursive: true }); - const stdoutPath = path.join(directory, 'stdout.txt'); - const stderrPath = path.join(directory, 'stderr.txt'); - yield* Effect.all( - [fs.writeFileString(stdoutPath, ''), fs.writeFileString(stderrPath, '')], - { - concurrency: 'unbounded', - } + if (installed.version !== TOOL_VERSIONS[step.tool]) { + return yield* failure(`Expected ${step.tool} ${TOOL_VERSIONS[step.tool]}, found ${installed.version}`); + } + const processHandle = yield* spawner.spawn( + ChildProcess.make(process.execPath, [binary, ...step.args], { + cwd: root, + // Inherited FORCE_COLOR overrides NO_COLOR and makes Node emit a warning. + // Disable forcing at the subprocess boundary; keep real diagnostics intact. + env: { FORCE_COLOR: '0', NO_COLOR: '1' }, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }), ); - const startedAt = yield* Clock.currentTimeMillis; - const binary = path.join( - root, - 'node_modules', - step.tool, - TOOL_BINS[step.tool] + const [exitCode] = yield* Effect.all( + [ + processHandle.exitCode, + Stream.run(processHandle.stdout, fs.sink(stdoutPath)), + Stream.run(processHandle.stderr, fs.sink(stderrPath)), + ], + { concurrency: 'unbounded' }, ); - const command = [process.execPath, binary, ...step.args]; - const execution = yield* Effect.gen(function* launchAnalyzer() { - if (!(yield* fs.exists(binary))) { - return yield* failure(`Missing pinned local binary: ${binary}`); - } - const installed = yield* decodeReport( - Schema.Struct({ version: Schema.String }), - yield* fs.readFileString( - path.join(root, 'node_modules', step.tool, 'package.json') - ) - ); - if (installed.version !== TOOL_VERSIONS[step.tool]) { - return yield* failure( - `Expected ${step.tool} ${TOOL_VERSIONS[step.tool]}, found ${installed.version}` - ); - } - const processHandle = yield* spawner.spawn( - ChildProcess.make(process.execPath, [binary, ...step.args], { - cwd: root, - // Inherited FORCE_COLOR overrides NO_COLOR and makes Node emit a warning. - // Disable forcing at the subprocess boundary; keep real diagnostics intact. - env: { FORCE_COLOR: '0', NO_COLOR: '1' }, - extendEnv: true, - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }) - ); - const [exitCode] = yield* Effect.all( - [ - processHandle.exitCode, - Stream.run(processHandle.stdout, fs.sink(stdoutPath)), - Stream.run(processHandle.stderr, fs.sink(stderrPath)), - ], - { concurrency: 'unbounded' } - ); - return { exitCode: Number(exitCode), verifiedVersion: installed.version }; - }).pipe(Effect.scoped, Effect.timeout('5 minutes'), Effect.result); - const endedAt = yield* Clock.currentTimeMillis; - const executionError = Result.isFailure(execution) - ? String(execution.failure) - : ''; - const exitCode = Result.isSuccess(execution) - ? execution.success.exitCode - : -1; - yield* writeJson(path.join(directory, 'metadata.json'), { - command, - cwd: root, - endedAt, - executionError, - exitCode, - expectedVersion: TOOL_VERSIONS[step.tool], - startedAt, - verifiedVersion: Result.isSuccess(execution) - ? execution.success.verifiedVersion - : '', - }); - const evaluated = yield* Effect.gen(function* evaluateAnalyzer() { - if (executionError.length > 0) { - return yield* failure(executionError); - } - if (exitCode !== 0) { - return yield* failure( - `Analyzer exited ${exitCode}; inspect stdout.txt and stderr.txt` - ); - } - const stderr = yield* fs.readFileString(stderrPath); - if ( - stderr.trim().length > 0 && - !( - step.name === 'jscpd' && - stderr.trim() === `Using config from ${step.args[1]}` - ) - ) { - return yield* failure( - 'Analyzer emitted diagnostics; inspect stderr.txt before trusting coverage' - ); - } - const reportPath = step.report ?? stdoutPath; - const report = yield* fs.readFileString(reportPath); - yield* fs.writeFileString( - path.join( - directory, - step.name === 'knip' ? 'report.ndjson' : 'report.json' - ), - report - ); - if (step.name === 'jscpd') { - const validated = yield* validateReport(step.name, report); - const imports = yield* importCloneEvidence(root, report); - yield* writeJson( - path.join(directory, 'import-clone-evidence.json'), - imports - ); - return { - ...validated, - coverage: { - ...validated.coverage, - declarationOnlyClones: imports.length, - }, - findings: validated.findings - imports.length, - }; - } - return step.name === 'knip' - ? yield* evaluateKnip(report, directory) - : yield* validateReport(step.name, report); - }).pipe(Effect.result); - if (Result.isFailure(evaluated)) { - const diagnostic = String(evaluated.failure); - yield* fs.writeFileString( - path.join(directory, 'validation-error.txt'), - `${diagnostic}\n` - ); - return errorResult(step.name, directory, diagnostic); + return { exitCode: Number(exitCode), verifiedVersion: installed.version }; + }).pipe(Effect.scoped, Effect.timeout('5 minutes'), Effect.result); + const endedAt = yield* Clock.currentTimeMillis; + const executionError = Result.isFailure(execution) ? String(execution.failure) : ''; + const exitCode = Result.isSuccess(execution) ? execution.success.exitCode : -1; + yield* writeJson(path.join(directory, 'metadata.json'), { + command, + cwd: root, + endedAt, + executionError, + exitCode, + expectedVersion: TOOL_VERSIONS[step.tool], + startedAt, + verifiedVersion: Result.isSuccess(execution) ? execution.success.verifiedVersion : '', + }); + const evaluated = yield* Effect.gen(function* evaluateAnalyzer() { + if (executionError.length > 0) { + return yield* failure(executionError); + } + if (exitCode !== 0) { + return yield* failure(`Analyzer exited ${exitCode}; inspect stdout.txt and stderr.txt`); } - if ('complexity' in evaluated.success) { - const { complexity, ...summary } = evaluated.success; - yield* writeJson(path.join(directory, 'complexity.json'), complexity); + const stderr = yield* fs.readFileString(stderrPath); + if (stderr.trim().length > 0 && !(step.name === 'jscpd' && stderr.trim() === `Using config from ${step.args[1]}`)) { + return yield* failure('Analyzer emitted diagnostics; inspect stderr.txt before trusting coverage'); + } + const reportPath = step.report ?? stdoutPath; + const report = yield* fs.readFileString(reportPath); + yield* fs.writeFileString(path.join(directory, step.name === 'knip' ? 'report.ndjson' : 'report.json'), report); + if (step.name === 'jscpd') { + const validated = yield* validateReport(step.name, report); + const imports = yield* importCloneEvidence(root, report); + yield* writeJson(path.join(directory, 'import-clone-evidence.json'), imports); return { - name: step.name, - status: 'reported', - ...summary, - diagnostic: '', - directory, + ...validated, + coverage: { + ...validated.coverage, + declarationOnlyClones: imports.length, + }, + findings: validated.findings - imports.length, }; } + return step.name === 'knip' ? yield* evaluateKnip(report, directory) : yield* validateReport(step.name, report); + }).pipe(Effect.result); + if (Result.isFailure(evaluated)) { + const diagnostic = String(evaluated.failure); + yield* fs.writeFileString(path.join(directory, 'validation-error.txt'), `${diagnostic}\n`); + return errorResult(step.name, directory, diagnostic); + } + if ('complexity' in evaluated.success) { + const { complexity, ...summary } = evaluated.success; + yield* writeJson(path.join(directory, 'complexity.json'), complexity); return { name: step.name, status: 'reported', - ...evaluated.success, + ...summary, diagnostic: '', directory, }; } -); + return { + name: step.name, + status: 'reported', + ...evaluated.success, + diagnostic: '', + directory, + }; +}); -export const auditSteps = ( - root: string, - runDirectory: string, - tool: AuditTool -): readonly AuditStep[] => { +export const auditSteps = (root: string, runDirectory: string, tool: AuditTool): readonly AuditStep[] => { const config = `${runDirectory}/configs`; const common = [ '--root', @@ -974,12 +780,7 @@ export const auditSteps = ( tool: 'knip', }, { - args: [ - '--config', - `${runDirectory}/jscpd.config.json`, - '--output', - `${runDirectory}/jscpd`, - ], + args: ['--config', `${runDirectory}/jscpd.config.json`, '--output', `${runDirectory}/jscpd`], name: 'jscpd', report: `${runDirectory}/jscpd/jscpd-report.json`, tool: 'jscpd', @@ -990,30 +791,12 @@ export const auditSteps = ( tool: 'fallow', }, { - args: [ - 'dupes', - '--mode', - 'strict', - '--min-tokens', - '100', - '--min-lines', - '10', - ...common, - ], + args: ['dupes', '--mode', 'strict', '--min-tokens', '100', '--min-lines', '10', ...common], name: 'fallow-clones', tool: 'fallow', }, { - args: [ - 'dupes', - '--mode', - 'semantic', - '--min-tokens', - '100', - '--min-lines', - '10', - ...common, - ], + args: ['dupes', '--mode', 'semantic', '--min-tokens', '100', '--min-lines', '10', ...common], name: FALLOW_SIMILARITY, tool: 'fallow', }, @@ -1053,218 +836,183 @@ const REPORT_MEANINGS = { knip: { advisory: false, unit: 'unused/dependency records' }, }; -const reportMeaning = (name: string) => - Object.entries(REPORT_MEANINGS).find(([analysis]) => analysis === name)?.[1]; +const reportMeaning = (name: string) => Object.entries(REPORT_MEANINGS).find(([analysis]) => analysis === name)?.[1]; -const writeSummary = Effect.fn('qualityAudit.writeSummary')( - function* writeSummaryEffect( - output: string, - runDirectory: string, - results: readonly AuditResult[] - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const rows = results.map( - (result) => - `| ${result.name} | ${result.status} | ${result.findings} | ${reportMeaning(result.name)?.unit ?? 'diagnostic'} | ${result.files} |` - ); - const errors = results.filter((result) => result.status === 'error'); - yield* writeJson(path.join(output, 'summary.json'), { - expectedToolVersions: TOOL_VERSIONS, - mode: 'report-only', - parserCompleteness: 'unavailable; use existing lint and compiler checks', - provenance: `${runDirectory}/provenance.json`, - results: results.map((result) => ({ - ...result, - ...reportMeaning(result.name), - })), - runDirectory, - status: errors.length > 0 ? 'error' : 'reported', - }); - yield* fs.writeFileString( - path.join(output, 'summary.md'), - [ - '# Code quality audit (report only)', - '', - '| Analysis | Status | Count | Unit | Files |', - '| --- | --- | ---: | --- | ---: |', - ...rows, - '', - 'Findings are audit evidence, not accepted debt or a delivery threshold.', - 'These counts overlap and have different units; do not add them into an error total.', - 'Knip counts below exclude only proven modeled consumers. Raw category counts and every corrected issue are retained in report.ndjson and modeled-usages.json.', - ...results - .filter((result) => result.name === 'knip') - .flatMap((result) => [ - '', - '| Knip category | Remaining records |', - '| --- | ---: |', - ...Object.entries(result.coverage.findingCounts ?? {}).map( - ([category, count]) => `| ${category} | ${count} |` - ), - '', - `Proven modeled usages retained separately: ${result.coverage.modeledUsages ?? 0}.`, - ]), - 'Unused exports describe an unused public binding; they do not establish that the implementation body is unused.', - 'JSCPD implementation counts exclude only complete static import-binding spans proven by parsing both files; raw clones and per-pair evidence remain in jscpd/report.json and jscpd/import-clone-evidence.json.', - 'Fallow strict clones preserve literal differences. Semantic similarity normalizes them and remains advisory; inspect both together with JSCPD before choosing a shared implementation.', - `Health counts cyclomatic > ${COMPLEXITY_LIMITS.cyclomatic} or control-flow cognitive > ${COMPLEXITY_LIMITS.cognitive}. The latter subtracts hook-density and prop-count penalties from the native weighted metric, with contribution arithmetic verified for every finding.`, - ...results - .filter((result) => result.name === FALLOW_HEALTH) - .map( - (result) => - `UI-only advisories excluded from the control-flow count: ${result.coverage.uiOnlyFindings ?? 0}. Native weighted findings: ${result.coverage.weightedFindings ?? 0}. Per-function evidence: fallow-health/complexity.json.` +const writeSummary = Effect.fn('qualityAudit.writeSummary')(function* writeSummaryEffect( + output: string, + runDirectory: string, + results: readonly AuditResult[], +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const rows = results.map( + (result) => + `| ${result.name} | ${result.status} | ${result.findings} | ${reportMeaning(result.name)?.unit ?? 'diagnostic'} | ${result.files} |`, + ); + const errors = results.filter((result) => result.status === 'error'); + yield* writeJson(path.join(output, 'summary.json'), { + expectedToolVersions: TOOL_VERSIONS, + mode: 'report-only', + parserCompleteness: 'unavailable; use existing lint and compiler checks', + provenance: `${runDirectory}/provenance.json`, + results: results.map((result) => ({ + ...result, + ...reportMeaning(result.name), + })), + runDirectory, + status: errors.length > 0 ? 'error' : 'reported', + }); + yield* fs.writeFileString( + path.join(output, 'summary.md'), + [ + '# Code quality audit (report only)', + '', + '| Analysis | Status | Count | Unit | Files |', + '| --- | --- | ---: | --- | ---: |', + ...rows, + '', + 'Findings are audit evidence, not accepted debt or a delivery threshold.', + 'These counts overlap and have different units; do not add them into an error total.', + 'Knip counts below exclude only proven modeled consumers. Raw category counts and every corrected issue are retained in report.ndjson and modeled-usages.json.', + ...results + .filter((result) => result.name === 'knip') + .flatMap((result) => [ + '', + '| Knip category | Remaining records |', + '| --- | ---: |', + ...Object.entries(result.coverage.findingCounts ?? {}).map( + ([category, count]) => `| ${category} | ${count} |`, ), - 'Source inventory and raw reports live in the run directory. Clone file counts include only token-eligible files; discovery and complexity counts cover the wider source corpus.', - 'Analyzer discovery is not proof of successful parsing: these tools can silently accept malformed source. Existing lint and compiler checks remain authoritative.', - '', - `Run directory: ${runDirectory}`, - '', - ...errors.map((result) => `- ${result.name}: ${result.diagnostic}`), - '', - ].join('\n') - ); - } -); + '', + `Proven modeled usages retained separately: ${result.coverage.modeledUsages ?? 0}.`, + ]), + 'Unused exports describe an unused public binding; they do not establish that the implementation body is unused.', + 'JSCPD implementation counts exclude only complete static import-binding spans proven by parsing both files; raw clones and per-pair evidence remain in jscpd/report.json and jscpd/import-clone-evidence.json.', + 'Fallow strict clones preserve literal differences. Semantic similarity normalizes them and remains advisory; inspect both together with JSCPD before choosing a shared implementation.', + `Health counts cyclomatic > ${COMPLEXITY_LIMITS.cyclomatic} or control-flow cognitive > ${COMPLEXITY_LIMITS.cognitive}. The latter subtracts hook-density and prop-count penalties from the native weighted metric, with contribution arithmetic verified for every finding.`, + ...results + .filter((result) => result.name === FALLOW_HEALTH) + .map( + (result) => + `UI-only advisories excluded from the control-flow count: ${result.coverage.uiOnlyFindings ?? 0}. Native weighted findings: ${result.coverage.weightedFindings ?? 0}. Per-function evidence: fallow-health/complexity.json.`, + ), + 'Source inventory and raw reports live in the run directory. Clone file counts include only token-eligible files; discovery and complexity counts cover the wider source corpus.', + 'Analyzer discovery is not proof of successful parsing: these tools can silently accept malformed source. Existing lint and compiler checks remain authoritative.', + '', + `Run directory: ${runDirectory}`, + '', + ...errors.map((result) => `- ${result.name}: ${result.diagnostic}`), + '', + ].join('\n'), + ); +}); -const createConsumerPath = Effect.fn('qualityAudit.createConsumerPath')( - function* createConsumerPathEffect(root: string) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const parent = path.join(root, '.codex'); - yield* fs.makeDirectory(parent, { recursive: true }); - const directory = yield* fs.makeTempDirectoryScoped({ - directory: parent, - prefix: 'quality-audit-model-', - }); - return path.join(directory, 'consumers.mts'); - } -); +const createConsumerPath = Effect.fn('qualityAudit.createConsumerPath')(function* createConsumerPathEffect( + root: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const parent = path.join(root, '.codex'); + yield* fs.makeDirectory(parent, { recursive: true }); + const directory = yield* fs.makeTempDirectoryScoped({ + directory: parent, + prefix: 'quality-audit-model-', + }); + return path.join(directory, 'consumers.mts'); +}); -export const runQualityAudit = Effect.fn('qualityAudit.runQualityAudit')( - function* runQualityAuditEffect( - root: string, - output: string, - tool: AuditTool - ) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const provenance = yield* readSourceProvenance(root); - yield* fs.makeDirectory(output, { recursive: true }); - const runDirectory = yield* fs.makeTempDirectory({ - directory: output, - prefix: 'run-', +export const runQualityAudit = Effect.fn('qualityAudit.runQualityAudit')(function* runQualityAuditEffect( + root: string, + output: string, + tool: AuditTool, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const provenance = yield* readSourceProvenance(root); + yield* fs.makeDirectory(output, { recursive: true }); + const runDirectory = yield* fs.makeTempDirectory({ + directory: output, + prefix: 'run-', + }); + yield* writeSummary(output, runDirectory, [errorResult('setup', runDirectory, 'Audit has not completed')]); + const results = yield* Effect.gen(function* collectAudit() { + const files = yield* collectSourceFiles(root, output); + yield* writeJson(path.join(runDirectory, 'source-inventory.json'), { + count: files.length, + entries: files.map((file) => ({ + group: sourceGroup(file), + path: file, + })), + files, + groupCounts: Object.fromEntries( + Object.values(SOURCE_GROUPS).map((group) => [ + group, + files.filter((file) => sourceGroup(file) === group).length, + ]), + ), + root, }); - yield* writeSummary(output, runDirectory, [ - errorResult('setup', runDirectory, 'Audit has not completed'), - ]); - const results = yield* Effect.gen(function* collectAudit() { - const files = yield* collectSourceFiles(root, output); - yield* writeJson(path.join(runDirectory, 'source-inventory.json'), { - count: files.length, - entries: files.map((file) => ({ - group: sourceGroup(file), - path: file, - })), - files, - groupCounts: Object.fromEntries( - Object.values(SOURCE_GROUPS).map((group) => [ - group, - files.filter((file) => sourceGroup(file) === group).length, - ]) - ), - root, - }); - const consumerPath = - tool === 'all' || tool === 'knip' - ? yield* createConsumerPath(root) - : undefined; - const configs = yield* snapshotConfiguration( - root, - runDirectory, - tool, - consumerPath + const consumerPath = tool === 'all' || tool === 'knip' ? yield* createConsumerPath(root) : undefined; + const configs = yield* snapshotConfiguration(root, runDirectory, tool, consumerPath); + yield* writeJson(path.join(runDirectory, 'provenance.json'), { + ...provenance, + configs: configs.map((name) => `configs/${name}`), + expectedToolVersions: TOOL_VERSIONS, + parserCompleteness: 'unavailable', + }); + if (tool === 'all' || tool === 'jscpd') { + const jscpdConfig = yield* decodeReport( + Schema.Record(Schema.String, Schema.Json), + yield* fs.readFileString(path.join(runDirectory, 'configs/jscpd.json')), + 'configs/jscpd.json', ); - yield* writeJson(path.join(runDirectory, 'provenance.json'), { - ...provenance, - configs: configs.map((name) => `configs/${name}`), - expectedToolVersions: TOOL_VERSIONS, - parserCompleteness: 'unavailable', + yield* writeJson(path.join(runDirectory, 'jscpd.config.json'), { + ...jscpdConfig, + path: files.map((file) => path.resolve(root, file)), }); - if (tool === 'all' || tool === 'jscpd') { - const jscpdConfig = yield* decodeReport( - Schema.Record(Schema.String, Schema.Json), - yield* fs.readFileString( - path.join(runDirectory, 'configs/jscpd.json') - ), - 'configs/jscpd.json' - ); - yield* writeJson(path.join(runDirectory, 'jscpd.config.json'), { - ...jscpdConfig, - path: files.map((file) => path.resolve(root, file)), - }); - } - const stepResults = yield* Effect.forEach( - auditSteps(root, runDirectory, tool), - (step) => executeStep(root, path.join(runDirectory, step.name), step), - { concurrency: 1 } - ); - return yield* reconcileCoverage( - root, - runDirectory, - files, - stepResults - ).pipe( - Effect.match({ - onFailure: (issue) => [ - ...stepResults, - errorResult('coverage', runDirectory, String(issue)), - ], - onSuccess: () => stepResults, - }) - ); - }).pipe( - Effect.scoped, + } + const stepResults = yield* Effect.forEach( + auditSteps(root, runDirectory, tool), + (step) => executeStep(root, path.join(runDirectory, step.name), step), + { concurrency: 1 }, + ); + return yield* reconcileCoverage(root, runDirectory, files, stepResults).pipe( Effect.match({ - onFailure: (issue) => [ - errorResult('setup', runDirectory, String(issue)), - ], - onSuccess: (collected) => collected, - }) + onFailure: (issue) => [...stepResults, errorResult('coverage', runDirectory, String(issue))], + onSuccess: () => stepResults, + }), ); - yield* writeSummary(output, runDirectory, results); - yield* Console.log(`Quality audit: ${path.join(output, 'summary.md')}`); - const errors = results.filter((result) => result.status === 'error'); - if (errors.length > 0) { - yield* Effect.forEach( - errors, - (result) => Console.error(`${result.name}: ${result.diagnostic}`), - { concurrency: 1 } - ); - return yield* failure( - 'Quality audit analysis failed; diagnostics preserved in summary and raw artifacts' - ); - } - return yield* Effect.void; + }).pipe( + Effect.scoped, + Effect.match({ + onFailure: (issue) => [errorResult('setup', runDirectory, String(issue))], + onSuccess: (collected) => collected, + }), + ); + yield* writeSummary(output, runDirectory, results); + yield* Console.log(`Quality audit: ${path.join(output, 'summary.md')}`); + const errors = results.filter((result) => result.status === 'error'); + if (errors.length > 0) { + yield* Effect.forEach(errors, (result) => Console.error(`${result.name}: ${result.diagnostic}`), { + concurrency: 1, + }); + return yield* failure('Quality audit analysis failed; diagnostics preserved in summary and raw artifacts'); } -); + return yield* Effect.void; +}); const cli = Command.make( 'quality-audit', { - output: Flag.string('output').pipe( - Flag.withDefault('.codex/reports/quality-audit') - ), - tool: Flag.choice('tool', ['all', 'knip', 'jscpd', 'fallow']).pipe( - Flag.withDefault('all') - ), + output: Flag.string('output').pipe(Flag.withDefault('.codex/reports/quality-audit')), + tool: Flag.choice('tool', ['all', 'knip', 'jscpd', 'fallow']).pipe(Flag.withDefault('all')), }, ({ output, tool }) => Effect.gen(function* qualityAuditCommand() { const path = yield* Path.Path; const root = yield* path.fromFileUrl(new URL('..', import.meta.url)); yield* runQualityAudit(root, path.resolve(root, output), tool); - }) + }), ); if (Schema.is(Schema.Struct({ main: Schema.Literal(true) }))(import.meta)) { diff --git a/app/scripts/quality-cli-lifecycle.mts b/app/scripts/quality-cli-lifecycle.mts index cde1d549d..79eec123d 100644 --- a/app/scripts/quality-cli-lifecycle.mts +++ b/app/scripts/quality-cli-lifecycle.mts @@ -4,15 +4,12 @@ import type { Scope } from 'effect'; /** Run a CLI at its main-module edge, retaining scoped cleanup and one error reporter. */ export const runQualityCli = ( - command: Effect.Effect + command: Effect.Effect, ) => { - const mainLayer = Layer.effectDiscard( - command.pipe(Effect.tapError((issue) => Console.error(String(issue)))) - ).pipe(Layer.provide(NodeServices.layer)); - NodeRuntime.runMain( - Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid), - { - disableErrorReporting: true, - } + const mainLayer = Layer.effectDiscard(command.pipe(Effect.tapError((issue) => Console.error(String(issue))))).pipe( + Layer.provide(NodeServices.layer), ); + NodeRuntime.runMain(Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid), { + disableErrorReporting: true, + }); }; diff --git a/app/scripts/report-fail-closed-authorization-impact.mts b/app/scripts/report-fail-closed-authorization-impact.mts index bb45ac423..723a660e1 100644 --- a/app/scripts/report-fail-closed-authorization-impact.mts +++ b/app/scripts/report-fail-closed-authorization-impact.mts @@ -18,28 +18,24 @@ import { } from 'effect'; import { Argument, Command } from 'effect/unstable/cli'; -const InventoryHashSchema = Schema.String.check( - Schema.isPattern(/^[a-f0-9]{64}$/u) -); +const InventoryHashSchema = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u)); const SourceRevisionSchema = Schema.String.check( Schema.isMinLength(1), Schema.isMaxLength(100), - Schema.isPattern(/^[a-zA-Z0-9._-]+$/u) + Schema.isPattern(/^[a-zA-Z0-9._-]+$/u), +); +const EntrypointKeySchema = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u)).pipe( + Schema.brand('EntrypointKey'), ); -const EntrypointKeySchema = Schema.String.check( - Schema.isPattern(/^[a-z][a-z0-9]*(?:[./_-][a-z0-9]+)*$/u) -).pipe(Schema.brand('EntrypointKey')); const CanonicalTimestampStringSchema = Schema.String.check( Schema.makeFilter((value) => { const parsed = DateTime.make(value); return Option.isSome(parsed) && DateTime.formatIso(parsed.value) === value ? undefined : 'timestamp must use canonical UTC ISO 8601 encoding'; - }) -); -const CanonicalTimestampSchema = CanonicalTimestampStringSchema.pipe( - Schema.decodeTo(Schema.DateTimeUtcFromString) + }), ); +const CanonicalTimestampSchema = CanonicalTimestampStringSchema.pipe(Schema.decodeTo(Schema.DateTimeUtcFromString)); const WouldDenyEvidenceSchema = Schema.Struct({ denialReason: Schema.Literals([ @@ -64,17 +60,11 @@ const WouldDenyEvidenceSchema = Schema.Struct({ ]), schemaVersion: Schema.Literal(1), sourceRevision: SourceRevisionSchema, - surface: Schema.Literals([ - 'action', - 'capability_issuance', - 'route', - 'worker', - ]), + surface: Schema.Literals(['action', 'capability_issuance', 'route', 'worker']), timestamp: CanonicalTimestampSchema, type: Schema.Literal('authorization.would_deny'), }).annotate({ - identifier: - 'authorization evidence is malformed or contains prohibited fields', + identifier: 'authorization evidence is malformed or contains prohibited fields', }); const NonEmptyEvidenceSchema = Schema.NonEmptyArray(WouldDenyEvidenceSchema); @@ -87,8 +77,7 @@ const EmptyAuthorizationObservationSchema = Schema.Struct({ startedAt: CanonicalTimestampSchema, }); -export type EmptyAuthorizationObservation = - (typeof EmptyAuthorizationObservationSchema)['Encoded']; +export type EmptyAuthorizationObservation = (typeof EmptyAuthorizationObservationSchema)['Encoded']; const AuthorizationImpactAggregateSchema = Schema.Struct({ count: Schema.Number, @@ -110,19 +99,17 @@ const AuthorizationImpactReportSchema = Schema.Struct({ totalWouldDeny: Schema.Number, }); -export type AuthorizationImpactReport = - (typeof AuthorizationImpactReportSchema)['Encoded']; +export type AuthorizationImpactReport = (typeof AuthorizationImpactReportSchema)['Encoded']; class AuthorizationImpactValidationError extends Schema.TaggedError()( 'AuthorizationImpactValidationError', - { message: Schema.String } + { message: Schema.String }, ) {} const validationError = (message: string): AuthorizationImpactValidationError => new AuthorizationImpactValidationError({ message }); -type AuthorizationImpactAggregate = - AuthorizationImpactReport['aggregates'][number]; +type AuthorizationImpactAggregate = AuthorizationImpactReport['aggregates'][number]; const localeStringOrder = Order.make((left, right) => { const comparison = left.localeCompare(right); if (comparison < 0) { @@ -131,55 +118,25 @@ const localeStringOrder = Order.make((left, right) => { return comparison > 0 ? 1 : 0; }); const aggregateOrder = Order.combineAll([ - Order.mapInput( - localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.surface - ), - Order.mapInput( - localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.entrypointKey - ), - Order.mapInput( - localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.policyClass - ), - Order.mapInput( - localeStringOrder, - (aggregate: AuthorizationImpactAggregate) => aggregate.denialReason - ), + Order.mapInput(localeStringOrder, (aggregate: AuthorizationImpactAggregate) => aggregate.surface), + Order.mapInput(localeStringOrder, (aggregate: AuthorizationImpactAggregate) => aggregate.entrypointKey), + Order.mapInput(localeStringOrder, (aggregate: AuthorizationImpactAggregate) => aggregate.policyClass), + Order.mapInput(localeStringOrder, (aggregate: AuthorizationImpactAggregate) => aggregate.denialReason), ]); -const reduceDecodedEvidence = ( - events: NonEmptyEvidence -): AuthorizationImpactReport => { +const reduceDecodedEvidence = (events: NonEmptyEvidence): AuthorizationImpactReport => { const [first] = events; if ( - events.some( - (event) => - event.sourceRevision !== first.sourceRevision || - event.inventoryHash !== first.inventoryHash - ) + events.some((event) => event.sourceRevision !== first.sourceRevision || event.inventoryHash !== first.inventoryHash) ) { return Result.getOrThrow( - Result.fail( - validationError( - 'authorization evidence mixes source revisions or inventory hashes' - ) - ) + Result.fail(validationError('authorization evidence mixes source revisions or inventory hashes')), ); } - const counts = new Map< - string, - AuthorizationImpactReport['aggregates'][number] - >(); + const counts = new Map(); for (const event of events) { - const key = [ - event.surface, - event.entrypointKey, - event.policyClass, - event.denialReason, - ].join('\0'); + const key = [event.surface, event.entrypointKey, event.policyClass, event.denialReason].join('\0'); const current = counts.get(key); counts.set(key, { count: (current?.count ?? 0) + 1, @@ -192,7 +149,7 @@ const reduceDecodedEvidence = ( const timestamps = EffectArray.sort( events.map((event) => DateTime.formatIso(event.timestamp)), - Order.String + Order.String, ); return { aggregates: EffectArray.sort([...counts.values()], aggregateOrder), @@ -209,17 +166,14 @@ const reduceDecodedEvidence = ( export const reduceAuthorizationImpact = ( rawEvents: readonly object[], - emptyObservation?: EmptyAuthorizationObservation + emptyObservation?: EmptyAuthorizationObservation, ): AuthorizationImpactReport => { if (rawEvents.length > 0) { const events = Result.getOrThrowWith( Schema.decodeUnknownResult(NonEmptyEvidenceSchema, { onExcessProperty: 'error', })(rawEvents), - () => - validationError( - 'authorization evidence is malformed or contains prohibited fields' - ) + () => validationError('authorization evidence is malformed or contains prohibited fields'), ); return reduceDecodedEvidence(events); } @@ -228,21 +182,11 @@ export const reduceAuthorizationImpact = ( Schema.decodeUnknownResult(EmptyAuthorizationObservationSchema, { onExcessProperty: 'preserve', })(emptyObservation), - () => - validationError( - 'empty authorization impact requires explicit observation bounds' - ) + () => validationError('empty authorization impact requires explicit observation bounds'), ); - if ( - DateTime.toEpochMillis(observation.startedAt) > - DateTime.toEpochMillis(observation.endedAt) - ) { + if (DateTime.toEpochMillis(observation.startedAt) > DateTime.toEpochMillis(observation.endedAt)) { return Result.getOrThrow( - Result.fail( - validationError( - 'empty authorization impact requires explicit observation bounds' - ) - ) + Result.fail(validationError('empty authorization impact requires explicit observation bounds')), ); } return { @@ -265,34 +209,21 @@ const BoundedEvidenceBatchSchema = Schema.Struct({ sourceRevision: SourceRevisionSchema, startedAt: CanonicalTimestampSchema, }); -const EvidenceDocumentSchema = Schema.Union([ - Schema.Array(WouldDenyEvidenceSchema), - BoundedEvidenceBatchSchema, -]); +const EvidenceDocumentSchema = Schema.Union([Schema.Array(WouldDenyEvidenceSchema), BoundedEvidenceBatchSchema]); -const writeAuthorizationImpactReport = Effect.fn( - 'writeAuthorizationImpactReport' -)(function* writeReport(inputPath: Option.Option) { +const writeAuthorizationImpactReport = Effect.fn('writeAuthorizationImpactReport')(function* writeReport( + inputPath: Option.Option, +) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const configuredRoot = yield* Config.option( - Config.string('ULTRAMODERN_WORKSPACE_ROOT') - ); - const root = Option.getOrElse(configuredRoot, () => - path.resolve(import.meta.dirname, '..') - ); - const input = Option.getOrElse(inputPath, () => - path.join(root, '.codex/reports/authorization/would-deny.json') - ); - const output = path.join( - root, - '.codex/reports/authorization/fail-closed-impact.json' - ); + const configuredRoot = yield* Config.option(Config.string('ULTRAMODERN_WORKSPACE_ROOT')); + const root = Option.getOrElse(configuredRoot, () => path.resolve(import.meta.dirname, '..')); + const input = Option.getOrElse(inputPath, () => path.join(root, '.codex/reports/authorization/would-deny.json')); + const output = path.join(root, '.codex/reports/authorization/fail-closed-impact.json'); const source = yield* fileSystem.readFileString(input); - const document = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(EvidenceDocumentSchema), - { onExcessProperty: 'error' } - )(source); + const document = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(EvidenceDocumentSchema), { + onExcessProperty: 'error', + })(source); let report: AuthorizationImpactReport; if (Schema.is(Schema.Array(WouldDenyEvidenceSchema))(document)) { @@ -310,12 +241,10 @@ const writeAuthorizationImpactReport = Effect.fn( }); } - const decodedReport = yield* Schema.decodeUnknownEffect( - AuthorizationImpactReportSchema - )(report); - const outputJson = yield* Schema.encodeEffect( - Schema.fromJsonString(AuthorizationImpactReportSchema, { space: 2 }) - )(decodedReport); + const decodedReport = yield* Schema.decodeUnknownEffect(AuthorizationImpactReportSchema)(report); + const outputJson = yield* Schema.encodeEffect(Schema.fromJsonString(AuthorizationImpactReportSchema, { space: 2 }))( + decodedReport, + ); yield* fileSystem.makeDirectory(path.dirname(output), { recursive: true }); yield* fileSystem.writeFileString(output, `${outputJson}\n`); yield* Console.log(output); @@ -324,18 +253,13 @@ const writeAuthorizationImpactReport = Effect.fn( const command = Command.make( 'report-fail-closed-authorization-impact', { input: Argument.file('input').pipe(Argument.optional) }, - ({ input }) => writeAuthorizationImpactReport(input) + ({ input }) => writeAuthorizationImpactReport(input), ); const [, invokedPath] = process.argv; -if ( - invokedPath !== undefined && - import.meta.url === pathToFileURL(invokedPath).href -) { - const mainLayer = Layer.effectDiscard( - Command.run(command, { version: '1.0.0' }) - ).pipe(Layer.provide(NodeServices.layer)); - NodeRuntime.runMain( - Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid) +if (invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href) { + const mainLayer = Layer.effectDiscard(Command.run(command, { version: '1.0.0' })).pipe( + Layer.provide(NodeServices.layer), ); + NodeRuntime.runMain(Effect.scoped(Layer.build(mainLayer)).pipe(Effect.asVoid)); } diff --git a/app/scripts/reset-workspace-dependencies.mjs b/app/scripts/reset-workspace-dependencies.mjs index e06baad0e..c2a6bbbfd 100644 --- a/app/scripts/reset-workspace-dependencies.mjs +++ b/app/scripts/reset-workspace-dependencies.mjs @@ -12,9 +12,7 @@ for (const scope of ['apps', 'packages', 'verticals']) { const entries = readdirSync(scopeDirectory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { - dependencyDirectories.push( - path.join(scopeDirectory, entry.name, 'node_modules') - ); + dependencyDirectories.push(path.join(scopeDirectory, entry.name, 'node_modules')); } } } @@ -23,6 +21,4 @@ for (const directory of dependencyDirectories) { rmSync(directory, { force: true, recursive: true }); } -console.log( - `Removed ${dependencyDirectories.length} workspace dependency directories` -); +console.log(`Removed ${dependencyDirectories.length} workspace dependency directories`); diff --git a/app/scripts/run-zerops-migrator.mjs b/app/scripts/run-zerops-migrator.mjs index 551c96758..0d6f05eb3 100644 --- a/app/scripts/run-zerops-migrator.mjs +++ b/app/scripts/run-zerops-migrator.mjs @@ -39,20 +39,15 @@ const run = Effect.fn('run')( function* runEffect(command, commandArguments, cwd = appDirectory) { const child = yield* Effect.acquireRelease( Effect.try({ - catch: (cause) => - new MigratorError(`${command} failed to start`, cause), + catch: (cause) => new MigratorError(`${command} failed to start`, cause), try: () => spawn(command, commandArguments, { cwd, stdio: 'inherit' }), }), - stopChild + stopChild, ); yield* Effect.callback((resume) => { const onError = (cause) => { - resume( - Effect.fail( - new MigratorError(`${command} failed while running`, cause) - ) - ); + resume(Effect.fail(new MigratorError(`${command} failed while running`, cause))); }; /** * @param {number | null} code - Numeric process exit code. @@ -64,9 +59,7 @@ const run = Effect.fn('run')( return; } const outcome = signal ?? `code ${String(code)}`; - resume( - Effect.fail(new MigratorError(`${command} exited with ${outcome}`)) - ); + resume(Effect.fail(new MigratorError(`${command} exited with ${outcome}`))); }; child.once('error', onError); @@ -76,12 +69,11 @@ const run = Effect.fn('run')( child.off('exit', onExit); }); }); - } + }, ); /** @param {string} relativePath - Application-relative script path. */ -const runAppScript = (relativePath) => - run(process.execPath, [path.join(appDirectory, relativePath)]); +const runAppScript = (relativePath) => run(process.execPath, [path.join(appDirectory, relativePath)]); /** * @param {string} relativeDirectory - Application-relative package directory. * @param {string} config - Drizzle configuration filename. @@ -91,7 +83,7 @@ const migrate = (relativeDirectory, config) => { return run( path.join(workingDirectory, 'node_modules', '.bin', 'drizzle-kit'), ['migrate', '--config', config], - workingDirectory + workingDirectory, ); }; @@ -122,22 +114,15 @@ const serveReadiness = Effect.fn('serveReadiness')( return; } response.writeHead(404).end(); - }) + }), ), - closeServer + closeServer, ); yield* Effect.callback((resume) => { - const onError = (cause) => - resume( - Effect.fail( - new MigratorError('The migration readiness server failed', cause) - ) - ); + const onError = (cause) => resume(Effect.fail(new MigratorError('The migration readiness server failed', cause))); const onListening = () => { - console.log( - `Migration verification complete; readiness listening on port ${String(port)}` - ); + console.log(`Migration verification complete; readiness listening on port ${String(port)}`); }; const onSignal = () => resume(Effect.void); @@ -154,7 +139,7 @@ const serveReadiness = Effect.fn('serveReadiness')( process.off('SIGTERM', onSignal); }); }); - } + }, ); const main = Effect.scoped( @@ -163,15 +148,13 @@ const main = Effect.scoped( yield* runAppScript('scripts/postgres/bootstrap-runtime-role.mts'); yield* migrate('packages/core-runtime', 'drizzle.config.ts'); yield* migrate('apps/shell-super-app', 'drizzle.auth.config.ts'); - yield* runAppScript( - 'verticals/party-registry/scripts/prepare-contacts-migration.mts' - ); + yield* runAppScript('verticals/party-registry/scripts/prepare-contacts-migration.mts'); yield* migrate('verticals/party-registry', 'drizzle.contacts.config.ts'); yield* migrate('verticals/party-registry', 'drizzle.config.ts'); yield* runAppScript('scripts/postgres/bootstrap-runtime-role.mts'); yield* runAppScript('scripts/verify-application-db-schema.mts'); yield* serveReadiness(yield* migratorPort); - }).pipe(Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause)))) + }).pipe(Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause)))), ); const exit = await Effect.runPromiseExit(main); diff --git a/app/scripts/scaffolding/action-service/scaffold.mts b/app/scripts/scaffolding/action-service/scaffold.mts index 99bcb95e6..0ec82fae8 100644 --- a/app/scripts/scaffolding/action-service/scaffold.mts +++ b/app/scripts/scaffolding/action-service/scaffold.mts @@ -10,53 +10,28 @@ import { toCamelCase, tryScaffold, } from '../shared.mts'; -import type { - ActionServiceScaffoldConfig, - ActionServiceScaffoldResult, - ScaffoldPlan, -} from '../shared.mts'; +import type { ActionServiceScaffoldConfig, ActionServiceScaffoldResult, ScaffoldPlan } from '../shared.mts'; -const renderActionService = ( - service: string -): string => `${ACTION_SERVICE_GENERATOR_HEADER} +const renderActionService = (service: string): string => `${ACTION_SERVICE_GENERATOR_HEADER} import { Effect } from 'effect'; export const ${toCamelCase(service)}Service = () => Effect.succeed({}); `; -const planActionServiceScaffold = Effect.fn('ActionServiceScaffold.plan')( - function* planActionServiceScaffold( - workspaceRoot: string, - config: ActionServiceScaffoldConfig - ) { - const service = yield* tryScaffold('service name is invalid', () => - requireCanonicalSlug(config.service, 'service') - ); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); - const servicePath = yield* tryScaffold( - 'failed to resolve Action service path', - () => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'src', - 'services', - `${service}.service.ts` - ) - ); - const mutation = yield* createMutationEffect( - servicePath, - renderActionService(service) - ); - return { - mutations: [mutation], - result: { servicePath }, - } satisfies ScaffoldPlan; - } -); +const planActionServiceScaffold = Effect.fn('ActionServiceScaffold.plan')(function* planActionServiceScaffold( + workspaceRoot: string, + config: ActionServiceScaffoldConfig, +) { + const service = yield* tryScaffold('service name is invalid', () => requireCanonicalSlug(config.service, 'service')); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + const servicePath = yield* tryScaffold('failed to resolve Action service path', () => + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'src', 'services', `${service}.service.ts`), + ); + const mutation = yield* createMutationEffect(servicePath, renderActionService(service)); + return { + mutations: [mutation], + result: { servicePath }, + } satisfies ScaffoldPlan; +}); export default createCodesmithGenerator(planActionServiceScaffold); diff --git a/app/scripts/scaffolding/action/scaffold.mts b/app/scripts/scaffolding/action/scaffold.mts index b680853c7..23d41ae08 100644 --- a/app/scripts/scaffolding/action/scaffold.mts +++ b/app/scripts/scaffolding/action/scaffold.mts @@ -35,10 +35,7 @@ import { updateMutation, withCoreDependency, } from '../shared.mts'; -import type { - ActionScaffoldConfig, - OntosVerticalMetadata, -} from '../shared.mts'; +import type { ActionScaffoldConfig, OntosVerticalMetadata } from '../shared.mts'; const CORE_RUNTIME_DIRECTORY = 'core-runtime'; @@ -46,7 +43,7 @@ const renderAction = ( vertical: OntosVerticalMetadata, action: string, legalEntityScope: ActionScaffoldConfig['legalEntityScope'], - provisioning: ActionScaffoldConfig['provisioning'] + provisioning: ActionScaffoldConfig['provisioning'], ): string => { const actionType = toPascalCase(action); const actionValue = `${toCamelCase(action)}Action`; @@ -116,7 +113,7 @@ const renderCoreAction = ( moduleKey: string, action: string, legalEntityScope: ActionScaffoldConfig['legalEntityScope'], - provisioning: ActionScaffoldConfig['provisioning'] + provisioning: ActionScaffoldConfig['provisioning'], ): string => { const actionType = toPascalCase(action); const actionValue = `${toCamelCase(action)}Action`; @@ -185,233 +182,171 @@ const coreExportEntry = (action: string): string => const isCoreActionExport = (candidate: string): boolean => /^export \{ [a-z][A-Za-z0-9]*Action \} from '\.\/modules\/actions\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.action\.ts';$/u.test( - candidate + candidate, ); const coreCatalogImportEntry = (action: string): string => `import { ${toCamelCase(action)}Action } from './${action}.action.ts';`; const isCoreActionCatalogImport = (candidate: string): boolean => - /^import \{ [a-z][A-Za-z0-9]*Action \} from '\.\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.action\.ts';$/u.test( - candidate - ); + /^import \{ [a-z][A-Za-z0-9]*Action \} from '\.\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.action\.ts';$/u.test(candidate); -const coreCatalogValueEntry = (action: string): string => - `${toCamelCase(action)}Action.descriptor,`; +const coreCatalogValueEntry = (action: string): string => `${toCamelCase(action)}Action.descriptor,`; const isCoreActionCatalogValue = (candidate: string): boolean => /^[a-z][A-Za-z0-9]*Action\.descriptor,$/u.test(candidate); -const planCoreActionScaffold = Effect.fn('ActionScaffold.planCore')( - function* planCoreActionScaffold( - workspaceRoot: string, - moduleKeyInput: string, - action: string, - legalEntityScope: ActionScaffoldConfig['legalEntityScope'], - provisioning: ActionScaffoldConfig['provisioning'] - ) { - const moduleKey = yield* tryScaffold('Core module key is invalid', () => - requireCoreModuleKey(moduleKeyInput) - ); - const [actionPath, indexPath, catalogPath] = yield* tryScaffold( - 'failed to resolve Core Action paths', - () => - [ - resolveContainedPath( - workspaceRoot, - 'packages', - CORE_RUNTIME_DIRECTORY, - 'src', - 'modules', - 'actions', - `${action}.action.ts` - ), - resolveContainedPath( - workspaceRoot, - 'packages', - CORE_RUNTIME_DIRECTORY, - 'src', - 'index.ts' - ), - resolveContainedPath( - workspaceRoot, - 'packages', - CORE_RUNTIME_DIRECTORY, - 'src', - 'modules', - 'actions', - 'catalog.ts' - ), - ] as const - ); - const actionMutation = yield* createMutationEffect( - actionPath, - renderCoreAction(moduleKey, action, legalEntityScope, provisioning) - ); - const fileSystem = yield* FileSystem.FileSystem; - const indexContent = yield* fileSystem - .readFileString(indexPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure(`failed to read ${indexPath}`, cause) - ) - ); - const nextIndex = yield* tryScaffold( - 'failed to patch the Core Action export slot', - () => - insertSortedSlot( - indexContent, - CORE_ACTION_SLOT_START, - CORE_ACTION_SLOT_END, - [coreExportEntry(action)], - isCoreActionExport - ) - ); - const indexMutation = updateMutation(indexPath, indexContent, nextIndex); - const catalogContent = yield* fileSystem - .readFileString(catalogPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure(`failed to read ${catalogPath}`, cause) - ) - ); - const nextCatalog = yield* tryScaffold( - 'failed to patch the Core Action catalog', - () => - insertSortedSlot( - insertSortedSlot( - catalogContent, - CORE_ACTION_CATALOG_IMPORT_SLOT_START, - CORE_ACTION_CATALOG_IMPORT_SLOT_END, - [coreCatalogImportEntry(action)], - isCoreActionCatalogImport - ), - CORE_ACTION_CATALOG_VALUE_SLOT_START, - CORE_ACTION_CATALOG_VALUE_SLOT_END, - [coreCatalogValueEntry(action)], - isCoreActionCatalogValue - ) - ); - const catalogMutation = updateMutation( - catalogPath, - catalogContent, - nextCatalog - ); - const mutations = [actionMutation, indexMutation, catalogMutation].filter( - (mutation) => mutation !== undefined - ); - yield* tryScaffold('Core Action mutation paths are invalid', () => - ensureUniqueMutationPaths(mutations) - ); - return { mutations, result: { actionPath } }; - } -); +const planCoreActionScaffold = Effect.fn('ActionScaffold.planCore')(function* planCoreActionScaffold( + workspaceRoot: string, + moduleKeyInput: string, + action: string, + legalEntityScope: ActionScaffoldConfig['legalEntityScope'], + provisioning: ActionScaffoldConfig['provisioning'], +) { + const moduleKey = yield* tryScaffold('Core module key is invalid', () => requireCoreModuleKey(moduleKeyInput)); + const [actionPath, indexPath, catalogPath] = yield* tryScaffold( + 'failed to resolve Core Action paths', + () => + [ + resolveContainedPath( + workspaceRoot, + 'packages', + CORE_RUNTIME_DIRECTORY, + 'src', + 'modules', + 'actions', + `${action}.action.ts`, + ), + resolveContainedPath(workspaceRoot, 'packages', CORE_RUNTIME_DIRECTORY, 'src', 'index.ts'), + resolveContainedPath( + workspaceRoot, + 'packages', + CORE_RUNTIME_DIRECTORY, + 'src', + 'modules', + 'actions', + 'catalog.ts', + ), + ] as const, + ); + const actionMutation = yield* createMutationEffect( + actionPath, + renderCoreAction(moduleKey, action, legalEntityScope, provisioning), + ); + const fileSystem = yield* FileSystem.FileSystem; + const indexContent = yield* fileSystem + .readFileString(indexPath) + .pipe(Effect.mapError((cause) => scaffoldFailure(`failed to read ${indexPath}`, cause))); + const nextIndex = yield* tryScaffold('failed to patch the Core Action export slot', () => + insertSortedSlot( + indexContent, + CORE_ACTION_SLOT_START, + CORE_ACTION_SLOT_END, + [coreExportEntry(action)], + isCoreActionExport, + ), + ); + const indexMutation = updateMutation(indexPath, indexContent, nextIndex); + const catalogContent = yield* fileSystem + .readFileString(catalogPath) + .pipe(Effect.mapError((cause) => scaffoldFailure(`failed to read ${catalogPath}`, cause))); + const nextCatalog = yield* tryScaffold('failed to patch the Core Action catalog', () => + insertSortedSlot( + insertSortedSlot( + catalogContent, + CORE_ACTION_CATALOG_IMPORT_SLOT_START, + CORE_ACTION_CATALOG_IMPORT_SLOT_END, + [coreCatalogImportEntry(action)], + isCoreActionCatalogImport, + ), + CORE_ACTION_CATALOG_VALUE_SLOT_START, + CORE_ACTION_CATALOG_VALUE_SLOT_END, + [coreCatalogValueEntry(action)], + isCoreActionCatalogValue, + ), + ); + const catalogMutation = updateMutation(catalogPath, catalogContent, nextCatalog); + const mutations = [actionMutation, indexMutation, catalogMutation].filter((mutation) => mutation !== undefined); + yield* tryScaffold('Core Action mutation paths are invalid', () => ensureUniqueMutationPaths(mutations)); + return { mutations, result: { actionPath } }; +}); -const planActionScaffold = Effect.fn('ActionScaffold.plan')( - function* planActionScaffold( - workspaceRoot: string, - config: ActionScaffoldConfig - ) { - const action = yield* tryScaffold('Action name is invalid', () => - requireCanonicalSlug(config.action, 'action') - ); - if (config.scope === 'core') { - return yield* planCoreActionScaffold( - workspaceRoot, - config.module, - action, - config.legalEntityScope, - config.provisioning - ); - } - const vertical = yield* discoverOntosModuleEffect( +const planActionScaffold = Effect.fn('ActionScaffold.plan')(function* planActionScaffold( + workspaceRoot: string, + config: ActionScaffoldConfig, +) { + const action = yield* tryScaffold('Action name is invalid', () => requireCanonicalSlug(config.action, 'action')); + if (config.scope === 'core') { + return yield* planCoreActionScaffold( workspaceRoot, - config.vertical + config.module, + action, + config.legalEntityScope, + config.provisioning, ); - const actionPath = yield* tryScaffold('failed to resolve Action path', () => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'src', - 'actions', - `${action}.action.ts` - ) - ); - const actionMutation = yield* createMutationEffect( - actionPath, - renderAction( - vertical, - action, - config.legalEntityScope, - config.provisioning - ) - ); - const actionValue = `${toCamelCase(action)}Action`; - const ownerImport = `import { ${actionValue} } from './src/actions/${action}.action.ts';`; - const [nextManifest, nextRegistration] = yield* tryScaffold( - 'failed to patch generated Action owner slots', - () => - [ + } + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + const actionPath = yield* tryScaffold('failed to resolve Action path', () => + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'src', 'actions', `${action}.action.ts`), + ); + const actionMutation = yield* createMutationEffect( + actionPath, + renderAction(vertical, action, config.legalEntityScope, config.provisioning), + ); + const actionValue = `${toCamelCase(action)}Action`; + const ownerImport = `import { ${actionValue} } from './src/actions/${action}.action.ts';`; + const [nextManifest, nextRegistration] = yield* tryScaffold( + 'failed to patch generated Action owner slots', + () => + [ + insertSortedSlot( insertSortedSlot( - insertSortedSlot( - vertical.manifestContent, - MODULE_MANIFEST_IMPORT_SLOT_START, - MODULE_MANIFEST_IMPORT_SLOT_END, - [ownerImport], - isModuleManifestImport - ), - MODULE_MANIFEST_ACTION_SLOT_START, - MODULE_MANIFEST_ACTION_SLOT_END, - [`${actionValue},`], - (candidate) => /^[a-z][A-Za-z0-9]*Action,$/u.test(candidate) + vertical.manifestContent, + MODULE_MANIFEST_IMPORT_SLOT_START, + MODULE_MANIFEST_IMPORT_SLOT_END, + [ownerImport], + isModuleManifestImport, ), + MODULE_MANIFEST_ACTION_SLOT_START, + MODULE_MANIFEST_ACTION_SLOT_END, + [`${actionValue},`], + (candidate) => /^[a-z][A-Za-z0-9]*Action,$/u.test(candidate), + ), + insertSortedSlot( insertSortedSlot( - insertSortedSlot( - vertical.registrationContent, - MODULE_REGISTRATION_IMPORT_SLOT_START, - MODULE_REGISTRATION_IMPORT_SLOT_END, - [ownerImport], - (candidate) => - /^import \{ [a-z][A-Za-z0-9]*Action \} from '\.\/src\/actions\/[a-z][a-z0-9-]*\.action\.ts';$/u.test( - candidate - ) || - /^import \{ [a-z][A-Za-z0-9]*Worker \} from '\.\/src\/workers\/[a-z][a-z0-9-]*\.worker\.ts';$/u.test( - candidate - ) - ), - MODULE_REGISTRATION_ACTION_SLOT_START, - MODULE_REGISTRATION_ACTION_SLOT_END, - [`${actionValue},`], - (candidate) => /^[a-z][A-Za-z0-9]*Action,$/u.test(candidate) + vertical.registrationContent, + MODULE_REGISTRATION_IMPORT_SLOT_START, + MODULE_REGISTRATION_IMPORT_SLOT_END, + [ownerImport], + (candidate) => + /^import \{ [a-z][A-Za-z0-9]*Action \} from '\.\/src\/actions\/[a-z][a-z0-9-]*\.action\.ts';$/u.test( + candidate, + ) || + /^import \{ [a-z][A-Za-z0-9]*Worker \} from '\.\/src\/workers\/[a-z][a-z0-9-]*\.worker\.ts';$/u.test( + candidate, + ), ), - ] as const - ); - const manifestMutation = updateMutation( - vertical.manifestPath, - vertical.manifestContent, - nextManifest - ); - const registrationMutation = updateMutation( - vertical.registrationPath, - vertical.registrationContent, - nextRegistration - ); - const dependencyMutation = yield* tryScaffold( - 'failed to patch the Core dependency', - () => withCoreDependency(vertical) - ); - const mutations = [ - actionMutation, - manifestMutation, - registrationMutation, - dependencyMutation, - ].filter((mutation) => mutation !== undefined); - yield* tryScaffold('Action mutation paths are invalid', () => - ensureUniqueMutationPaths(mutations) - ); - return { mutations, result: { actionPath } }; - } -); + MODULE_REGISTRATION_ACTION_SLOT_START, + MODULE_REGISTRATION_ACTION_SLOT_END, + [`${actionValue},`], + (candidate) => /^[a-z][A-Za-z0-9]*Action,$/u.test(candidate), + ), + ] as const, + ); + const manifestMutation = updateMutation(vertical.manifestPath, vertical.manifestContent, nextManifest); + const registrationMutation = updateMutation( + vertical.registrationPath, + vertical.registrationContent, + nextRegistration, + ); + const dependencyMutation = yield* tryScaffold('failed to patch the Core dependency', () => + withCoreDependency(vertical), + ); + const mutations = [actionMutation, manifestMutation, registrationMutation, dependencyMutation].filter( + (mutation) => mutation !== undefined, + ); + yield* tryScaffold('Action mutation paths are invalid', () => ensureUniqueMutationPaths(mutations)); + return { mutations, result: { actionPath } }; +}); export default createCodesmithGenerator(planActionScaffold); diff --git a/app/scripts/scaffolding/cli.mts b/app/scripts/scaffolding/cli.mts index 43e9e731e..831dac898 100644 --- a/app/scripts/scaffolding/cli.mts +++ b/app/scripts/scaffolding/cli.mts @@ -6,13 +6,7 @@ import { NodeServices } from '@effect/platform-node'; import { CodeSmith, FsMaterial, GeneratorCore } from '@modern-js/codesmith'; import type { GeneratorContext } from '@modern-js/codesmith'; import { Console, Effect, Option, Predicate, Schema } from 'effect'; -import { - Argument, - CliConfig, - Command, - Flag, - GlobalFlag, -} from 'effect/unstable/cli'; +import { Argument, CliConfig, Command, Flag, GlobalFlag } from 'effect/unstable/cli'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import actionServiceGenerator from './action-service/scaffold.mts'; @@ -87,13 +81,10 @@ const TENANT_PERMISSION_FLAG = 'tenant-permission'; export const ScaffoldCommandSchema = Schema.Literals(scaffoldCommandValues); export type ScaffoldCommand = typeof ScaffoldCommandSchema.Type; -export class ScaffoldingError extends Schema.TaggedError()( - 'ScaffoldingError', - { - cause: Schema.optional(Schema.Defect()), - message: Schema.String, - } -) {} +export class ScaffoldingError extends Schema.TaggedError()('ScaffoldingError', { + cause: Schema.optional(Schema.Defect()), + message: Schema.String, +}) {} type GeneratorResult = | ActionBoundaryScaffoldResult @@ -131,7 +122,7 @@ type TypedGeneratorContext = Omit & { type LocalGenerator = ( context: TypedGeneratorContext, - core: GeneratorCore + core: GeneratorCore, ) => Effect.Effect; export interface RouteRefreshInput { @@ -139,9 +130,7 @@ export interface RouteRefreshInput { readonly workspaceRoot: string; } -export type RouteRefreshExecutor = ( - input: RouteRefreshInput -) => Effect.Effect; +export type RouteRefreshExecutor = (input: RouteRefreshInput) => Effect.Effect; export interface RunScaffoldOptions { readonly routeRefresh?: RouteRefreshExecutor; @@ -179,22 +168,14 @@ interface CommandDefinition { | (( result: GeneratorResult, options: RunScaffoldOptions, - workspaceRoot: string - ) => Effect.Effect< - void, - ScaffoldingError, - ChildProcessSpawner.ChildProcessSpawner - >) + workspaceRoot: string, + ) => Effect.Effect) | undefined; readonly flags: readonly string[]; readonly generate: ( flags: ParsedScaffoldFlags, - workspaceRoot: string - ) => Effect.Effect< - GeneratorResult, - ScaffoldingError, - NodeServices.NodeServices - >; + workspaceRoot: string, + ) => Effect.Effect; readonly help: string; readonly requiredFlags: readonly string[]; } @@ -203,109 +184,82 @@ interface CommandDefinitionInput { readonly afterGenerate?: ( result: GeneratorResult, options: RunScaffoldOptions, - workspaceRoot: string - ) => Effect.Effect< - void, - ScaffoldingError, - ChildProcessSpawner.ChildProcessSpawner - >; + workspaceRoot: string, + ) => Effect.Effect; readonly flags: readonly string[]; readonly generator: LocalGenerator; readonly help: string; readonly requiredFlags: readonly string[]; - readonly toConfig: ( - flags: ParsedScaffoldFlags - ) => Effect.Effect; + readonly toConfig: (flags: ParsedScaffoldFlags) => Effect.Effect; } export type RunScaffoldResult = | { readonly help: string; readonly kind: 'help' } | { readonly kind: 'generated'; readonly result: GeneratorResult }; -const failScaffolding = ( - message: string, - cause?: unknown -): Effect.Effect => - Effect.fail( - new ScaffoldingError(cause === undefined ? { message } : { cause, message }) - ); +const failScaffolding = (message: string, cause?: unknown): Effect.Effect => + Effect.fail(new ScaffoldingError(cause === undefined ? { message } : { cause, message })); -const runCodesmithGenerator = Effect.fn('runCodesmithGenerator')( - function* runCodesmithGeneratorEffect< - Config extends GeneratorConfig, - Result extends GeneratorResult, - >( - generator: LocalGenerator, - workspaceRoot: string, - config: Config - ): Effect.fn.Return { - const prepared = yield* Effect.try({ - catch: (cause) => +const runCodesmithGenerator = Effect.fn('runCodesmithGenerator')(function* runCodesmithGeneratorEffect< + Config extends GeneratorConfig, + Result extends GeneratorResult, +>( + generator: LocalGenerator, + workspaceRoot: string, + config: Config, +): Effect.fn.Return { + const prepared = yield* Effect.try({ + catch: (cause) => + new ScaffoldingError({ + cause, + message: 'failed to prepare the Codesmith generator', + }), + try: () => { + const smith = new CodeSmith({ namespace: 'ontos-scaffolding' }); + const core = new GeneratorCore({ + logger: smith.logger, + materialsManager: smith.materialsManager, + outputPath: workspaceRoot, + }); + const workspaceMaterial = new FsMaterial(workspaceRoot); + const generatorMaterial = new FsMaterial(path.resolve(import.meta.dirname)); + core.addMaterial('default', workspaceMaterial); + core.addMaterial('ontos-local-generator', generatorMaterial); + core._context.config = config; + core._context.current = { material: generatorMaterial }; + const generatorContext = { ...core._context, config }; + return { core, generatorContext }; + }, + }); + const result = yield* generator(prepared.generatorContext, prepared.core).pipe( + Effect.catchDefect((cause) => + Effect.fail( new ScaffoldingError({ cause, - message: 'failed to prepare the Codesmith generator', + message: Predicate.isError(cause) ? cause.message : 'Codesmith generation failed', }), - try: () => { - const smith = new CodeSmith({ namespace: 'ontos-scaffolding' }); - const core = new GeneratorCore({ - logger: smith.logger, - materialsManager: smith.materialsManager, - outputPath: workspaceRoot, - }); - const workspaceMaterial = new FsMaterial(workspaceRoot); - const generatorMaterial = new FsMaterial( - path.resolve(import.meta.dirname) - ); - core.addMaterial('default', workspaceMaterial); - core.addMaterial('ontos-local-generator', generatorMaterial); - core._context.config = config; - core._context.current = { material: generatorMaterial }; - const generatorContext = { ...core._context, config }; - return { core, generatorContext }; - }, - }); - const result = yield* generator( - prepared.generatorContext, - prepared.core - ).pipe( - Effect.catchDefect((cause) => - Effect.fail( - new ScaffoldingError({ - cause, - message: Predicate.isError(cause) - ? cause.message - : 'Codesmith generation failed', - }) - ) ), - Effect.mapError( - (cause) => - new ScaffoldingError({ - cause, - message: Predicate.isError(cause) - ? cause.message - : 'Codesmith generation failed', - }) - ), - Effect.ensuring( - Effect.sync(() => (prepared.core._context.current = null)) - ) - ); - return result; - } -); + ), + Effect.mapError( + (cause) => + new ScaffoldingError({ + cause, + message: Predicate.isError(cause) ? cause.message : 'Codesmith generation failed', + }), + ), + Effect.ensuring(Effect.sync(() => (prepared.core._context.current = null))), + ); + return result; +}); -const defineCommand = < - Config extends GeneratorConfig, - Result extends GeneratorResult, ->( - definition: CommandDefinitionInput +const defineCommand = ( + definition: CommandDefinitionInput, ): CommandDefinition => ({ afterGenerate: definition.afterGenerate, flags: definition.flags, generate: (flags, workspaceRoot) => Effect.flatMap(definition.toConfig(flags), (config) => - runCodesmithGenerator(definition.generator, workspaceRoot, config) + runCodesmithGenerator(definition.generator, workspaceRoot, config), ), help: definition.help, requiredFlags: definition.requiredFlags, @@ -313,11 +267,7 @@ const defineCommand = < const defaultRouteRefresh = ({ appId, workspaceRoot }: RouteRefreshInput) => Effect.gen(function* defaultRouteRefreshEffect() { - const script = path.join( - workspaceRoot, - 'scripts', - 'generate-tanstack-routes.mts' - ); + const script = path.join(workspaceRoot, 'scripts', 'generate-tanstack-routes.mts'); const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const exitCode = yield* spawner .exitCode( @@ -326,7 +276,7 @@ const defaultRouteRefresh = ({ appId, workspaceRoot }: RouteRefreshInput) => stderr: 'inherit', stdin: 'inherit', stdout: 'inherit', - }) + }), ) .pipe( Effect.mapError( @@ -334,13 +284,11 @@ const defaultRouteRefresh = ({ appId, workspaceRoot }: RouteRefreshInput) => new ScaffoldingError({ cause, message: `route refresh failed for ${appId}`, - }) - ) + }), + ), ); if (exitCode !== ChildProcessSpawner.ExitCode(0)) { - return yield* failScaffolding( - `route refresh failed for ${appId}: exit ${exitCode}` - ); + return yield* failScaffolding(`route refresh failed for ${appId}: exit ${exitCode}`); } return yield* Effect.void; }); @@ -348,32 +296,21 @@ const defaultRouteRefresh = ({ appId, workspaceRoot }: RouteRefreshInput) => const LegalEntityScope = Schema.Literals(['required', 'optional', 'forbidden']); const isLegalEntityScope = Schema.is(LegalEntityScope); -const ReadAuthorization = Schema.Literals([ - 'authenticated_principal', - 'context_permission', - 'public', -]); +const ReadAuthorization = Schema.Literals(['authenticated_principal', 'context_permission', 'public']); const isReadAuthorization = Schema.is(ReadAuthorization); const RequestFilter = Schema.Literals(['includeArchived', 'role']); const isRequestFilter = Schema.is(RequestFilter); const requireReadAuthorization = ( - flags: ParsedScaffoldFlags -): Effect.Effect< - Pick, - ScaffoldingError -> => + flags: ParsedScaffoldFlags, +): Effect.Effect, ScaffoldingError> => Effect.gen(function* requireReadAuthorizationEffect() { if (!isReadAuthorization(flags.authorizationMode)) { - return yield* failScaffolding( - '--authorization must be public, authenticated_principal, or context_permission' - ); + return yield* failScaffolding('--authorization must be public, authenticated_principal, or context_permission'); } if (flags.authorizationMode === 'context_permission') { if (flags.permission === undefined) { - return yield* failScaffolding( - '--permission is required for context_permission authorization' - ); + return yield* failScaffolding('--permission is required for context_permission authorization'); } return { authorization: flags.authorizationMode, @@ -381,34 +318,18 @@ const requireReadAuthorization = ( }; } if (flags.permission !== undefined) { - return yield* failScaffolding( - '--permission is valid only for context_permission authorization' - ); + return yield* failScaffolding('--permission is valid only for context_permission authorization'); } return { authorization: flags.authorizationMode }; }); -const isActionProvisioning = Schema.is( - Schema.Literals(['tenant_membership_default', 'explicit']) -); -const isAccessFiltering = Schema.is( - Schema.Literals(['resource_permission', 'tenant_scope']) -); -const isSearchLegalEntityScope = Schema.is( - Schema.Literals(['required', 'optional']) -); +const isActionProvisioning = Schema.is(Schema.Literals(['tenant_membership_default', 'explicit'])); +const isAccessFiltering = Schema.is(Schema.Literals(['resource_permission', 'tenant_scope'])); +const isSearchLegalEntityScope = Schema.is(Schema.Literals(['required', 'optional'])); const commandDefinitions = { action: defineCommand({ - flags: [ - 'action', - 'authorization', - LEGAL_ENTITY_SCOPE_FLAG, - 'module', - 'provisioning', - 'scope', - 'vertical', - ], + flags: ['action', 'authorization', LEGAL_ENTITY_SCOPE_FLAG, 'module', 'provisioning', 'scope', 'vertical'], generator: actionGenerator, help: `Usage: pnpm scaffold:action -- --vertical --action --legal-entity-scope --authorization action_execution --provisioning @@ -429,37 +350,24 @@ Required flags: Options: --help Show this help without writing `, - requiredFlags: [ - 'action', - 'authorization', - LEGAL_ENTITY_SCOPE_FLAG, - 'provisioning', - ], + requiredFlags: ['action', 'authorization', LEGAL_ENTITY_SCOPE_FLAG, 'provisioning'], toConfig: (flags) => Effect.gen(function* actionConfigEffect() { const action = flags.action ?? ''; const { legalEntityScope } = flags; if (!isLegalEntityScope(legalEntityScope)) { - return yield* failScaffolding( - '--legal-entity-scope must be required, optional, or forbidden' - ); + return yield* failScaffolding('--legal-entity-scope must be required, optional, or forbidden'); } if (flags.authorizationMode !== 'action_execution') { - return yield* failScaffolding( - '--authorization must be action_execution for Actions' - ); + return yield* failScaffolding('--authorization must be action_execution for Actions'); } if (!isActionProvisioning(flags.provisioning)) { - return yield* failScaffolding( - '--provisioning must be tenant_membership_default or explicit' - ); + return yield* failScaffolding('--provisioning must be tenant_membership_default or explicit'); } const { module, scope, vertical } = flags; if (vertical !== undefined) { if (scope !== undefined || module !== undefined) { - return yield* failScaffolding( - '--vertical is mutually exclusive with --scope and --module' - ); + return yield* failScaffolding('--vertical is mutually exclusive with --scope and --module'); } return { action, @@ -470,14 +378,10 @@ Options: }; } if (scope !== 'core') { - return yield* failScaffolding( - '--scope core is required when --vertical is not supplied' - ); + return yield* failScaffolding('--scope core is required when --vertical is not supplied'); } if (module === undefined) { - return yield* failScaffolding( - '--module is required for Core Action ownership' - ); + return yield* failScaffolding('--module is required for Core Action ownership'); } return { action, @@ -556,9 +460,7 @@ Options: afterGenerate: (result, options, workspaceRoot) => Effect.gen(function* refreshGeneratedPagesEffect() { if (!('appId' in result) || !Predicate.isString(result.appId)) { - return yield* failScaffolding( - 'microvertical-page generator returned an invalid result' - ); + return yield* failScaffolding('microvertical-page generator returned an invalid result'); } const refresh = (input: RouteRefreshInput) => { if (options.routeRefresh === undefined) { @@ -596,9 +498,7 @@ Example: const vertical = flags.vertical ?? ''; const { url } = flags; const authorization = yield* requireReadAuthorization(flags); - return url === undefined - ? { ...authorization, page, vertical } - : { ...authorization, page, url, vertical }; + return url === undefined ? { ...authorization, page, vertical } : { ...authorization, page, url, vertical }; }), }), 'module-api': defineCommand({ @@ -693,9 +593,7 @@ Options: toConfig: (flags) => Effect.gen(function* outboxWorkerConfigEffect() { if (flags.authorizationMode !== 'owner_local_background') { - return yield* failScaffolding( - '--authorization must be owner_local_background for Outbox Workers' - ); + return yield* failScaffolding('--authorization must be owner_local_background for Outbox Workers'); } return { authorization: 'owner_local_background', @@ -728,14 +626,10 @@ Options: Effect.gen(function* policyConfigEffect() { const { scope, vertical } = flags; if (scope !== 'global' && scope !== 'microvertical') { - return yield* failScaffolding( - '--scope must be global or microvertical' - ); + return yield* failScaffolding('--scope must be global or microvertical'); } const policy = flags.policy ?? ''; - return vertical === undefined - ? { policy, scope } - : { policy, scope, vertical }; + return vertical === undefined ? { policy, scope } : { policy, scope, vertical }; }), }), 'public-component': defineCommand({ @@ -894,41 +788,22 @@ Options: --tenant-permission read_party_identity Required exactly for tenant_scope --help Show this help without writing `, - requiredFlags: [ - ACCESS_FILTERING_FLAG, - LEGAL_ENTITY_SCOPE_FLAG, - 'name', - REQUEST_FILTERS_FLAG, - 'vertical', - ], + requiredFlags: [ACCESS_FILTERING_FLAG, LEGAL_ENTITY_SCOPE_FLAG, 'name', REQUEST_FILTERS_FLAG, 'vertical'], toConfig: (flags) => Effect.gen(function* searchProviderAccessConfigEffect() { const { accessFiltering, legalEntityScope, tenantPermission } = flags; - const filters = (flags.requestFilters ?? '') - .split(',') - .filter((value) => value !== ''); + const filters = (flags.requestFilters ?? '').split(',').filter((value) => value !== ''); if (!isAccessFiltering(accessFiltering)) { - return yield* failScaffolding( - '--access-filtering must be resource_permission or tenant_scope' - ); + return yield* failScaffolding('--access-filtering must be resource_permission or tenant_scope'); } if (!isSearchLegalEntityScope(legalEntityScope)) { - return yield* failScaffolding( - '--legal-entity-scope must be required or optional' - ); + return yield* failScaffolding('--legal-entity-scope must be required or optional'); } if (!filters.every(isRequestFilter)) { - return yield* failScaffolding( - '--request-filters may contain only includeArchived and role' - ); + return yield* failScaffolding('--request-filters may contain only includeArchived and role'); } - if ( - tenantPermission !== undefined && - tenantPermission !== 'read_party_identity' - ) { - return yield* failScaffolding( - '--tenant-permission must be read_party_identity' - ); + if (tenantPermission !== undefined && tenantPermission !== 'read_party_identity') { + return yield* failScaffolding('--tenant-permission must be read_party_identity'); } const validatedFilters = filters.filter(isRequestFilter); const config: SearchProviderAccessScaffoldConfig = { @@ -948,50 +823,36 @@ Options: export const isScaffoldCommand = Schema.is(ScaffoldCommandSchema); -export const getHelpText = (command: ScaffoldCommand): string => - commandDefinitions[command].help; +export const getHelpText = (command: ScaffoldCommand): string => commandDefinitions[command].help; -const isFlagArgument = (flag: string): boolean => - flag.startsWith('--') && flag !== '--' && !flag.includes('='); +const isFlagArgument = (flag: string): boolean => flag.startsWith('--') && flag !== '--' && !flag.includes('='); const parseFlagPair = ( command: ScaffoldCommand, allowed: ReadonlySet, parsed: Map, flag: string | undefined, - value: string | undefined + value: string | undefined, ) => Effect.gen(function* parseFlagPairEffect() { if (flag === undefined || !isFlagArgument(flag)) { - return yield* failScaffolding( - `invalid argument ${flag ?? ''}; use separate --flag value pairs` - ); + return yield* failScaffolding(`invalid argument ${flag ?? ''}; use separate --flag value pairs`); } const name = flag.slice(2); if (!allowed.has(name)) { - return yield* failScaffolding( - `unknown flag --${name} for scaffold:${command}` - ); + return yield* failScaffolding(`unknown flag --${name} for scaffold:${command}`); } if (parsed.has(name)) { return yield* failScaffolding(`flag --${name} may be supplied only once`); } - if ( - value === undefined || - value.startsWith('--') || - value.trim().length === 0 - ) { - return yield* failScaffolding( - `flag --${name} requires one non-empty value` - ); + if (value === undefined || value.startsWith('--') || value.trim().length === 0) { + return yield* failScaffolding(`flag --${name} requires one non-empty value`); } parsed.set(name, value); return yield* Effect.void; }); -const normalizeForwardedArguments = ( - argumentsList: readonly string[] -): readonly string[] => { +const normalizeForwardedArguments = (argumentsList: readonly string[]): readonly string[] => { if (argumentsList[0] === '--') { return argumentsList.slice(1); } @@ -1000,7 +861,7 @@ const normalizeForwardedArguments = ( const parseFlags = ( command: ScaffoldCommand, - argumentsList: readonly string[] + argumentsList: readonly string[], ): Effect.Effect => Effect.gen(function* parseFlagsEffect() { const definition = commandDefinitions[command]; @@ -1043,36 +904,27 @@ const parseFlags = ( }; }); -export const runScaffoldEffect = Effect.fn('runScaffold')( - function* runScaffoldEffectGenerator( - command: ScaffoldCommand, - rawArguments: readonly string[], - options: RunScaffoldOptions = {} - ): Effect.fn.Return< - RunScaffoldResult, - ScaffoldingError, - NodeServices.NodeServices - > { - const argumentsList = normalizeForwardedArguments(rawArguments); - if (argumentsList.length === 1 && argumentsList[0] === '--help') { - return { help: getHelpText(command), kind: 'help' }; - } - const flags = yield* parseFlags(command, argumentsList); - const workspaceRoot = path.resolve(options.workspaceRoot ?? process.cwd()); - const definition = commandDefinitions[command]; - const result = yield* definition.generate(flags, workspaceRoot); - if (definition.afterGenerate !== undefined) { - yield* definition.afterGenerate(result, options, workspaceRoot); - } - return { kind: 'generated', result }; +export const runScaffoldEffect = Effect.fn('runScaffold')(function* runScaffoldEffectGenerator( + command: ScaffoldCommand, + rawArguments: readonly string[], + options: RunScaffoldOptions = {}, +): Effect.fn.Return { + const argumentsList = normalizeForwardedArguments(rawArguments); + if (argumentsList.length === 1 && argumentsList[0] === '--help') { + return { help: getHelpText(command), kind: 'help' }; } -); + const flags = yield* parseFlags(command, argumentsList); + const workspaceRoot = path.resolve(options.workspaceRoot ?? process.cwd()); + const definition = commandDefinitions[command]; + const result = yield* definition.generate(flags, workspaceRoot); + if (definition.afterGenerate !== undefined) { + yield* definition.afterGenerate(result, options, workspaceRoot); + } + return { kind: 'generated', result }; +}); -const optionalTextFlag = (name: string) => - Flag.string(name).pipe(Flag.optional); -const forwardedArguments = Argument.variadic( - Argument.string('forwarded flags') -); +const optionalTextFlag = (name: string) => Flag.string(name).pipe(Flag.optional); +const forwardedArguments = Argument.variadic(Argument.string('forwarded flags')); const cliFlags = { accessFiltering: optionalTextFlag(ACCESS_FILTERING_FLAG), action: optionalTextFlag('action'), @@ -1099,18 +951,13 @@ const cliFlags = { worker: optionalTextFlag('worker'), } as const; -const cliFlagName = (key: string): string => - key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`); +const cliFlagName = (key: string): string => key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`); const toCliArguments = ( - values: Readonly< - Partial>> - > + values: Readonly>>>, ): readonly string[] => Object.entries(values).flatMap(([key, value]) => - value !== undefined && Option.isSome(value) - ? [`--${cliFlagName(key)}`, value.value] - : [] + value !== undefined && Option.isSome(value) ? [`--${cliFlagName(key)}`, value.value] : [], ); const executeCliCommand = @@ -1122,10 +969,7 @@ const executeCliCommand = readonly forwarded: readonly string[]; }) => Effect.gen(function* executeCliCommandEffect() { - const result = yield* runScaffoldEffect(command, [ - ...toCliArguments(values), - ...forwarded, - ]); + const result = yield* runScaffoldEffect(command, [...toCliArguments(values), ...forwarded]); if (result.kind === 'help') { yield* Console.log(result.help); } @@ -1136,19 +980,15 @@ const cliSubcommands = scaffoldCommandValues.map((command) => command, { ...Object.fromEntries( - Object.entries(cliFlags).filter(([key]) => - commandDefinitions[command].flags.includes(cliFlagName(key)) - ) + Object.entries(cliFlags).filter(([key]) => commandDefinitions[command].flags.includes(cliFlagName(key))), ), forwarded: forwardedArguments, }, - executeCliCommand(command) - ) + executeCliCommand(command), + ), ); -const cliRoot = Command.make('scaffold').pipe( - Command.withSubcommands(cliSubcommands) -); +const cliRoot = Command.make('scaffold').pipe(Command.withSubcommands(cliSubcommands)); const customHelp = GlobalFlag.action({ flag: Flag.boolean('help').pipe(Flag.withAlias('h')), @@ -1156,25 +996,19 @@ const customHelp = GlobalFlag.action({ const command = commandPath.at(-1); return command !== undefined && isScaffoldCommand(command) ? Console.log(getHelpText(command)) - : Console.log( - `Available scaffold commands:\n${scaffoldCommandValues.join('\n')}` - ); + : Console.log(`Available scaffold commands:\n${scaffoldCommandValues.join('\n')}`); }, }); const [, entryPath] = process.argv; -if ( - entryPath !== undefined && - import.meta.url === pathToFileURL(path.resolve(entryPath)).href -) { +if (entryPath !== undefined && import.meta.url === pathToFileURL(path.resolve(entryPath)).href) { const cliProgram = Effect.updateService( Effect.matchEffect(Command.run(cliRoot, { version: '0.1.0' }), { - onFailure: (error) => - Effect.logError(`Scaffold failed: ${String(error)}`), + onFailure: (error) => Effect.logError(`Scaffold failed: ${String(error)}`), onSuccess: () => Effect.void, }), CliConfig.CliConfig, - () => CliConfig.make({ builtIns: [customHelp] }) + () => CliConfig.make({ builtIns: [customHelp] }), ); await Effect.runPromise(cliProgram.pipe(Effect.provide(NodeServices.layer))); } diff --git a/app/scripts/scaffolding/external-http-adapter/scaffold.mts b/app/scripts/scaffolding/external-http-adapter/scaffold.mts index d09042abd..8f6a24931 100644 --- a/app/scripts/scaffolding/external-http-adapter/scaffold.mts +++ b/app/scripts/scaffolding/external-http-adapter/scaffold.mts @@ -22,16 +22,10 @@ import type { const preserveFileSystemCause = (failure: ScaffoldFailure): ScaffoldFailure => { const { cause } = failure; const underlying = Predicate.isError(cause) ? cause.cause : undefined; - return Predicate.isError(underlying) - ? scaffoldFailure(`${failure.message}: ${underlying.message}`, cause) - : failure; + return Predicate.isError(underlying) ? scaffoldFailure(`${failure.message}: ${underlying.message}`, cause) : failure; }; -const renderExternalHttpAdapter = ( - packageName: string, - provider: string, - operation: string -): string => { +const renderExternalHttpAdapter = (packageName: string, provider: string, operation: string): string => { const providerType = toPascalCase(provider); const operationType = toPascalCase(operation); const adapterType = `${providerType}${operationType}`; @@ -80,25 +74,16 @@ export const ${adapterType}ServiceLive = Layer.effect( `; }; -const planExternalHttpAdapterScaffold = Effect.fn( - 'ExternalHttpAdapterScaffold.plan' -)(function* planExternalHttpAdapterScaffold( - workspaceRoot: string, - config: ExternalHttpAdapterScaffoldConfig -) { - const provider = yield* tryScaffold('provider name is invalid', () => - requireCanonicalSlug(config.provider, 'provider') - ); - const operation = yield* tryScaffold('operation name is invalid', () => - requireCanonicalSlug(config.operation, 'operation') - ); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); - const adapterPath = yield* tryScaffold( - 'failed to resolve external HTTP adapter path', - () => +const planExternalHttpAdapterScaffold = Effect.fn('ExternalHttpAdapterScaffold.plan')( + function* planExternalHttpAdapterScaffold(workspaceRoot: string, config: ExternalHttpAdapterScaffoldConfig) { + const provider = yield* tryScaffold('provider name is invalid', () => + requireCanonicalSlug(config.provider, 'provider'), + ); + const operation = yield* tryScaffold('operation name is invalid', () => + requireCanonicalSlug(config.operation, 'operation'), + ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + const adapterPath = yield* tryScaffold('failed to resolve external HTTP adapter path', () => resolveContainedPath( workspaceRoot, 'verticals', @@ -106,17 +91,18 @@ const planExternalHttpAdapterScaffold = Effect.fn( 'src', 'integrations', provider, - `${provider}-${operation}.service.ts` - ) - ); - const mutation = yield* createMutationEffect( - adapterPath, - renderExternalHttpAdapter(vertical.packageName, provider, operation) - ).pipe(Effect.mapError(preserveFileSystemCause)); - return { - mutations: [mutation], - result: { adapterPath }, - } satisfies ScaffoldPlan; -}); + `${provider}-${operation}.service.ts`, + ), + ); + const mutation = yield* createMutationEffect( + adapterPath, + renderExternalHttpAdapter(vertical.packageName, provider, operation), + ).pipe(Effect.mapError(preserveFileSystemCause)); + return { + mutations: [mutation], + result: { adapterPath }, + } satisfies ScaffoldPlan; + }, +); export default createCodesmithGenerator(planExternalHttpAdapterScaffold); diff --git a/app/scripts/scaffolding/generator-adapter.mts b/app/scripts/scaffolding/generator-adapter.mts index 04d1c2890..f7d3dc537 100644 --- a/app/scripts/scaffolding/generator-adapter.mts +++ b/app/scripts/scaffolding/generator-adapter.mts @@ -11,20 +11,15 @@ type TypedGeneratorContext = Omit & { type EffectScaffoldPlanner = ( workspaceRoot: string, - config: Config + config: Config, ) => Effect.Effect, PlannerError, Services>; -export const createCodesmithGenerator = < - Config, - Result, - PlannerError, - Services extends NodeServices.NodeServices, ->( - planner: EffectScaffoldPlanner +export const createCodesmithGenerator = ( + planner: EffectScaffoldPlanner, ) => Effect.fn('planAndApplyScaffold')(function* planAndApplyScaffold( context: TypedGeneratorContext, - core: GeneratorCore + core: GeneratorCore, ) { const plan = yield* planner(core.outputPath, context.config); return yield* applyMutationPlanEffect(core, plan); diff --git a/app/scripts/scaffolding/governed-contribution/scaffold.mts b/app/scripts/scaffolding/governed-contribution/scaffold.mts index 012b4cc8c..fbca5bbb9 100644 --- a/app/scripts/scaffolding/governed-contribution/scaffold.mts +++ b/app/scripts/scaffolding/governed-contribution/scaffold.mts @@ -1,16 +1,7 @@ import { SyntaxKind } from '@typescript/native/unstable/ast'; -import { - Array as EffectArray, - Effect, - FileSystem, - Option, - Schema, -} from 'effect'; +import { Array as EffectArray, Effect, FileSystem, Option, Schema } from 'effect'; -import { - governedApiBinding, - hasValidGovernedHttpCompositionRoot, -} from '../../generated-governed-http-boundary.mts'; +import { governedApiBinding, hasValidGovernedHttpCompositionRoot } from '../../generated-governed-http-boundary.mts'; import { hasGeneratedOperationGatewayContract, hasGeneratedGovernedClientContract, @@ -99,26 +90,17 @@ const GovernedContributionKindSchema = Schema.Literals([ REPORT_KIND, SEARCH_PROVIDER_KIND, ]); -export type GovernedContributionKind = - typeof GovernedContributionKindSchema.Type; +export type GovernedContributionKind = typeof GovernedContributionKindSchema.Type; -const ProviderContributionKindSchema = Schema.Literals([ - REPORT_KIND, - SEARCH_PROVIDER_KIND, -]); +const ProviderContributionKindSchema = Schema.Literals([REPORT_KIND, SEARCH_PROVIDER_KIND]); type ProviderContributionKind = typeof ProviderContributionKindSchema.Type; const isProviderContribution = Schema.is(ProviderContributionKindSchema); type GovernedToken = ReturnType[number]; -const propertyValueKind = ( - tokens: readonly GovernedToken[], - property: string -): SyntaxKind | undefined => { +const propertyValueKind = (tokens: readonly GovernedToken[], property: string): SyntaxKind | undefined => { const [key, colon, value] = tokens; - return key?.kind === SyntaxKind.Identifier && - key.value === property && - colon?.kind === SyntaxKind.ColonToken + return key?.kind === SyntaxKind.Identifier && key.value === property && colon?.kind === SyntaxKind.ColonToken ? value?.kind : undefined; }; @@ -126,15 +108,12 @@ const propertyValueKind = ( const directPropertyIndexes = ( tokens: readonly GovernedToken[], property: string, - kind: SyntaxKind + kind: SyntaxKind, ): readonly number[] => { const indexes: number[] = []; let braceDepth = 0; for (const [index, token] of tokens.slice(0, -2).entries()) { - if ( - braceDepth === 1 && - propertyValueKind(tokens.slice(index, index + 3), property) === kind - ) { + if (braceDepth === 1 && propertyValueKind(tokens.slice(index, index + 3), property) === kind) { indexes.push(index); } if (token.kind === SyntaxKind.OpenBraceToken) { @@ -146,25 +125,13 @@ const directPropertyIndexes = ( return indexes; }; -const directTokenStringProperty = ( - tokens: readonly GovernedToken[], - property: string -): string | undefined => { - const indexes = directPropertyIndexes( - tokens, - property, - SyntaxKind.StringLiteral - ); +const directTokenStringProperty = (tokens: readonly GovernedToken[], property: string): string | undefined => { + const indexes = directPropertyIndexes(tokens, property, SyntaxKind.StringLiteral); const [index] = indexes; - return indexes.length === 1 && index !== undefined - ? tokens[index + 2]?.value - : undefined; + return indexes.length === 1 && index !== undefined ? tokens[index + 2]?.value : undefined; }; -const identityTokenKinds = new Set([ - SyntaxKind.Identifier, - SyntaxKind.StringLiteral, -]); +const identityTokenKinds = new Set([SyntaxKind.Identifier, SyntaxKind.StringLiteral]); const compositionIdentity = (source: string): string | undefined => { for (const pattern of [ @@ -208,13 +175,11 @@ const insertSortedSlotIdempotently = ( start: string, end: string, entry: string, - validateEntry: (candidate: string) => boolean + validateEntry: (candidate: string) => boolean, ): string => { const entries = readGeneratedSlotEntries(content, start, end); if (entries.some((candidate) => !validateEntry(candidate))) { - return raiseScaffoldFailure( - `generated owner slot contains unsupported developer content: ${start}` - ); + return raiseScaffoldFailure(`generated owner slot contains unsupported developer content: ${start}`); } if (generatedSlotContainsExactEntry(content, start, end, entry)) { return content; @@ -222,13 +187,9 @@ const insertSortedSlotIdempotently = ( const expectedIdentity = slotEntryIdentity(entry); if ( expectedIdentity !== undefined && - entries.some( - (candidate) => slotEntryIdentity(candidate) === expectedIdentity - ) + entries.some((candidate) => slotEntryIdentity(candidate) === expectedIdentity) ) { - return raiseScaffoldFailure( - `generated owner slot contains drift for ${expectedIdentity}` - ); + return raiseScaffoldFailure(`generated owner slot contains drift for ${expectedIdentity}`); } return insertSortedSlot(content, start, end, [entry], validateEntry); }; @@ -238,10 +199,7 @@ const generatedHeader = (kind: GovernedContributionKind) => ? `// @generated by OntOS Codesmith Governed Contribution v1\n// @ontos-contribution-kind ${kind}` : `// @generated by OntOS Codesmith ${kind} v1`; -const manifestImport = ( - kind: GovernedContributionKind, - name: string -): string | undefined => { +const manifestImport = (kind: GovernedContributionKind, name: string): string | undefined => { const value = `${toPascalCase(name)}${kind === PUBLIC_COMPONENT_KIND ? '' : 'Api'}`; if (kind === PUBLIC_COMPONENT_KIND) { return `import { ${value} } from './src/components/${name}.tsx';`; @@ -252,15 +210,9 @@ const manifestImport = ( return undefined; }; -const manifestImportIdentity = ( - kind: GovernedContributionKind, - name: string -) => ({ +const manifestImportIdentity = (kind: GovernedContributionKind, name: string) => ({ binding: `${toPascalCase(name)}${kind === PUBLIC_COMPONENT_KIND ? '' : 'Api'}`, - specifier: - kind === PUBLIC_COMPONENT_KIND - ? `./src/components/${name}.tsx` - : `./shared/apis/${name}.ts`, + specifier: kind === PUBLIC_COMPONENT_KIND ? `./src/components/${name}.tsx` : `./shared/apis/${name}.ts`, }); const renderPublicComponent = (name: string): string => { @@ -326,35 +278,22 @@ export const ${value} = HttpApi.make('${value}').add( }; const renderReadAuthorization = ( - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ): string => { if (config.authorization === 'context_permission') { - if ( - config.permission === undefined || - !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u.test(config.permission) - ) { - return raiseScaffoldFailure( - 'context_permission authorization requires a stable --permission value' - ); + if (config.permission === undefined || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u.test(config.permission)) { + return raiseScaffoldFailure('context_permission authorization requires a stable --permission value'); } return `{ kind: 'context_permission', permission: '${config.permission}' }`; } if (config.permission !== undefined) { - return raiseScaffoldFailure( - '--permission is valid only for context_permission authorization' - ); + return raiseScaffoldFailure('--permission is valid only for context_permission authorization'); } return `{ kind: '${config.authorization}' }`; }; const readAuthorizationExpectation = ( - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ) => config.permission === undefined ? { kind: config.authorization } @@ -363,10 +302,7 @@ const readAuthorizationExpectation = ( const renderModuleApiRead = ( vertical: OntosVerticalMetadata, name: string, - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ): string => { const type = toPascalCase(name); return `${generatedHeader(MODULE_API_KIND)} @@ -410,7 +346,7 @@ const renderGovernedClientConstruction = ( vertical: OntosVerticalMetadata, apiValue: string, clientName: string, - optionsType: string + optionsType: string, ): string => `const ${clientName} = ( credential: Redacted.Redacted, requestCorrelation: string, @@ -426,10 +362,7 @@ const renderGovernedClientConstruction = ( options, );`; -const renderApiClient = ( - vertical: OntosVerticalMetadata, - name: string -): string => { +const renderApiClient = (vertical: OntosVerticalMetadata, name: string): string => { const type = toPascalCase(name); const value = `${toPascalCase(name)}Api`; const clientName = `${toCamelCase(name)}Client`; @@ -484,10 +417,7 @@ const renderProvider = ( kind: ProviderContributionKind, vertical: OntosVerticalMetadata, name: string, - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ): string => { const type = toPascalCase(name); const role = kind === REPORT_KIND ? REPORT_KIND : 'search'; @@ -497,8 +427,7 @@ const renderProvider = ( const resultSchema = `${type}ProviderResponseSchema`; const resultType = `${type}ProviderResponse`; const emptyResult = kind === REPORT_KIND ? '{ rows: [] }' : '[]'; - const resultCount = - kind === REPORT_KIND ? 'result.rows.length' : 'result.length'; + const resultCount = kind === REPORT_KIND ? 'result.rows.length' : 'result.length'; return `${generatedHeader(kind)} import { Effect } from 'effect'; import { defineRead, defineTenantModuleEntrypoint } from '@app/core-runtime'; @@ -549,7 +478,7 @@ ${kind === SEARCH_PROVIDER_KIND ? ' (result) => result.map(({ ref }) => ref),\n const renderProviderClient = ( kind: ProviderContributionKind, vertical: OntosVerticalMetadata, - name: string + name: string, ): string => { const type = toPascalCase(name); const apiValue = `${type}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`; @@ -602,7 +531,7 @@ export const load${type}Client = ( const renderProviderApiContract = ( kind: ProviderContributionKind, vertical: OntosVerticalMetadata, - name: string + name: string, ): string => { const type = toPascalCase(name); const apiValue = `${type}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`; @@ -682,7 +611,7 @@ export const ${apiValue} = HttpApi.make('${apiValue}').add( const renderGovernedServer = ( apiBinding: string, kind: Exclude, - name: string + name: string, ): string => { const type = toPascalCase(name); const names = { @@ -754,106 +683,79 @@ export const ${toCamelCase(name)}ReadApiLive = HttpApiBuilder.group( `; }; -const patchGovernedHttpComposition = Effect.fn( - 'GovernedContributionScaffold.patchHttpComposition' -)(function* patchGovernedHttpComposition( - vertical: OntosVerticalMetadata, - kind: Exclude, - name: string -) { - const isModuleApi = kind === MODULE_API_KIND; - const type = toPascalCase(name); - const contractSuffix = kind === REPORT_KIND ? REPORT_KIND : 'search'; - const contract = isModuleApi ? name : `${name}-${contractSuffix}`; - const apiValue = isModuleApi - ? `${type}Api` - : `${type}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`; - const serverSuffix = isModuleApi ? 'read' : contractSuffix; - const layerValue = `${toCamelCase(name)}ReadApiLive`; - const sharedApiPath = yield* tryScaffold( - 'failed to resolve governed HTTP API path', - () => resolveContainedPath(vertical.directory, 'shared', 'api.ts') - ); - const handlerRootPath = yield* tryScaffold( - 'failed to resolve governed HTTP handler root path', - () => resolveContainedPath(vertical.directory, 'api', 'index.ts') - ); - const fileSystem = yield* FileSystem.FileSystem; - const [sharedApi, handlerRoot] = yield* Effect.all([ - fileSystem - .readFileString(sharedApiPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure( - `failed to read governed HTTP API root ${sharedApiPath}`, - cause - ) - ) - ), - fileSystem - .readFileString(handlerRootPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure( - `failed to read governed HTTP handler root ${handlerRootPath}`, - cause - ) - ) - ), - ]); - if (!hasValidGovernedHttpCompositionRoot(sharedApi, handlerRoot)) { - return raiseScaffoldFailure( - 'governed HTTP composition slots are not bound to the exported runtime root' +const patchGovernedHttpComposition = Effect.fn('GovernedContributionScaffold.patchHttpComposition')( + function* patchGovernedHttpComposition( + vertical: OntosVerticalMetadata, + kind: Exclude, + name: string, + ) { + const isModuleApi = kind === MODULE_API_KIND; + const type = toPascalCase(name); + const contractSuffix = kind === REPORT_KIND ? REPORT_KIND : 'search'; + const contract = isModuleApi ? name : `${name}-${contractSuffix}`; + const apiValue = isModuleApi ? `${type}Api` : `${type}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`; + const serverSuffix = isModuleApi ? 'read' : contractSuffix; + const layerValue = `${toCamelCase(name)}ReadApiLive`; + const sharedApiPath = yield* tryScaffold('failed to resolve governed HTTP API path', () => + resolveContainedPath(vertical.directory, 'shared', 'api.ts'), ); - } - const nextSharedApi = yield* tryScaffold( - 'failed to patch governed HTTP API root', - () => + const handlerRootPath = yield* tryScaffold('failed to resolve governed HTTP handler root path', () => + resolveContainedPath(vertical.directory, 'api', 'index.ts'), + ); + const fileSystem = yield* FileSystem.FileSystem; + const [sharedApi, handlerRoot] = yield* Effect.all([ + fileSystem + .readFileString(sharedApiPath) + .pipe( + Effect.mapError((cause) => scaffoldFailure(`failed to read governed HTTP API root ${sharedApiPath}`, cause)), + ), + fileSystem + .readFileString(handlerRootPath) + .pipe( + Effect.mapError((cause) => + scaffoldFailure(`failed to read governed HTTP handler root ${handlerRootPath}`, cause), + ), + ), + ]); + if (!hasValidGovernedHttpCompositionRoot(sharedApi, handlerRoot)) { + return raiseScaffoldFailure('governed HTTP composition slots are not bound to the exported runtime root'); + } + const nextSharedApi = yield* tryScaffold('failed to patch governed HTTP API root', () => insertSortedSlotIdempotently( insertSortedSlotIdempotently( sharedApi - .replace( - '// ;', - '// \n;' - ) + .replace('// ;', '// \n;') .replace( /(?:\/\*\* Canonical composition-root binding consumed by generated governed HTTP adapters\. \*\/\n)?export const governedHttpApi = [A-Za-z][A-Za-z0-9]*;\n?/u, - '' + '', ), GOVERNED_HTTP_API_IMPORT_SLOT_START, GOVERNED_HTTP_API_IMPORT_SLOT_END, `import { ${apiValue} } from './apis/${contract}.ts';`, - (candidate) => - candidate.startsWith('import { ') && candidate.endsWith("';") + (candidate) => candidate.startsWith('import { ') && candidate.endsWith("';"), ), GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, `.addHttpApi(${apiValue})`, - (candidate) => - candidate.startsWith('.addHttpApi(') && candidate.endsWith(')') - ) - ); - const nextHandlerRoot = yield* tryScaffold( - 'failed to patch governed HTTP handler root', - () => { + (candidate) => candidate.startsWith('.addHttpApi(') && candidate.endsWith(')'), + ), + ); + const nextHandlerRoot = yield* tryScaffold('failed to patch governed HTTP handler root', () => { let next = insertSortedSlotIdempotently( insertSortedSlotIdempotently( handlerRoot, GOVERNED_HTTP_HANDLER_IMPORT_SLOT_START, GOVERNED_HTTP_HANDLER_IMPORT_SLOT_END, `import { ${layerValue} } from './${name}-${serverSuffix}-server.ts';`, - (candidate) => - candidate.startsWith('import { ') && candidate.endsWith("';") + (candidate) => candidate.startsWith('import { ') && candidate.endsWith("';"), ), GOVERNED_HTTP_HANDLER_LAYER_SLOT_START, GOVERNED_HTTP_HANDLER_LAYER_SLOT_END, `${layerValue}.pipe(GovernedReadLayer.provide(governedReadRuntimeLive)),`, (candidate) => /^[A-Za-z][A-Za-z0-9]*ReadApiLive\.pipe\(/u.test(candidate) && - candidate.includes( - 'GovernedReadLayer.provide(governedReadRuntimeLive)' - ) && - candidate.endsWith('),') + candidate.includes('GovernedReadLayer.provide(governedReadRuntimeLive)') && + candidate.endsWith('),'), ); if (next.includes(GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START)) { for (const supportImport of [ @@ -865,8 +767,7 @@ const patchGovernedHttpComposition = Effect.fn( GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END, supportImport, - (candidate) => - candidate.startsWith('import { ') && candidate.endsWith("';") + (candidate) => candidate.startsWith('import { ') && candidate.endsWith("';"), ); } } @@ -876,31 +777,27 @@ const patchGovernedHttpComposition = Effect.fn( GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END, 'GovernedReadLayer.provide(GovernedReadLayer.mergeAll(GovernedActionPrincipalVerifierLive, GovernedGatewayAssertionRedemptionLive)),', - (candidate) => - candidate.startsWith('GovernedReadLayer.provide(') && - candidate.endsWith('),') + (candidate) => candidate.startsWith('GovernedReadLayer.provide(') && candidate.endsWith('),'), ); } return next; + }); + const mutations: Mutation[] = []; + const sharedApiMutation = yield* tryScaffold('failed to update governed HTTP API root', () => + updateMutation(sharedApiPath, sharedApi, nextSharedApi), + ); + const handlerRootMutation = yield* tryScaffold('failed to update governed HTTP handler root', () => + updateMutation(handlerRootPath, handlerRoot, nextHandlerRoot), + ); + if (sharedApiMutation !== undefined) { + mutations.push(sharedApiMutation); } - ); - const mutations: Mutation[] = []; - const sharedApiMutation = yield* tryScaffold( - 'failed to update governed HTTP API root', - () => updateMutation(sharedApiPath, sharedApi, nextSharedApi) - ); - const handlerRootMutation = yield* tryScaffold( - 'failed to update governed HTTP handler root', - () => updateMutation(handlerRootPath, handlerRoot, nextHandlerRoot) - ); - if (sharedApiMutation !== undefined) { - mutations.push(sharedApiMutation); - } - if (handlerRootMutation !== undefined) { - mutations.push(handlerRootMutation); - } - return mutations; -}); + if (handlerRootMutation !== undefined) { + mutations.push(handlerRootMutation); + } + return mutations; + }, +); interface GovernedContributionSlots { readonly manifest: readonly [string, string, string][]; @@ -912,26 +809,14 @@ const manifestOwnerSlots = [ [MODULE_MANIFEST_COMPONENT_SLOT_START, MODULE_MANIFEST_COMPONENT_SLOT_END], [MODULE_MANIFEST_REPORT_SLOT_START, MODULE_MANIFEST_REPORT_SLOT_END], [MODULE_MANIFEST_SEARCH_SLOT_START, MODULE_MANIFEST_SEARCH_SLOT_END], - [ - MODULE_MANIFEST_SHELL_COMPONENT_SLOT_START, - MODULE_MANIFEST_SHELL_COMPONENT_SLOT_END, - ], - [ - MODULE_MANIFEST_SHELL_REPORT_SLOT_START, - MODULE_MANIFEST_SHELL_REPORT_SLOT_END, - ], - [ - MODULE_MANIFEST_SHELL_SEARCH_SLOT_START, - MODULE_MANIFEST_SHELL_SEARCH_SLOT_END, - ], + [MODULE_MANIFEST_SHELL_COMPONENT_SLOT_START, MODULE_MANIFEST_SHELL_COMPONENT_SLOT_END], + [MODULE_MANIFEST_SHELL_REPORT_SLOT_START, MODULE_MANIFEST_SHELL_REPORT_SLOT_END], + [MODULE_MANIFEST_SHELL_SEARCH_SLOT_START, MODULE_MANIFEST_SHELL_SEARCH_SLOT_END], ] as const; const registrationOwnerSlots = [ [MODULE_REGISTRATION_API_SLOT_START, MODULE_REGISTRATION_API_SLOT_END], - [ - MODULE_REGISTRATION_COMPONENT_SLOT_START, - MODULE_REGISTRATION_COMPONENT_SLOT_END, - ], + [MODULE_REGISTRATION_COMPONENT_SLOT_START, MODULE_REGISTRATION_COMPONENT_SLOT_END], [MODULE_REGISTRATION_REPORT_SLOT_START, MODULE_REGISTRATION_REPORT_SLOT_END], [MODULE_REGISTRATION_SEARCH_SLOT_START, MODULE_REGISTRATION_SEARCH_SLOT_END], ] as const; @@ -941,20 +826,13 @@ const slotLine = ( vertical: OntosVerticalMetadata, name: string, resource: string | undefined, - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ): GovernedContributionSlots => { const key = `${vertical.moduleId}.${name}`; if (kind === PUBLIC_COMPONENT_KIND) { return { manifest: [ - [ - MODULE_MANIFEST_COMPONENT_SLOT_START, - MODULE_MANIFEST_COMPONENT_SLOT_END, - `'${name}': ${toPascalCase(name)},`, - ], + [MODULE_MANIFEST_COMPONENT_SLOT_START, MODULE_MANIFEST_COMPONENT_SLOT_END, `'${name}': ${toPascalCase(name)},`], [ MODULE_MANIFEST_SHELL_COMPONENT_SLOT_START, MODULE_MANIFEST_SHELL_COMPONENT_SLOT_END, @@ -973,11 +851,7 @@ const slotLine = ( if (kind === MODULE_API_KIND) { return { manifest: [ - [ - MODULE_MANIFEST_API_SLOT_START, - MODULE_MANIFEST_API_SLOT_END, - `'${name}': ${toPascalCase(name)}Api,`, - ], + [MODULE_MANIFEST_API_SLOT_START, MODULE_MANIFEST_API_SLOT_END, `'${name}': ${toPascalCase(name)}Api,`], ], registration: [ [ @@ -1042,7 +916,7 @@ const slotLine = ( const readStringArray = ( tokens: ReturnType, - start: number + start: number, ): readonly string[] | undefined => { const values: string[] = []; for (let cursor = start; cursor < tokens.length; cursor += 1) { @@ -1059,78 +933,47 @@ const readStringArray = ( return undefined; }; -const directStringProperty = ( - source: string, - property: string -): string | undefined => +const directStringProperty = (source: string, property: string): string | undefined => directTokenStringProperty(tokenizeGovernedClient(source), property); -const directStringArrayProperty = ( - source: string, - property: string -): readonly string[] | undefined => { +const directStringArrayProperty = (source: string, property: string): readonly string[] | undefined => { const tokens = tokenizeGovernedClient(source); - const [index] = directPropertyIndexes( - tokens, - property, - SyntaxKind.OpenBracketToken - ); + const [index] = directPropertyIndexes(tokens, property, SyntaxKind.OpenBracketToken); return index === undefined ? undefined : readStringArray(tokens, index + 3); }; // Owners may adapt accessFiltering/tenantPermission and report label/dimensions. These describe // presentation and report shape; the generated provider identity and resource ownership stay fixed. -const acceptsAdaptedProviderDescriptor = ( - start: string, - current: string, - expected: string -): boolean => { - if ( - start !== MODULE_MANIFEST_SEARCH_SLOT_START && - start !== MODULE_MANIFEST_REPORT_SLOT_START - ) { +const acceptsAdaptedProviderDescriptor = (start: string, current: string, expected: string): boolean => { + if (start !== MODULE_MANIFEST_SEARCH_SLOT_START && start !== MODULE_MANIFEST_REPORT_SLOT_START) { return false; } const requiredStringProperties = ['key', 'owningModuleId', 'resourceType']; if ( requiredStringProperties.some( - (property) => - directStringProperty(current, property) !== - directStringProperty(expected, property) + (property) => directStringProperty(current, property) !== directStringProperty(expected, property), ) ) { return false; } - const expectedResourceTypes = directStringArrayProperty( - expected, - 'resourceTypes' - ); - const currentResourceTypes = directStringArrayProperty( - current, - 'resourceTypes' - ); + const expectedResourceTypes = directStringArrayProperty(expected, 'resourceTypes'); + const currentResourceTypes = directStringArrayProperty(current, 'resourceTypes'); if ( expectedResourceTypes !== undefined && (currentResourceTypes === undefined || currentResourceTypes.length !== expectedResourceTypes.length || - expectedResourceTypes.some( - (value, index) => currentResourceTypes[index] !== value - )) + expectedResourceTypes.some((value, index) => currentResourceTypes[index] !== value)) ) { return false; } const accessFiltering = directStringProperty(current, 'accessFiltering'); return ( accessFiltering === 'resource_permission' || - (accessFiltering === 'tenant_scope' && - directStringProperty(current, 'tenantPermission') !== undefined) + (accessFiltering === 'tenant_scope' && directStringProperty(current, 'tenantPermission') !== undefined) ); }; -const structurallyMatchesGeneratedEntry = ( - current: string, - expected: string -): boolean => { +const structurallyMatchesGeneratedEntry = (current: string, expected: string): boolean => { const currentTokens = tokenizeGovernedClient(current); const expectedTokens = tokenizeGovernedClient(expected); if (currentTokens.length !== expectedTokens.length) { @@ -1142,10 +985,7 @@ const structurallyMatchesGeneratedEntry = ( return false; } const carriesIdentity = identityTokenKinds.has(expectedToken.kind); - const isPropertyKey = - index === 0 && - carriesIdentity && - identityTokenKinds.has(currentToken.kind); + const isPropertyKey = index === 0 && carriesIdentity && identityTokenKinds.has(currentToken.kind); return ( (isPropertyKey || currentToken.kind === expectedToken.kind) && (!carriesIdentity || currentToken.value === expectedToken.value) @@ -1156,51 +996,35 @@ const structurallyMatchesGeneratedEntry = ( const patchSlots = ( content: string, slots: readonly [string, string, string][], - ownerSlots: readonly (readonly [string, string])[] + ownerSlots: readonly (readonly [string, string])[], ): string => slots.reduce((current, [start, end, line]) => { const entries = readGeneratedSlotEntries(current, start, end); if (entries.some((candidate) => !candidate.endsWith(','))) { - return raiseScaffoldFailure( - `generated owner slot contains unsupported developer content: ${start}` - ); + return raiseScaffoldFailure(`generated owner slot contains unsupported developer content: ${start}`); } const identity = slotEntryIdentity(line); const allOwnerEntries = ownerSlots - .filter( - ([ownerStart, ownerEnd]) => - current.includes(ownerStart) && current.includes(ownerEnd) - ) + .filter(([ownerStart, ownerEnd]) => current.includes(ownerStart) && current.includes(ownerEnd)) .flatMap(([ownerStart, ownerEnd]) => - readGeneratedSlotEntries(current, ownerStart, ownerEnd).map( - (entry) => ({ - entry, - start: ownerStart, - }) - ) - ); - if ( - allOwnerEntries.some( - ({ entry }) => slotEntryIdentity(entry) === undefined - ) - ) { - return raiseScaffoldFailure( - `generated owner slot contains unsupported developer content: ${start}` + readGeneratedSlotEntries(current, ownerStart, ownerEnd).map((entry) => ({ + entry, + start: ownerStart, + })), ); + if (allOwnerEntries.some(({ entry }) => slotEntryIdentity(entry) === undefined)) { + return raiseScaffoldFailure(`generated owner slot contains unsupported developer content: ${start}`); } const identityMatches = allOwnerEntries.filter( - ({ entry }) => - identity !== undefined && slotEntryIdentity(entry) === identity + ({ entry }) => identity !== undefined && slotEntryIdentity(entry) === identity, ); if (identityMatches.some((match) => match.start !== start)) { return raiseScaffoldFailure( - `generated owner slot contains mismatched identity in the wrong contribution category: ${identity}` + `generated owner slot contains mismatched identity in the wrong contribution category: ${identity}`, ); } if (identityMatches.length > 1) { - return raiseScaffoldFailure( - `generated owner slot contains duplicate identity: ${identity}` - ); + return raiseScaffoldFailure(`generated owner slot contains duplicate identity: ${identity}`); } if (generatedSlotContainsExactEntry(current, start, end, line)) { return current; @@ -1214,54 +1038,34 @@ const patchSlots = ( return current; } if (identityMatch !== undefined) { - return raiseScaffoldFailure( - `generated owner slot contains mismatched identity: ${identity}` - ); + return raiseScaffoldFailure(`generated owner slot contains mismatched identity: ${identity}`); } - return insertSortedSlot(current, start, end, [line], (candidate) => - candidate.endsWith(',') - ); + return insertSortedSlot(current, start, end, [line], (candidate) => candidate.endsWith(',')); }, content); /* eslint-enable unicorn/no-array-reduce */ -const patchFederationExposure = Effect.fn( - 'GovernedContributionScaffold.patchFederationExposure' -)(function* patchFederationExposure( - vertical: OntosVerticalMetadata, - name: string -) { - const configPath = yield* tryScaffold( - 'failed to resolve Module Federation config path', - () => - resolveContainedPath(vertical.directory, 'module-federation.config.ts') - ); - const fileSystem = yield* FileSystem.FileSystem; - const content = yield* fileSystem - .readFileString(configPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure( - `failed to read Module Federation config ${configPath}`, - cause - ) - ) +const patchFederationExposure = Effect.fn('GovernedContributionScaffold.patchFederationExposure')( + function* patchFederationExposure(vertical: OntosVerticalMetadata, name: string) { + const configPath = yield* tryScaffold('failed to resolve Module Federation config path', () => + resolveContainedPath(vertical.directory, 'module-federation.config.ts'), ); - const next = yield* tryScaffold( - 'failed to patch Module Federation exposure', - () => - insertModuleFederationExposure( - content, - `./${toPascalCase(name)}`, - `./src/components/${name}.tsx` - ) - ); - return { content: next, kind: 'update' as const, path: configPath }; -}); + const fileSystem = yield* FileSystem.FileSystem; + const content = yield* fileSystem + .readFileString(configPath) + .pipe( + Effect.mapError((cause) => scaffoldFailure(`failed to read Module Federation config ${configPath}`, cause)), + ); + const next = yield* tryScaffold('failed to patch Module Federation exposure', () => + insertModuleFederationExposure(content, `./${toPascalCase(name)}`, `./src/components/${name}.tsx`), + ); + return { content: next, kind: 'update' as const, path: configPath }; + }, +); const acceptsGeneratedClient = ( kind: GovernedContributionKind, vertical: OntosVerticalMetadata, - name: string + name: string, ): ((current: string) => boolean) => { const type = toPascalCase(name); const isModuleApi = kind === MODULE_API_KIND; @@ -1274,10 +1078,7 @@ const acceptsGeneratedClient = ( const operationStem = isModuleApi ? `execute${type}` : `load${type}Client`; const expectedGroups = isModuleApi ? [toCamelCase(name)] - : [ - kind === REPORT_KIND ? 'reports' : 'search', - `${toCamelCase(name)}${providerKind}`, - ]; + : [kind === REPORT_KIND ? 'reports' : 'search', `${toCamelCase(name)}${providerKind}`]; return (current) => expectedGroups.some((endpointGroup) => hasGeneratedGovernedClientContract(current, { @@ -1291,7 +1092,7 @@ const acceptsGeneratedClient = ( ? `../../shared/apis/${name}.ts` : `../../shared/apis/${name}-${kind === REPORT_KIND ? REPORT_KIND : 'search'}.ts`, publicOperation: operationStem, - }) + }), ); }; @@ -1301,18 +1102,13 @@ const operationBoundaryPaths = (vertical: OntosVerticalMetadata) => ({ }); const hasExistingOperationBoundary = ( - vertical: OntosVerticalMetadata + vertical: OntosVerticalMetadata, ): Effect.Effect => Effect.gen(function* hasExistingOperationBoundaryEffect() { const fileSystem = yield* FileSystem.FileSystem; const { gatewayPath, principalPath } = operationBoundaryPaths(vertical); - const exists = yield* Effect.all([ - fileSystem.exists(principalPath), - fileSystem.exists(gatewayPath), - ]).pipe( - Effect.mapError((cause) => - scaffoldFailure('failed to inspect operation boundary', cause) - ) + const exists = yield* Effect.all([fileSystem.exists(principalPath), fileSystem.exists(gatewayPath)]).pipe( + Effect.mapError((cause) => scaffoldFailure('failed to inspect operation boundary', cause)), ); if (!exists.every(Boolean)) { return false; @@ -1320,11 +1116,7 @@ const hasExistingOperationBoundary = ( const [principal, gateway] = yield* Effect.all([ fileSystem.readFileString(principalPath), fileSystem.readFileString(gatewayPath), - ]).pipe( - Effect.mapError((cause) => - scaffoldFailure('failed to read operation boundary', cause) - ) - ); + ]).pipe(Effect.mapError((cause) => scaffoldFailure('failed to read operation boundary', cause))); const header = `// @generated by OntOS Codesmith MicroVertical Action Boundary v1\n// @ontos-action-boundary-owner ${vertical.appId}\n`; return ( principal.startsWith(header) && @@ -1333,63 +1125,42 @@ const hasExistingOperationBoundary = ( ); }); -const planOperationBoundary = Effect.fn( - 'GovernedContributionScaffold.planOperationBoundary' -)(function* planOperationBoundary( - workspaceRoot: string, - vertical: OntosVerticalMetadata -) { - if (!(yield* hasExistingOperationBoundary(vertical))) { - const fileSystem = yield* FileSystem.FileSystem; - const { gatewayPath, principalPath } = operationBoundaryPaths(vertical); - const existingBoundaryFiles = yield* Effect.all([ - fileSystem.exists(principalPath), - fileSystem.exists(gatewayPath), - ]).pipe( - Effect.mapError((cause) => - scaffoldFailure('failed to inspect operation boundary files', cause) - ) - ); - if (existingBoundaryFiles.every(Boolean)) { - return yield* Effect.fail( - scaffoldFailure( - 'refusing to overwrite existing business file: operation boundary' - ) - ); +const planOperationBoundary = Effect.fn('GovernedContributionScaffold.planOperationBoundary')( + function* planOperationBoundary(workspaceRoot: string, vertical: OntosVerticalMetadata) { + if (!(yield* hasExistingOperationBoundary(vertical))) { + const fileSystem = yield* FileSystem.FileSystem; + const { gatewayPath, principalPath } = operationBoundaryPaths(vertical); + const existingBoundaryFiles = yield* Effect.all([ + fileSystem.exists(principalPath), + fileSystem.exists(gatewayPath), + ]).pipe(Effect.mapError((cause) => scaffoldFailure('failed to inspect operation boundary files', cause))); + if (existingBoundaryFiles.every(Boolean)) { + return yield* Effect.fail(scaffoldFailure('refusing to overwrite existing business file: operation boundary')); + } + const boundary = yield* planActionBoundaryScaffold(workspaceRoot, { + vertical: vertical.slug, + }); + return boundary.mutations; } - const boundary = yield* planActionBoundaryScaffold(workspaceRoot, { - vertical: vertical.slug, - }); - return boundary.mutations; - } - const dependencyMutation = yield* tryScaffold( - 'failed to ensure governed client dependency', - () => + const dependencyMutation = yield* tryScaffold('failed to ensure governed client dependency', () => withExactDependencies(vertical, { '@app/shared-contracts': 'workspace:*', - }) - ); - return EffectArray.getSomes([Option.fromNullishOr(dependencyMutation)]); -}); + }), + ); + return EffectArray.getSomes([Option.fromNullishOr(dependencyMutation)]); + }, +); const acceptsGovernedArtifact = ( kind: typeof MODULE_API_KIND | ProviderContributionKind, vertical: OntosVerticalMetadata, name: string, - config: Pick< - GovernedContributionScaffoldConfig, - 'authorization' | 'permission' - > + config: Pick, ): ((current: string) => boolean) => { if (kind === MODULE_API_KIND) { return (current) => current.startsWith(`${generatedHeader(kind)}\n`) && - hasGeneratedModuleApiContract( - current, - `${toPascalCase(name)}Api`, - toCamelCase(name), - name - ); + hasGeneratedModuleApiContract(current, `${toPascalCase(name)}Api`, toCamelCase(name), name); } if (!isProviderContribution(kind)) { return () => false; @@ -1401,17 +1172,15 @@ const acceptsGovernedArtifact = ( vertical.moduleId, name, kind === REPORT_KIND ? 'report' : 'search', - readAuthorizationExpectation(config) + readAuthorizationExpectation(config), ); }; -const planGovernedTransport = Effect.fn( - 'GovernedContributionScaffold.transport' -)(function* planGovernedTransport( +const planGovernedTransport = Effect.fn('GovernedContributionScaffold.transport')(function* planGovernedTransport( workspaceRoot: string, kind: GovernedContributionKind, vertical: OntosVerticalMetadata, - name: string + name: string, ) { const isApi = kind === MODULE_API_KIND; const mutations: Mutation[] = []; @@ -1423,126 +1192,98 @@ const planGovernedTransport = Effect.fn( [REPORT_KIND]: 'report-client', [SEARCH_PROVIDER_KIND]: 'search-client', }[kind]; - clientPath = yield* tryScaffold( - 'failed to resolve governed client path', - () => - resolveContainedPath( - vertical.directory, - 'src', - 'api', - `${name}-${suffix}.ts` - ) + clientPath = yield* tryScaffold('failed to resolve governed client path', () => + resolveContainedPath(vertical.directory, 'src', 'api', `${name}-${suffix}.ts`), ); const clientMutation = yield* createOrAcceptGeneratedMutationEffect( clientPath, - isApi - ? renderApiClient(vertical, name) - : renderProviderClient(kind, vertical, name), - acceptsGeneratedClient(kind, vertical, name) + isApi ? renderApiClient(vertical, name) : renderProviderClient(kind, vertical, name), + acceptsGeneratedClient(kind, vertical, name), ); mutations.push(...EffectArray.getSomes([clientMutation])); if (isProviderContribution(kind)) { - const providerContractPath = yield* tryScaffold( - 'failed to resolve provider contract path', - () => - resolveContainedPath( - vertical.directory, - 'shared', - 'apis', - `${name}-${kind === REPORT_KIND ? REPORT_KIND : 'search'}.ts` - ) + const providerContractPath = yield* tryScaffold('failed to resolve provider contract path', () => + resolveContainedPath( + vertical.directory, + 'shared', + 'apis', + `${name}-${kind === REPORT_KIND ? REPORT_KIND : 'search'}.ts`, + ), + ); + const providerContractMutation = yield* createOrAcceptGeneratedMutationEffect( + providerContractPath, + renderProviderApiContract(kind, vertical, name), + (current) => + current.startsWith(`${generatedHeader(kind)}\n`) && + hasGeneratedProviderApiContract( + current, + `${toPascalCase(name)}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`, + vertical.moduleId, + name, + kind === REPORT_KIND ? 'report' : 'search', + ), ); - const providerContractMutation = - yield* createOrAcceptGeneratedMutationEffect( - providerContractPath, - renderProviderApiContract(kind, vertical, name), - (current) => - current.startsWith(`${generatedHeader(kind)}\n`) && - hasGeneratedProviderApiContract( - current, - `${toPascalCase(name)}${kind === REPORT_KIND ? 'Report' : 'Search'}Api`, - vertical.moduleId, - name, - kind === REPORT_KIND ? 'report' : 'search' - ) - ); mutations.push(...EffectArray.getSomes([providerContractMutation])); } - serverPath = yield* tryScaffold( - 'failed to resolve governed server path', - () => - resolveContainedPath( - vertical.directory, - 'api', - `${name}-${{ [MODULE_API_KIND]: 'read', [REPORT_KIND]: REPORT_KIND, [SEARCH_PROVIDER_KIND]: 'search' }[kind]}-server.ts` - ) + serverPath = yield* tryScaffold('failed to resolve governed server path', () => + resolveContainedPath( + vertical.directory, + 'api', + `${name}-${{ [MODULE_API_KIND]: 'read', [REPORT_KIND]: REPORT_KIND, [SEARCH_PROVIDER_KIND]: 'search' }[kind]}-server.ts`, + ), ); - const sharedApiPath = yield* tryScaffold( - 'failed to resolve governed API binding', - () => resolveContainedPath(vertical.directory, 'shared', 'api.ts') + const sharedApiPath = yield* tryScaffold('failed to resolve governed API binding', () => + resolveContainedPath(vertical.directory, 'shared', 'api.ts'), ); const fileSystem = yield* FileSystem.FileSystem; const sharedApi = yield* fileSystem .readFileString(sharedApiPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure('failed to read governed API binding', cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldFailure('failed to read governed API binding', cause))); const apiBinding = governedApiBinding(sharedApi); if (apiBinding === undefined) { - return raiseScaffoldFailure( - 'governed HTTP composition slots are not bound to the exported runtime root' - ); + return raiseScaffoldFailure('governed HTTP composition slots are not bound to the exported runtime root'); } const serverMutation = yield* createOrAcceptGeneratedMutationEffect( serverPath, - renderGovernedServer(apiBinding, kind, name) + renderGovernedServer(apiBinding, kind, name), ); mutations.push( ...EffectArray.getSomes([serverMutation]), ...(yield* patchGovernedHttpComposition(vertical, kind, name)), - ...(yield* planOperationBoundary(workspaceRoot, vertical)) + ...(yield* planOperationBoundary(workspaceRoot, vertical)), ); } return { clientPath, mutations, serverPath }; }); -export const planGovernedContributionScaffold = Effect.fn( - 'GovernedContributionScaffold.plan' -)(function* planGovernedContributionScaffold( - workspaceRoot: string, - kind: GovernedContributionKind, - config: GovernedContributionScaffoldConfig -) { - const name = yield* tryScaffold('governed contribution name is invalid', () => - requireCanonicalSlug(config.name, kind) - ); - const resource = - config.resource === undefined - ? undefined - : yield* tryScaffold('governed contribution resource is invalid', () => - requireCanonicalSlug(config.resource ?? '', 'resource') - ); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); - const isComponent = kind === PUBLIC_COMPONENT_KIND; - const isApi = kind === MODULE_API_KIND; - const artifactSegments = { - [MODULE_API_KIND]: ['shared', 'apis', `${name}.ts`], - [PUBLIC_COMPONENT_KIND]: ['src', 'components', `${name}.tsx`], - [REPORT_KIND]: ['src', 'reports', `${name}.provider.ts`], - [SEARCH_PROVIDER_KIND]: ['src', 'search', `${name}.provider.ts`], - }; - const artifactPath = yield* tryScaffold( - 'failed to resolve governed contribution path', - () => resolveContainedPath(vertical.directory, ...artifactSegments[kind]) - ); - const artifact = yield* tryScaffold( - 'failed to render governed contribution', - () => { +export const planGovernedContributionScaffold = Effect.fn('GovernedContributionScaffold.plan')( + function* planGovernedContributionScaffold( + workspaceRoot: string, + kind: GovernedContributionKind, + config: GovernedContributionScaffoldConfig, + ) { + const name = yield* tryScaffold('governed contribution name is invalid', () => + requireCanonicalSlug(config.name, kind), + ); + const resource = + config.resource === undefined + ? undefined + : yield* tryScaffold('governed contribution resource is invalid', () => + requireCanonicalSlug(config.resource ?? '', 'resource'), + ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + const isComponent = kind === PUBLIC_COMPONENT_KIND; + const isApi = kind === MODULE_API_KIND; + const artifactSegments = { + [MODULE_API_KIND]: ['shared', 'apis', `${name}.ts`], + [PUBLIC_COMPONENT_KIND]: ['src', 'components', `${name}.tsx`], + [REPORT_KIND]: ['src', 'reports', `${name}.provider.ts`], + [SEARCH_PROVIDER_KIND]: ['src', 'search', `${name}.provider.ts`], + }; + const artifactPath = yield* tryScaffold('failed to resolve governed contribution path', () => + resolveContainedPath(vertical.directory, ...artifactSegments[kind]), + ); + const artifact = yield* tryScaffold('failed to render governed contribution', () => { if (isComponent) { return renderPublicComponent(name); } @@ -1550,124 +1291,76 @@ export const planGovernedContributionScaffold = Effect.fn( return renderApiContract(name); } return renderProvider(kind, vertical, name, config); - } - ); - const artifactMutation = isComponent - ? Option.some(yield* createMutationEffect(artifactPath, artifact)) - : yield* createOrAcceptGeneratedMutationEffect( - artifactPath, - artifact, - acceptsGovernedArtifact(kind, vertical, name, config) + }); + const artifactMutation = isComponent + ? Option.some(yield* createMutationEffect(artifactPath, artifact)) + : yield* createOrAcceptGeneratedMutationEffect( + artifactPath, + artifact, + acceptsGovernedArtifact(kind, vertical, name, config), + ); + const mutations: Mutation[] = EffectArray.getSomes([artifactMutation]); + if (isApi) { + const readPath = yield* tryScaffold('failed to resolve governed read path', () => + resolveContainedPath(vertical.directory, 'src', 'api', `${name}.read.ts`), ); - const mutations: Mutation[] = EffectArray.getSomes([artifactMutation]); - if (isApi) { - const readPath = yield* tryScaffold( - 'failed to resolve governed read path', - () => - resolveContainedPath( - vertical.directory, - 'src', - 'api', - `${name}.read.ts` - ) - ); - const readSource = yield* tryScaffold( - 'failed to render governed read', - () => renderModuleApiRead(vertical, name, config) - ); - const readMutation = yield* createOrAcceptGeneratedMutationEffect( - readPath, - readSource, - (current) => - current.startsWith(`${generatedHeader(MODULE_API_KIND)}\n`) && - hasGeneratedModuleApiReadContract( - current, - vertical.moduleId, - name, - readAuthorizationExpectation(config) - ) - ); - mutations.push(...EffectArray.getSomes([readMutation])); - } - const transport = yield* planGovernedTransport( - workspaceRoot, - kind, - vertical, - name - ); - const { clientPath, serverPath } = transport; - mutations.push(...transport.mutations); - const ownerImport = manifestImport(kind, name); - const ownerImportIdentity = manifestImportIdentity(kind, name); - let manifest = vertical.manifestContent; - if ( - ownerImport !== undefined && - !hasUniqueExactNamedImport( - manifest, - ownerImportIdentity.binding, - ownerImportIdentity.specifier - ) - ) { - if (hasNamedImportBinding(manifest, ownerImportIdentity.binding)) { - return yield* scaffoldFailure( - `generated owner import binding conflicts with ${ownerImportIdentity.binding}` + const readSource = yield* tryScaffold('failed to render governed read', () => + renderModuleApiRead(vertical, name, config), + ); + const readMutation = yield* createOrAcceptGeneratedMutationEffect( + readPath, + readSource, + (current) => + current.startsWith(`${generatedHeader(MODULE_API_KIND)}\n`) && + hasGeneratedModuleApiReadContract(current, vertical.moduleId, name, readAuthorizationExpectation(config)), ); + mutations.push(...EffectArray.getSomes([readMutation])); } - manifest = yield* tryScaffold( - 'failed to patch module manifest imports', - () => + const transport = yield* planGovernedTransport(workspaceRoot, kind, vertical, name); + const { clientPath, serverPath } = transport; + mutations.push(...transport.mutations); + const ownerImport = manifestImport(kind, name); + const ownerImportIdentity = manifestImportIdentity(kind, name); + let manifest = vertical.manifestContent; + if ( + ownerImport !== undefined && + !hasUniqueExactNamedImport(manifest, ownerImportIdentity.binding, ownerImportIdentity.specifier) + ) { + if (hasNamedImportBinding(manifest, ownerImportIdentity.binding)) { + return yield* scaffoldFailure(`generated owner import binding conflicts with ${ownerImportIdentity.binding}`); + } + manifest = yield* tryScaffold('failed to patch module manifest imports', () => insertSortedSlotIdempotently( manifest, MODULE_MANIFEST_IMPORT_SLOT_START, MODULE_MANIFEST_IMPORT_SLOT_END, ownerImport, - isModuleManifestImport - ) + isModuleManifestImport, + ), + ); + } + const slots = yield* tryScaffold('failed to plan governed contribution owner slots', () => + slotLine(kind, vertical, name, resource, config), ); - } - const slots = yield* tryScaffold( - 'failed to plan governed contribution owner slots', - () => slotLine(kind, vertical, name, resource, config) - ); - const registration = yield* tryScaffold( - 'failed to patch governed contribution slots', - () => { + const registration = yield* tryScaffold('failed to patch governed contribution slots', () => { manifest = patchSlots(manifest, slots.manifest, manifestOwnerSlots); - return patchSlots( - vertical.registrationContent, - slots.registration, - registrationOwnerSlots - ); + return patchSlots(vertical.registrationContent, slots.registration, registrationOwnerSlots); + }); + const manifestMutation = yield* tryScaffold('failed to update module manifest', () => + updateMutation(vertical.manifestPath, vertical.manifestContent, manifest), + ); + const registrationMutation = yield* tryScaffold('failed to update module registration', () => + updateMutation(vertical.registrationPath, vertical.registrationContent, registration), + ); + mutations.push(...[manifestMutation, registrationMutation].filter((mutation) => mutation !== undefined)); + if (isComponent) { + mutations.push(yield* patchFederationExposure(vertical, name)); } - ); - const manifestMutation = yield* tryScaffold( - 'failed to update module manifest', - () => - updateMutation(vertical.manifestPath, vertical.manifestContent, manifest) - ); - const registrationMutation = yield* tryScaffold( - 'failed to update module registration', - () => - updateMutation( - vertical.registrationPath, - vertical.registrationContent, - registration - ) - ); - mutations.push( - ...[manifestMutation, registrationMutation].filter( - (mutation) => mutation !== undefined - ) - ); - if (isComponent) { - mutations.push(yield* patchFederationExposure(vertical, name)); - } - yield* tryScaffold('governed contribution mutation paths are invalid', () => - ensureUniqueMutationPaths(mutations) - ); - const result = - clientPath === undefined || serverPath === undefined - ? { artifactPath } - : { artifactPath, clientPath, serverPath }; - return { mutations, result }; -}); + yield* tryScaffold('governed contribution mutation paths are invalid', () => ensureUniqueMutationPaths(mutations)); + const result = + clientPath === undefined || serverPath === undefined + ? { artifactPath } + : { artifactPath, clientPath, serverPath }; + return { mutations, result }; + }, +); diff --git a/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts b/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts index bb60d194d..838fefb83 100644 --- a/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts +++ b/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts @@ -1,10 +1,4 @@ -import { - Array as EffectArray, - Effect, - FileSystem, - Option, - Schema, -} from 'effect'; +import { Array as EffectArray, Effect, FileSystem, Option, Schema } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { @@ -24,8 +18,7 @@ import type { VerticalMetadata, } from '../shared.mts'; -const ACTION_BOUNDARY_GENERATOR_HEADER = - '// @generated by OntOS Codesmith MicroVertical Action Boundary v1'; +const ACTION_BOUNDARY_GENERATOR_HEADER = '// @generated by OntOS Codesmith MicroVertical Action Boundary v1'; const WORKSPACE_DEPENDENCY_VERSION = 'workspace:*'; class ActionBoundaryScaffoldError extends Schema.TaggedError()( @@ -33,65 +26,48 @@ class ActionBoundaryScaffoldError extends Schema.TaggedError, - ActionBoundaryScaffoldError | ScaffoldFailure, - FileSystem.FileSystem -> => + requiredContract?: { readonly marker: string; readonly migration: string }, +): Effect.Effect, ActionBoundaryScaffoldError | ScaffoldFailure, FileSystem.FileSystem> => Effect.gen(function* createOrAcceptOwnedMutationEffect() { const fileSystem = yield* FileSystem.FileSystem; const exists = yield* fileSystem .exists(filePath) - .pipe( - Effect.mapError((cause) => - scaffoldError(`failed to inspect ${filePath}`, cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldError(`failed to inspect ${filePath}`, cause))); if (!exists) { return Option.some(yield* createMutationEffect(filePath, content)); } const current = yield* fileSystem .readFileString(filePath) - .pipe( - Effect.mapError((cause) => - scaffoldError(`failed to read ${filePath}`, cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldError(`failed to read ${filePath}`, cause))); if ( current.startsWith(`${ACTION_BOUNDARY_GENERATOR_HEADER}\n`) && requiredMarkers.every((marker) => current.includes(marker)) ) { - if ( - requiredContract !== undefined && - !current.includes(requiredContract.marker) - ) { + if (requiredContract !== undefined && !current.includes(requiredContract.marker)) { return yield* scaffoldError( - `incompatible generated Action boundary: ${filePath}. ${requiredContract.migration}` + `incompatible generated Action boundary: ${filePath}. ${requiredContract.migration}`, ); } return Option.none(); } - return yield* scaffoldError( - `refusing to overwrite existing business file: ${filePath}` - ); + return yield* scaffoldError(`refusing to overwrite existing business file: ${filePath}`); }); export const renderActionPrincipalServer = ( - vertical: Pick + vertical: Pick, ): string => `${ACTION_BOUNDARY_GENERATOR_HEADER} // @ontos-action-boundary-owner ${vertical.appId} // @ontos-action-boundary-audience ${vertical.appId} @@ -149,7 +125,7 @@ export const authenticateOperationPrincipal = makeMicroverticalHttpPrincipalAuth `; const renderGatewayAssertionRedemptionAdapter = ( - vertical: Pick + vertical: Pick, ): string => `${ACTION_BOUNDARY_GENERATOR_HEADER} // @ontos-action-boundary-owner ${vertical.appId} import { @@ -179,7 +155,7 @@ export const GatewayAssertionRedemptionLive = Layer.succeed( `; const renderActionHttpRunner = ( - vertical: Pick + vertical: Pick, ): string => `${ACTION_BOUNDARY_GENERATOR_HEADER} // @ontos-action-boundary-owner ${vertical.appId} import { bindGovernedActionHttp } from '@app/core-runtime/http/action-runner'; @@ -198,9 +174,7 @@ export const bindActionHttpRunner = ( }); `; -const renderClient = ( - vertical: VerticalMetadata -): string => `${ACTION_BOUNDARY_GENERATOR_HEADER} +const renderClient = (vertical: VerticalMetadata): string => `${ACTION_BOUNDARY_GENERATOR_HEADER} // @ontos-action-boundary-owner ${vertical.appId} // @ontos-action-boundary-audience ${vertical.appId} import { @@ -227,96 +201,58 @@ export const operationGateway = makeOperationGateway(); export const planActionBoundaryScaffold = ( workspaceRoot: string, - config: ActionBoundaryScaffoldConfig + config: ActionBoundaryScaffoldConfig, ): Effect.Effect< ScaffoldPlan, ActionBoundaryScaffoldError | ScaffoldFailure, FileSystem.FileSystem > => Effect.gen(function* planActionBoundaryScaffoldEffect() { - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); const serverPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'api', - 'auth', - 'action-principal.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'api', 'auth', 'action-principal.ts'), ); const clientPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'src', - 'api', - 'action-gateway.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'src', 'api', 'action-gateway.ts'), ); const redemptionPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'api', - 'auth', - 'gateway-assertion-redemption.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'api', 'auth', 'gateway-assertion-redemption.ts'), ); const runnerPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'api', - 'action-http-runner.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'api', 'action-http-runner.ts'), ); const serverMutation = yield* createOrAcceptOwnedMutation( serverPath, renderActionPrincipalServer(vertical), - [ - `@ontos-action-boundary-owner ${vertical.appId}`, - `@ontos-action-boundary-audience ${vertical.appId}`, - ], + [`@ontos-action-boundary-owner ${vertical.appId}`, `@ontos-action-boundary-audience ${vertical.appId}`], { marker: 'export const authenticateOperationPrincipal', migration: 'Preserve owner adaptations and export authenticateOperationPrincipal using makeMicroverticalHttpPrincipalAuthentication with the audience-bound verifier; provide ActionPrincipalVerifierLive at the owning API runtime before generating governed contributions.', - } - ); - const clientMutation = yield* createOrAcceptOwnedMutation( - clientPath, - renderClient(vertical), - [`ACTION_GATEWAY_AUDIENCE = '${vertical.appId}'`, 'makeOperationGateway'] + }, ); + const clientMutation = yield* createOrAcceptOwnedMutation(clientPath, renderClient(vertical), [ + `ACTION_GATEWAY_AUDIENCE = '${vertical.appId}'`, + 'makeOperationGateway', + ]); const redemptionMutation = yield* createOrAcceptOwnedMutation( redemptionPath, renderGatewayAssertionRedemptionAdapter(vertical), - [ - 'GatewayAssertionRedemption', - `@ontos-action-boundary-owner ${vertical.appId}`, - ] - ); - const runnerMutation = yield* createOrAcceptOwnedMutation( - runnerPath, - renderActionHttpRunner(vertical), - ['bindActionHttpRunner', `@ontos-action-boundary-owner ${vertical.appId}`] + ['GatewayAssertionRedemption', `@ontos-action-boundary-owner ${vertical.appId}`], ); + const runnerMutation = yield* createOrAcceptOwnedMutation(runnerPath, renderActionHttpRunner(vertical), [ + 'bindActionHttpRunner', + `@ontos-action-boundary-owner ${vertical.appId}`, + ]); const dependencyMutation = yield* trySync(() => Option.fromNullishOr( withExactDependencies(vertical, { '@app/core-runtime': WORKSPACE_DEPENDENCY_VERSION, '@app/gateway-principal-verifier': WORKSPACE_DEPENDENCY_VERSION, '@app/shared-contracts': WORKSPACE_DEPENDENCY_VERSION, - effect: '4.0.0-beta.107', - }) - ) + effect: '4.0.0-rc.112', + }), + ), ); const mutations = EffectArray.getSomes([ serverMutation, diff --git a/app/scripts/scaffolding/microvertical-page/scaffold.mts b/app/scripts/scaffolding/microvertical-page/scaffold.mts index f2ae97b3c..73300f122 100644 --- a/app/scripts/scaffolding/microvertical-page/scaffold.mts +++ b/app/scripts/scaffolding/microvertical-page/scaffold.mts @@ -16,6 +16,7 @@ import { createMutationEffect as createSharedMutation, discoverOntosModuleEffect as discoverSharedModule, ensureUniqueMutationPaths, + formatGeneratedMutationContent, generatedSlotContainsExactEntry, insertModuleFederationExposure, insertSortedSlot, @@ -65,80 +66,48 @@ const routeLocalePrefixPattern = /^[a-z]{2}(?:-[a-z]{2})?$/u; const staticRouteSegmentPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; const parameterRouteSegmentPattern = /^:(?[a-z][A-Za-z0-9]*)$/u; const pageStarterLocales = new Set(['cs', 'en']); -const SHELL_PAGE_CLIENT_SLOT_START = - '// @ontos-codegen-start shell-page-clients'; +const SHELL_PAGE_CLIENT_SLOT_START = '// @ontos-codegen-start shell-page-clients'; const SHELL_PAGE_CLIENT_SLOT_END = '// @ontos-codegen-end shell-page-clients'; const SHELL_APP_ID = 'shell-super-app'; const PAGE_FILE_NAME = 'page.tsx'; const PAGE_LOADER_FILE_NAME = 'page.data.ts'; const ROUTE_METADATA_FILE_NAME = 'route.meta.ts'; -class PageScaffoldError extends Schema.TaggedError()( - 'PageScaffoldError', - { - cause: Schema.optionalKey(Schema.Unknown), - message: Schema.String, - } -) {} +class PageScaffoldError extends Schema.TaggedError()('PageScaffoldError', { + cause: Schema.optionalKey(Schema.Unknown), + message: Schema.String, +}) {} -const pageScaffoldFailure = ( - message: string, - cause?: unknown -): PageScaffoldError => +const pageScaffoldFailure = (message: string, cause?: unknown): PageScaffoldError => new PageScaffoldError(cause === undefined ? { message } : { cause, message }); -const pageScaffoldFailureFromUnknown = ( - cause: unknown, - fallback: string -): PageScaffoldError => +const pageScaffoldFailureFromUnknown = (cause: unknown, fallback: string): PageScaffoldError => Schema.is(PageScaffoldError)(cause) ? cause - : pageScaffoldFailure( - Predicate.isError(cause) ? cause.message : fallback, - cause - ); + : pageScaffoldFailure(Predicate.isError(cause) ? cause.message : fallback, cause); -const discoverOntosModuleEffect = ( - workspaceRoot: string, - requestedVertical: string -) => +const discoverOntosModuleEffect = (workspaceRoot: string, requestedVertical: string) => discoverSharedModule(workspaceRoot, requestedVertical).pipe( Effect.mapError((cause) => - pageScaffoldFailureFromUnknown( - cause, - `vertical ${requestedVertical} could not be discovered` - ) - ) + pageScaffoldFailureFromUnknown(cause, `vertical ${requestedVertical} could not be discovered`), + ), ); -const readJsonEffect = ( - filePath: string, - description: string, - fallback: string -) => +const readJsonEffect = (filePath: string, description: string, fallback: string) => readSharedJson(filePath, description).pipe( - Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)) + Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)), ); -const createMutationEffect = ( - filePath: string, - content: string, - fallback: string -) => +const createMutationEffect = (filePath: string, content: string, fallback: string) => createSharedMutation(filePath, content).pipe( - Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)) + Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)), ); const mapFileSystemError = ( - operation: Effect.Effect + operation: Effect.Effect, ): Effect.Effect => operation.pipe( - Effect.mapError((cause) => - pageScaffoldFailureFromUnknown( - cause, - 'page scaffold file-system operation failed' - ) - ) + Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, 'page scaffold file-system operation failed')), ); interface DirectoryEntry { @@ -150,22 +119,18 @@ interface DirectoryEntry { const readDirectoryEntries = (directory: string) => Effect.gen(function* readDirectoryEntriesEffect() { const fileSystem = yield* FileSystem.FileSystem; - const names = yield* mapFileSystemError( - fileSystem.readDirectory(directory) - ); + const names = yield* mapFileSystemError(fileSystem.readDirectory(directory)); return yield* Effect.all( names.map((name) => - mapFileSystemError( - fileSystem.stat(resolveContainedPath(directory, name)) - ).pipe( + mapFileSystemError(fileSystem.stat(resolveContainedPath(directory, name))).pipe( Effect.map((info): DirectoryEntry => ({ isDirectory: info.type === 'Directory', isFile: info.type === 'File', name, - })) - ) + })), + ), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); }); @@ -185,15 +150,13 @@ const pageRouteIsInvalid = ( canonicalPath: string, canonicalSegments: readonly string[], parameterNames: readonly string[], - requestedUrl: string | undefined + requestedUrl: string | undefined, ): boolean => canonicalPath.length < 2 || canonicalPath.length > 200 || canonicalSegments.length === 0 || canonicalSegments.some( - (segment) => - !staticRouteSegmentPattern.test(segment) && - !parameterRouteSegmentPattern.test(segment) + (segment) => !staticRouteSegmentPattern.test(segment) && !parameterRouteSegmentPattern.test(segment), ) || new Set(parameterNames).size !== parameterNames.length || (requestedUrl === undefined && parameterNames.length > 0); @@ -201,40 +164,25 @@ const pageRouteIsInvalid = ( const resolvePageRoute = ( vertical: PageVerticalMetadata, page: string, - requestedUrl: string | undefined + requestedUrl: string | undefined, ): Effect.Effect => Effect.gen(function* resolvePageRouteEffect() { const canonicalPath = requestedUrl ?? `/${vertical.slug}/${page}`; - const canonicalSegments = canonicalPath.startsWith('/') - ? canonicalPath.slice(1).split('/') - : []; + const canonicalSegments = canonicalPath.startsWith('/') ? canonicalPath.slice(1).split('/') : []; const parameterNames = canonicalSegments.flatMap((segment) => { const name = parameterRouteSegmentPattern.exec(segment)?.groups?.['name']; return name === undefined ? [] : [name]; }); - if ( - pageRouteIsInvalid( - canonicalPath, - canonicalSegments, - parameterNames, - requestedUrl - ) - ) { + if (pageRouteIsInvalid(canonicalPath, canonicalSegments, parameterNames, requestedUrl)) { return yield* pageScaffoldFailure( - '--url must be a root-relative path of lowercase kebab-case segments and unique named :parameters, with no locale, query, fragment, wildcard, optional/catch-all syntax, or trailing slash' + '--url must be a root-relative path of lowercase kebab-case segments and unique named :parameters, with no locale, query, fragment, wildcard, optional/catch-all syntax, or trailing slash', ); } - if ( - requestedUrl !== undefined && - routeLocalePrefixPattern.test(canonicalSegments[0] ?? '') - ) { - return yield* pageScaffoldFailure( - '--url must not include a locale prefix; the localized router adds it' - ); + if (requestedUrl !== undefined && routeLocalePrefixPattern.test(canonicalSegments[0] ?? '')) { + return yield* pageScaffoldFailure('--url must not include a locale prefix; the localized router adds it'); } const filesystemSegments = canonicalSegments.map((segment) => { - const parameterName = - parameterRouteSegmentPattern.exec(segment)?.groups?.['name']; + const parameterName = parameterRouteSegmentPattern.exec(segment)?.groups?.['name']; return parameterName === undefined ? segment : `[${parameterName}]`; }); return { @@ -247,21 +195,13 @@ const resolvePageRoute = ( }; }); -const relativeFromRoute = ( - route: PageRoute, - target: string, - extraLevels = 0 -): string => +const relativeFromRoute = (route: PageRoute, target: string, extraLevels = 0): string => `${'../'.repeat(route.filesystemSegments.length + extraLevels)}${target}`; -const renderRouteParameterSchemaFields = ( - componentName: string, - route: PageRoute -): string => +const renderRouteParameterSchemaFields = (componentName: string, route: PageRoute): string => route.parameterNames .map( - (name) => - ` ${name}: Schema.String.pipe(Schema.brand('${componentName}${toPascalCase(name)}RouteParameter')),` + (name) => ` ${name}: Schema.String.pipe(Schema.brand('${componentName}${toPascalCase(name)}RouteParameter')),`, ) .join('\n'); @@ -270,14 +210,12 @@ const validateLocale = ( vertical: OntosVerticalMetadata, namespace: string, packageExports: JsonObject, - locale: string + locale: string, ): Effect.Effect => Effect.gen(function* validateLocaleEffect() { const expectedExport = `./locales/${locale}/${namespace}.json`; if (packageExports[`./locales/${locale}`] !== expectedExport) { - yield* pageScaffoldFailure( - `vertical ${vertical.slug} is missing its generated ${locale} locale export` - ); + yield* pageScaffoldFailure(`vertical ${vertical.slug} is missing its generated ${locale} locale export`); } const localePath = resolveContainedPath( workspaceRoot, @@ -285,98 +223,59 @@ const validateLocale = ( vertical.slug, 'locales', locale, - `${namespace}.json` + `${namespace}.json`, ); yield* readJsonEffect( localePath, `vertical ${vertical.slug} ${locale} locale catalog`, - `vertical ${vertical.slug} ${locale} locale catalog is invalid` + `vertical ${vertical.slug} ${locale} locale catalog is invalid`, ); }); const discoverPageVertical = ( workspaceRoot: string, - requestedVertical: string -): Effect.Effect< - PageVerticalMetadata, - PageScaffoldError, - FileSystem.FileSystem -> => + requestedVertical: string, +): Effect.Effect => Effect.gen(function* discoverPageVerticalEffect() { - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - requestedVertical - ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, requestedVertical); const { topologyEntry } = vertical; - const namespace = requiredString( - topologyEntry['domain'], - `vertical ${vertical.slug} namespace` - ); + const namespace = requiredString(topologyEntry['domain'], `vertical ${vertical.slug} namespace`); if (!namespacePattern.test(namespace)) { - return yield* pageScaffoldFailure( - `vertical ${vertical.slug} namespace is not a safe generated identifier` - ); + return yield* pageScaffoldFailure(`vertical ${vertical.slug} namespace is not a safe generated identifier`); } const moduleFederation = asJsonObject( topologyEntry['moduleFederation'], - `vertical ${vertical.slug} Module Federation metadata` + `vertical ${vertical.slug} Module Federation metadata`, ); const mfBoundaryId = requiredString( moduleFederation['name'], - `vertical ${vertical.slug} Module Federation boundary` + `vertical ${vertical.slug} Module Federation boundary`, ); if (!moduleFederationNamePattern.test(mfBoundaryId)) { - return yield* pageScaffoldFailure( - `vertical ${vertical.slug} Module Federation boundary is invalid` - ); + return yield* pageScaffoldFailure(`vertical ${vertical.slug} Module Federation boundary is invalid`); } - const localeRoot = resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'locales' - ); + const localeRoot = resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'locales'); if (!(yield* fileExists(localeRoot))) { - return yield* pageScaffoldFailure( - `vertical ${vertical.slug} locale directory is missing` - ); + return yield* pageScaffoldFailure(`vertical ${vertical.slug} locale directory is missing`); } const localeEntries = yield* readDirectoryEntries(localeRoot); const locales = localeEntries .filter((entry) => entry.isDirectory) .map((entry) => entry.name) .toSorted(); - if ( - locales.length === 0 || - locales.some((locale) => !localePattern.test(locale)) - ) { + if (locales.length === 0 || locales.some((locale) => !localePattern.test(locale))) { return yield* pageScaffoldFailure( - `vertical ${vertical.slug} must have one or more valid generated locale directories` + `vertical ${vertical.slug} must have one or more valid generated locale directories`, ); } - const unsupportedLocale = locales.find( - (locale) => !pageStarterLocales.has(locale) - ); + const unsupportedLocale = locales.find((locale) => !pageStarterLocales.has(locale)); if (unsupportedLocale !== undefined) { - return yield* pageScaffoldFailure( - `page scaffold has no starter translation for locale ${unsupportedLocale}` - ); + return yield* pageScaffoldFailure(`page scaffold has no starter translation for locale ${unsupportedLocale}`); } - const packageExports = asJsonObject( - vertical.packageJson['exports'], - `vertical ${vertical.slug} package exports` - ); + const packageExports = asJsonObject(vertical.packageJson['exports'], `vertical ${vertical.slug} package exports`); yield* Effect.all( - locales.map((locale) => - validateLocale( - workspaceRoot, - vertical, - namespace, - packageExports, - locale - ) - ), - { concurrency: 'unbounded' } + locales.map((locale) => validateLocale(workspaceRoot, vertical, namespace, packageExports, locale)), + { concurrency: 'unbounded' }, ); const routeHeadPath = resolveContainedPath( workspaceRoot, @@ -384,12 +283,10 @@ const discoverPageVertical = ( vertical.slug, 'src', 'routes', - 'ultramodern-route-head.tsx' + 'ultramodern-route-head.tsx', ); if (!(yield* fileExists(routeHeadPath))) { - return yield* pageScaffoldFailure( - `vertical ${vertical.slug} generated UltramodernRouteHead is missing` - ); + return yield* pageScaffoldFailure(`vertical ${vertical.slug} generated UltramodernRouteHead is missing`); } const resourcesName = `${toCamelCase(vertical.slug)}I18nResources`; const resourcesPath = resolveContainedPath( @@ -398,17 +295,15 @@ const discoverPageVertical = ( vertical.slug, 'src', 'i18n', - 'resources.ts' + 'resources.ts', ); if (!(yield* fileExists(resourcesPath))) { - return yield* pageScaffoldFailure( - `vertical ${vertical.slug} generated i18n resources are missing` - ); + return yield* pageScaffoldFailure(`vertical ${vertical.slug} generated i18n resources are missing`); } const resourcesContent = yield* readTextFile(resourcesPath); if (!resourcesContent.includes(`export const ${resourcesName} =`)) { return yield* pageScaffoldFailure( - `vertical ${vertical.slug} generated i18n resources must export ${resourcesName}` + `vertical ${vertical.slug} generated i18n resources must export ${resourcesName}`, ); } return { @@ -420,17 +315,11 @@ const discoverPageVertical = ( }; }); -const renderPage = ( - vertical: PageVerticalMetadata, - page: string, - route: PageRoute -): string => { +const renderPage = (vertical: PageVerticalMetadata, page: string, route: PageRoute): string => { const componentName = `${toPascalCase(page)}Page`; const keyRoot = `${vertical.namespace}.pages.${toCamelCase(page)}`; const prefix = vertical.tailwindPrefix; - const schemaImport = route.isDynamic - ? `import { Schema } from 'effect';\n` - : ''; + const schemaImport = route.isDynamic ? `import { Schema } from 'effect';\n` : ''; const props = route.isDynamic ? `export const ${componentName}RouteParams = Schema.Struct({ ${renderRouteParameterSchemaFields(componentName, route)} @@ -479,9 +368,7 @@ export default ${componentName}; `; }; -const renderReadAuthorization = ( - config: Pick -): string => { +const renderReadAuthorization = (config: Pick): string => { if (config.authorization === 'context_permission') { return `{ kind: 'context_permission', permission: '${config.permission ?? ''}' }`; } @@ -489,25 +376,17 @@ const renderReadAuthorization = ( }; const validateReadAuthorization = ( - config: Pick + config: Pick, ): Effect.Effect => Effect.gen(function* validateReadAuthorizationEffect() { if ( config.authorization === 'context_permission' && - (config.permission === undefined || - !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u.test(config.permission)) + (config.permission === undefined || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u.test(config.permission)) ) { - yield* pageScaffoldFailure( - 'context_permission authorization requires a stable --permission value' - ); + yield* pageScaffoldFailure('context_permission authorization requires a stable --permission value'); } - if ( - config.authorization !== 'context_permission' && - config.permission !== undefined - ) { - yield* pageScaffoldFailure( - '--permission is valid only for context_permission authorization' - ); + if (config.authorization !== 'context_permission' && config.permission !== undefined) { + yield* pageScaffoldFailure('--permission is valid only for context_permission authorization'); } }); @@ -515,7 +394,7 @@ const pageWiring = ( vertical: PageVerticalMetadata, page: string, route: PageRoute, - config: Pick + config: Pick, ) => { const componentName = `${toPascalCase(page)}Page`; const componentKey = `${vertical.moduleId}.page-${page}`; @@ -536,11 +415,7 @@ const pageWiring = ( } as const; }; -const renderFederatedPage = ( - vertical: PageVerticalMetadata, - page: string, - route: PageRoute -): string => { +const renderFederatedPage = (vertical: PageVerticalMetadata, page: string, route: PageRoute): string => { const componentName = `${toPascalCase(page)}Page`; const federatedComponentName = `${toPascalCase(page)}FederatedPage`; const resourcesName = `${toCamelCase(vertical.slug)}I18nResources`; @@ -554,9 +429,7 @@ const renderFederatedPage = ( const declaration = route.isDynamic ? `const ${federatedComponentName} = ({ routeParams }: ${federatedComponentName}Props) => (` : `const ${federatedComponentName} = () => (`; - const ownerPage = route.isDynamic - ? `<${componentName} routeParams={routeParams} />` - : `<${componentName} />`; + const ownerPage = route.isDynamic ? `<${componentName} routeParams={routeParams} />` : `<${componentName} />`; const ownerPageImport = route.isDynamic ? `import { ${componentName}, @@ -591,7 +464,7 @@ const patchPageWiring = ( vertical: PageVerticalMetadata, page: string, route: PageRoute, - config: Pick + config: Pick, ): PageOwnerWiring => { const wiring = pageWiring(vertical, page, route, config); let manifest = insertSortedSlot( @@ -599,21 +472,21 @@ const patchPageWiring = ( MODULE_MANIFEST_IMPORT_SLOT_START, MODULE_MANIFEST_IMPORT_SLOT_END, [wiring.manifestImport], - isModuleManifestImport + isModuleManifestImport, ); manifest = insertSortedSlot( manifest, MODULE_MANIFEST_COMPONENT_SLOT_START, MODULE_MANIFEST_COMPONENT_SLOT_END, [wiring.manifestComponent], - (candidate) => candidate.endsWith(',') + (candidate) => candidate.endsWith(','), ); manifest = insertSortedSlot( manifest, MODULE_MANIFEST_SHELL_PAGE_SLOT_START, MODULE_MANIFEST_SHELL_PAGE_SLOT_END, [wiring.manifestPage], - (candidate) => candidate.endsWith(',') + (candidate) => candidate.endsWith(','), ); if (wiring.manifestNavigation !== undefined) { manifest = insertSortedSlot( @@ -621,7 +494,7 @@ const patchPageWiring = ( MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START, MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END, [wiring.manifestNavigation], - (candidate) => candidate.endsWith(',') + (candidate) => candidate.endsWith(','), ); } const registration = insertSortedSlot( @@ -629,7 +502,7 @@ const patchPageWiring = ( MODULE_REGISTRATION_PAGE_SLOT_START, MODULE_REGISTRATION_PAGE_SLOT_END, [wiring.registrationPage], - (candidate) => candidate.endsWith(',') + (candidate) => candidate.endsWith(','), ); return { manifest, registration }; }; @@ -638,12 +511,10 @@ const renderRouteMetadata = ( vertical: PageVerticalMetadata, page: string, route: PageRoute, - config: Pick + config: Pick, ): string => { const keyRoot = `${vertical.namespace}.pages.${toCamelCase(page)}`; - const localisedPaths = vertical.locales - .map((locale) => ` ${locale}: '${route.canonicalPath}',`) - .join('\n'); + const localisedPaths = vertical.locales.map((locale) => ` ${locale}: '${route.canonicalPath}',`).join('\n'); return `import { defineTenantModuleEntrypoint } from '@app/core-runtime'; const routeMeta = { @@ -679,20 +550,14 @@ const renderShellConnectorPage = (route: PageRoute): string => `export { default } from '${relativeFromRoute(route, 'modules/[moduleId]/page.tsx')}'; `; -const renderShellConnectorLoader = ( - vertical: PageVerticalMetadata, - page: string, - route: PageRoute -): string => { +const renderShellConnectorLoader = (vertical: PageVerticalMetadata, page: string, route: PageRoute): string => { const loaderImport = route.isDynamic ? `{ loader as loadModuleTarget, selectRouteParams, }` : '{ loader as loadModuleTarget }'; - const parameterNames = route.parameterNames - .map((name) => `'${name}'`) - .join(', '); + const parameterNames = route.parameterNames.map((name) => `'${name}'`).join(', '); const loaderArguments = route.isDynamic ? `interface ShellPageLoaderArguments { readonly params: Readonly>; @@ -732,11 +597,9 @@ const renderShellConnectorMetadata = ( vertical: PageVerticalMetadata, page: string, route: PageRoute, - config: Pick + config: Pick, ): string => { - const localisedPaths = vertical.locales - .map((locale) => ` ${locale}: '${route.canonicalPath}',`) - .join('\n'); + const localisedPaths = vertical.locales.map((locale) => ` ${locale}: '${route.canonicalPath}',`).join('\n'); return `import { defineSystemModuleEntrypoint } from '@app/core-runtime'; const routeMeta = { @@ -784,7 +647,7 @@ const patchLocale = ( workspaceRoot: string, vertical: PageVerticalMetadata, locale: string, - page: string + page: string, ): Effect.Effect => Effect.gen(function* patchLocaleEffect() { const localePath = resolveContainedPath( @@ -793,48 +656,34 @@ const patchLocale = ( vertical.slug, 'locales', locale, - `${vertical.namespace}.json` + `${vertical.namespace}.json`, ); const { content, value } = yield* readJsonEffect( localePath, `${locale} locale catalog`, - `${locale} locale catalog is invalid` + `${locale} locale catalog is invalid`, ); const namespaceValue = value[vertical.namespace]; const namespace: MutableJsonObject = { - ...asJsonObject( - namespaceValue, - `${locale} ${vertical.namespace} namespace` - ), + ...asJsonObject(namespaceValue, `${locale} ${vertical.namespace} namespace`), }; const pagesValue = namespace['pages']; const pages: MutableJsonObject = - pagesValue === undefined - ? {} - : { ...asJsonObject(pagesValue, `${locale} pages catalog`) }; + pagesValue === undefined ? {} : { ...asJsonObject(pagesValue, `${locale} pages catalog`) }; const pageKey = toCamelCase(page); if (pages[pageKey] !== undefined) { return yield* pageScaffoldFailure( - `locale key ${vertical.namespace}.pages.${pageKey} already exists in ${locale}` + `locale key ${vertical.namespace}.pages.${pageKey} already exists in ${locale}`, ); } pages[pageKey] = localizedPageCopy(locale); const sortedPages = Object.fromEntries( - Object.entries(pages).toSorted(([left], [right]) => - left.localeCompare(right) - ) - ); - const patched = patchJsonObjectProperty( - content, - [vertical.namespace], - 'pages', - sortedPages + Object.entries(pages).toSorted(([left], [right]) => left.localeCompare(right)), ); + const patched = patchJsonObjectProperty(content, [vertical.namespace], 'pages', sortedPages); const mutation = updateMutation(localePath, content, patched); if (mutation === undefined) { - return yield* pageScaffoldFailure( - `locale patch unexpectedly made no change for ${locale}` - ); + return yield* pageScaffoldFailure(`locale patch unexpectedly made no change for ${locale}`); } return mutation; }); @@ -847,18 +696,13 @@ interface OwnedPageRoute { readonly routePath: string; } -const ownedPageRoute = ( - owner: string, - candidate: string -): Effect.Effect => +const ownedPageRoute = (owner: string, candidate: string): Effect.Effect => Effect.gen(function* ownedPageRouteEffect() { - const matches = [ - ...candidate.matchAll(/\broutePath:\s*'(?\/[^']+)'/gu), - ]; + const matches = [...candidate.matchAll(/\broutePath:\s*'(?\/[^']+)'/gu)]; const routePath = matches[0]?.groups?.['routePath']; if (matches.length !== 1 || routePath === undefined) { return yield* pageScaffoldFailure( - `generated owner slot contains unsupported developer content: ${MODULE_MANIFEST_SHELL_PAGE_SLOT_START}` + `generated owner slot contains unsupported developer content: ${MODULE_MANIFEST_SHELL_PAGE_SLOT_START}`, ); } return { owner, routePath }; @@ -866,18 +710,10 @@ const ownedPageRoute = ( const readOwnerRoutes = ( verticalRoot: string, - owner: string -): Effect.Effect< - readonly OwnedPageRoute[], - PageScaffoldError, - FileSystem.FileSystem -> => + owner: string, +): Effect.Effect => Effect.gen(function* readOwnerRoutesEffect() { - const manifestPath = resolveContainedPath( - verticalRoot, - owner, - 'vertical.manifest.ts' - ); + const manifestPath = resolveContainedPath(verticalRoot, owner, 'vertical.manifest.ts'); if (!(yield* fileExists(manifestPath))) { return []; } @@ -886,62 +722,44 @@ const readOwnerRoutes = ( readGeneratedSlotEntries( manifest, MODULE_MANIFEST_SHELL_PAGE_SLOT_START, - MODULE_MANIFEST_SHELL_PAGE_SLOT_END + MODULE_MANIFEST_SHELL_PAGE_SLOT_END, ).map((candidate) => ownedPageRoute(owner, candidate)), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); }); const ownedPageRoutes = ( - workspaceRoot: string -): Effect.Effect< - readonly OwnedPageRoute[], - PageScaffoldError, - FileSystem.FileSystem -> => + workspaceRoot: string, +): Effect.Effect => Effect.gen(function* ownedPageRoutesEffect() { const verticalRoot = resolveContainedPath(workspaceRoot, 'verticals'); const verticals = yield* readDirectoryEntries(verticalRoot); const ownedRoutes = yield* Effect.all( - verticals - .filter((entry) => entry.isDirectory) - .map((entry) => readOwnerRoutes(verticalRoot, entry.name)), - { concurrency: 'unbounded' } + verticals.filter((entry) => entry.isDirectory).map((entry) => readOwnerRoutes(verticalRoot, entry.name)), + { concurrency: 'unbounded' }, ); return ownedRoutes.flat(); }); const isDynamicShellRouteSegment = (segment: string): boolean => - /^\[.+\]$/u.test(segment) || - segment.startsWith('$') || - segment.startsWith('*'); + /^\[.+\]$/u.test(segment) || segment.startsWith('$') || segment.startsWith('*'); const routeCollisionIdentity = (routePath: string): string => routePath .split('/') - .map((segment) => - parameterRouteSegmentPattern.test(segment) ? ':parameter' : segment - ) + .map((segment) => (parameterRouteSegmentPattern.test(segment) ? ':parameter' : segment)) .join('/'); -const assertShellRouteSiblingsAreAvailable = ( - entries: readonly DirectoryEntry[], - segment: string, - route: PageRoute -) => +const assertShellRouteSiblingsAreAvailable = (entries: readonly DirectoryEntry[], segment: string, route: PageRoute) => Effect.gen(function* assertShellRouteSiblingsAreAvailableEffect() { const desiredSegmentIsDynamic = isDynamicShellRouteSegment(segment); const siblingCollision = entries.find( - (entry) => - entry.isDirectory && - (desiredSegmentIsDynamic || isDynamicShellRouteSegment(entry.name)) + (entry) => entry.isDirectory && (desiredSegmentIsDynamic || isDynamicShellRouteSegment(entry.name)), ); if (siblingCollision !== undefined) { - const collisionKind = isDynamicShellRouteSegment(siblingCollision.name) - ? 'dynamic' - : 'static'; + const collisionKind = isDynamicShellRouteSegment(siblingCollision.name) ? 'dynamic' : 'static'; yield* pageScaffoldFailure( - `Shell route ${route.canonicalPath} collides with ${collisionKind} route segment ${siblingCollision.name}` + `Shell route ${route.canonicalPath} collides with ${collisionKind} route segment ${siblingCollision.name}`, ); } }); @@ -950,7 +768,7 @@ const assertShellRouteSegmentIsAvailable = ( parent: string, index: number, route: PageRoute, - registeredRoutes: ReadonlySet + registeredRoutes: ReadonlySet, ): Effect.Effect => Effect.gen(function* assertShellRouteSegmentIsAvailableEffect() { const segment = route.filesystemSegments[index]; @@ -965,14 +783,10 @@ const assertShellRouteSegmentIsAvailable = ( } const child = resolveContainedPath(parent, segment); if (index === route.filesystemSegments.length - 1) { - yield* pageScaffoldFailure( - `Shell route already exists or collides with generated page: ${child}` - ); + yield* pageScaffoldFailure(`Shell route already exists or collides with generated page: ${child}`); } if (!childEntry.isDirectory) { - yield* pageScaffoldFailure( - `Shell route ${route.canonicalPath} collides with reserved route content` - ); + yield* pageScaffoldFailure(`Shell route ${route.canonicalPath} collides with reserved route content`); } const prefix = `/${route.canonicalSegments.slice(0, index + 1).join('/')}`; const ownsPrefix = registeredRoutes.has(prefix); @@ -981,65 +795,34 @@ const assertShellRouteSegmentIsAvailable = ( fileExists(resolveContainedPath(child, PAGE_FILE_NAME)), fileExists(resolveContainedPath(child, ROUTE_METADATA_FILE_NAME)), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); if ((pageRouteExists || routeMetadataExists) && !ownsPrefix) { - yield* pageScaffoldFailure( - `Shell route ${route.canonicalPath} uses reserved route prefix ${prefix}` - ); + yield* pageScaffoldFailure(`Shell route ${route.canonicalPath} uses reserved route prefix ${prefix}`); } - yield* assertShellRouteSegmentIsAvailable( - child, - index + 1, - route, - registeredRoutes - ); + yield* assertShellRouteSegmentIsAvailable(child, index + 1, route, registeredRoutes); }); const assertShellRouteIsAvailable = ( workspaceRoot: string, route: PageRoute, - registeredRoutes: ReadonlySet + registeredRoutes: ReadonlySet, ): Effect.Effect => assertShellRouteSegmentIsAvailable( - resolveContainedPath( - workspaceRoot, - 'apps', - SHELL_APP_ID, - 'src', - 'routes', - '[lang]' - ), + resolveContainedPath(workspaceRoot, 'apps', SHELL_APP_ID, 'src', 'routes', '[lang]'), 0, route, - registeredRoutes + registeredRoutes, ); -const resolveGeneratedPageContentState = ( - pageContent: string, - vertical: PageVerticalMetadata, - page: string, - route: PageRoute -): GeneratedPageState => { - if (pageContent === renderPage(vertical, page, route)) { - return 'current'; - } - return 'invalid'; -}; - const generatedWiringEntryMatches = ( content: string, startMarker: string, endMarker: string, expectedEntry: string, - identityPattern: RegExp + identityPattern: RegExp, ): boolean => - generatedSlotContainsExactEntry( - content, - startMarker, - endMarker, - expectedEntry - ) && + generatedSlotContainsExactEntry(content, startMarker, endMarker, expectedEntry) && readGeneratedSlotEntries(content, startMarker, endMarker).filter((entry) => { identityPattern.lastIndex = 0; return identityPattern.test(entry); @@ -1047,38 +830,38 @@ const generatedWiringEntryMatches = ( const generatedFileMatches = ( filePath: string, - expected: string + expected: string, ): Effect.Effect => - fileExists(filePath).pipe( - Effect.flatMap((exists) => - exists - ? readTextFile(filePath).pipe( - Effect.map((content) => content === expected) - ) - : Effect.succeed(false) - ) - ); + Effect.gen(function* generatedFileMatchesProgram() { + if (!(yield* fileExists(filePath))) return false; + const content = yield* readTextFile(filePath); + const [actual, generated] = yield* Effect.all([ + formatGeneratedMutationContent(filePath, content), + formatGeneratedMutationContent(filePath, expected), + ]).pipe(Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, 'generated page formatting failed'))); + return actual === generated; + }); const generatedWiringContentMatches = ( vertical: PageVerticalMetadata, page: string, wiring: ReturnType, shellClients: string, - navigationMatches: boolean + navigationMatches: boolean, ): boolean => generatedWiringEntryMatches( vertical.manifestContent, MODULE_MANIFEST_IMPORT_SLOT_START, MODULE_MANIFEST_IMPORT_SLOT_END, wiring.manifestImport, - new RegExp(`\\b${wiring.componentName}\\b`, 'u') + new RegExp(`\\b${wiring.componentName}\\b`, 'u'), ) && generatedWiringEntryMatches( vertical.manifestContent, MODULE_MANIFEST_COMPONENT_SLOT_START, MODULE_MANIFEST_COMPONENT_SLOT_END, wiring.manifestComponent, - new RegExp(`["']page-${page}["']\\s*:`, 'u') + new RegExp(`["']page-${page}["']\\s*:`, 'u'), ) && navigationMatches && generatedWiringEntryMatches( @@ -1086,27 +869,21 @@ const generatedWiringContentMatches = ( MODULE_MANIFEST_SHELL_PAGE_SLOT_START, MODULE_MANIFEST_SHELL_PAGE_SLOT_END, wiring.manifestPage, - new RegExp( - `\\bcontributionKey\\s*:\\s*["']${vertical.moduleId}\\.page\\.${page}["']`, - 'u' - ) + new RegExp(`\\bcontributionKey\\s*:\\s*["']${vertical.moduleId}\\.page\\.${page}["']`, 'u'), ) && generatedWiringEntryMatches( vertical.registrationContent, MODULE_REGISTRATION_PAGE_SLOT_START, MODULE_REGISTRATION_PAGE_SLOT_END, wiring.registrationPage, - new RegExp(`["']page-${page}["']\\s*:`, 'u') + new RegExp(`["']page-${page}["']\\s*:`, 'u'), ) && generatedWiringEntryMatches( shellClients, SHELL_PAGE_CLIENT_SLOT_START, SHELL_PAGE_CLIENT_SLOT_END, wiring.shellClient, - new RegExp( - `\\bcomponentKey\\s*:\\s*["']${vertical.moduleId}\\.page-${page}["']`, - 'u' - ) + new RegExp(`\\bcomponentKey\\s*:\\s*["']${vertical.moduleId}\\.page-${page}["']`, 'u'), ); const generatedWiringMatches = ( @@ -1114,29 +891,17 @@ const generatedWiringMatches = ( vertical: PageVerticalMetadata, page: string, route: PageRoute, - config: Pick + config: Pick, ): Effect.Effect => Effect.gen(function* generatedWiringMatchesEffect() { const wiring = pageWiring(vertical, page, route, config); - const federationPath = resolveContainedPath( - vertical.directory, - 'module-federation.config.ts' - ); + const federationPath = resolveContainedPath(vertical.directory, 'module-federation.config.ts'); const [federation, shellClients] = yield* Effect.all( [ readTextFile(federationPath), - readTextFile( - resolveContainedPath( - workspaceRoot, - 'apps', - SHELL_APP_ID, - 'src', - 'api', - 'vertical-clients.ts' - ) - ), + readTextFile(resolveContainedPath(workspaceRoot, 'apps', SHELL_APP_ID, 'src', 'api', 'vertical-clients.ts')), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const shellRouteDirectory = resolveContainedPath( workspaceRoot, @@ -1145,86 +910,54 @@ const generatedWiringMatches = ( 'src', 'routes', '[lang]', - ...route.filesystemSegments + ...route.filesystemSegments, ); const expectedShellFiles = [ [PAGE_FILE_NAME, renderShellConnectorPage(route)], - [ - PAGE_LOADER_FILE_NAME, - renderShellConnectorLoader(vertical, page, route), - ], - [ - ROUTE_METADATA_FILE_NAME, - renderShellConnectorMetadata(vertical, page, route, config), - ], + [PAGE_LOADER_FILE_NAME, renderShellConnectorLoader(vertical, page, route)], + [ROUTE_METADATA_FILE_NAME, renderShellConnectorMetadata(vertical, page, route, config)], ] as const; const shellRouteMatches = yield* Effect.all( expectedShellFiles.map(([fileName, expected]) => - generatedFileMatches( - resolveContainedPath(shellRouteDirectory, fileName), - expected - ) + generatedFileMatches(resolveContainedPath(shellRouteDirectory, fileName), expected), ), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const shellRouteEntries = yield* readDirectoryEntries(shellRouteDirectory); const shellRouteInventoryMatches = shellRouteEntries.length === expectedShellFiles.length && shellRouteEntries.every( - (entry) => - entry.isFile && - expectedShellFiles.some( - ([expectedName]) => expectedName === entry.name - ) + (entry) => entry.isFile && expectedShellFiles.some(([expectedName]) => expectedName === entry.name), ); const exposureKey = `./Page${toPascalCase(page)}`; const expectedExposureSource = `./src/federation/page-${page}.tsx`; - const exposureSource = moduleFederationExposureSource( - federation, - exposureKey - ); - const federatedPagePath = resolveContainedPath( - vertical.directory, - expectedExposureSource - ); - const federatedPageExists = yield* fileExists(federatedPagePath); + const exposureSource = moduleFederationExposureSource(federation, exposureKey); + const federatedPagePath = resolveContainedPath(vertical.directory, expectedExposureSource); const federationMatches = exposureSource === expectedExposureSource && - federatedPageExists && - (yield* readTextFile(federatedPagePath)) === - renderFederatedPage(vertical, page, route); + (yield* generatedFileMatches(federatedPagePath, renderFederatedPage(vertical, page, route))); const escapedModuleId = vertical.moduleId.replaceAll('.', String.raw`\.`); const navigationMatches = wiring.manifestNavigation === undefined ? readGeneratedSlotEntries( vertical.manifestContent, MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START, - MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END + MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END, ).every( (entry) => - !new RegExp( - `\\bcontributionKey\\s*:\\s*["']${escapedModuleId}\\.navigation\\.${page}["']`, - 'u' - ).test(entry) + !new RegExp(`\\bcontributionKey\\s*:\\s*["']${escapedModuleId}\\.navigation\\.${page}["']`, 'u').test( + entry, + ), ) : generatedWiringEntryMatches( vertical.manifestContent, MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START, MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END, wiring.manifestNavigation, - new RegExp( - `\\bcontributionKey\\s*:\\s*["']${vertical.moduleId}\\.navigation\\.${page}["']`, - 'u' - ) + new RegExp(`\\bcontributionKey\\s*:\\s*["']${vertical.moduleId}\\.navigation\\.${page}["']`, 'u'), ); return ( - generatedWiringContentMatches( - vertical, - page, - wiring, - shellClients, - navigationMatches - ) && + generatedWiringContentMatches(vertical, page, wiring, shellClients, navigationMatches) && federationMatches && shellRouteMatches.every(Boolean) && shellRouteInventoryMatches @@ -1235,12 +968,8 @@ const generatedLocaleState = ( workspaceRoot: string, vertical: PageVerticalMetadata, locale: string, - pageKey: string -): Effect.Effect< - GeneratedPageState, - PageScaffoldError, - FileSystem.FileSystem -> => + pageKey: string, +): Effect.Effect => Effect.gen(function* generatedLocaleStateEffect() { const localePath = resolveContainedPath( workspaceRoot, @@ -1248,17 +977,14 @@ const generatedLocaleState = ( vertical.slug, 'locales', locale, - `${vertical.namespace}.json` + `${vertical.namespace}.json`, ); const { value } = yield* readJsonEffect( localePath, `${locale} locale catalog`, - `${locale} locale catalog is invalid` - ); - const namespace = asJsonObject( - value[vertical.namespace], - `${locale} namespace` + `${locale} locale catalog is invalid`, ); + const namespace = asJsonObject(value[vertical.namespace], `${locale} namespace`); const pages = asJsonObject(namespace['pages'], `${locale} pages catalog`); const copy = pages[pageKey]; if (Equal.equals(copy, localizedPageCopy(locale))) { @@ -1275,12 +1001,8 @@ const generatedPageState = ( routeDirectory: string, pagePath: string, routeMetadataPath: string, - config: Pick -): Effect.Effect< - GeneratedPageState, - PageScaffoldError, - FileSystem.FileSystem -> => + config: Pick, +): Effect.Effect => Effect.gen(function* generatedPageStateEffect() { const entries = yield* readDirectoryEntries(routeDirectory); if ( @@ -1291,63 +1013,33 @@ const generatedPageState = ( ) { return 'invalid'; } - const [pageContent, routeMetadataContent] = yield* Effect.all( - [readTextFile(pagePath), readTextFile(routeMetadataPath)], - { concurrency: 'unbounded' } - ); - const expectedMetadata = renderRouteMetadata(vertical, page, route, config); - if (routeMetadataContent !== expectedMetadata) { - return 'invalid'; - } - const pageState = resolveGeneratedPageContentState( - pageContent, - vertical, - page, - route - ); - if (pageState === 'invalid') { - return 'invalid'; - } + const fileMatches = yield* Effect.all([ + generatedFileMatches(pagePath, renderPage(vertical, page, route)), + generatedFileMatches(routeMetadataPath, renderRouteMetadata(vertical, page, route, config)), + ]); + if (!fileMatches.every(Boolean)) return 'invalid'; const pageKey = toCamelCase(page); const localeStates = yield* Effect.all( - vertical.locales.map((locale) => - generatedLocaleState(workspaceRoot, vertical, locale, pageKey) - ), - { concurrency: 'unbounded' } + vertical.locales.map((locale) => generatedLocaleState(workspaceRoot, vertical, locale, pageKey)), + { concurrency: 'unbounded' }, ); - if (!localeStates.every((state) => state === pageState)) { + if (!localeStates.every((state) => state === 'current')) { return 'invalid'; } - return (yield* generatedWiringMatches( - workspaceRoot, - vertical, - page, - route, - config - )) - ? pageState - : 'invalid'; + return (yield* generatedWiringMatches(workspaceRoot, vertical, page, route, config)) ? 'current' : 'invalid'; }); const planPageScaffold = ( workspaceRoot: string, - config: PageScaffoldConfig -): Effect.Effect< - ScaffoldPlan, - PageScaffoldError, - FileSystem.FileSystem -> => + config: PageScaffoldConfig, +): Effect.Effect, PageScaffoldError, FileSystem.FileSystem> => Effect.gen(function* planPageScaffoldEffect() { yield* validateReadAuthorization(config); const page = yield* Effect.try({ - catch: (cause) => - pageScaffoldFailureFromUnknown(cause, 'page name is invalid'), + catch: (cause) => pageScaffoldFailureFromUnknown(cause, 'page name is invalid'), try: () => requireCanonicalSlug(config.page, 'page'), }); - const vertical = yield* discoverPageVertical( - workspaceRoot, - config.vertical - ); + const vertical = yield* discoverPageVertical(workspaceRoot, config.vertical); const route = yield* resolvePageRoute(vertical, page, config.url); const routeDirectory = resolveContainedPath( workspaceRoot, @@ -1356,13 +1048,10 @@ const planPageScaffold = ( 'src', 'routes', '[lang]', - ...route.filesystemSegments + ...route.filesystemSegments, ); const pagePath = resolveContainedPath(routeDirectory, PAGE_FILE_NAME); - const routeMetadataPath = resolveContainedPath( - routeDirectory, - ROUTE_METADATA_FILE_NAME - ); + const routeMetadataPath = resolveContainedPath(routeDirectory, ROUTE_METADATA_FILE_NAME); const shellRouteDirectory = resolveContainedPath( workspaceRoot, 'apps', @@ -1370,7 +1059,7 @@ const planPageScaffold = ( 'src', 'routes', '[lang]', - ...route.filesystemSegments + ...route.filesystemSegments, ); if (yield* fileExists(routeDirectory)) { const state = yield* generatedPageState( @@ -1381,7 +1070,7 @@ const planPageScaffold = ( routeDirectory, pagePath, routeMetadataPath, - config + config, ); if (state === 'current') { return { @@ -1389,107 +1078,72 @@ const planPageScaffold = ( result: { appId: vertical.appId, pagePath, routeMetadataPath }, }; } - return yield* pageScaffoldFailure( - `page route already exists or collides with nested content: ${routeDirectory}` - ); + return yield* pageScaffoldFailure(`page route already exists or collides with nested content: ${routeDirectory}`); } const identity = `${vertical.moduleId}.page.${page}`; const pageComponentIdentity = new RegExp(`["']page-${page}["']\\s*:`, 'u'); const escapedModuleId = vertical.moduleId.replaceAll('.', String.raw`\.`); - const pageContributionIdentity = new RegExp( - `["']${escapedModuleId}\\.page\\.${page}["']`, - 'u' - ); + const pageContributionIdentity = new RegExp(`["']${escapedModuleId}\\.page\\.${page}["']`, 'u'); if ( pageComponentIdentity.test(vertical.manifestContent) || pageContributionIdentity.test(vertical.manifestContent) ) { - return yield* pageScaffoldFailure( - `page identity ${identity} already exists at another URL` - ); + return yield* pageScaffoldFailure(`page identity ${identity} already exists at another URL`); } const registeredRoutes = yield* ownedPageRoutes(workspaceRoot); const existingRouteOwner = registeredRoutes.find( - (registered) => - routeCollisionIdentity(registered.routePath) === - routeCollisionIdentity(route.canonicalPath) + (registered) => routeCollisionIdentity(registered.routePath) === routeCollisionIdentity(route.canonicalPath), ); if (existingRouteOwner !== undefined) { const reason = existingRouteOwner.routePath === route.canonicalPath ? `is already registered by ${existingRouteOwner.owner}` : `has a routing collision with ${existingRouteOwner.routePath} registered by ${existingRouteOwner.owner}`; - return yield* pageScaffoldFailure( - `page URL ${route.canonicalPath} ${reason}` - ); + return yield* pageScaffoldFailure(`page URL ${route.canonicalPath} ${reason}`); } yield* assertShellRouteIsAvailable( workspaceRoot, route, - new Set(registeredRoutes.map((registered) => registered.routePath)) - ); - const federationPath = resolveContainedPath( - vertical.directory, - 'module-federation.config.ts' - ); - const federatedPagePath = resolveContainedPath( - vertical.directory, - 'src', - 'federation', - `page-${page}.tsx` + new Set(registeredRoutes.map((registered) => registered.routePath)), ); + const federationPath = resolveContainedPath(vertical.directory, 'module-federation.config.ts'); + const federatedPagePath = resolveContainedPath(vertical.directory, 'src', 'federation', `page-${page}.tsx`); const shellClientsPath = resolveContainedPath( workspaceRoot, 'apps', SHELL_APP_ID, 'src', 'api', - 'vertical-clients.ts' + 'vertical-clients.ts', ); - const [ - pageMutation, - routeMutation, - localeMutations, - federationContent, - shellClientsContent, - ] = yield* Effect.all( + const [pageMutation, routeMutation, localeMutations, federationContent, shellClientsContent] = yield* Effect.all( [ - createMutationEffect( - pagePath, - renderPage(vertical, page, route), - 'page route could not be created' - ), + createMutationEffect(pagePath, renderPage(vertical, page, route), 'page route could not be created'), createMutationEffect( routeMetadataPath, renderRouteMetadata(vertical, page, route, config), - 'page route metadata could not be created' + 'page route metadata could not be created', ), Effect.all( - vertical.locales.map((locale) => - patchLocale(workspaceRoot, vertical, locale, page) - ), - { concurrency: 'unbounded' } + vertical.locales.map((locale) => patchLocale(workspaceRoot, vertical, locale, page)), + { concurrency: 'unbounded' }, ), readTextFile(federationPath), readTextFile(shellClientsPath), ], - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const wiring = patchPageWiring(vertical, page, route, config); - const manifestMutation = updateMutation( - vertical.manifestPath, - vertical.manifestContent, - wiring.manifest - ); + const manifestMutation = updateMutation(vertical.manifestPath, vertical.manifestContent, wiring.manifest); const registrationMutation = updateMutation( vertical.registrationPath, vertical.registrationContent, - wiring.registration + wiring.registration, ); const federatedPageMutation = yield* createMutationEffect( federatedPagePath, renderFederatedPage(vertical, page, route), - 'federated page could not be created' + 'federated page could not be created', ); const federationMutation = updateMutation( federationPath, @@ -1497,8 +1151,8 @@ const planPageScaffold = ( insertModuleFederationExposure( federationContent, `./Page${toPascalCase(page)}`, - `./src/federation/page-${page}.tsx` - ) + `./src/federation/page-${page}.tsx`, + ), ); const shellClientsMutation = updateMutation( shellClientsPath, @@ -1508,30 +1162,29 @@ const planPageScaffold = ( SHELL_PAGE_CLIENT_SLOT_START, SHELL_PAGE_CLIENT_SLOT_END, [pageWiring(vertical, page, route, config).shellClient], - (candidate) => candidate.endsWith(',') - ) + (candidate) => candidate.endsWith(','), + ), + ); + const [shellPageMutation, shellLoaderMutation, shellRouteMetadataMutation] = yield* Effect.all( + [ + createMutationEffect( + resolveContainedPath(shellRouteDirectory, PAGE_FILE_NAME), + renderShellConnectorPage(route), + 'Shell page connector could not be created', + ), + createMutationEffect( + resolveContainedPath(shellRouteDirectory, PAGE_LOADER_FILE_NAME), + renderShellConnectorLoader(vertical, page, route), + 'Shell page loader could not be created', + ), + createMutationEffect( + resolveContainedPath(shellRouteDirectory, ROUTE_METADATA_FILE_NAME), + renderShellConnectorMetadata(vertical, page, route, config), + 'Shell route metadata could not be created', + ), + ], + { concurrency: 'unbounded' }, ); - const [shellPageMutation, shellLoaderMutation, shellRouteMetadataMutation] = - yield* Effect.all( - [ - createMutationEffect( - resolveContainedPath(shellRouteDirectory, PAGE_FILE_NAME), - renderShellConnectorPage(route), - 'Shell page connector could not be created' - ), - createMutationEffect( - resolveContainedPath(shellRouteDirectory, PAGE_LOADER_FILE_NAME), - renderShellConnectorLoader(vertical, page, route), - 'Shell page loader could not be created' - ), - createMutationEffect( - resolveContainedPath(shellRouteDirectory, ROUTE_METADATA_FILE_NAME), - renderShellConnectorMetadata(vertical, page, route, config), - 'Shell route metadata could not be created' - ), - ], - { concurrency: 'unbounded' } - ); const mutations = [ pageMutation, routeMutation, diff --git a/app/scripts/scaffolding/module-api/scaffold.mts b/app/scripts/scaffolding/module-api/scaffold.mts index a18d03c8a..f4d725ab6 100644 --- a/app/scripts/scaffolding/module-api/scaffold.mts +++ b/app/scripts/scaffolding/module-api/scaffold.mts @@ -2,7 +2,6 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { planGovernedContributionScaffold } from '../governed-contribution/scaffold.mts'; import type { GovernedContributionScaffoldConfig } from '../shared.mts'; -export default createCodesmithGenerator( - (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'module-api', config) +export default createCodesmithGenerator((workspaceRoot: string, config: GovernedContributionScaffoldConfig) => + planGovernedContributionScaffold(workspaceRoot, 'module-api', config), ); diff --git a/app/scripts/scaffolding/module-contract/scaffold.mts b/app/scripts/scaffolding/module-contract/scaffold.mts index 8a7e7508e..7773544a2 100644 --- a/app/scripts/scaffolding/module-contract/scaffold.mts +++ b/app/scripts/scaffolding/module-contract/scaffold.mts @@ -1,10 +1,4 @@ -import { - Array as EffectArray, - Effect, - FileSystem, - Option, - Schema, -} from 'effect'; +import { Array as EffectArray, Effect, FileSystem, Option, Schema } from 'effect'; import { topLevelSeparators } from '../../boundary-source-structure.mts'; import { createCodesmithGenerator } from '../generator-adapter.mts'; @@ -98,9 +92,7 @@ import type { const moduleMarkerPattern = /^\/\/ @ontos-module-id (?[^\s]+)$/mu; const MANIFEST_FILE_NAME = 'vertical.manifest.ts'; -const renderGovernedHttpApiRoot = ( - vertical: VerticalMetadata -): string => `${MODULE_CONTRACT_GENERATOR_HEADER} +const renderGovernedHttpApiRoot = (vertical: VerticalMetadata): string => `${MODULE_CONTRACT_GENERATOR_HEADER} // @ontos-deployment-app-id ${vertical.appId} import { HttpApi } from '@modern-js/plugin-bff/effect-client'; import { identity } from 'effect'; @@ -117,59 +109,44 @@ export const governedHttpApi = HttpApi.make('${toCamelCase(vertical.slug)}Govern const topLevelStatementEnd = (structure: string, start: number): number => topLevelSeparators(structure, ';', start)[0] ?? -1; -const initializeGovernedHttpApiRoot = ( - source: string, - vertical: VerticalMetadata -): string => { +const initializeGovernedHttpApiRoot = (source: string, vertical: VerticalMetadata): string => { if ( source.includes(GOVERNED_HTTP_API_IMPORT_SLOT_START) || source.includes(GOVERNED_HTTP_API_ADDITION_SLOT_START) || source.includes('governedHttpApi') ) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} shared API already uses reserved governed-read composition` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} shared API already uses reserved governed-read composition`); } const structure = maskNonCode(source); - const declarations = [ - ...structure.matchAll( - /export const (?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApi\.make\(/gu - ), - ]; + const declarations = [...structure.matchAll(/export const (?[A-Za-z][A-Za-z0-9]*)\s*=\s*HttpApi\.make\(/gu)]; if (declarations.length !== 1) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} shared API must contain exactly one generated HttpApi root` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} shared API must contain exactly one generated HttpApi root`); } const [declaration] = declarations; const apiValue = declaration?.groups?.['api']; const declarationStart = declaration?.index; if (apiValue === undefined || declarationStart === undefined) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} shared API root is malformed` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} shared API root is malformed`); } const statementEnd = topLevelStatementEnd(structure, declarationStart); if (statementEnd === -1) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} shared API root has no terminator` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} shared API root has no terminator`); } return `${source.slice(0, declarationStart)}${GOVERNED_HTTP_API_IMPORT_SLOT_START} ${GOVERNED_HTTP_API_IMPORT_SLOT_END} +import { identity as governedHttpApiIdentity } from 'effect'; + ${source.slice(declarationStart, statementEnd)} ${GOVERNED_HTTP_API_ADDITION_SLOT_START} - ${GOVERNED_HTTP_API_ADDITION_SLOT_END}${source.slice(statementEnd, statementEnd + 1)} + ${GOVERNED_HTTP_API_ADDITION_SLOT_END} + .pipe(governedHttpApiIdentity)${source.slice(statementEnd, statementEnd + 1)} /** Canonical composition-root binding consumed by generated governed HTTP adapters. */ export const governedHttpApi = ${apiValue};${source.slice(statementEnd + 1)}`; }; -const initializeGovernedHttpHandlerRoot = ( - source: string, - vertical: VerticalMetadata -): string => { +const initializeGovernedHttpHandlerRoot = (source: string, vertical: VerticalMetadata): string => { for (const reserved of [ 'GovernedReadRuntime', 'GovernedReadLayer', @@ -182,25 +159,18 @@ const initializeGovernedHttpHandlerRoot = ( ]) { if (source.includes(reserved)) { return raiseScaffoldFailure( - `vertical ${vertical.slug} API root already uses reserved governed-read composition ${reserved}` + `vertical ${vertical.slug} API root already uses reserved governed-read composition ${reserved}`, ); } } const runtimeLayerNeedle = ') satisfies EffectRuntimeLayer;'; const runtimeLayerEnd = source.lastIndexOf(runtimeLayerNeedle); if (runtimeLayerEnd === -1) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} API root must expose the pinned Effect runtime layer` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} API root must expose the pinned Effect runtime layer`); } - const runtimeLayerStart = source.lastIndexOf( - 'const layer = HttpApiBuilder.layer(', - runtimeLayerEnd - ); + const runtimeLayerStart = source.lastIndexOf('const layer = HttpApiBuilder.layer(', runtimeLayerEnd); if (runtimeLayerStart === -1) { - return raiseScaffoldFailure( - `vertical ${vertical.slug} API root must contain the pinned HttpApiBuilder layer` - ); + return raiseScaffoldFailure(`vertical ${vertical.slug} API root must contain the pinned HttpApiBuilder layer`); } const generatedRoot = `import { ContextAccessLive as GovernedContextAccessLive, @@ -253,7 +223,7 @@ export const governedReadApiHandlersLive = GovernedReadLayer.mergeAll( `; return `${generatedRoot}\n${source.slice(0, runtimeLayerStart)}${source.slice( runtimeLayerStart, - runtimeLayerEnd + runtimeLayerEnd, )} GovernedReadLayer.provide(governedReadApiHandlersLive), GovernedReadLayer.provide(GovernedDatabaseConfigLive), GovernedReadLayer.orDie, @@ -265,43 +235,27 @@ class ModuleContractScaffoldError extends Schema.TaggedError { - const manifestPath = resolveContainedPath( - verticalsRoot, - entryName, - MANIFEST_FILE_NAME - ); +const readModuleOwner = (fileSystem: FileSystem.FileSystem, verticalsRoot: string, entryName: string) => { + const manifestPath = resolveContainedPath(verticalsRoot, entryName, MANIFEST_FILE_NAME); return Effect.gen(function* readModuleOwnerEffect() { const exists = yield* fileSystem .exists(manifestPath) - .pipe( - Effect.mapError((cause) => - scaffoldError(`failed to inspect ${manifestPath}`, cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldError(`failed to inspect ${manifestPath}`, cause))); if (!exists) { return null; } const content = yield* fileSystem .readFileString(manifestPath) - .pipe( - Effect.mapError((cause) => - scaffoldError(`failed to read ${manifestPath}`, cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldError(`failed to read ${manifestPath}`, cause))); return { entryName, moduleId: moduleMarkerPattern.exec(content)?.groups?.['moduleId'], @@ -312,41 +266,29 @@ const readModuleOwner = ( const assertUniqueModuleId = ( workspaceRoot: string, targetSlug: string, - moduleId: string + moduleId: string, ): Effect.Effect => Effect.gen(function* assertUniqueModuleIdEffect() { - const verticalsRoot = yield* trySync(() => - resolveContainedPath(workspaceRoot, 'verticals') - ); + const verticalsRoot = yield* trySync(() => resolveContainedPath(workspaceRoot, 'verticals')); const fileSystem = yield* FileSystem.FileSystem; const entries = yield* fileSystem .readDirectory(verticalsRoot) .pipe( - Effect.mapError((cause) => - scaffoldError( - `failed to inspect generated verticals at ${verticalsRoot}`, - cause - ) - ) + Effect.mapError((cause) => scaffoldError(`failed to inspect generated verticals at ${verticalsRoot}`, cause)), ); const owners = yield* Effect.forEach( entries.filter((entryName) => entryName !== targetSlug), (entryName) => readModuleOwner(fileSystem, verticalsRoot, entryName), - { concurrency: 'unbounded' } + { concurrency: 'unbounded' }, ); const duplicate = owners.find((owner) => owner?.moduleId === moduleId); if (duplicate !== undefined && duplicate !== null) { - return yield* scaffoldError( - `duplicate OntOS module ID ${moduleId} in vertical ${duplicate.entryName}` - ); + return yield* scaffoldError(`duplicate OntOS module ID ${moduleId} in vertical ${duplicate.entryName}`); } return yield* Effect.void; }); -const renderManifest = ( - vertical: VerticalMetadata, - moduleId: string -): string => { +const renderManifest = (vertical: VerticalMetadata, moduleId: string): string => { const valueName = `${toCamelCase(vertical.slug)}Manifest`; return `${MODULE_CONTRACT_GENERATOR_HEADER} // @ontos-deployment-app-id ${vertical.appId} @@ -458,10 +400,7 @@ export const ${valueName} = defineOntosModuleManifest({ `; }; -const renderRegistration = ( - vertical: VerticalMetadata, - moduleId: string -): string => { +const renderRegistration = (vertical: VerticalMetadata, moduleId: string): string => { const prefix = toCamelCase(vertical.slug); return `${MODULE_CONTRACT_GENERATOR_HEADER} // @ontos-deployment-app-id ${vertical.appId} @@ -518,78 +457,49 @@ const addArtifactCommand = ( current: JsonValue | undefined, vertical: VerticalMetadata, target: 'cloudflare-dist' | 'dist', - label: string + label: string, ): Effect.Effect => Effect.gen(function* addArtifactCommandEffect() { - const script = yield* trySync(() => - requiredString(current, `vertical ${vertical.slug} ${label} script`) - ); + const script = yield* trySync(() => requiredString(current, `vertical ${vertical.slug} ${label} script`)); const command = `node ../../scripts/generate-ontos-module-contract.mts --vertical ${vertical.slug} --target ${target}`; if (script.includes('generate-ontos-module-contract.mts')) { - return yield* scaffoldError( - `vertical ${vertical.slug} ${label} script already contains module emission` - ); + return yield* scaffoldError(`vertical ${vertical.slug} ${label} script already contains module emission`); } - const buildToken = - target === 'dist' - ? 'modern build' - : 'MODERNJS_DEPLOY=cloudflare modern build'; + const buildToken = target === 'dist' ? 'modern build' : 'MODERNJS_DEPLOY=cloudflare modern build'; if (!script.includes(buildToken)) { - return yield* scaffoldError( - `vertical ${vertical.slug} ${label} script is not a generated Modern build` - ); + return yield* scaffoldError(`vertical ${vertical.slug} ${label} script is not a generated Modern build`); } return script.replace(buildToken, `${buildToken} && ${command}`); }); const patchPackage = ( vertical: VerticalMetadata, - moduleId: string + moduleId: string, ): Effect.Effect => Effect.gen(function* patchPackageEffect() { const dependencies = yield* trySync(() => ({ - ...asJsonObject( - vertical.packageJson['dependencies'], - `vertical ${vertical.slug} dependencies` - ), + ...asJsonObject(vertical.packageJson['dependencies'], `vertical ${vertical.slug} dependencies`), })); const currentCore = dependencies['@app/core-runtime']; if (currentCore !== undefined && currentCore !== 'workspace:*') { - return yield* scaffoldError( - `vertical ${vertical.slug} has an incompatible @app/core-runtime dependency` - ); + return yield* scaffoldError(`vertical ${vertical.slug} has an incompatible @app/core-runtime dependency`); } dependencies['@app/core-runtime'] = 'workspace:*'; const sortedDependencies = Object.fromEntries( - Object.entries(dependencies).toSorted(([left], [right]) => - left.localeCompare(right) - ) + Object.entries(dependencies).toSorted(([left], [right]) => left.localeCompare(right)), ); const scripts = yield* trySync(() => ({ - ...asJsonObject( - vertical.packageJson['scripts'], - `vertical ${vertical.slug} scripts` - ), + ...asJsonObject(vertical.packageJson['scripts'], `vertical ${vertical.slug} scripts`), })); - scripts['build'] = yield* addArtifactCommand( - scripts['build'], - vertical, - 'dist', - 'build' - ); + scripts['build'] = yield* addArtifactCommand(scripts['build'], vertical, 'dist', 'build'); scripts['cloudflare:build'] = yield* addArtifactCommand( scripts['cloudflare:build'], vertical, 'cloudflare-dist', - 'cloudflare:build' + 'cloudflare:build', ); return yield* trySync(() => { - let content = patchJsonObjectProperty( - vertical.packageContent, - [], - 'dependencies', - sortedDependencies - ); + let content = patchJsonObjectProperty(vertical.packageContent, [], 'dependencies', sortedDependencies); content = patchJsonObjectProperty(content, [], 'scripts', scripts); return patchJsonObjectProperty(content, ['modernjs'], 'ontosModule', { contractPath: '/.well-known/ontos-module-manifest.json', @@ -602,50 +512,30 @@ const patchPackage = ( }); const patchTsconfig = ( - vertical: VerticalMetadata -): Effect.Effect< - Option.Option, - ModuleContractScaffoldError | ScaffoldFailure, - FileSystem.FileSystem -> => + vertical: VerticalMetadata, +): Effect.Effect, ModuleContractScaffoldError | ScaffoldFailure, FileSystem.FileSystem> => Effect.gen(function* patchTsconfigEffect() { - const tsconfigPath = yield* trySync(() => - resolveContainedPath(vertical.directory, 'tsconfig.json') - ); - const { content, value } = yield* readJsonEffect( - tsconfigPath, - `vertical ${vertical.slug} tsconfig` - ); - const include = yield* Schema.decodeUnknownEffect( - Schema.Array(Schema.String) - )(value['include']).pipe( + const tsconfigPath = yield* trySync(() => resolveContainedPath(vertical.directory, 'tsconfig.json')); + const { content, value } = yield* readJsonEffect(tsconfigPath, `vertical ${vertical.slug} tsconfig`); + const include = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.String))(value['include']).pipe( Effect.mapError((cause) => - scaffoldError( - `vertical ${vertical.slug} tsconfig include must be a string array`, - cause - ) - ) + scaffoldError(`vertical ${vertical.slug} tsconfig include must be a string array`, cause), + ), ); const nextInclude = [ ...include, - ...[MANIFEST_FILE_NAME, 'vertical.registration.ts'].filter( - (entry) => !include.includes(entry) - ), + ...[MANIFEST_FILE_NAME, 'vertical.registration.ts'].filter((entry) => !include.includes(entry)), ]; return yield* trySync(() => Option.fromNullishOr( - updateMutation( - tsconfigPath, - content, - patchJsonObjectProperty(content, [], 'include', nextInclude) - ) - ) + updateMutation(tsconfigPath, content, patchJsonObjectProperty(content, [], 'include', nextInclude)), + ), ); }); const planModuleContractScaffold = ( workspaceRoot: string, - config: ModuleContractScaffoldConfig + config: ModuleContractScaffoldConfig, ): Effect.Effect< ScaffoldPlan, ModuleContractScaffoldError | ScaffoldFailure, @@ -653,90 +543,37 @@ const planModuleContractScaffold = ( > => Effect.gen(function* planModuleContractScaffoldEffect() { const moduleId = yield* trySync(() => requireOntosModuleId(config.module)); - const vertical = yield* discoverVerticalEffect( - workspaceRoot, - config.vertical - ); + const vertical = yield* discoverVerticalEffect(workspaceRoot, config.vertical); yield* assertUniqueModuleId(workspaceRoot, vertical.slug, moduleId); - const manifestPath = yield* trySync(() => - resolveContainedPath(vertical.directory, MANIFEST_FILE_NAME) - ); - const registrationPath = yield* trySync(() => - resolveContainedPath(vertical.directory, 'vertical.registration.ts') - ); - const sharedApiPath = yield* trySync(() => - resolveContainedPath(vertical.directory, 'shared', 'api.ts') - ); - const apiRootPath = yield* trySync(() => - resolveContainedPath(vertical.directory, 'api', 'index.ts') - ); - const manifestMutation = yield* createMutationEffect( - manifestPath, - renderManifest(vertical, moduleId) - ); - const registrationMutation = yield* createMutationEffect( - registrationPath, - renderRegistration(vertical, moduleId) - ); + const manifestPath = yield* trySync(() => resolveContainedPath(vertical.directory, MANIFEST_FILE_NAME)); + const registrationPath = yield* trySync(() => resolveContainedPath(vertical.directory, 'vertical.registration.ts')); + const sharedApiPath = yield* trySync(() => resolveContainedPath(vertical.directory, 'shared', 'api.ts')); + const apiRootPath = yield* trySync(() => resolveContainedPath(vertical.directory, 'api', 'index.ts')); + const manifestMutation = yield* createMutationEffect(manifestPath, renderManifest(vertical, moduleId)); + const registrationMutation = yield* createMutationEffect(registrationPath, renderRegistration(vertical, moduleId)); const fileSystem = yield* FileSystem.FileSystem; const sharedApiExists = yield* fileSystem .exists(sharedApiPath) .pipe( - Effect.mapError((cause) => - scaffoldError( - `failed to inspect vertical ${vertical.slug} shared API root`, - cause - ) - ) + Effect.mapError((cause) => scaffoldError(`failed to inspect vertical ${vertical.slug} shared API root`, cause)), ); const sharedApiMutation = sharedApiExists ? yield* fileSystem.readFileString(sharedApiPath).pipe( - Effect.mapError((cause) => - scaffoldError( - `failed to read vertical ${vertical.slug} shared API root`, - cause - ) - ), + Effect.mapError((cause) => scaffoldError(`failed to read vertical ${vertical.slug} shared API root`, cause)), Effect.flatMap((content) => - trySync(() => - updateMutation( - sharedApiPath, - content, - initializeGovernedHttpApiRoot(content, vertical) - ) - ) - ) + trySync(() => updateMutation(sharedApiPath, content, initializeGovernedHttpApiRoot(content, vertical))), + ), ) - : yield* createMutationEffect( - sharedApiPath, - renderGovernedHttpApiRoot(vertical) - ); + : yield* createMutationEffect(sharedApiPath, renderGovernedHttpApiRoot(vertical)); const apiRootContent = yield* fileSystem .readFileString(apiRootPath) - .pipe( - Effect.mapError((cause) => - scaffoldError( - `failed to read vertical ${vertical.slug} API root`, - cause - ) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldError(`failed to read vertical ${vertical.slug} API root`, cause))); const apiRootMutation = yield* trySync(() => - updateMutation( - apiRootPath, - apiRootContent, - initializeGovernedHttpHandlerRoot(apiRootContent, vertical) - ) + updateMutation(apiRootPath, apiRootContent, initializeGovernedHttpHandlerRoot(apiRootContent, vertical)), ); const packageContent = yield* patchPackage(vertical, moduleId); const packageMutation = yield* trySync(() => - Option.fromNullishOr( - updateMutation( - vertical.packagePath, - vertical.packageContent, - packageContent - ) - ) + Option.fromNullishOr(updateMutation(vertical.packagePath, vertical.packageContent, packageContent)), ); const tsconfigMutation = yield* patchTsconfig(vertical); const mutations = EffectArray.getSomes([ diff --git a/app/scripts/scaffolding/outbox-message/scaffold.mts b/app/scripts/scaffolding/outbox-message/scaffold.mts index 68eefcb49..af0c3f9e4 100644 --- a/app/scripts/scaffolding/outbox-message/scaffold.mts +++ b/app/scripts/scaffolding/outbox-message/scaffold.mts @@ -20,39 +20,24 @@ import { topicToSlug, updateMutation, } from '../shared.mts'; -import type { - OntosVerticalMetadata, - OutboxScaffoldConfig, - OutboxScaffoldResult, - ScaffoldPlan, -} from '../shared.mts'; +import type { OntosVerticalMetadata, OutboxScaffoldConfig, OutboxScaffoldResult, ScaffoldPlan } from '../shared.mts'; class OutboxMessageScaffoldError extends Schema.TaggedError()( 'OutboxMessageScaffoldError', - { cause: Schema.optional(Schema.Unknown), reason: Schema.String } + { cause: Schema.optional(Schema.Unknown), reason: Schema.String }, ) { override get message(): string { return this.reason; } } -const planningFailure = ( - reason: string, - cause?: unknown -): OutboxMessageScaffoldError => - cause === undefined - ? new OutboxMessageScaffoldError({ reason }) - : new OutboxMessageScaffoldError({ cause, reason }); +const planningFailure = (reason: string, cause?: unknown): OutboxMessageScaffoldError => + cause === undefined ? new OutboxMessageScaffoldError({ reason }) : new OutboxMessageScaffoldError({ cause, reason }); const failureFromCause = (cause: unknown): OutboxMessageScaffoldError => - planningFailure( - Predicate.isError(cause) ? cause.message : String(cause), - cause - ); + planningFailure(Predicate.isError(cause) ? cause.message : String(cause), cause); -const fromLegacySync = ( - operation: () => Value -): Effect.Effect => +const fromLegacySync = (operation: () => Value): Effect.Effect => Effect.try({ catch: failureFromCause, try: operation }); const PackageExportsSchema = Schema.Struct({ @@ -63,7 +48,7 @@ const isNotFoundPlatformError = Schema.is( Schema.Struct({ _tag: Schema.Literal('PlatformError'), reason: Schema.Struct({ _tag: Schema.Literal('NotFound') }), - }) + }), ); const isScaffoldFailure = Schema.is(ScaffoldFailure); @@ -73,7 +58,7 @@ const rethrowDiscoveryCause = (cause: Cause.Cause) => const recoverDiscoveryCause = (cause: Cause.Cause) => { const hasUnexpectedDefect = cause.reasons.some( - (reason) => Cause.isDieReason(reason) && !isScaffoldFailure(reason.defect) + (reason) => Cause.isDieReason(reason) && !isScaffoldFailure(reason.defect), ); if (Cause.hasInterrupts(cause) || hasUnexpectedDefect) { return rethrowDiscoveryCause(cause); @@ -91,11 +76,7 @@ const recoverDiscoveryCause = (cause: Cause.Cause) => { const FORMATTED_ACTION_GENERATOR_PREFIX = "import { defineAction, defineTenantModuleEntrypoint } from '@app/core-runtime';\n"; -const renderOutboxMessage = ( - vertical: OntosVerticalMetadata, - action: string, - topic: string -): string => { +const renderOutboxMessage = (vertical: OntosVerticalMetadata, action: string, topic: string): string => { const actionType = toPascalCase(action); const topicType = toPascalCase(topicToSlug(topic)); const base = `${actionType}${topicType}Outbox`; @@ -119,10 +100,7 @@ export const create${base}Message = ( `; }; -const renderOutboxContract = ( - vertical: OntosVerticalMetadata, - topic: string -): string => +const renderOutboxContract = (vertical: OntosVerticalMetadata, topic: string): string => `${OUTBOX_CONTRACT_GENERATOR_HEADER} // @ontos-outbox-producer ${vertical.moduleId} // @ontos-outbox-topic ${topic} @@ -137,16 +115,10 @@ export const outboxTopic = '${topic}' as const; export const outboxProducerModuleKey = '${vertical.moduleId}' as const; `; -const isMatchingGeneratedAction = ( - actionContent: string, - vertical: OntosVerticalMetadata, - action: string -): boolean => { +const isMatchingGeneratedAction = (actionContent: string, vertical: OntosVerticalMetadata, action: string): boolean => { const hasGeneratedActionPrefix = actionContent.startsWith(`${ACTION_GENERATOR_HEADER}\n`) || - actionContent.startsWith( - `${FORMATTED_ACTION_GENERATOR_PREFIX}${ACTION_GENERATOR_HEADER}\n` - ); + actionContent.startsWith(`${FORMATTED_ACTION_GENERATOR_PREFIX}${ACTION_GENERATOR_HEADER}\n`); return ( hasGeneratedActionPrefix && [ @@ -163,7 +135,7 @@ const isMatchingGeneratedAction = ( const planOutboxScaffold = ( workspaceRoot: string, - config: OutboxScaffoldConfig + config: OutboxScaffoldConfig, ): Effect.Effect< ScaffoldPlan, OutboxMessageScaffoldError | PlatformError.PlatformError | ScaffoldFailure, @@ -171,54 +143,36 @@ const planOutboxScaffold = ( > => Effect.gen(function* planOutboxScaffoldEffect() { const fileSystem = yield* FileSystem.FileSystem; - const action = yield* fromLegacySync(() => - requireCanonicalSlug(config.action, 'action') - ); + const action = yield* fromLegacySync(() => requireCanonicalSlug(config.action, 'action')); const topic = yield* fromLegacySync(() => requireTopic(config.topic)); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ).pipe(Effect.catchCause(recoverDiscoveryCause)); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical).pipe( + Effect.catchCause(recoverDiscoveryCause), + ); const actionPath = yield* fromLegacySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'src', - 'actions', - `${action}.action.ts` - ) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'src', 'actions', `${action}.action.ts`), ); const actionContent = yield* fileSystem .readFileString(actionPath) .pipe( Effect.catchIf(isNotFoundPlatformError, (cause) => - Effect.fail( - planningFailure( - `Outbox Message requires the generated Action at ${actionPath}`, - cause - ) - ) - ) + Effect.fail(planningFailure(`Outbox Message requires the generated Action at ${actionPath}`, cause)), + ), ); if (!isMatchingGeneratedAction(actionContent, vertical, action)) { return yield* Effect.fail( planningFailure( - 'Outbox Message can extend only the matching generated Action with its governed write entrypoint' - ) + 'Outbox Message can extend only the matching generated Action with its governed write entrypoint', + ), ); } const topicSlug = topicToSlug(topic); const base = `${toPascalCase(action)}${toPascalCase(topicSlug)}Outbox`; if ( - new RegExp( - `\\b(?:${base}(?:Payload|PayloadSchema|ProducerModuleKey|Topic)|create${base}Message)\\b`, - 'u' - ).test(actionContent) + new RegExp(`\\b(?:${base}(?:Payload|PayloadSchema|ProducerModuleKey|Topic)|create${base}Message)\\b`, 'u').test( + actionContent, + ) ) { - return yield* Effect.fail( - planningFailure(`Outbox identifier ${base} already exists`) - ); + return yield* Effect.fail(planningFailure(`Outbox identifier ${base} already exists`)); } const messagePath = yield* fromLegacySync(() => resolveContainedPath( @@ -227,27 +181,14 @@ const planOutboxScaffold = ( vertical.slug, 'src', 'actions', - `${action}.${topicSlug}.outbox-message.ts` - ) + `${action}.${topicSlug}.outbox-message.ts`, + ), ); const contractPath = yield* fromLegacySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'shared', - 'outbox', - `${topicSlug}.ts` - ) - ); - const contractMutation = yield* createMutationEffect( - contractPath, - renderOutboxContract(vertical, topic) - ); - const messageMutation = yield* createMutationEffect( - messagePath, - renderOutboxMessage(vertical, action, topic) + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'shared', 'outbox', `${topicSlug}.ts`), ); + const contractMutation = yield* createMutationEffect(contractPath, renderOutboxContract(vertical, topic)); + const messageMutation = yield* createMutationEffect(messagePath, renderOutboxMessage(vertical, action, topic)); const exportSource = `./${action}.${topicSlug}.outbox-message.ts`; const exportEntries = [ `export { ${base}PayloadSchema } from '${exportSource}';`, @@ -257,80 +198,43 @@ const planOutboxScaffold = ( `export type { ${base}Payload } from '${exportSource}';`, ]; const patchedAction = yield* fromLegacySync(() => - insertSortedSlot( - actionContent, - OUTBOX_SLOT_START, - OUTBOX_SLOT_END, - exportEntries, - (candidate) => - /^export (?:type )?\{ [A-Za-z0-9]+ \} from '\.\/[a-z0-9.-]+\.outbox-message\.ts';$/u.test( - candidate - ) - ) - ); - const actionMutation = updateMutation( - actionPath, - actionContent, - patchedAction + insertSortedSlot(actionContent, OUTBOX_SLOT_START, OUTBOX_SLOT_END, exportEntries, (candidate) => + /^export (?:type )?\{ [A-Za-z0-9]+ \} from '\.\/[a-z0-9.-]+\.outbox-message\.ts';$/u.test(candidate), + ), ); + const actionMutation = updateMutation(actionPath, actionContent, patchedAction); if (actionMutation === undefined) { - return yield* Effect.fail( - planningFailure( - 'Outbox Message Action export patch unexpectedly made no change' - ) - ); + return yield* Effect.fail(planningFailure('Outbox Message Action export patch unexpectedly made no change')); } - const packageDocument = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(PackageExportsSchema), - { onExcessProperty: 'preserve' } - )(vertical.packageContent).pipe( + const packageDocument = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PackageExportsSchema), { + onExcessProperty: 'preserve', + })(vertical.packageContent).pipe( Effect.mapError((cause) => - planningFailure( - `vertical ${vertical.slug} package exports must be a JSON object`, - cause - ) - ) + planningFailure(`vertical ${vertical.slug} package exports must be a JSON object`, cause), + ), ); const exportsValue = packageDocument.exports; const contractExport = `./outbox/${topicSlug}`; if (exportsValue[contractExport] !== undefined) { - return yield* Effect.fail( - planningFailure( - `Outbox contract export ${contractExport} already exists` - ) - ); + return yield* Effect.fail(planningFailure(`Outbox contract export ${contractExport} already exists`)); } const patchedExports = Object.fromEntries( Object.entries({ ...exportsValue, [contractExport]: `./shared/outbox/${topicSlug}.ts`, - }).toSorted(([left], [right]) => left.localeCompare(right)) + }).toSorted(([left], [right]) => left.localeCompare(right)), ); const packageMutation = yield* fromLegacySync(() => updateMutation( vertical.packagePath, vertical.packageContent, - patchJsonObjectProperty( - vertical.packageContent, - [], - 'exports', - patchedExports - ) - ) + patchJsonObjectProperty(vertical.packageContent, [], 'exports', patchedExports), + ), ); if (packageMutation === undefined) { - return yield* Effect.fail( - planningFailure( - 'Outbox Message package export patch unexpectedly made no change' - ) - ); + return yield* Effect.fail(planningFailure('Outbox Message package export patch unexpectedly made no change')); } - const mutations = [ - contractMutation, - messageMutation, - actionMutation, - packageMutation, - ]; + const mutations = [contractMutation, messageMutation, actionMutation, packageMutation]; yield* fromLegacySync(() => ensureUniqueMutationPaths(mutations)); return { mutations, result: { contractPath, messagePath } }; }); diff --git a/app/scripts/scaffolding/outbox-worker/scaffold.mts b/app/scripts/scaffolding/outbox-worker/scaffold.mts index 77660dea7..a3591b674 100644 --- a/app/scripts/scaffolding/outbox-worker/scaffold.mts +++ b/app/scripts/scaffolding/outbox-worker/scaffold.mts @@ -36,22 +36,15 @@ import type { ScaffoldPlan, } from '../shared.mts'; -class OutboxWorkerScaffoldError extends Schema.TaggedError()( - 'OutboxWorkerScaffoldError', - { - cause: Schema.Unknown, - message: Schema.String, - } -) {} +class OutboxWorkerScaffoldError extends Schema.TaggedError()('OutboxWorkerScaffoldError', { + cause: Schema.Unknown, + message: Schema.String, +}) {} -const scaffoldError = ( - cause: unknown, - message?: string -): OutboxWorkerScaffoldError => +const scaffoldError = (cause: unknown, message?: string): OutboxWorkerScaffoldError => new OutboxWorkerScaffoldError({ cause, - message: - message ?? (Predicate.isError(cause) ? cause.message : String(cause)), + message: message ?? (Predicate.isError(cause) ? cause.message : String(cause)), }); const trySync = (operation: () => Value) => @@ -66,12 +59,10 @@ const readRequiredFile = (filePath: string, label: string) => return yield* fileSystem.readFileString(filePath).pipe( Effect.mapError((error) => Match.value(error.reason).pipe( - Match.tag('NotFound', () => - scaffoldError(error, `${label} is missing at ${filePath}`) - ), - Match.orElse(() => scaffoldError(error)) - ) - ) + Match.tag('NotFound', () => scaffoldError(error, `${label} is missing at ${filePath}`)), + Match.orElse(() => scaffoldError(error)), + ), + ), ); }); @@ -83,25 +74,23 @@ const readOptionalFile = (filePath: string) => Effect.catch((error) => Match.value(error.reason).pipe( Match.tag('NotFound', () => Effect.succeed(Option.none())), - Match.orElse(() => Effect.fail(scaffoldError(error))) - ) - ) + Match.orElse(() => Effect.fail(scaffoldError(error))), + ), + ), ); }); -const OUTBOX_WORKER_HOST_HEADER = - '// @generated by scaffold:outbox-worker worker-host'; -const TsconfigReferenceSchema = Schema.StructWithRest( - Schema.Struct({ path: Schema.String }), - [Schema.Record(Schema.String, Schema.Json)] -); +const OUTBOX_WORKER_HOST_HEADER = '// @generated by scaffold:outbox-worker worker-host'; +const TsconfigReferenceSchema = Schema.StructWithRest(Schema.Struct({ path: Schema.String }), [ + Schema.Record(Schema.String, Schema.Json), +]); const TsconfigReferencesSchema = Schema.Array(TsconfigReferenceSchema); const renderWorker = ( consumer: OntosVerticalMetadata, producer: OntosVerticalMetadata, worker: string, - topic: string + topic: string, ): string => { const workerType = toPascalCase(worker); const workerVariable = `${toCamelCase(worker)}Worker`; @@ -162,8 +151,7 @@ export const ${workerVariable} = defineOutboxWorker( `; }; -const renderRegistry = - (): string => `import type { AnyOutboxWorkerRegistration } from '@app/core-runtime'; +const renderRegistry = (): string => `import type { AnyOutboxWorkerRegistration } from '@app/core-runtime'; ${OUTBOX_WORKER_IMPORT_SLOT_START} ${OUTBOX_WORKER_IMPORT_SLOT_END} @@ -174,9 +162,7 @@ export const outboxWorkers = Object.freeze([ ]) satisfies readonly AnyOutboxWorkerRegistration[]; `; -const renderWorkerHostLayer = ( - consumer: OntosVerticalMetadata -): string => `${OUTBOX_WORKER_HOST_HEADER} +const renderWorkerHostLayer = (consumer: OntosVerticalMetadata): string => `${OUTBOX_WORKER_HOST_HEADER} // @ontos-outbox-worker-host-owner ${consumer.moduleId} import { Layer } from 'effect'; import { OutboxWorkerInfrastructureLive } from '@app/core-runtime/outbox/worker'; @@ -194,18 +180,14 @@ export const outboxWorkerLayer = Layer.merge( ); `; -const renderWorkerHostMain = ( - consumer: OntosVerticalMetadata -): string => `${OUTBOX_WORKER_HOST_HEADER} +const renderWorkerHostMain = (consumer: OntosVerticalMetadata): string => `${OUTBOX_WORKER_HOST_HEADER} // @ontos-outbox-worker-host-owner ${consumer.moduleId} import { start${toPascalCase(consumer.slug)}OutboxWorker } from '../../scripts/outbox-worker.ts'; start${toPascalCase(consumer.slug)}OutboxWorker(); `; -const renderWorkerHostScript = ( - consumer: OntosVerticalMetadata -): string => `${OUTBOX_WORKER_HOST_HEADER} +const renderWorkerHostScript = (consumer: OntosVerticalMetadata): string => `${OUTBOX_WORKER_HOST_HEADER} // @ontos-outbox-worker-host-owner ${consumer.moduleId} import { Layer } from 'effect'; import { @@ -237,11 +219,7 @@ export const start${toPascalCase(consumer.slug)}OutboxWorker = (): void => }); `; -const planWorkerHostFile = ( - filePath: string, - consumer: OntosVerticalMetadata, - content: string -) => +const planWorkerHostFile = (filePath: string, consumer: OntosVerticalMetadata, content: string) => Effect.gen(function* planWorkerHostFileEffect() { const existing = yield* readOptionalFile(filePath); if (Option.isNone(existing)) { @@ -249,9 +227,7 @@ const planWorkerHostFile = ( } if ( !existing.value.startsWith(`${OUTBOX_WORKER_HOST_HEADER}\n`) || - !existing.value.includes( - `// @ontos-outbox-worker-host-owner ${consumer.moduleId}\n` - ) + !existing.value.includes(`// @ontos-outbox-worker-host-owner ${consumer.moduleId}\n`) ) { return yield* new OutboxWorkerScaffoldError({ cause: existing.value, @@ -261,22 +237,14 @@ const planWorkerHostFile = ( return Option.none(); }); -const patchConsumerPackage = ( - consumer: OntosVerticalMetadata, - producer: OntosVerticalMetadata -) => +const patchConsumerPackage = (consumer: OntosVerticalMetadata, producer: OntosVerticalMetadata) => Effect.gen(function* patchConsumerPackageEffect() { const dependenciesValue = consumer.packageJson['dependencies']; const dependencies: MutableJsonObject = dependenciesValue === undefined ? {} : { - ...(yield* trySync(() => - asJsonObject( - dependenciesValue, - `vertical ${consumer.slug} dependencies` - ) - )), + ...(yield* trySync(() => asJsonObject(dependenciesValue, `vertical ${consumer.slug} dependencies`))), }; for (const [name, version] of [ ['@app/core-runtime', 'workspace:*'], @@ -295,9 +263,7 @@ const patchConsumerPackage = ( dependencies[name] = version; } const sortedDependencies = Object.fromEntries( - Object.entries(dependencies).toSorted(([left], [right]) => - left.localeCompare(right) - ) + Object.entries(dependencies).toSorted(([left], [right]) => left.localeCompare(right)), ); const packageScripts = consumer.packageJson['scripts']; if (packageScripts === undefined) { @@ -307,10 +273,9 @@ const patchConsumerPackage = ( }); } const scriptsValue = yield* trySync(() => - asJsonObject(packageScripts, `vertical ${consumer.slug} package scripts`) + asJsonObject(packageScripts, `vertical ${consumer.slug} package scripts`), ); - const workerCommand = - 'node --experimental-strip-types ./src/worker-host/main.ts'; + const workerCommand = 'node --experimental-strip-types ./src/worker-host/main.ts'; const scripts: MutableJsonObject = { ...scriptsValue }; for (const scriptName of ['dev:worker', 'worker:start'] as const) { const current = scripts[scriptName]; @@ -323,57 +288,28 @@ const patchConsumerPackage = ( scripts[scriptName] = workerCommand; } const sortedScripts = Object.fromEntries( - Object.entries(scripts).toSorted(([left], [right]) => - left.localeCompare(right) - ) + Object.entries(scripts).toSorted(([left], [right]) => left.localeCompare(right)), ); const withDependencies = yield* trySync(() => - patchJsonObjectProperty( - consumer.packageContent, - [], - 'dependencies', - sortedDependencies - ) - ); - return yield* trySync(() => - patchJsonObjectProperty(withDependencies, [], 'scripts', sortedScripts) + patchJsonObjectProperty(consumer.packageContent, [], 'dependencies', sortedDependencies), ); + return yield* trySync(() => patchJsonObjectProperty(withDependencies, [], 'scripts', sortedScripts)); }); -const patchConsumerTsconfig = ( - content: string, - consumer: OntosVerticalMetadata, - producer: OntosVerticalMetadata -) => +const patchConsumerTsconfig = (content: string, consumer: OntosVerticalMetadata, producer: OntosVerticalMetadata) => Effect.gen(function* patchConsumerTsconfigEffect() { - const parsed = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString(Schema.Json) - )(content).pipe( - Effect.mapError((cause) => - scaffoldError( - cause, - `vertical ${consumer.slug} tsconfig is not valid JSON` - ) - ) - ); - const root = yield* trySync(() => - asJsonObject(parsed, `vertical ${consumer.slug} tsconfig`) + const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))(content).pipe( + Effect.mapError((cause) => scaffoldError(cause, `vertical ${consumer.slug} tsconfig is not valid JSON`)), ); + const root = yield* trySync(() => asJsonObject(parsed, `vertical ${consumer.slug} tsconfig`)); const referencesValue = root['references']; - const references = yield* Schema.decodeUnknownEffect( - TsconfigReferencesSchema - )(referencesValue).pipe( + const references = yield* Schema.decodeUnknownEffect(TsconfigReferencesSchema)(referencesValue).pipe( Effect.mapError((cause) => - scaffoldError( - cause, - `vertical ${consumer.slug} tsconfig references must be an array` - ) - ) + scaffoldError(cause, `vertical ${consumer.slug} tsconfig references must be an array`), + ), ); const producerReference = `../${producer.slug}`; - const matching = references.filter( - (reference) => reference.path === producerReference - ); + const matching = references.filter((reference) => reference.path === producerReference); if (consumer.slug === producer.slug) { if (matching.length > 0) { return yield* new OutboxWorkerScaffoldError({ @@ -392,19 +328,13 @@ const patchConsumerTsconfig = ( if (matching.length === 1) { return content; } - const patched = [...references, { path: producerReference }].toSorted( - (left, right) => left.path.localeCompare(right.path) - ); - return yield* trySync(() => - patchJsonObjectProperty(content, [], 'references', patched) + const patched = [...references, { path: producerReference }].toSorted((left, right) => + left.path.localeCompare(right.path), ); + return yield* trySync(() => patchJsonObjectProperty(content, [], 'references', patched)); }); -const isMatchingOutboxContract = ( - contract: string, - producer: OntosVerticalMetadata, - topic: string -): boolean => +const isMatchingOutboxContract = (contract: string, producer: OntosVerticalMetadata, topic: string): boolean => contract.startsWith(`${OUTBOX_CONTRACT_GENERATOR_HEADER}\n`) && [ `// @ontos-outbox-producer ${producer.moduleId}\n`, @@ -413,15 +343,9 @@ const isMatchingOutboxContract = ( `export const outboxProducerModuleKey = '${producer.moduleId}' as const;`, 'export const OutboxPayloadSchema =', ].every((fragment) => contract.includes(fragment)) && - !/(?:src\/actions|create[A-Za-z0-9]+Message|handler|repository|transport)/u.test( - contract - ); + !/(?:src\/actions|create[A-Za-z0-9]+Message|handler|repository|transport)/u.test(contract); -const planRegistryMutation = ( - registryPath: string, - registryContent: Option.Option, - worker: string -) => +const planRegistryMutation = (registryPath: string, registryContent: Option.Option, worker: string) => Effect.gen(function* planRegistryMutationEffect() { const workerVariable = `${toCamelCase(worker)}Worker`; let registryMutation: Mutation; @@ -435,9 +359,7 @@ const planRegistryMutation = ( message: 'generated worker registry does not contain its owned slots', }); } - if ( - new RegExp(`\\b${workerVariable}\\b`, 'u').test(registryContent.value) - ) { + if (new RegExp(`\\b${workerVariable}\\b`, 'u').test(registryContent.value)) { return yield* new OutboxWorkerScaffoldError({ cause: workerVariable, message: `Outbox Worker identifier ${workerVariable} already exists`, @@ -449,21 +371,18 @@ const planRegistryMutation = ( OUTBOX_WORKER_IMPORT_SLOT_START, OUTBOX_WORKER_IMPORT_SLOT_END, [`import { ${workerVariable} } from './${worker}.worker.ts';`], - (candidate) => - /^import \{ [A-Za-z0-9]+Worker \} from '\.\/[a-z0-9-]+\.worker\.ts';$/u.test( - candidate - ) + (candidate) => /^import \{ [A-Za-z0-9]+Worker \} from '\.\/[a-z0-9-]+\.worker\.ts';$/u.test(candidate), ); return insertSortedSlot( withImport, OUTBOX_WORKER_REGISTRY_SLOT_START, OUTBOX_WORKER_REGISTRY_SLOT_END, [`${workerVariable},`], - (candidate) => /^[a-z][A-Za-z0-9]+Worker,$/u.test(candidate) + (candidate) => /^[a-z][A-Za-z0-9]+Worker,$/u.test(candidate), ); }); const updatedRegistry = yield* trySync(() => - updateMutation(registryPath, registryContent.value, withRegistration) + updateMutation(registryPath, registryContent.value, withRegistration), ); if (updatedRegistry === undefined) { return yield* new OutboxWorkerScaffoldError({ @@ -480,20 +399,17 @@ const planRegistryMutation = ( OUTBOX_WORKER_IMPORT_SLOT_START, OUTBOX_WORKER_IMPORT_SLOT_END, [`import { ${workerVariable} } from './${worker}.worker.ts';`], - () => true + () => true, ); return insertSortedSlot( withImport, OUTBOX_WORKER_REGISTRY_SLOT_START, OUTBOX_WORKER_REGISTRY_SLOT_END, [`${workerVariable},`], - () => true + () => true, ); }); - registryMutation = yield* createMutationEffect( - registryPath, - withRegistration - ); + registryMutation = yield* createMutationEffect(registryPath, withRegistration); } return registryMutation; @@ -501,16 +417,14 @@ const planRegistryMutation = ( const planOutboxWorkerScaffoldEffect = ( workspaceRoot: string, - config: OutboxWorkerScaffoldConfig + config: OutboxWorkerScaffoldConfig, ): Effect.Effect< ScaffoldPlan, OutboxWorkerScaffoldError | ScaffoldFailure, FileSystem.FileSystem > => Effect.gen(function* planOutboxWorkerScaffoldProgram() { - const worker = yield* trySync(() => - requireCanonicalSlug(config.worker, 'worker') - ); + const worker = yield* trySync(() => requireCanonicalSlug(config.worker, 'worker')); const topic = yield* trySync(() => requireTopic(config.topic)); const [consumer, producer] = yield* Effect.all([ discoverOntosModuleEffect(workspaceRoot, config.vertical), @@ -518,19 +432,9 @@ const planOutboxWorkerScaffoldEffect = ( ]); const topicSlug = yield* trySync(() => topicToSlug(topic)); const contractPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - producer.slug, - 'shared', - 'outbox', - `${topicSlug}.ts` - ) - ); - const contract = yield* readRequiredFile( - contractPath, - 'published producer Outbox contract' + resolveContainedPath(workspaceRoot, 'verticals', producer.slug, 'shared', 'outbox', `${topicSlug}.ts`), ); + const contract = yield* readRequiredFile(contractPath, 'published producer Outbox contract'); const contractExport = `./outbox/${topicSlug}`; const packageExports = producer.packageJson['exports']; if (packageExports === undefined) { @@ -540,7 +444,7 @@ const planOutboxWorkerScaffoldEffect = ( }); } const producerExports = yield* trySync(() => - asJsonObject(packageExports, `vertical ${producer.slug} package exports`) + asJsonObject(packageExports, `vertical ${producer.slug} package exports`), ); if (producerExports[contractExport] !== `./shared/outbox/${topicSlug}.ts`) { return yield* new OutboxWorkerScaffoldError({ @@ -556,86 +460,29 @@ const planOutboxWorkerScaffoldEffect = ( } const workerPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'src', - 'workers', - `${worker}.worker.ts` - ) - ); - const workerMutation = yield* createMutationEffect( - workerPath, - renderWorker(consumer, producer, worker, topic) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'src', 'workers', `${worker}.worker.ts`), ); + const workerMutation = yield* createMutationEffect(workerPath, renderWorker(consumer, producer, worker, topic)); const registryPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'src', - 'workers', - 'index.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'src', 'workers', 'index.ts'), ); const registryContent = yield* readOptionalFile(registryPath); const workerVariable = `${toCamelCase(worker)}Worker`; - const registryMutation = yield* planRegistryMutation( - registryPath, - registryContent, - worker - ); + const registryMutation = yield* planRegistryMutation(registryPath, registryContent, worker); const workerHostLayerPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'src', - 'worker-host', - 'layer.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'src', 'worker-host', 'layer.ts'), ); const workerHostMainPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'src', - 'worker-host', - 'main.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'src', 'worker-host', 'main.ts'), ); const workerHostScriptPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'scripts', - 'outbox-worker.ts' - ) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'scripts', 'outbox-worker.ts'), ); - const [ - workerHostLayerMutation, - workerHostMainMutation, - workerHostScriptMutation, - ] = yield* Effect.all([ - planWorkerHostFile( - workerHostLayerPath, - consumer, - renderWorkerHostLayer(consumer) - ), - planWorkerHostFile( - workerHostMainPath, - consumer, - renderWorkerHostMain(consumer) - ), - planWorkerHostFile( - workerHostScriptPath, - consumer, - renderWorkerHostScript(consumer) - ), + const [workerHostLayerMutation, workerHostMainMutation, workerHostScriptMutation] = yield* Effect.all([ + planWorkerHostFile(workerHostLayerPath, consumer, renderWorkerHostLayer(consumer)), + planWorkerHostFile(workerHostMainPath, consumer, renderWorkerHostMain(consumer)), + planWorkerHostFile(workerHostScriptPath, consumer, renderWorkerHostScript(consumer)), ]); const registrationImport = `import { ${workerVariable} } from './src/workers/${worker}.worker.ts';`; @@ -648,60 +495,36 @@ const planOutboxWorkerScaffoldEffect = ( [registrationImport], (candidate) => /^import \{ [a-z][A-Za-z0-9]*Worker \} from '\.\/src\/workers\/[a-z][a-z0-9-]*\.worker\.ts';$/u.test( - candidate + candidate, ) || /^import \{ [a-z][A-Za-z0-9]*Action \} from '\.\/src\/actions\/[a-z][a-z0-9-]*\.action\.ts';$/u.test( - candidate - ) + candidate, + ), ), MODULE_REGISTRATION_WORKER_SLOT_START, MODULE_REGISTRATION_WORKER_SLOT_END, [`${workerVariable},`], - (candidate) => /^[a-z][A-Za-z0-9]*Worker,$/u.test(candidate) - ) + (candidate) => /^[a-z][A-Za-z0-9]*Worker,$/u.test(candidate), + ), ); const ownerRegistrationMutation = yield* trySync(() => - updateMutation( - consumer.registrationPath, - consumer.registrationContent, - nextRegistration - ) + updateMutation(consumer.registrationPath, consumer.registrationContent, nextRegistration), ); const packageContent = yield* patchConsumerPackage(consumer, producer); const packageMutation = yield* trySync(() => - updateMutation( - consumer.packagePath, - consumer.packageContent, - packageContent - ) + updateMutation(consumer.packagePath, consumer.packageContent, packageContent), ); const tsconfigPath = yield* trySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - consumer.slug, - 'tsconfig.json' - ) - ); - const tsconfigContent = yield* readRequiredFile( - tsconfigPath, - `vertical ${consumer.slug} tsconfig` - ); - const patchedTsconfig = yield* patchConsumerTsconfig( - tsconfigContent, - consumer, - producer - ); - const tsconfigMutation = yield* trySync(() => - updateMutation(tsconfigPath, tsconfigContent, patchedTsconfig) + resolveContainedPath(workspaceRoot, 'verticals', consumer.slug, 'tsconfig.json'), ); + const tsconfigContent = yield* readRequiredFile(tsconfigPath, `vertical ${consumer.slug} tsconfig`); + const patchedTsconfig = yield* patchConsumerTsconfig(tsconfigContent, consumer, producer); + const tsconfigMutation = yield* trySync(() => updateMutation(tsconfigPath, tsconfigContent, patchedTsconfig)); const mutations = [ workerMutation, registryMutation, - ...(ownerRegistrationMutation === undefined - ? [] - : [ownerRegistrationMutation]), + ...(ownerRegistrationMutation === undefined ? [] : [ownerRegistrationMutation]), ...Option.toArray(workerHostLayerMutation), ...Option.toArray(workerHostMainMutation), ...Option.toArray(workerHostScriptMutation), diff --git a/app/scripts/scaffolding/policy/scaffold.mts b/app/scripts/scaffolding/policy/scaffold.mts index ff41a7c59..02b87ef17 100644 --- a/app/scripts/scaffolding/policy/scaffold.mts +++ b/app/scripts/scaffolding/policy/scaffold.mts @@ -4,6 +4,7 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { CORE_POLICY_SLOT_END, CORE_POLICY_SLOT_START, + createMutationEffect, discoverOntosModuleEffect, ensureUniqueMutationPaths, insertSortedSlot, @@ -14,19 +15,11 @@ import { updateMutation, withCoreDependency, } from '../shared.mts'; -import type { - Mutation, - PolicyScaffoldConfig, - PolicyScaffoldResult, - ScaffoldPlan, -} from '../shared.mts'; +import type { Mutation, PolicyScaffoldConfig, PolicyScaffoldResult, ScaffoldPlan } from '../shared.mts'; -class PolicyScaffoldError extends Schema.TaggedError()( - 'PolicyScaffoldError', - { - reason: Schema.String, - } -) { +class PolicyScaffoldError extends Schema.TaggedError()('PolicyScaffoldError', { + reason: Schema.String, +}) { override get message(): string { return this.reason; } @@ -37,44 +30,21 @@ const planningFailure = (cause: unknown): PolicyScaffoldError => reason: Predicate.isError(cause) ? cause.message : String(cause), }); -const fromLegacySync = ( - operation: () => Value -): Effect.Effect => +const fromLegacySync = (operation: () => Value): Effect.Effect => Effect.try({ catch: planningFailure, try: operation }); const createPolicyMutation = ( filePath: string, - content: string + content: string, ): Effect.Effect => - Effect.gen(function* createPolicyMutationEffect() { - const fileSystem = yield* FileSystem.FileSystem; - const exists = yield* fileSystem - .exists(filePath) - .pipe(Effect.mapError(planningFailure)); - if (exists) { - return yield* Effect.fail( - new PolicyScaffoldError({ - reason: `refusing to overwrite existing business file: ${filePath}`, - }) - ); - } - return { content, kind: 'create', path: filePath }; - }); + createMutationEffect(filePath, content).pipe(Effect.mapError(planningFailure)); -const renderPolicy = ( - policy: string, - scope: 'global' | 'microvertical', - owner?: string -): string => { +const renderPolicy = (policy: string, scope: 'global' | 'microvertical', owner?: string): string => { const valueName = `${toCamelCase(policy)}Policy`; - const definition = - scope === 'global' ? 'defineGlobalPolicy' : 'defineMicroverticalPolicy'; - const policyKey = - scope === 'global' ? `global.${policy}.v1` : `${owner}.${policy}.v1`; - const ownerLine = - scope === 'global' ? '' : ` owningModuleKey: '${owner}',\n`; - const policyImport = - scope === 'global' ? '../actions/policy.ts' : '@app/core-runtime'; + const definition = scope === 'global' ? 'defineGlobalPolicy' : 'defineMicroverticalPolicy'; + const policyKey = scope === 'global' ? `global.${policy}.v1` : `${owner}.${policy}.v1`; + const ownerLine = scope === 'global' ? '' : ` owningModuleKey: '${owner}',\n`; + const policyImport = scope === 'global' ? '../actions/policy.ts' : '@app/core-runtime'; return `import { Effect } from 'effect'; import { ${definition}, denyPolicy } from '${policyImport}'; @@ -88,142 +58,90 @@ ${ownerLine} policyKey: '${policyKey}', `; }; -const planPolicyScaffold = Effect.fn('PolicyScaffold.planPolicyScaffold')( - function* planPolicyScaffoldEffect( - workspaceRoot: string, - config: PolicyScaffoldConfig - ): Effect.fn.Return< - ScaffoldPlan, - PolicyScaffoldError, - FileSystem.FileSystem - > { - const fileSystem = yield* FileSystem.FileSystem; - const policy = yield* fromLegacySync(() => - requireCanonicalSlug(config.policy, 'policy') - ); - if (config.scope === 'global') { - if (config.vertical !== undefined) { - return yield* Effect.fail( - new PolicyScaffoldError({ - reason: '--vertical is forbidden when --scope is global', - }) - ); - } - const policyPath = yield* fromLegacySync(() => - resolveContainedPath( - workspaceRoot, - 'packages', - 'core-runtime', - 'src', - 'policies', - `${policy}.policy.ts` - ) - ); - const policyMutation = yield* createPolicyMutation( - policyPath, - renderPolicy(policy, 'global') - ); - const indexPath = yield* fromLegacySync(() => - resolveContainedPath( - workspaceRoot, - 'packages', - 'core-runtime', - 'src', - 'index.ts' - ) - ); - const indexContent = yield* fileSystem.readFileString(indexPath).pipe( - Effect.mapError((cause) => - Match.value(cause.reason).pipe( - Match.tag( - 'NotFound', - () => - new PolicyScaffoldError({ - reason: `Core public index is missing at ${indexPath}`, - }) - ), - Match.orElse(planningFailure) - ) - ) - ); - const exportIdentifier = `${toCamelCase(policy)}Policy`; - if ( - new RegExp(`^export \\{ ${exportIdentifier} \\} from `, 'mu').test( - indexContent - ) - ) { - return yield* Effect.fail( - new PolicyScaffoldError({ - reason: `Policy identifier ${exportIdentifier} already exists`, - }) - ); - } - const exportEntry = `export { ${exportIdentifier} } from './policies/${policy}.policy.ts';`; - const patchedIndex = yield* fromLegacySync(() => - insertSortedSlot( - indexContent, - CORE_POLICY_SLOT_START, - CORE_POLICY_SLOT_END, - [exportEntry], - (candidate) => - /^export \{ [A-Za-z][A-Za-z0-9]*Policy \} from '\.\/policies\/[a-z0-9-]+\.policy\.ts';$/u.test( - candidate - ) - ) - ); - const indexMutation = updateMutation( - indexPath, - indexContent, - patchedIndex - ); - if (indexMutation === undefined) { - return yield* Effect.fail( - new PolicyScaffoldError({ - reason: 'global Policy export patch unexpectedly made no change', - }) - ); - } - const mutations = [policyMutation, indexMutation]; - yield* fromLegacySync(() => ensureUniqueMutationPaths(mutations)); - return { mutations, result: { policyPath } }; - } - - if (config.vertical === undefined) { +const planPolicyScaffold = Effect.fn('PolicyScaffold.planPolicyScaffold')(function* planPolicyScaffoldEffect( + workspaceRoot: string, + config: PolicyScaffoldConfig, +): Effect.fn.Return, PolicyScaffoldError, FileSystem.FileSystem> { + const fileSystem = yield* FileSystem.FileSystem; + const policy = yield* fromLegacySync(() => requireCanonicalSlug(config.policy, 'policy')); + if (config.scope === 'global') { + if (config.vertical !== undefined) { return yield* Effect.fail( new PolicyScaffoldError({ - reason: '--vertical is required when --scope is microvertical', - }) + reason: '--vertical is forbidden when --scope is global', + }), ); } - const requestedVertical = config.vertical; - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - requestedVertical - ).pipe(Effect.mapError(planningFailure)); const policyPath = yield* fromLegacySync(() => - resolveContainedPath( - workspaceRoot, - 'verticals', - vertical.slug, - 'src', - 'policies', - `${policy}.policy.ts` - ) + resolveContainedPath(workspaceRoot, 'packages', 'core-runtime', 'src', 'policies', `${policy}.policy.ts`), ); - const policyMutation = yield* createPolicyMutation( - policyPath, - renderPolicy(policy, 'microvertical', vertical.moduleId) + const policyMutation = yield* createPolicyMutation(policyPath, renderPolicy(policy, 'global')); + const indexPath = yield* fromLegacySync(() => + resolveContainedPath(workspaceRoot, 'packages', 'core-runtime', 'src', 'index.ts'), ); - const dependencyMutation = yield* fromLegacySync(() => - withCoreDependency(vertical) + const indexContent = yield* fileSystem.readFileString(indexPath).pipe( + Effect.mapError((cause) => + Match.value(cause.reason).pipe( + Match.tag( + 'NotFound', + () => + new PolicyScaffoldError({ + reason: `Core public index is missing at ${indexPath}`, + }), + ), + Match.orElse(planningFailure), + ), + ), ); - const mutations = - dependencyMutation === undefined - ? [policyMutation] - : [policyMutation, dependencyMutation]; + const exportIdentifier = `${toCamelCase(policy)}Policy`; + if (new RegExp(`^export \\{ ${exportIdentifier} \\} from `, 'mu').test(indexContent)) { + return yield* Effect.fail( + new PolicyScaffoldError({ + reason: `Policy identifier ${exportIdentifier} already exists`, + }), + ); + } + const exportEntry = `export { ${exportIdentifier} } from './policies/${policy}.policy.ts';`; + const patchedIndex = yield* fromLegacySync(() => + insertSortedSlot(indexContent, CORE_POLICY_SLOT_START, CORE_POLICY_SLOT_END, [exportEntry], (candidate) => + /^export \{ [A-Za-z][A-Za-z0-9]*Policy \} from '\.\/policies\/[a-z0-9-]+\.policy\.ts';$/u.test(candidate), + ), + ); + const indexMutation = updateMutation(indexPath, indexContent, patchedIndex); + if (indexMutation === undefined) { + return yield* Effect.fail( + new PolicyScaffoldError({ + reason: 'global Policy export patch unexpectedly made no change', + }), + ); + } + const mutations = [policyMutation, indexMutation]; yield* fromLegacySync(() => ensureUniqueMutationPaths(mutations)); return { mutations, result: { policyPath } }; } -); + + if (config.vertical === undefined) { + return yield* Effect.fail( + new PolicyScaffoldError({ + reason: '--vertical is required when --scope is microvertical', + }), + ); + } + const requestedVertical = config.vertical; + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, requestedVertical).pipe( + Effect.mapError(planningFailure), + ); + const policyPath = yield* fromLegacySync(() => + resolveContainedPath(workspaceRoot, 'verticals', vertical.slug, 'src', 'policies', `${policy}.policy.ts`), + ); + const policyMutation = yield* createPolicyMutation( + policyPath, + renderPolicy(policy, 'microvertical', vertical.moduleId), + ); + const dependencyMutation = yield* fromLegacySync(() => withCoreDependency(vertical)); + const mutations = dependencyMutation === undefined ? [policyMutation] : [policyMutation, dependencyMutation]; + yield* fromLegacySync(() => ensureUniqueMutationPaths(mutations)); + return { mutations, result: { policyPath } }; +}); export default createCodesmithGenerator(planPolicyScaffold); diff --git a/app/scripts/scaffolding/public-component/scaffold.mts b/app/scripts/scaffolding/public-component/scaffold.mts index 06b236590..2204f2ec2 100644 --- a/app/scripts/scaffolding/public-component/scaffold.mts +++ b/app/scripts/scaffolding/public-component/scaffold.mts @@ -2,7 +2,6 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { planGovernedContributionScaffold } from '../governed-contribution/scaffold.mts'; import type { GovernedContributionScaffoldConfig } from '../shared.mts'; -export default createCodesmithGenerator( - (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'public-component', config) +export default createCodesmithGenerator((workspaceRoot: string, config: GovernedContributionScaffoldConfig) => + planGovernedContributionScaffold(workspaceRoot, 'public-component', config), ); diff --git a/app/scripts/scaffolding/report/scaffold.mts b/app/scripts/scaffolding/report/scaffold.mts index 81d6db2f7..6bb40050b 100644 --- a/app/scripts/scaffolding/report/scaffold.mts +++ b/app/scripts/scaffolding/report/scaffold.mts @@ -2,7 +2,6 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { planGovernedContributionScaffold } from '../governed-contribution/scaffold.mts'; import type { GovernedContributionScaffoldConfig } from '../shared.mts'; -export default createCodesmithGenerator( - (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'report', config) +export default createCodesmithGenerator((workspaceRoot: string, config: GovernedContributionScaffoldConfig) => + planGovernedContributionScaffold(workspaceRoot, 'report', config), ); diff --git a/app/scripts/scaffolding/resource/scaffold.mts b/app/scripts/scaffolding/resource/scaffold.mts index 84384a6b8..1467a75c0 100644 --- a/app/scripts/scaffolding/resource/scaffold.mts +++ b/app/scripts/scaffolding/resource/scaffold.mts @@ -23,15 +23,9 @@ import { tryScaffold, updateMutation, } from '../shared.mts'; -import type { - OntosVerticalMetadata, - ResourceScaffoldConfig, -} from '../shared.mts'; +import type { OntosVerticalMetadata, ResourceScaffoldConfig } from '../shared.mts'; -const renderResource = ( - vertical: OntosVerticalMetadata, - resource: string -): string => { +const renderResource = (vertical: OntosVerticalMetadata, resource: string): string => { const type = toPascalCase(resource); const descriptor = `${toCamelCase(resource)}ResourceDescriptor`; const resourceType = `${vertical.moduleId}.${resource}`; @@ -68,113 +62,70 @@ export const ${descriptor} = { `; }; -const isResourceDescriptor = (candidate: string): boolean => - /^[a-z][A-Za-z0-9]*ResourceDescriptor,$/u.test(candidate); +const isResourceDescriptor = (candidate: string): boolean => /^[a-z][A-Za-z0-9]*ResourceDescriptor,$/u.test(candidate); -const planResourceScaffold = Effect.fn('ResourceScaffold.plan')( - function* planResourceScaffold( - workspaceRoot: string, - config: ResourceScaffoldConfig - ) { - const resource = yield* tryScaffold('resource name is invalid', () => - requireCanonicalSlug(config.resource, 'resource') - ); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); - const resourcePath = yield* tryScaffold( - 'failed to resolve resource path', - () => - resolveContainedPath( - vertical.directory, - 'shared', - 'resources', - `${resource}.ts` - ) - ); - const resourceMutation = yield* createMutationEffect( - resourcePath, - renderResource(vertical, resource) - ); +const planResourceScaffold = Effect.fn('ResourceScaffold.plan')(function* planResourceScaffold( + workspaceRoot: string, + config: ResourceScaffoldConfig, +) { + const resource = yield* tryScaffold('resource name is invalid', () => + requireCanonicalSlug(config.resource, 'resource'), + ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + const resourcePath = yield* tryScaffold('failed to resolve resource path', () => + resolveContainedPath(vertical.directory, 'shared', 'resources', `${resource}.ts`), + ); + const resourceMutation = yield* createMutationEffect(resourcePath, renderResource(vertical, resource)); - const descriptor = `${toCamelCase(resource)}ResourceDescriptor`; - const ownerImport = `import { ${descriptor} } from './shared/resources/${resource}.ts';`; - const nextManifest = yield* tryScaffold( - 'failed to patch resource manifest', - () => - insertSortedSlot( - insertSortedSlot( - vertical.manifestContent, - MODULE_MANIFEST_IMPORT_SLOT_START, - MODULE_MANIFEST_IMPORT_SLOT_END, - [ownerImport], - isModuleManifestImport - ), - MODULE_MANIFEST_RESOURCE_SLOT_START, - MODULE_MANIFEST_RESOURCE_SLOT_END, - [`${descriptor},`], - isResourceDescriptor - ) - ); - const manifestMutation = updateMutation( - vertical.manifestPath, - vertical.manifestContent, - nextManifest - ); - if (manifestMutation === undefined) { - return yield* scaffoldFailure( - 'Resource manifest patch unexpectedly made no change' - ); - } + const descriptor = `${toCamelCase(resource)}ResourceDescriptor`; + const ownerImport = `import { ${descriptor} } from './shared/resources/${resource}.ts';`; + const nextManifest = yield* tryScaffold('failed to patch resource manifest', () => + insertSortedSlot( + insertSortedSlot( + vertical.manifestContent, + MODULE_MANIFEST_IMPORT_SLOT_START, + MODULE_MANIFEST_IMPORT_SLOT_END, + [ownerImport], + isModuleManifestImport, + ), + MODULE_MANIFEST_RESOURCE_SLOT_START, + MODULE_MANIFEST_RESOURCE_SLOT_END, + [`${descriptor},`], + isResourceDescriptor, + ), + ); + const manifestMutation = updateMutation(vertical.manifestPath, vertical.manifestContent, nextManifest); + if (manifestMutation === undefined) { + return yield* scaffoldFailure('Resource manifest patch unexpectedly made no change'); + } - const exportsValue = yield* tryScaffold( - 'failed to read resource package exports', - () => - asJsonObject( - vertical.packageJson['exports'], - `vertical ${vertical.slug} package exports` - ) + const exportsValue = yield* tryScaffold('failed to read resource package exports', () => + asJsonObject(vertical.packageJson['exports'], `vertical ${vertical.slug} package exports`), + ); + const contractExport = `./resources/${resource}`; + if (exportsValue[contractExport] !== undefined) { + return yield* scaffoldFailure(`resource contract export ${contractExport} already exists`); + } + const packageMutation = yield* tryScaffold('failed to patch resource package export', () => { + const patchedExports = Object.fromEntries( + Object.entries({ + ...exportsValue, + [contractExport]: `./shared/resources/${resource}.ts`, + }).toSorted(([left], [right]) => left.localeCompare(right)), ); - const contractExport = `./resources/${resource}`; - if (exportsValue[contractExport] !== undefined) { - return yield* scaffoldFailure( - `resource contract export ${contractExport} already exists` - ); - } - const packageMutation = yield* tryScaffold( - 'failed to patch resource package export', - () => { - const patchedExports = Object.fromEntries( - Object.entries({ - ...exportsValue, - [contractExport]: `./shared/resources/${resource}.ts`, - }).toSorted(([left], [right]) => left.localeCompare(right)) - ); - return updateMutation( - vertical.packagePath, - vertical.packageContent, - patchJsonObjectProperty( - vertical.packageContent, - [], - 'exports', - patchedExports - ) - ); - } + return updateMutation( + vertical.packagePath, + vertical.packageContent, + patchJsonObjectProperty(vertical.packageContent, [], 'exports', patchedExports), ); - if (packageMutation === undefined) { - return yield* scaffoldFailure( - 'Resource package export patch unexpectedly made no change' - ); - } - - const mutations = [resourceMutation, manifestMutation, packageMutation]; - yield* tryScaffold('resource mutation paths are invalid', () => - ensureUniqueMutationPaths(mutations) - ); - return { mutations, result: { resourcePath } }; + }); + if (packageMutation === undefined) { + return yield* scaffoldFailure('Resource package export patch unexpectedly made no change'); } -); + + const mutations = [resourceMutation, manifestMutation, packageMutation]; + yield* tryScaffold('resource mutation paths are invalid', () => ensureUniqueMutationPaths(mutations)); + return { mutations, result: { resourcePath } }; +}); export default createCodesmithGenerator(planResourceScaffold); diff --git a/app/scripts/scaffolding/retire-contribution/scaffold.mts b/app/scripts/scaffolding/retire-contribution/scaffold.mts index 6bf86ad62..34382574d 100644 --- a/app/scripts/scaffolding/retire-contribution/scaffold.mts +++ b/app/scripts/scaffolding/retire-contribution/scaffold.mts @@ -51,379 +51,301 @@ class RetireContributionScaffoldError extends Schema.TaggedError - new RetireContributionScaffoldError( - cause === undefined ? { message } : { cause, message } - ); +const scaffoldError = (message: string, cause?: unknown): RetireContributionScaffoldError => + new RetireContributionScaffoldError(cause === undefined ? { message } : { cause, message }); const trySync = (operation: () => Value, fallback: string) => Effect.try({ - catch: (cause) => - scaffoldError(Predicate.isError(cause) ? cause.message : fallback, cause), + catch: (cause) => scaffoldError(Predicate.isError(cause) ? cause.message : fallback, cause), try: operation, }); -const readGeneratedArtifact = Effect.fn('readGeneratedArtifact')( - function* readGeneratedArtifactEffect( - filePath: string, - label: string, - checks: readonly string[] - ) { - const fileSystem = yield* FileSystem.FileSystem; - const content = yield* fileSystem - .readFileString(filePath) - .pipe( - Effect.mapError((cause) => - scaffoldError( - `matching generated ${label} is missing at ${filePath}`, - cause - ) - ) - ); - if (checks.some((check) => !content.includes(check))) { - return yield* scaffoldError( - `matching generated ${label} metadata is missing at ${filePath}` - ); - } - return content; - } -); - -const removeOptionalGeneratedSlotEntry = Effect.fn( - 'removeOptionalGeneratedSlotEntry' -)(function* removeOptionalGeneratedSlotEntryEffect( - content: string, - start: string, - end: string, - matches: (candidate: string) => boolean, - label: string +const readGeneratedArtifact = Effect.fn('readGeneratedArtifact')(function* readGeneratedArtifactEffect( + filePath: string, + label: string, + checks: readonly string[], ) { - const count = yield* trySync( - () => readGeneratedSlotEntries(content, start, end).filter(matches).length, - `failed to inspect generated ${label}` - ); - if (count === 0) { - return content; + const fileSystem = yield* FileSystem.FileSystem; + const content = yield* fileSystem + .readFileString(filePath) + .pipe(Effect.mapError((cause) => scaffoldError(`matching generated ${label} is missing at ${filePath}`, cause))); + if (checks.some((check) => !content.includes(check))) { + return yield* scaffoldError(`matching generated ${label} metadata is missing at ${filePath}`); } - if (count > 1) { - return yield* scaffoldError( - `expected at most one generated ${label}; found ${count}` - ); - } - return yield* trySync( - () => removeGeneratedSlotEntry(content, start, end, matches, label), - `failed to remove generated ${label}` - ); + return content; }); -const planActionRetirement = Effect.fn('planActionRetirement')( - function* planActionRetirementEffect( - vertical: OntosVerticalMetadata, - name: string +const removeOptionalGeneratedSlotEntry = Effect.fn('removeOptionalGeneratedSlotEntry')( + function* removeOptionalGeneratedSlotEntryEffect( + content: string, + start: string, + end: string, + matches: (candidate: string) => boolean, + label: string, ) { - const symbol = `${toCamelCase(name)}Action`; - const artifactPath = yield* trySync( - () => - resolveContainedPath( - vertical.directory, - 'src', - 'actions', - `${name}.action.ts` - ), - `failed to resolve generated Action ${name}` - ); - const artifact = yield* readGeneratedArtifact(artifactPath, 'Action', [ - `${ACTION_GENERATOR_HEADER}\n`, - `// @ontos-action-owner ${vertical.moduleId}\n`, - `// @ontos-action-slug ${name}\n`, - `export const ${symbol}`, - ]); - const dependentCount = yield* trySync( - () => - readGeneratedSlotEntries(artifact, OUTBOX_SLOT_START, OUTBOX_SLOT_END) - .length, - `failed to inspect Action ${name} Outbox dependents` + const count = yield* trySync( + () => readGeneratedSlotEntries(content, start, end).filter(matches).length, + `failed to inspect generated ${label}`, ); - if (dependentCount > 0) { - return yield* scaffoldError( - `cannot retire Action ${name} while it has published Outbox dependents` - ); + if (count === 0) { + return content; } - const importLine = `import { ${symbol} } from './src/actions/${name}.action.ts';`; - const { nextManifest, nextRegistration } = yield* trySync( - () => ({ - nextManifest: removeGeneratedSlotEntry( - removeGeneratedSlotEntry( - vertical.manifestContent, - MODULE_MANIFEST_IMPORT_SLOT_START, - MODULE_MANIFEST_IMPORT_SLOT_END, - (entry) => entry === importLine, - `Action import ${name}` - ), - MODULE_MANIFEST_ACTION_SLOT_START, - MODULE_MANIFEST_ACTION_SLOT_END, - (entry) => entry === `${symbol},`, - `Action descriptor ${name}` - ), - nextRegistration: removeGeneratedSlotEntry( - removeGeneratedSlotEntry( - vertical.registrationContent, - MODULE_REGISTRATION_IMPORT_SLOT_START, - MODULE_REGISTRATION_IMPORT_SLOT_END, - (entry) => entry === importLine, - `Action registration import ${name}` - ), - MODULE_REGISTRATION_ACTION_SLOT_START, - MODULE_REGISTRATION_ACTION_SLOT_END, - (entry) => entry === `${symbol},`, - `Action registration ${name}` - ), - }), - `failed to retire generated Action ${name}` + if (count > 1) { + return yield* scaffoldError(`expected at most one generated ${label}; found ${count}`); + } + return yield* trySync( + () => removeGeneratedSlotEntry(content, start, end, matches, label), + `failed to remove generated ${label}`, ); - return [ - { - content: nextManifest, - kind: 'update' as const, - path: vertical.manifestPath, - }, - { - content: nextRegistration, - kind: 'update' as const, - path: vertical.registrationPath, - }, - yield* deleteMutationEffect(artifactPath), - ]; - } + }, ); -const planApiRetirement = Effect.fn('planApiRetirement')( - function* planApiRetirementEffect( - vertical: OntosVerticalMetadata, - name: string - ) { - const type = toPascalCase(name); - const value = `${type}Api`; - const paths = yield* trySync( - () => - [ - resolveContainedPath( - vertical.directory, - 'shared', - 'apis', - `${name}.ts` - ), - resolveContainedPath( - vertical.directory, - 'src', - 'api', - `${name}.read.ts` - ), - resolveContainedPath( - vertical.directory, - 'src', - 'api', - `${name}-client.ts` - ), - resolveContainedPath( - vertical.directory, - 'api', - `${name}-read-server.ts` - ), - ] as const, - `failed to resolve generated module API ${name}` - ); - const checks = [ - [`${API_GENERATOR_HEADER}\n`, `export const ${value}`], - [`${API_GENERATOR_HEADER}\n`, `export const ${toCamelCase(name)}Read`], - [`${API_GENERATOR_HEADER}\n`, `execute${type}WithAuthorization`], - [ - `${API_GENERATOR_HEADER}\n`, - `export const ${toCamelCase(name)}ReadApiLive`, - ], - ] as const; - yield* Effect.all( - paths.map((filePath, index) => - readGeneratedArtifact( - filePath, - `module API ${name}`, - checks[index] ?? [] - ) - ), - { concurrency: 'unbounded' } - ); - const { nextManifest, nextRegistration } = yield* trySync( - () => ({ - nextManifest: removeGeneratedSlotEntry( - removeGeneratedSlotEntry( - vertical.manifestContent, - MODULE_MANIFEST_IMPORT_SLOT_START, - MODULE_MANIFEST_IMPORT_SLOT_END, - (entry) => - entry === `import { ${value} } from './shared/apis/${name}.ts';`, - `module API import ${name}` - ), - MODULE_MANIFEST_API_SLOT_START, - MODULE_MANIFEST_API_SLOT_END, - (entry) => entry === `'${name}': ${value},`, - `module API descriptor ${name}` - ), - nextRegistration: removeGeneratedSlotEntry( - vertical.registrationContent, - MODULE_REGISTRATION_API_SLOT_START, - MODULE_REGISTRATION_API_SLOT_END, - (entry) => - entry === `'${name}': () => import('./src/api/${name}-client.ts'),`, - `module API registration ${name}` - ), - }), - `failed to retire generated module API ${name}` - ); - const deletes = yield* Effect.all(paths.map(deleteMutationEffect), { - concurrency: 'unbounded', - }); - return [ - { - content: nextManifest, - kind: 'update' as const, - path: vertical.manifestPath, - }, - { - content: nextRegistration, - kind: 'update' as const, - path: vertical.registrationPath, - }, - ...deletes, - ]; +const planActionRetirement = Effect.fn('planActionRetirement')(function* planActionRetirementEffect( + vertical: OntosVerticalMetadata, + name: string, +) { + const symbol = `${toCamelCase(name)}Action`; + const artifactPath = yield* trySync( + () => resolveContainedPath(vertical.directory, 'src', 'actions', `${name}.action.ts`), + `failed to resolve generated Action ${name}`, + ); + const artifact = yield* readGeneratedArtifact(artifactPath, 'Action', [ + `${ACTION_GENERATOR_HEADER}\n`, + `// @ontos-action-owner ${vertical.moduleId}\n`, + `// @ontos-action-slug ${name}\n`, + `export const ${symbol}`, + ]); + const dependentCount = yield* trySync( + () => readGeneratedSlotEntries(artifact, OUTBOX_SLOT_START, OUTBOX_SLOT_END).length, + `failed to inspect Action ${name} Outbox dependents`, + ); + if (dependentCount > 0) { + return yield* scaffoldError(`cannot retire Action ${name} while it has published Outbox dependents`); } -); - -const planPageRetirement = Effect.fn('planPageRetirement')( - function* planPageRetirementEffect( - vertical: OntosVerticalMetadata, - name: string - ) { - const type = `${toPascalCase(name)}Page`; - const componentKey = `${vertical.moduleId}.page-${name}`; - const contributionKey = `${vertical.moduleId}.page.${name}`; - const importPattern = new RegExp( - `^import \\{ ${type} \\} from '\\.\\/src\\/routes\\/.+\\/page\\.tsx';$`, - 'u' - ); - let nextManifest = yield* trySync( - () => + const importLine = `import { ${symbol} } from './src/actions/${name}.action.ts';`; + const { nextManifest, nextRegistration } = yield* trySync( + () => ({ + nextManifest: removeGeneratedSlotEntry( removeGeneratedSlotEntry( vertical.manifestContent, MODULE_MANIFEST_IMPORT_SLOT_START, MODULE_MANIFEST_IMPORT_SLOT_END, - (entry) => importPattern.test(entry), - `page import ${name}` - ), - `failed to remove generated page import ${name}` - ); - nextManifest = yield* trySync( - () => - removeGeneratedSlotEntry( - nextManifest, - MODULE_MANIFEST_COMPONENT_SLOT_START, - MODULE_MANIFEST_COMPONENT_SLOT_END, - (entry) => entry === `'page-${name}': ${type},`, - `page component ${name}` + (entry) => entry === importLine, + `Action import ${name}`, ), - `failed to remove generated page component ${name}` - ); - nextManifest = yield* trySync( - () => + MODULE_MANIFEST_ACTION_SLOT_START, + MODULE_MANIFEST_ACTION_SLOT_END, + (entry) => entry === `${symbol},`, + `Action descriptor ${name}`, + ), + nextRegistration: removeGeneratedSlotEntry( removeGeneratedSlotEntry( - nextManifest, - MODULE_MANIFEST_SHELL_PAGE_SLOT_START, - MODULE_MANIFEST_SHELL_PAGE_SLOT_END, - (entry) => - entry.includes(`componentKey: '${componentKey}'`) && - entry.includes(`contributionKey: '${contributionKey}'`), - `shell page ${name}` + vertical.registrationContent, + MODULE_REGISTRATION_IMPORT_SLOT_START, + MODULE_REGISTRATION_IMPORT_SLOT_END, + (entry) => entry === importLine, + `Action registration import ${name}`, ), - `failed to remove generated shell page ${name}` - ); - nextManifest = yield* removeOptionalGeneratedSlotEntry( - nextManifest, - MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START, - MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END, - (entry) => - entry.includes( - `contributionKey: '${vertical.moduleId}.navigation.${name}'` - ) && entry.includes(`pageKey: '${contributionKey}'`), - `shell navigation ${name}` - ); - const nextRegistration = yield* trySync( - () => + MODULE_REGISTRATION_ACTION_SLOT_START, + MODULE_REGISTRATION_ACTION_SLOT_END, + (entry) => entry === `${symbol},`, + `Action registration ${name}`, + ), + }), + `failed to retire generated Action ${name}`, + ); + return [ + { + content: nextManifest, + kind: 'update' as const, + path: vertical.manifestPath, + }, + { + content: nextRegistration, + kind: 'update' as const, + path: vertical.registrationPath, + }, + yield* deleteMutationEffect(artifactPath), + ]; +}); + +const planApiRetirement = Effect.fn('planApiRetirement')(function* planApiRetirementEffect( + vertical: OntosVerticalMetadata, + name: string, +) { + const type = toPascalCase(name); + const value = `${type}Api`; + const paths = yield* trySync( + () => + [ + resolveContainedPath(vertical.directory, 'shared', 'apis', `${name}.ts`), + resolveContainedPath(vertical.directory, 'src', 'api', `${name}.read.ts`), + resolveContainedPath(vertical.directory, 'src', 'api', `${name}-client.ts`), + resolveContainedPath(vertical.directory, 'api', `${name}-read-server.ts`), + ] as const, + `failed to resolve generated module API ${name}`, + ); + const checks = [ + [`${API_GENERATOR_HEADER}\n`, `export const ${value}`], + [`${API_GENERATOR_HEADER}\n`, `export const ${toCamelCase(name)}Read`], + [`${API_GENERATOR_HEADER}\n`, `execute${type}WithAuthorization`], + [`${API_GENERATOR_HEADER}\n`, `export const ${toCamelCase(name)}ReadApiLive`], + ] as const; + yield* Effect.all( + paths.map((filePath, index) => readGeneratedArtifact(filePath, `module API ${name}`, checks[index] ?? [])), + { concurrency: 'unbounded' }, + ); + const { nextManifest, nextRegistration } = yield* trySync( + () => ({ + nextManifest: removeGeneratedSlotEntry( removeGeneratedSlotEntry( - vertical.registrationContent, - MODULE_REGISTRATION_PAGE_SLOT_START, - MODULE_REGISTRATION_PAGE_SLOT_END, - (entry) => - entry.startsWith(`'page-${name}':`) && - entry.includes("import('./src/routes/") && - entry.includes("/page.tsx')"), - `page registration ${name}` + vertical.manifestContent, + MODULE_MANIFEST_IMPORT_SLOT_START, + MODULE_MANIFEST_IMPORT_SLOT_END, + (entry) => entry === `import { ${value} } from './shared/apis/${name}.ts';`, + `module API import ${name}`, ), - `failed to remove generated page registration ${name}` - ); - return [ - { - content: nextManifest, - kind: 'update' as const, - path: vertical.manifestPath, - }, - { - content: nextRegistration, - kind: 'update' as const, - path: vertical.registrationPath, - }, - ]; - } -); + MODULE_MANIFEST_API_SLOT_START, + MODULE_MANIFEST_API_SLOT_END, + (entry) => entry === `'${name}': ${value},`, + `module API descriptor ${name}`, + ), + nextRegistration: removeGeneratedSlotEntry( + vertical.registrationContent, + MODULE_REGISTRATION_API_SLOT_START, + MODULE_REGISTRATION_API_SLOT_END, + (entry) => entry === `'${name}': () => import('./src/api/${name}-client.ts'),`, + `module API registration ${name}`, + ), + }), + `failed to retire generated module API ${name}`, + ); + const deletes = yield* Effect.all(paths.map(deleteMutationEffect), { + concurrency: 'unbounded', + }); + return [ + { + content: nextManifest, + kind: 'update' as const, + path: vertical.manifestPath, + }, + { + content: nextRegistration, + kind: 'update' as const, + path: vertical.registrationPath, + }, + ...deletes, + ]; +}); -const planRetireContributionScaffold = Effect.fn( - 'RetireContributionScaffold.plan' -)(function* planRetireContributionScaffoldEffect( - workspaceRoot: string, - config: RetireContributionScaffoldConfig -): Effect.fn.Return< - ScaffoldPlan, - RetireContributionScaffoldError | ScaffoldFailure, - FileSystem.FileSystem -> { - const name = yield* trySync( - () => requireCanonicalSlug(config.name, 'name'), - 'failed to validate retirement contribution name' +const planPageRetirement = Effect.fn('planPageRetirement')(function* planPageRetirementEffect( + vertical: OntosVerticalMetadata, + name: string, +) { + const type = `${toPascalCase(name)}Page`; + const componentKey = `${vertical.moduleId}.page-${name}`; + const contributionKey = `${vertical.moduleId}.page.${name}`; + const importPattern = new RegExp(`^import \\{ ${type} \\} from '\\.\\/src\\/routes\\/.+\\/page\\.tsx';$`, 'u'); + let nextManifest = yield* trySync( + () => + removeGeneratedSlotEntry( + vertical.manifestContent, + MODULE_MANIFEST_IMPORT_SLOT_START, + MODULE_MANIFEST_IMPORT_SLOT_END, + (entry) => importPattern.test(entry), + `page import ${name}`, + ), + `failed to remove generated page import ${name}`, + ); + nextManifest = yield* trySync( + () => + removeGeneratedSlotEntry( + nextManifest, + MODULE_MANIFEST_COMPONENT_SLOT_START, + MODULE_MANIFEST_COMPONENT_SLOT_END, + (entry) => entry === `'page-${name}': ${type},`, + `page component ${name}`, + ), + `failed to remove generated page component ${name}`, ); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical + nextManifest = yield* trySync( + () => + removeGeneratedSlotEntry( + nextManifest, + MODULE_MANIFEST_SHELL_PAGE_SLOT_START, + MODULE_MANIFEST_SHELL_PAGE_SLOT_END, + (entry) => + entry.includes(`componentKey: '${componentKey}'`) && entry.includes(`contributionKey: '${contributionKey}'`), + `shell page ${name}`, + ), + `failed to remove generated shell page ${name}`, ); - let mutations: readonly Mutation[]; - if (config.kind === 'action') { - mutations = yield* planActionRetirement(vertical, name); - } else if (config.kind === 'api') { - mutations = yield* planApiRetirement(vertical, name); - } else { - mutations = yield* planPageRetirement(vertical, name); - } - yield* trySync( - () => ensureUniqueMutationPaths(mutations), - 'failed to validate retirement mutation paths' + nextManifest = yield* removeOptionalGeneratedSlotEntry( + nextManifest, + MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START, + MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END, + (entry) => + entry.includes(`contributionKey: '${vertical.moduleId}.navigation.${name}'`) && + entry.includes(`pageKey: '${contributionKey}'`), + `shell navigation ${name}`, + ); + const nextRegistration = yield* trySync( + () => + removeGeneratedSlotEntry( + vertical.registrationContent, + MODULE_REGISTRATION_PAGE_SLOT_START, + MODULE_REGISTRATION_PAGE_SLOT_END, + (entry) => + entry.startsWith(`'page-${name}':`) && + entry.includes("import('./src/routes/") && + entry.includes("/page.tsx')"), + `page registration ${name}`, + ), + `failed to remove generated page registration ${name}`, ); - const deletedPaths = mutations - .filter((mutation) => mutation.kind === 'delete') - .map(({ path }) => path); - return { mutations, result: { deletedPaths, kind: config.kind, name } }; + return [ + { + content: nextManifest, + kind: 'update' as const, + path: vertical.manifestPath, + }, + { + content: nextRegistration, + kind: 'update' as const, + path: vertical.registrationPath, + }, + ]; }); +const planRetireContributionScaffold = Effect.fn('RetireContributionScaffold.plan')( + function* planRetireContributionScaffoldEffect( + workspaceRoot: string, + config: RetireContributionScaffoldConfig, + ): Effect.fn.Return< + ScaffoldPlan, + RetireContributionScaffoldError | ScaffoldFailure, + FileSystem.FileSystem + > { + const name = yield* trySync( + () => requireCanonicalSlug(config.name, 'name'), + 'failed to validate retirement contribution name', + ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); + let mutations: readonly Mutation[]; + if (config.kind === 'action') { + mutations = yield* planActionRetirement(vertical, name); + } else if (config.kind === 'api') { + mutations = yield* planApiRetirement(vertical, name); + } else { + mutations = yield* planPageRetirement(vertical, name); + } + yield* trySync(() => ensureUniqueMutationPaths(mutations), 'failed to validate retirement mutation paths'); + const deletedPaths = mutations.filter((mutation) => mutation.kind === 'delete').map(({ path }) => path); + return { mutations, result: { deletedPaths, kind: config.kind, name } }; + }, +); + export default createCodesmithGenerator(planRetireContributionScaffold); diff --git a/app/scripts/scaffolding/search-provider-access/scaffold.mts b/app/scripts/scaffolding/search-provider-access/scaffold.mts index e051a389f..84f11e2a6 100644 --- a/app/scripts/scaffolding/search-provider-access/scaffold.mts +++ b/app/scripts/scaffolding/search-provider-access/scaffold.mts @@ -28,34 +28,30 @@ class SearchProviderAccessScaffoldError extends Schema.TaggedError => Effect.gen(function* replaceOwnedLineEffect() { const matches = [...content.matchAll(pattern)]; if (matches.length !== 1) { - return yield* scaffoldError( - `expected exactly one generated ${description}; found ${matches.length}` - ); + return yield* scaffoldError(`expected exactly one generated ${description}; found ${matches.length}`); } return content.replace(pattern, replacement); }); -const requestedPermissionTarget = ( - config: SearchProviderAccessScaffoldConfig -) => +const requestedPermissionTarget = (config: SearchProviderAccessScaffoldConfig) => config.accessFiltering === 'tenant_scope' ? `() => ({ kind: 'tenant', permission: '${config.tenantPermission}' }),` : `() => ({ kind: 'legal_entity', permission: 'read_counterparty' }),`; @@ -63,31 +59,29 @@ const requestedPermissionTarget = ( const patchProvider = ( content: string, moduleId: string, - config: SearchProviderAccessScaffoldConfig + config: SearchProviderAccessScaffoldConfig, ): Effect.Effect => Effect.gen(function* patchProviderEffect() { if (!content.startsWith(generatedHeader)) { - return yield* scaffoldError( - 'search provider access updates require a Codesmith-owned provider' - ); + return yield* scaffoldError('search provider access updates require a Codesmith-owned provider'); } let next = yield* replaceOwnedLine( content, /^ {4}legalEntityScope: '(?:required|optional)',$/gmu, ` legalEntityScope: '${config.legalEntityScope}',`, - 'legalEntityScope line' + 'legalEntityScope line', ); next = yield* replaceOwnedLine( next, /^ {4}permissionTarget: '(?:module|tenant|legal_entity)',$/gmu, ` permissionTarget: '${config.accessFiltering === 'tenant_scope' ? 'tenant' : 'legal_entity'}',`, - 'permissionTarget line' + 'permissionTarget line', ); return yield* replaceOwnedLine( next, /^ {2}\(\) => \(\{ kind: '(?:module|tenant|legal_entity)'(?:, moduleId: '[^']+'|, permission: '[^']+') \}\),$/gmu, ` ${requestedPermissionTarget(config)}`, - `${moduleId} permission target resolver` + `${moduleId} permission target resolver`, ); }); @@ -98,47 +92,31 @@ const requestField = (filter: 'includeArchived' | 'role'): string => const patchContract = ( content: string, - config: SearchProviderAccessScaffoldConfig + config: SearchProviderAccessScaffoldConfig, ): Effect.Effect => Effect.gen(function* patchContractEffect() { if (!content.startsWith(generatedHeader)) { - return yield* scaffoldError( - 'search provider access updates require a Codesmith-owned server contract' - ); + return yield* scaffoldError('search provider access updates require a Codesmith-owned server contract'); } const type = toPascalCase(config.name); const start = `export const ${type}ProviderRequestSchema = Schema.Struct({\n`; const startIndex = content.indexOf(start); if (startIndex === -1 || content.includes(start, startIndex + 1)) { - return yield* scaffoldError( - 'expected exactly one generated provider request schema' - ); + return yield* scaffoldError('expected exactly one generated provider request schema'); } const fieldsStart = startIndex + start.length; const fieldsEnd = content.indexOf('});', fieldsStart); if (fieldsEnd === -1) { - return yield* scaffoldError( - 'generated provider request schema is incomplete' - ); + return yield* scaffoldError('generated provider request schema is incomplete'); } const fields = content.slice(fieldsStart, fieldsEnd); if (!/^ {2}query: .+,$/mu.test(fields)) { - return yield* scaffoldError( - 'generated provider request schema must retain its query field' - ); + return yield* scaffoldError('generated provider request schema must retain its query field'); } - const knownFields = [ - ...fields.matchAll(/^ {2}(?[A-Za-z][A-Za-z0-9]*):/gmu), - ].map(([, field]) => field); + const knownFields = [...fields.matchAll(/^ {2}(?[A-Za-z][A-Za-z0-9]*):/gmu)].map(([, field]) => field); const expectedFields = new Set(['query', ...config.requestFilters]); - if ( - knownFields.some( - (field) => field === undefined || !expectedFields.has(field) - ) - ) { - return yield* scaffoldError( - 'provider request schema contains an unowned request filter' - ); + if (knownFields.some((field) => field === undefined || !expectedFields.has(field))) { + return yield* scaffoldError('provider request schema contains an unowned request filter'); } let nextFields = fields; for (const filter of config.requestFilters) { @@ -152,7 +130,7 @@ const patchContract = ( const patchManifest = ( content: string, moduleId: string, - config: SearchProviderAccessScaffoldConfig + config: SearchProviderAccessScaffoldConfig, ): Effect.Effect => Effect.gen(function* patchManifestEffect() { const key = `${moduleId}.${config.name}`; @@ -164,81 +142,54 @@ const patchManifest = ( content.includes(MODULE_MANIFEST_SEARCH_SLOT_START, start + 1) || content.includes(MODULE_MANIFEST_SEARCH_SLOT_END, end + 1) ) { - return yield* scaffoldError( - 'generated search descriptor slot is missing or duplicated' - ); + return yield* scaffoldError('generated search descriptor slot is missing or duplicated'); } - const slot = content.slice( - start + MODULE_MANIFEST_SEARCH_SLOT_START.length, - end - ); + const slot = content.slice(start + MODULE_MANIFEST_SEARCH_SLOT_START.length, end); const escapedKey = key.replaceAll('.', String.raw`\.`); const escapedModuleId = moduleId.replaceAll('.', String.raw`\.`); const pattern = new RegExp( `\\{\\s*accessFiltering: '(?:resource_permission|tenant_scope)',\\s*key: '${escapedKey}',\\s*owningModuleId: '${escapedModuleId}',(?:\\s*requestFilters: \\[[^\\]]*\\],)?\\s*resourceType: '([^']+)'(?:,\\s*tenantPermission: '[^']+')?,?\\s*\\},`, - 'gmu' + 'gmu', ); const matches = [...slot.matchAll(pattern)]; const [match] = matches; const resourceType = match?.[1]; - if ( - matches.length !== 1 || - resourceType === undefined || - !resourceType.startsWith(`${moduleId}.`) - ) { - return yield* scaffoldError( - `expected exactly one generated search descriptor for ${key}` - ); + if (matches.length !== 1 || resourceType === undefined || !resourceType.startsWith(`${moduleId}.`)) { + return yield* scaffoldError(`expected exactly one generated search descriptor for ${key}`); } - const requestFilterValues = config.requestFilters - .map((filter) => `'${filter}'`) - .join(', '); + const requestFilterValues = config.requestFilters.map((filter) => `'${filter}'`).join(', '); const requestFilters = `[${requestFilterValues}]`; const tenantPermission = - config.tenantPermission === undefined - ? '' - : `, tenantPermission: '${config.tenantPermission}'`; + config.tenantPermission === undefined ? '' : `, tenantPermission: '${config.tenantPermission}'`; const replacement = `{ accessFiltering: '${config.accessFiltering}', key: '${key}', owningModuleId: '${moduleId}', requestFilters: ${requestFilters}, resourceType: '${resourceType}'${tenantPermission} },`; return `${content.slice(0, start + MODULE_MANIFEST_SEARCH_SLOT_START.length)}${slot.replace(pattern, replacement)}${content.slice(end)}`; }); -const hasConsistentAccessScope = ( - config: SearchProviderAccessScaffoldConfig -): boolean => - (config.accessFiltering === 'tenant_scope') === - (config.tenantPermission !== undefined) && - (config.accessFiltering !== 'tenant_scope' || - config.legalEntityScope === 'optional') && - (config.accessFiltering !== 'resource_permission' || - config.legalEntityScope === 'required'); +const hasConsistentAccessScope = (config: SearchProviderAccessScaffoldConfig): boolean => + (config.accessFiltering === 'tenant_scope') === (config.tenantPermission !== undefined) && + (config.accessFiltering !== 'tenant_scope' || config.legalEntityScope === 'optional') && + (config.accessFiltering !== 'resource_permission' || config.legalEntityScope === 'required'); -const hasValidAccessFlags = ( - config: SearchProviderAccessScaffoldConfig -): boolean => +const hasValidAccessFlags = (config: SearchProviderAccessScaffoldConfig): boolean => ['tenant_scope', 'resource_permission'].includes(config.accessFiltering) && ['optional', 'required'].includes(config.legalEntityScope) && - (config.tenantPermission === undefined || - config.tenantPermission === 'read_party_identity') && - config.requestFilters.every((filter) => - ['includeArchived', 'role'].includes(filter) - ) && + (config.tenantPermission === undefined || config.tenantPermission === 'read_party_identity') && + config.requestFilters.every((filter) => ['includeArchived', 'role'].includes(filter)) && new Set(config.requestFilters).size === config.requestFilters.length; const validateConfig = ( - config: SearchProviderAccessScaffoldConfig + config: SearchProviderAccessScaffoldConfig, ): Effect.Effect => Effect.gen(function* validateConfigEffect() { yield* trySync(() => requireCanonicalSlug(config.name, 'search provider')); if (!hasValidAccessFlags(config) || !hasConsistentAccessScope(config)) { - yield* scaffoldError( - 'search provider access flags are internally inconsistent' - ); + yield* scaffoldError('search provider access flags are internally inconsistent'); } }); export const planSearchProviderAccessScaffold = ( workspaceRoot: string, - config: SearchProviderAccessScaffoldConfig + config: SearchProviderAccessScaffoldConfig, ): Effect.Effect< ScaffoldPlan, ScaffoldFailure | SearchProviderAccessScaffoldError, @@ -246,44 +197,22 @@ export const planSearchProviderAccessScaffold = ( > => Effect.gen(function* planSearchProviderAccessScaffoldEffect() { yield* validateConfig(config); - const vertical = yield* discoverOntosModuleEffect( - workspaceRoot, - config.vertical - ); + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, config.vertical); const providerPath = yield* trySync(() => - resolveContainedPath( - vertical.directory, - 'src', - 'search', - `${config.name}.provider.ts` - ) + resolveContainedPath(vertical.directory, 'src', 'search', `${config.name}.provider.ts`), ); const contractPath = yield* trySync(() => - resolveContainedPath( - vertical.directory, - 'shared', - 'apis', - `${config.name}-search.ts` - ) + resolveContainedPath(vertical.directory, 'shared', 'apis', `${config.name}-search.ts`), ); const serverPath = yield* trySync(() => - resolveContainedPath( - vertical.directory, - 'api', - `${config.name}-search-server.ts` - ) + resolveContainedPath(vertical.directory, 'api', `${config.name}-search-server.ts`), ); const fileSystem = yield* FileSystem.FileSystem; const readGeneratedFile = (filePath: string) => fileSystem .readFileString(filePath) .pipe( - Effect.mapError((cause) => - scaffoldError( - `failed to read generated search provider file ${filePath}`, - cause - ) - ) + Effect.mapError((cause) => scaffoldError(`failed to read generated search provider file ${filePath}`, cause)), ); const [provider, contract, server] = yield* Effect.all([ readGeneratedFile(providerPath), @@ -293,35 +222,19 @@ export const planSearchProviderAccessScaffold = ( const expectedRead = `${toCamelCase(config.name)}Read`; if ( !server.startsWith(generatedHeader) || - !server.includes( - `import { ${expectedRead} } from '../src/search/${config.name}.provider.ts';` - ) || + !server.includes(`import { ${expectedRead} } from '../src/search/${config.name}.provider.ts';`) || !server.includes(`registration: ${expectedRead},`) ) { - return yield* scaffoldError( - 'generated search server no longer owns the expected provider registration' - ); + return yield* scaffoldError('generated search server no longer owns the expected provider registration'); } const mutations: Mutation[] = []; - const nextProvider = yield* patchProvider( - provider, - vertical.moduleId, - config - ); + const nextProvider = yield* patchProvider(provider, vertical.moduleId, config); const nextContract = yield* patchContract(contract, config); - const nextManifest = yield* patchManifest( - vertical.manifestContent, - vertical.moduleId, - config - ); + const nextManifest = yield* patchManifest(vertical.manifestContent, vertical.moduleId, config); for (const mutation of [ updateMutation(providerPath, provider, nextProvider), updateMutation(contractPath, contract, nextContract), - updateMutation( - vertical.manifestPath, - vertical.manifestContent, - nextManifest - ), + updateMutation(vertical.manifestPath, vertical.manifestContent, nextManifest), ]) { if (mutation !== undefined) { mutations.push(mutation); diff --git a/app/scripts/scaffolding/search-provider/scaffold.mts b/app/scripts/scaffolding/search-provider/scaffold.mts index a0c701093..a6037a544 100644 --- a/app/scripts/scaffolding/search-provider/scaffold.mts +++ b/app/scripts/scaffolding/search-provider/scaffold.mts @@ -2,7 +2,6 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { planGovernedContributionScaffold } from '../governed-contribution/scaffold.mts'; import type { GovernedContributionScaffoldConfig } from '../shared.mts'; -export default createCodesmithGenerator( - (workspaceRoot: string, config: GovernedContributionScaffoldConfig) => - planGovernedContributionScaffold(workspaceRoot, 'search-provider', config) +export default createCodesmithGenerator((workspaceRoot: string, config: GovernedContributionScaffoldConfig) => + planGovernedContributionScaffold(workspaceRoot, 'search-provider', config), ); diff --git a/app/scripts/scaffolding/shared.mts b/app/scripts/scaffolding/shared.mts index 0bee03474..d157132ef 100644 --- a/app/scripts/scaffolding/shared.mts +++ b/app/scripts/scaffolding/shared.mts @@ -1,197 +1,107 @@ import { NodePath } from '@effect/platform-node'; import type { GeneratorCore } from '@modern-js/codesmith'; -import { - Effect, - FileSystem, - Option, - Path, - Predicate, - Result, - Schema, -} from 'effect'; +import { Effect, FileSystem, Option, Path, Predicate, Result, Schema } from 'effect'; import { format } from 'oxfmt'; -import ultraciteOxfmt from 'ultracite/oxfmt'; +import oxfmtConfig from '../../oxfmt.config.ts'; import { ONTOS_MODULE_CONTRACT_SCHEMA_VERSION } from '../../packages/core-runtime/src/index.ts'; import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; /* eslint-disable unicorn/prefer-number-coercion -- The schema version is parsed as a base-10 integer by contract. expires: 2026-12-31. */ -export const ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION = Number.parseInt( - ONTOS_MODULE_CONTRACT_SCHEMA_VERSION, - 10 -); +export const ONTOS_MODULE_CONTRACT_PACKAGE_SCHEMA_VERSION = Number.parseInt(ONTOS_MODULE_CONTRACT_SCHEMA_VERSION, 10); /* eslint-enable unicorn/prefer-number-coercion */ -export const ACTION_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Action v1'; -export const ACTION_SERVICE_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Action Service v1'; -export const EXTERNAL_HTTP_ADAPTER_GENERATOR_HEADER = - '// @generated by OntOS Codesmith External HTTP Adapter v1'; -export const RESOURCE_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Resource v1'; +export const ACTION_GENERATOR_HEADER = '// @generated by OntOS Codesmith Action v1'; +export const ACTION_SERVICE_GENERATOR_HEADER = '// @generated by OntOS Codesmith Action Service v1'; +export const EXTERNAL_HTTP_ADAPTER_GENERATOR_HEADER = '// @generated by OntOS Codesmith External HTTP Adapter v1'; +export const RESOURCE_GENERATOR_HEADER = '// @generated by OntOS Codesmith Resource v1'; export const CORE_ACTION_SLOT_START = '// '; export const CORE_ACTION_SLOT_END = '// '; -export const CORE_ACTION_CATALOG_IMPORT_SLOT_START = - '// '; -export const CORE_ACTION_CATALOG_IMPORT_SLOT_END = - '// '; -export const CORE_ACTION_CATALOG_VALUE_SLOT_START = - '// '; -export const CORE_ACTION_CATALOG_VALUE_SLOT_END = - '// '; +export const CORE_ACTION_CATALOG_IMPORT_SLOT_START = '// '; +export const CORE_ACTION_CATALOG_IMPORT_SLOT_END = '// '; +export const CORE_ACTION_CATALOG_VALUE_SLOT_START = '// '; +export const CORE_ACTION_CATALOG_VALUE_SLOT_END = '// '; export const CORE_POLICY_SLOT_START = '// '; export const CORE_POLICY_SLOT_END = '// '; export const OUTBOX_SLOT_START = '// '; export const OUTBOX_SLOT_END = '// '; -export const OUTBOX_CONTRACT_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Outbox Message Contract v1'; -export const OUTBOX_WORKER_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Outbox Worker v1'; -export const OUTBOX_WORKER_IMPORT_SLOT_START = - '// '; -export const OUTBOX_WORKER_IMPORT_SLOT_END = - '// '; -export const OUTBOX_WORKER_REGISTRY_SLOT_START = - '// '; -export const OUTBOX_WORKER_REGISTRY_SLOT_END = - '// '; -export const VERTICAL_PUBLIC_COMPONENT_SLOT_START = - '// '; -export const VERTICAL_PUBLIC_COMPONENT_SLOT_END = - '// '; +export const OUTBOX_CONTRACT_GENERATOR_HEADER = '// @generated by OntOS Codesmith Outbox Message Contract v1'; +export const OUTBOX_WORKER_GENERATOR_HEADER = '// @generated by OntOS Codesmith Outbox Worker v1'; +export const OUTBOX_WORKER_IMPORT_SLOT_START = '// '; +export const OUTBOX_WORKER_IMPORT_SLOT_END = '// '; +export const OUTBOX_WORKER_REGISTRY_SLOT_START = '// '; +export const OUTBOX_WORKER_REGISTRY_SLOT_END = '// '; +export const VERTICAL_PUBLIC_COMPONENT_SLOT_START = '// '; +export const VERTICAL_PUBLIC_COMPONENT_SLOT_END = '// '; export const VERTICAL_SEARCH_SLOT_START = '// '; export const VERTICAL_SEARCH_SLOT_END = '// '; export const VERTICAL_REPORT_SLOT_START = '// '; export const VERTICAL_REPORT_SLOT_END = '// '; -export const GOVERNED_HTTP_API_IMPORT_SLOT_START = - '// '; -export const GOVERNED_HTTP_API_IMPORT_SLOT_END = - '// '; -export const GOVERNED_HTTP_API_ADDITION_SLOT_START = - '// '; -export const GOVERNED_HTTP_API_ADDITION_SLOT_END = - '// '; -export const GOVERNED_HTTP_HANDLER_IMPORT_SLOT_START = - '// '; -export const GOVERNED_HTTP_HANDLER_IMPORT_SLOT_END = - '// '; -export const GOVERNED_HTTP_HANDLER_LAYER_SLOT_START = - '// '; -export const GOVERNED_HTTP_HANDLER_LAYER_SLOT_END = - '// '; -export const GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START = - '// '; -export const GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END = - '// '; -export const GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START = - '// '; -export const GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END = - '// '; -export const MODULE_CONTRACT_GENERATOR_HEADER = - '// @generated by OntOS Codesmith Module Contract v1'; -export const MODULE_MANIFEST_IMPORT_SLOT_START = - '// '; -export const MODULE_MANIFEST_IMPORT_SLOT_END = - '// '; -export const MODULE_MANIFEST_ACTION_SLOT_START = - '// '; -export const MODULE_MANIFEST_ACTION_SLOT_END = - '// '; -export const MODULE_MANIFEST_API_SLOT_START = - '// '; -export const MODULE_MANIFEST_API_SLOT_END = - '// '; -export const MODULE_MANIFEST_COMPONENT_SLOT_START = - '// '; -export const MODULE_MANIFEST_COMPONENT_SLOT_END = - '// '; -export const MODULE_MANIFEST_REPORT_SLOT_START = - '// '; -export const MODULE_MANIFEST_REPORT_SLOT_END = - '// '; -export const MODULE_MANIFEST_RESOURCE_SLOT_START = - '// '; -export const MODULE_MANIFEST_RESOURCE_SLOT_END = - '// '; -export const MODULE_MANIFEST_SEARCH_SLOT_START = - '// '; -export const MODULE_MANIFEST_SEARCH_SLOT_END = - '// '; -export const MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START = - '// '; -export const MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END = - '// '; -export const MODULE_MANIFEST_SHELL_PAGE_SLOT_START = - '// '; -export const MODULE_MANIFEST_SHELL_PAGE_SLOT_END = - '// '; -export const MODULE_MANIFEST_SHELL_COMPONENT_SLOT_START = - '// '; -export const MODULE_MANIFEST_SHELL_COMPONENT_SLOT_END = - '// '; -export const MODULE_MANIFEST_SHELL_REPORT_SLOT_START = - '// '; -export const MODULE_MANIFEST_SHELL_REPORT_SLOT_END = - '// '; -export const MODULE_MANIFEST_SHELL_SEARCH_SLOT_START = - '// '; -export const MODULE_MANIFEST_SHELL_SEARCH_SLOT_END = - '// '; -export const MODULE_REGISTRATION_IMPORT_SLOT_START = - '// '; -export const MODULE_REGISTRATION_IMPORT_SLOT_END = - '// '; -export const MODULE_REGISTRATION_ACTION_SLOT_START = - '// '; -export const MODULE_REGISTRATION_ACTION_SLOT_END = - '// '; -export const MODULE_REGISTRATION_API_SLOT_START = - '// '; -export const MODULE_REGISTRATION_API_SLOT_END = - '// '; -export const MODULE_REGISTRATION_COMPONENT_SLOT_START = - '// '; -export const MODULE_REGISTRATION_COMPONENT_SLOT_END = - '// '; -export const MODULE_REGISTRATION_PAGE_SLOT_START = - '// '; -export const MODULE_REGISTRATION_PAGE_SLOT_END = - '// '; -export const MODULE_REGISTRATION_REPORT_SLOT_START = - '// '; -export const MODULE_REGISTRATION_REPORT_SLOT_END = - '// '; -export const MODULE_REGISTRATION_SEARCH_SLOT_START = - '// '; -export const MODULE_REGISTRATION_SEARCH_SLOT_END = - '// '; -export const MODULE_REGISTRATION_WORKER_SLOT_START = - '// '; -export const MODULE_REGISTRATION_WORKER_SLOT_END = - '// '; +export const GOVERNED_HTTP_API_IMPORT_SLOT_START = '// '; +export const GOVERNED_HTTP_API_IMPORT_SLOT_END = '// '; +export const GOVERNED_HTTP_API_ADDITION_SLOT_START = '// '; +export const GOVERNED_HTTP_API_ADDITION_SLOT_END = '// '; +export const GOVERNED_HTTP_HANDLER_IMPORT_SLOT_START = '// '; +export const GOVERNED_HTTP_HANDLER_IMPORT_SLOT_END = '// '; +export const GOVERNED_HTTP_HANDLER_LAYER_SLOT_START = '// '; +export const GOVERNED_HTTP_HANDLER_LAYER_SLOT_END = '// '; +export const GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START = '// '; +export const GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END = '// '; +export const GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START = '// '; +export const GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END = '// '; +export const MODULE_CONTRACT_GENERATOR_HEADER = '// @generated by OntOS Codesmith Module Contract v1'; +export const MODULE_MANIFEST_IMPORT_SLOT_START = '// '; +export const MODULE_MANIFEST_IMPORT_SLOT_END = '// '; +export const MODULE_MANIFEST_ACTION_SLOT_START = '// '; +export const MODULE_MANIFEST_ACTION_SLOT_END = '// '; +export const MODULE_MANIFEST_API_SLOT_START = '// '; +export const MODULE_MANIFEST_API_SLOT_END = '// '; +export const MODULE_MANIFEST_COMPONENT_SLOT_START = '// '; +export const MODULE_MANIFEST_COMPONENT_SLOT_END = '// '; +export const MODULE_MANIFEST_REPORT_SLOT_START = '// '; +export const MODULE_MANIFEST_REPORT_SLOT_END = '// '; +export const MODULE_MANIFEST_RESOURCE_SLOT_START = '// '; +export const MODULE_MANIFEST_RESOURCE_SLOT_END = '// '; +export const MODULE_MANIFEST_SEARCH_SLOT_START = '// '; +export const MODULE_MANIFEST_SEARCH_SLOT_END = '// '; +export const MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_START = '// '; +export const MODULE_MANIFEST_SHELL_NAVIGATION_SLOT_END = '// '; +export const MODULE_MANIFEST_SHELL_PAGE_SLOT_START = '// '; +export const MODULE_MANIFEST_SHELL_PAGE_SLOT_END = '// '; +export const MODULE_MANIFEST_SHELL_COMPONENT_SLOT_START = '// '; +export const MODULE_MANIFEST_SHELL_COMPONENT_SLOT_END = '// '; +export const MODULE_MANIFEST_SHELL_REPORT_SLOT_START = '// '; +export const MODULE_MANIFEST_SHELL_REPORT_SLOT_END = '// '; +export const MODULE_MANIFEST_SHELL_SEARCH_SLOT_START = '// '; +export const MODULE_MANIFEST_SHELL_SEARCH_SLOT_END = '// '; +export const MODULE_REGISTRATION_IMPORT_SLOT_START = '// '; +export const MODULE_REGISTRATION_IMPORT_SLOT_END = '// '; +export const MODULE_REGISTRATION_ACTION_SLOT_START = '// '; +export const MODULE_REGISTRATION_ACTION_SLOT_END = '// '; +export const MODULE_REGISTRATION_API_SLOT_START = '// '; +export const MODULE_REGISTRATION_API_SLOT_END = '// '; +export const MODULE_REGISTRATION_COMPONENT_SLOT_START = '// '; +export const MODULE_REGISTRATION_COMPONENT_SLOT_END = '// '; +export const MODULE_REGISTRATION_PAGE_SLOT_START = '// '; +export const MODULE_REGISTRATION_PAGE_SLOT_END = '// '; +export const MODULE_REGISTRATION_REPORT_SLOT_START = '// '; +export const MODULE_REGISTRATION_REPORT_SLOT_END = '// '; +export const MODULE_REGISTRATION_SEARCH_SLOT_START = '// '; +export const MODULE_REGISTRATION_SEARCH_SLOT_END = '// '; +export const MODULE_REGISTRATION_WORKER_SLOT_START = '// '; +export const MODULE_REGISTRATION_WORKER_SLOT_END = '// '; export const createScaffoldErrorTools = ( - ErrorClass: new (fields: { - readonly cause?: unknown; - readonly message: string; - }) => Failure, + ErrorClass: new (fields: { readonly cause?: unknown; readonly message: string }) => Failure, isOwnError: Predicate.Refinement, - fallbackMessage: string + fallbackMessage: string, ) => { const scaffoldError = (message: string, cause?: unknown): Failure => new ErrorClass(cause === undefined ? { message } : { cause, message }); - const trySync = ( - operation: () => Value - ): Effect.Effect => + const trySync = (operation: () => Value): Effect.Effect => Effect.try({ catch: (cause) => - isOwnError(cause) - ? cause - : scaffoldError( - Predicate.isError(cause) ? cause.message : fallbackMessage, - cause - ), + isOwnError(cause) ? cause : scaffoldError(Predicate.isError(cause) ? cause.message : fallbackMessage, cause), try: operation, }); return { scaffoldError, trySync }; @@ -217,9 +127,7 @@ interface CoreActionScaffoldConfig { readonly vertical?: never; } -export type ActionScaffoldConfig = - | CoreActionScaffoldConfig - | VerticalActionScaffoldConfig; +export type ActionScaffoldConfig = CoreActionScaffoldConfig | VerticalActionScaffoldConfig; export interface ActionServiceScaffoldConfig { readonly service: string; @@ -247,10 +155,7 @@ export interface OutboxWorkerScaffoldConfig { } export interface PageScaffoldConfig { - readonly authorization: - | 'authenticated_principal' - | 'context_permission' - | 'public'; + readonly authorization: 'authenticated_principal' | 'context_permission' | 'public'; readonly page: string; readonly permission?: string; readonly url?: string; @@ -258,10 +163,7 @@ export interface PageScaffoldConfig { } export interface GovernedContributionScaffoldConfig { - readonly authorization: - | 'authenticated_principal' - | 'context_permission' - | 'public'; + readonly authorization: 'authenticated_principal' | 'context_permission' | 'public'; readonly name: string; readonly permission?: string; readonly resource?: string; @@ -400,7 +302,7 @@ const JsonValueSchema: Schema.Codec = Schema.suspend(() => Schema.String, Schema.Array(JsonValueSchema), Schema.Record(Schema.String, JsonValueSchema), - ]) + ]), ); const JsonObjectSchema = Schema.Record(Schema.String, JsonValueSchema); const JsonObjectArraySchema = Schema.Array(JsonObjectSchema); @@ -432,10 +334,8 @@ export interface OntosVerticalMetadata extends VerticalMetadata { const canonicalSlugPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; const stableAppIdPattern = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u; const stableCoreModulePattern = /^core(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; -const stableOntosModulePattern = - /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; -const topicPattern = - /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; +const stableOntosModulePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; +const topicPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)+$/u; const reservedSegments = new Set([ 'aux', 'con', @@ -450,29 +350,18 @@ const reservedSegments = new Set([ const nodePath = Effect.runSync(Path.Path.pipe(Effect.provide(NodePath.layer))); -export class ScaffoldFailure extends Schema.TaggedError()( - 'ScaffoldFailure', - { - cause: Schema.optionalKey(Schema.Unknown), - message: Schema.String, - } -) {} +export class ScaffoldFailure extends Schema.TaggedError()('ScaffoldFailure', { + cause: Schema.optionalKey(Schema.Unknown), + message: Schema.String, +}) {} -export const scaffoldFailure = ( - message: string, - cause?: unknown -): ScaffoldFailure => - cause === undefined - ? new ScaffoldFailure({ message }) - : new ScaffoldFailure({ cause, message }); +export const scaffoldFailure = (message: string, cause?: unknown): ScaffoldFailure => + cause === undefined ? new ScaffoldFailure({ message }) : new ScaffoldFailure({ cause, message }); const isScaffoldFailure = Schema.is(ScaffoldFailure); const isScaffoldFailureMessage = Schema.is(Schema.String); -const scaffoldFailureFromUnknown = ( - cause: unknown, - fallback: string -): ScaffoldFailure => { +const scaffoldFailureFromUnknown = (cause: unknown, fallback: string): ScaffoldFailure => { if (isScaffoldFailure(cause)) { return cause; } @@ -480,10 +369,7 @@ const scaffoldFailureFromUnknown = ( return cause.cause; } const message = - Predicate.hasProperty(cause, 'message') && - isScaffoldFailureMessage(cause.message) - ? cause.message - : fallback; + Predicate.hasProperty(cause, 'message') && isScaffoldFailureMessage(cause.message) ? cause.message : fallback; return scaffoldFailure(message, cause); }; @@ -496,10 +382,7 @@ export const tryScaffold = (message: string, evaluate: () => Value) => export const raiseScaffoldFailure = (message: string, cause?: unknown): never => scaffoldingRuntime.runSync(Effect.die(scaffoldFailure(message, cause))); -const decodeResultOrRaise = ( - result: Result.Result, - message: string -): Value => { +const decodeResultOrRaise = (result: Result.Result, message: string): Value => { if (Result.isFailure(result)) { return raiseScaffoldFailure(message, result.failure); } @@ -509,10 +392,7 @@ const decodeResultOrRaise = ( const isJsonObject = Schema.is(JsonObjectSchema); const isJsonObjectArray = Schema.is(JsonObjectArraySchema); -export const asJsonObject = ( - value: JsonValue | undefined, - label: string -): JsonObject => { +export const asJsonObject = (value: JsonValue | undefined, label: string): JsonObject => { if (!isJsonObject(value)) { return raiseScaffoldFailure(`${label} must be a JSON object`); } @@ -521,10 +401,7 @@ export const asJsonObject = ( const isStringValue = Schema.is(Schema.String); -export const requiredString = ( - value: JsonValue | undefined, - label: string -): string => { +export const requiredString = (value: JsonValue | undefined, label: string): string => { if (!isStringValue(value) || value.trim().length === 0) { return raiseScaffoldFailure(`${label} must be a non-empty string`); } @@ -536,11 +413,7 @@ const pathExistsEffect = (targetPath: string) => const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem .exists(targetPath) - .pipe( - Effect.mapError((cause) => - scaffoldFailure(`failed to inspect ${targetPath}`, cause) - ) - ); + .pipe(Effect.mapError((cause) => scaffoldFailure(`failed to inspect ${targetPath}`, cause))); }); const regexMayStartAt = (content: string, index: number): boolean => { @@ -552,7 +425,7 @@ const regexMayStartAt = (content: string, index: number): boolean => { return true; } return /(?:\bcase|\bdefault|\bdelete|\bdo|\belse|\bin|\binstanceof|\bnew|\breturn|\bthrow|\btypeof|\bvoid|\byield|=>)$/u.test( - prefix + prefix, ); }; @@ -576,7 +449,7 @@ const nonCodeStateAt = ( content: string, index: number, character: string, - next: null | string + next: null | string, ): NonCodeState | null => { if (character === '/' && next === '/') { return LINE_COMMENT_STATE; @@ -593,9 +466,7 @@ const nonCodeStateAt = ( if (character === '`') { return TEMPLATE_STATE; } - return character === '/' && regexMayStartAt(content, index) - ? REGEX_STATE - : null; + return character === '/' && regexMayStartAt(content, index) ? REGEX_STATE : null; }; interface NonCodeTransition { @@ -609,11 +480,7 @@ const closesNonCodeQuote = (state: NonCodeState, character: string): boolean => (state === DOUBLE_QUOTE_STATE && character === '"') || (state === TEMPLATE_STATE && character === '`'); -const advanceRegexState = ( - state: NonCodeState, - character: string, - regexCharacterClass: boolean -): NonCodeTransition => { +const advanceRegexState = (state: NonCodeState, character: string, regexCharacterClass: boolean): NonCodeTransition => { if (state !== REGEX_STATE) { return { escaped: false, regexCharacterClass, state }; } @@ -635,7 +502,7 @@ const advanceNonCodeState = ( character: string, next: null | string, escaped: boolean, - regexCharacterClass: boolean + regexCharacterClass: boolean, ): NonCodeTransition => { if (state === LINE_COMMENT_STATE) { return { @@ -663,14 +530,8 @@ const advanceNonCodeState = ( return advanceRegexState(state, character, regexCharacterClass); }; -const shouldMaskNonCodeState = ( - state: NonCodeState, - preserveStrings: boolean -): boolean => - !preserveStrings || - state === BLOCK_COMMENT_STATE || - state === LINE_COMMENT_STATE || - state === REGEX_STATE; +const shouldMaskNonCodeState = (state: NonCodeState, preserveStrings: boolean): boolean => + !preserveStrings || state === BLOCK_COMMENT_STATE || state === LINE_COMMENT_STATE || state === REGEX_STATE; const stringCodeUnits = (content: string): string[] => { const units: string[] = []; @@ -686,7 +547,7 @@ const maskNonCodeOpening = ( masked: string[], index: number, state: NonCodeState | null, - preserveStrings: boolean + preserveStrings: boolean, ): number => { if (state === null) { return 0; @@ -701,10 +562,7 @@ const maskNonCodeOpening = ( return 0; }; -export const maskNonCode = ( - content: string, - preserveStrings = false -): string => { +export const maskNonCode = (content: string, preserveStrings = false): string => { const masked = stringCodeUnits(content); let state: NonCodeState | null = null; let escaped = false; @@ -718,16 +576,9 @@ export const maskNonCode = ( continue; } if (shouldMaskNonCodeState(state, preserveStrings)) { - masked[index] = - character === '\n' || character === '\r' ? character : ' '; + masked[index] = character === '\n' || character === '\r' ? character : ' '; } - const transition = advanceNonCodeState( - state, - character, - next, - escaped, - regexCharacterClass - ); + const transition = advanceNonCodeState(state, character, next, escaped, regexCharacterClass); if (state === BLOCK_COMMENT_STATE && transition.state === null) { masked[index + 1] = ' '; index += 1; @@ -751,13 +602,7 @@ export const isCodePosition = (content: string, target: number): boolean => { } continue; } - const transition = advanceNonCodeState( - state, - character, - next, - escaped, - regexCharacterClass - ); + const transition = advanceNonCodeState(state, character, next, escaped, regexCharacterClass); if (state === BLOCK_COMMENT_STATE && transition.state === null) { index += 1; } @@ -772,15 +617,11 @@ interface ModuleFederationExposesRange { readonly propertyIndex: number; } -const moduleFederationExposesRange = ( - content: string -): ModuleFederationExposesRange => { +const moduleFederationExposesRange = (content: string): ModuleFederationExposesRange => { const code = maskNonCode(content); const propertyMatches = [...code.matchAll(/\bexposes\s*:\s*\{/gu)]; if (propertyMatches.length === 0) { - return raiseScaffoldFailure( - 'generated Module Federation exposes object is missing' - ); + return raiseScaffoldFailure('generated Module Federation exposes object is missing'); } const braceDepthAt = (targetIndex: number) => { let depth = 0; @@ -797,23 +638,15 @@ const moduleFederationExposesRange = ( depth: braceDepthAt(match.index ?? -1), match, })); - const shallowestDepth = Math.min( - ...matchesWithDepth.map(({ depth }) => depth) - ); - const shallowestMatches = matchesWithDepth.filter( - ({ depth }) => depth === shallowestDepth - ); + const shallowestDepth = Math.min(...matchesWithDepth.map(({ depth }) => depth)); + const shallowestMatches = matchesWithDepth.filter(({ depth }) => depth === shallowestDepth); if (shallowestMatches.length > 1) { - return raiseScaffoldFailure( - 'generated Module Federation exposes object is duplicated' - ); + return raiseScaffoldFailure('generated Module Federation exposes object is duplicated'); } const propertyIndex = shallowestMatches[0]?.match.index ?? -1; const openIndex = code.indexOf('{', propertyIndex); if (openIndex === -1) { - return raiseScaffoldFailure( - 'generated Module Federation exposes object is malformed' - ); + return raiseScaffoldFailure('generated Module Federation exposes object is malformed'); } let depth = 0; let closeIndex = -1; @@ -830,74 +663,45 @@ const moduleFederationExposesRange = ( } } if (closeIndex === -1) { - return raiseScaffoldFailure( - 'generated Module Federation exposes object is malformed' - ); + return raiseScaffoldFailure('generated Module Federation exposes object is malformed'); } return { closeIndex, openIndex, propertyIndex }; }; -export const moduleFederationExposureSource = ( - content: string, - exposureKey: string -): string | undefined => { +export const moduleFederationExposureSource = (content: string, exposureKey: string): string | undefined => { const { closeIndex, openIndex } = moduleFederationExposesRange(content); const body = content.slice(openIndex + 1, closeIndex); const searchableBody = maskNonCode(body, true); - const escapedKey = exposureKey.replaceAll( - /[.*+?^${}()|[\]\\]/gu, - String.raw`\$&` - ); + const escapedKey = exposureKey.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`); const keyMatches = [ - ...searchableBody.matchAll( - new RegExp( - `(?:'${escapedKey}'|"${escapedKey}"|\`${escapedKey}\`)\\s*:`, - 'gu' - ) - ), + ...searchableBody.matchAll(new RegExp(`(?:'${escapedKey}'|"${escapedKey}"|\`${escapedKey}\`)\\s*:`, 'gu')), ]; if (keyMatches.length > 1) { - return raiseScaffoldFailure( - `Module Federation exposure ${exposureKey} is duplicated` - ); + return raiseScaffoldFailure(`Module Federation exposure ${exposureKey} is duplicated`); } const matches = [ ...searchableBody.matchAll( new RegExp( `(?:'${escapedKey}'|"${escapedKey}"|\`${escapedKey}\`)\\s*:\\s*(?:'(?[^']+)'|"(?[^"]+)"|\`(?