@aws-blocks/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +174 -0
- package/dist/api.d.ts +147 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +10 -0
- package/dist/api.test.d.ts +2 -0
- package/dist/api.test.d.ts.map +1 -0
- package/dist/api.test.js +19 -0
- package/dist/bb-utils.d.ts +11 -0
- package/dist/bb-utils.d.ts.map +1 -0
- package/dist/bb-utils.js +12 -0
- package/dist/builtin-routes.d.ts +6 -0
- package/dist/builtin-routes.d.ts.map +1 -0
- package/dist/builtin-routes.js +57 -0
- package/dist/cdk/__fixtures__/factory-backend.d.ts +3 -0
- package/dist/cdk/__fixtures__/factory-backend.d.ts.map +1 -0
- package/dist/cdk/__fixtures__/factory-backend.js +6 -0
- package/dist/cdk/__fixtures__/fullid-construct-backend.d.ts +2 -0
- package/dist/cdk/__fixtures__/fullid-construct-backend.d.ts.map +1 -0
- package/dist/cdk/__fixtures__/fullid-construct-backend.js +37 -0
- package/dist/cdk/__fixtures__/handler.d.ts +5 -0
- package/dist/cdk/__fixtures__/handler.d.ts.map +1 -0
- package/dist/cdk/__fixtures__/handler.js +3 -0
- package/dist/cdk/__fixtures__/side-effect-backend.d.ts +2 -0
- package/dist/cdk/__fixtures__/side-effect-backend.d.ts.map +1 -0
- package/dist/cdk/__fixtures__/side-effect-backend.js +5 -0
- package/dist/cdk/blocks-backend.d.ts +68 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -0
- package/dist/cdk/blocks-backend.js +214 -0
- package/dist/cdk/blocks-backend.test.d.ts +2 -0
- package/dist/cdk/blocks-backend.test.d.ts.map +1 -0
- package/dist/cdk/blocks-backend.test.js +151 -0
- package/dist/cdk/blocks-stack.test.d.ts +2 -0
- package/dist/cdk/blocks-stack.test.d.ts.map +1 -0
- package/dist/cdk/blocks-stack.test.js +78 -0
- package/dist/cdk/config-registry.d.ts +26 -0
- package/dist/cdk/config-registry.d.ts.map +1 -0
- package/dist/cdk/config-registry.js +80 -0
- package/dist/cdk/index.d.ts +35 -0
- package/dist/cdk/index.d.ts.map +1 -0
- package/dist/cdk/index.js +90 -0
- package/dist/cdk/mixins.d.ts +28 -0
- package/dist/cdk/mixins.d.ts.map +1 -0
- package/dist/cdk/mixins.js +37 -0
- package/dist/cdk/mixins.test.d.ts +2 -0
- package/dist/cdk/mixins.test.d.ts.map +1 -0
- package/dist/cdk/mixins.test.js +108 -0
- package/dist/cdk/node-version.d.ts +14 -0
- package/dist/cdk/node-version.d.ts.map +1 -0
- package/dist/cdk/node-version.js +15 -0
- package/dist/cdk/stack-metadata.d.ts +12 -0
- package/dist/cdk/stack-metadata.d.ts.map +1 -0
- package/dist/cdk/stack-metadata.js +22 -0
- package/dist/cdk/stack-metadata.test.d.ts +2 -0
- package/dist/cdk/stack-metadata.test.d.ts.map +1 -0
- package/dist/cdk/stack-metadata.test.js +56 -0
- package/dist/cdk/synth-guard.d.ts +14 -0
- package/dist/cdk/synth-guard.d.ts.map +1 -0
- package/dist/cdk/synth-guard.js +20 -0
- package/dist/cdk/synth-guard.test.d.ts +2 -0
- package/dist/cdk/synth-guard.test.d.ts.map +1 -0
- package/dist/cdk/synth-guard.test.js +15 -0
- package/dist/client/index.d.ts +97 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +238 -0
- package/dist/client/index.test.d.ts +2 -0
- package/dist/client/index.test.d.ts.map +1 -0
- package/dist/client/index.test.js +113 -0
- package/dist/common/config.d.ts +45 -0
- package/dist/common/config.d.ts.map +1 -0
- package/dist/common/config.js +154 -0
- package/dist/common/config.test.d.ts +2 -0
- package/dist/common/config.test.d.ts.map +1 -0
- package/dist/common/config.test.js +173 -0
- package/dist/common/constants.d.ts +11 -0
- package/dist/common/constants.d.ts.map +1 -0
- package/dist/common/constants.js +12 -0
- package/dist/common/crypto.d.ts +6 -0
- package/dist/common/crypto.d.ts.map +1 -0
- package/dist/common/crypto.js +14 -0
- package/dist/common/index.d.ts +220 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +180 -0
- package/dist/common/index.test.d.ts +2 -0
- package/dist/common/index.test.d.ts.map +1 -0
- package/dist/common/index.test.js +195 -0
- package/dist/common/mock-data.d.ts +40 -0
- package/dist/common/mock-data.d.ts.map +1 -0
- package/dist/common/mock-data.js +39 -0
- package/dist/common/official-bb-names.generated.d.ts +7 -0
- package/dist/common/official-bb-names.generated.d.ts.map +1 -0
- package/dist/common/official-bb-names.generated.js +28 -0
- package/dist/common/sdk-registry.d.ts +136 -0
- package/dist/common/sdk-registry.d.ts.map +1 -0
- package/dist/common/sdk-registry.js +125 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +28 -0
- package/dist/cors.d.ts +39 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +85 -0
- package/dist/cors.test.d.ts +2 -0
- package/dist/cors.test.d.ts.map +1 -0
- package/dist/cors.test.js +241 -0
- package/dist/db-naming.d.ts +21 -0
- package/dist/db-naming.d.ts.map +1 -0
- package/dist/db-naming.js +38 -0
- package/dist/db-naming.test.d.ts +2 -0
- package/dist/db-naming.test.d.ts.map +1 -0
- package/dist/db-naming.test.js +29 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/hosting.d.ts +258 -0
- package/dist/hosting.d.ts.map +1 -0
- package/dist/hosting.js +340 -0
- package/dist/hosting.test.d.ts +2 -0
- package/dist/hosting.test.d.ts.map +1 -0
- package/dist/hosting.test.js +1077 -0
- package/dist/index.cdk.d.ts +13 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/lambda-handler.d.ts +131 -0
- package/dist/lambda-handler.d.ts.map +1 -0
- package/dist/lambda-handler.js +610 -0
- package/dist/lambda-handler.test.d.ts +2 -0
- package/dist/lambda-handler.test.d.ts.map +1 -0
- package/dist/lambda-handler.test.js +835 -0
- package/dist/pipeline/index.d.ts +4 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +3 -0
- package/dist/pipeline/pipeline-construct.d.ts +130 -0
- package/dist/pipeline/pipeline-construct.d.ts.map +1 -0
- package/dist/pipeline/pipeline-construct.js +484 -0
- package/dist/pipeline/pipeline.test.d.ts +2 -0
- package/dist/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/pipeline.test.js +1231 -0
- package/dist/pipeline/types.d.ts +351 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +3 -0
- package/dist/raw-route.cdk.d.ts +47 -0
- package/dist/raw-route.cdk.d.ts.map +1 -0
- package/dist/raw-route.cdk.js +51 -0
- package/dist/raw-route.d.ts +119 -0
- package/dist/raw-route.d.ts.map +1 -0
- package/dist/raw-route.js +239 -0
- package/dist/raw-route.mock.d.ts +47 -0
- package/dist/raw-route.mock.d.ts.map +1 -0
- package/dist/raw-route.mock.js +51 -0
- package/dist/raw-route.test.d.ts +2 -0
- package/dist/raw-route.test.d.ts.map +1 -0
- package/dist/raw-route.test.js +597 -0
- package/dist/redact.d.ts +51 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +134 -0
- package/dist/redact.test.d.ts +2 -0
- package/dist/redact.test.d.ts.map +1 -0
- package/dist/redact.test.js +199 -0
- package/dist/rpc.d.ts +54 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +127 -0
- package/dist/rpc.test.d.ts +2 -0
- package/dist/rpc.test.d.ts.map +1 -0
- package/dist/rpc.test.js +60 -0
- package/dist/scripts/cdk-telemetry-env.d.ts +13 -0
- package/dist/scripts/cdk-telemetry-env.d.ts.map +1 -0
- package/dist/scripts/cdk-telemetry-env.js +21 -0
- package/dist/scripts/cdk-telemetry-env.test.d.ts +2 -0
- package/dist/scripts/cdk-telemetry-env.test.d.ts.map +1 -0
- package/dist/scripts/cdk-telemetry-env.test.js +56 -0
- package/dist/scripts/cleanup.d.ts +3 -0
- package/dist/scripts/cleanup.d.ts.map +1 -0
- package/dist/scripts/cleanup.js +35 -0
- package/dist/scripts/console.d.ts +6 -0
- package/dist/scripts/console.d.ts.map +1 -0
- package/dist/scripts/console.js +25 -0
- package/dist/scripts/deploy.d.ts +6 -0
- package/dist/scripts/deploy.d.ts.map +1 -0
- package/dist/scripts/deploy.js +82 -0
- package/dist/scripts/destroy.d.ts +6 -0
- package/dist/scripts/destroy.d.ts.map +1 -0
- package/dist/scripts/destroy.js +30 -0
- package/dist/scripts/dev-server-cors.test.d.ts +2 -0
- package/dist/scripts/dev-server-cors.test.d.ts.map +1 -0
- package/dist/scripts/dev-server-cors.test.js +23 -0
- package/dist/scripts/dev-server-telemetry.test.d.ts +2 -0
- package/dist/scripts/dev-server-telemetry.test.d.ts.map +1 -0
- package/dist/scripts/dev-server-telemetry.test.js +113 -0
- package/dist/scripts/dev-server.d.ts +21 -0
- package/dist/scripts/dev-server.d.ts.map +1 -0
- package/dist/scripts/dev-server.js +456 -0
- package/dist/scripts/ensure-secrets.d.ts +31 -0
- package/dist/scripts/ensure-secrets.d.ts.map +1 -0
- package/dist/scripts/ensure-secrets.js +100 -0
- package/dist/scripts/ensure-secrets.test.d.ts +2 -0
- package/dist/scripts/ensure-secrets.test.d.ts.map +1 -0
- package/dist/scripts/ensure-secrets.test.js +44 -0
- package/dist/scripts/external-migrations-step.d.ts +62 -0
- package/dist/scripts/external-migrations-step.d.ts.map +1 -0
- package/dist/scripts/external-migrations-step.js +243 -0
- package/dist/scripts/external-migrations-step.test.d.ts +2 -0
- package/dist/scripts/external-migrations-step.test.d.ts.map +1 -0
- package/dist/scripts/external-migrations-step.test.js +47 -0
- package/dist/scripts/extract-ts-types.d.ts +65 -0
- package/dist/scripts/extract-ts-types.d.ts.map +1 -0
- package/dist/scripts/extract-ts-types.js +664 -0
- package/dist/scripts/extract-ts-types.test.d.ts +2 -0
- package/dist/scripts/extract-ts-types.test.d.ts.map +1 -0
- package/dist/scripts/extract-ts-types.test.js +497 -0
- package/dist/scripts/generate-client-worker.d.ts +2 -0
- package/dist/scripts/generate-client-worker.d.ts.map +1 -0
- package/dist/scripts/generate-client-worker.js +13 -0
- package/dist/scripts/generate-client.d.ts +14 -0
- package/dist/scripts/generate-client.d.ts.map +1 -0
- package/dist/scripts/generate-client.js +73 -0
- package/dist/scripts/generate-client.test.d.ts +2 -0
- package/dist/scripts/generate-client.test.d.ts.map +1 -0
- package/dist/scripts/generate-client.test.js +48 -0
- package/dist/scripts/generate-spec-cli.d.ts +3 -0
- package/dist/scripts/generate-spec-cli.d.ts.map +1 -0
- package/dist/scripts/generate-spec-cli.js +77 -0
- package/dist/scripts/generate-spec-cli.test.d.ts +14 -0
- package/dist/scripts/generate-spec-cli.test.d.ts.map +1 -0
- package/dist/scripts/generate-spec-cli.test.js +90 -0
- package/dist/scripts/generate-spec.d.ts +61 -0
- package/dist/scripts/generate-spec.d.ts.map +1 -0
- package/dist/scripts/generate-spec.js +746 -0
- package/dist/scripts/generate-spec.test.d.ts +12 -0
- package/dist/scripts/generate-spec.test.d.ts.map +1 -0
- package/dist/scripts/generate-spec.test.js +88 -0
- package/dist/scripts/index.d.ts +12 -0
- package/dist/scripts/index.d.ts.map +1 -0
- package/dist/scripts/index.js +13 -0
- package/dist/scripts/sandbox.d.ts +11 -0
- package/dist/scripts/sandbox.d.ts.map +1 -0
- package/dist/scripts/sandbox.js +200 -0
- package/dist/scripts/telemetry-cli.d.ts +3 -0
- package/dist/scripts/telemetry-cli.d.ts.map +1 -0
- package/dist/scripts/telemetry-cli.js +8 -0
- package/dist/scripts/telemetry.d.ts +17 -0
- package/dist/scripts/telemetry.d.ts.map +1 -0
- package/dist/scripts/telemetry.js +150 -0
- package/dist/scripts/telemetry.test.d.ts +2 -0
- package/dist/scripts/telemetry.test.d.ts.map +1 -0
- package/dist/scripts/telemetry.test.js +203 -0
- package/dist/scripts/validate-spec.d.ts +22 -0
- package/dist/scripts/validate-spec.d.ts.map +1 -0
- package/dist/scripts/validate-spec.js +159 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/withAuth.d.ts +142 -0
- package/dist/server/withAuth.d.ts.map +1 -0
- package/dist/server/withAuth.js +261 -0
- package/dist/server/withAuth.test.d.ts +2 -0
- package/dist/server/withAuth.test.d.ts.map +1 -0
- package/dist/server/withAuth.test.js +278 -0
- package/dist/telemetry/client.d.ts +59 -0
- package/dist/telemetry/client.d.ts.map +1 -0
- package/dist/telemetry/client.js +215 -0
- package/dist/telemetry/config-writer.d.ts +16 -0
- package/dist/telemetry/config-writer.d.ts.map +1 -0
- package/dist/telemetry/config-writer.js +36 -0
- package/dist/telemetry/consent-commands.test.d.ts +2 -0
- package/dist/telemetry/consent-commands.test.d.ts.map +1 -0
- package/dist/telemetry/consent-commands.test.js +161 -0
- package/dist/telemetry/consent.d.ts +60 -0
- package/dist/telemetry/consent.d.ts.map +1 -0
- package/dist/telemetry/consent.js +122 -0
- package/dist/telemetry/environment.d.ts +33 -0
- package/dist/telemetry/environment.d.ts.map +1 -0
- package/dist/telemetry/environment.js +87 -0
- package/dist/telemetry/identifiers.d.ts +20 -0
- package/dist/telemetry/identifiers.d.ts.map +1 -0
- package/dist/telemetry/identifiers.js +86 -0
- package/dist/telemetry/index.d.ts +6 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry/telemetry.test.d.ts +2 -0
- package/dist/telemetry/telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/telemetry.test.js +1131 -0
- package/dist/telemetry/trackCommand.d.ts +32 -0
- package/dist/telemetry/trackCommand.d.ts.map +1 -0
- package/dist/telemetry/trackCommand.js +102 -0
- package/dist/telemetry/types.d.ts +92 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/package.json +83 -0
- package/src/api.test.ts +25 -0
- package/src/api.ts +163 -0
- package/src/bb-utils.ts +13 -0
- package/src/builtin-routes.ts +62 -0
- package/src/cdk/__fixtures__/factory-backend.ts +8 -0
- package/src/cdk/__fixtures__/fullid-construct-backend.ts +42 -0
- package/src/cdk/__fixtures__/handler.ts +4 -0
- package/src/cdk/__fixtures__/side-effect-backend.ts +7 -0
- package/src/cdk/blocks-backend.test.ts +207 -0
- package/src/cdk/blocks-backend.ts +249 -0
- package/src/cdk/blocks-stack.test.ts +106 -0
- package/src/cdk/config-registry.ts +106 -0
- package/src/cdk/index.ts +115 -0
- package/src/cdk/mixins.test.ts +129 -0
- package/src/cdk/mixins.ts +41 -0
- package/src/cdk/node-version.ts +17 -0
- package/src/cdk/stack-metadata.test.ts +71 -0
- package/src/cdk/stack-metadata.ts +29 -0
- package/src/cdk/synth-guard.test.ts +21 -0
- package/src/cdk/synth-guard.ts +23 -0
- package/src/client/index.test.ts +118 -0
- package/src/client/index.ts +320 -0
- package/src/common/config.test.ts +236 -0
- package/src/common/config.ts +169 -0
- package/src/common/constants.ts +15 -0
- package/src/common/crypto.ts +15 -0
- package/src/common/index.test.ts +242 -0
- package/src/common/index.ts +335 -0
- package/src/common/mock-data.ts +52 -0
- package/src/common/official-bb-names.generated.ts +29 -0
- package/src/common/sdk-registry.ts +150 -0
- package/src/constants.ts +31 -0
- package/src/cors.test.ts +279 -0
- package/src/cors.ts +91 -0
- package/src/db-naming.test.ts +48 -0
- package/src/db-naming.ts +41 -0
- package/src/errors.ts +72 -0
- package/src/hosting.test.ts +1431 -0
- package/src/hosting.ts +626 -0
- package/src/index.cdk.ts +45 -0
- package/src/index.ts +22 -0
- package/src/lambda-handler.test.ts +1005 -0
- package/src/lambda-handler.ts +706 -0
- package/src/pipeline/index.ts +12 -0
- package/src/pipeline/pipeline-construct.ts +662 -0
- package/src/pipeline/pipeline.test.ts +1565 -0
- package/src/pipeline/types.ts +384 -0
- package/src/raw-route.cdk.ts +56 -0
- package/src/raw-route.mock.ts +56 -0
- package/src/raw-route.test.ts +819 -0
- package/src/raw-route.ts +316 -0
- package/src/redact.test.ts +222 -0
- package/src/redact.ts +143 -0
- package/src/rpc.test.ts +73 -0
- package/src/rpc.ts +174 -0
- package/src/scripts/cdk-telemetry-env.test.ts +75 -0
- package/src/scripts/cdk-telemetry-env.ts +23 -0
- package/src/scripts/cleanup.ts +39 -0
- package/src/scripts/console.ts +34 -0
- package/src/scripts/deploy.ts +106 -0
- package/src/scripts/destroy.ts +42 -0
- package/src/scripts/dev-server-cors.test.ts +28 -0
- package/src/scripts/dev-server-telemetry.test.ts +127 -0
- package/src/scripts/dev-server.ts +515 -0
- package/src/scripts/ensure-secrets.test.ts +50 -0
- package/src/scripts/ensure-secrets.ts +113 -0
- package/src/scripts/external-migrations-step.test.ts +64 -0
- package/src/scripts/external-migrations-step.ts +269 -0
- package/src/scripts/extract-ts-types.test.ts +516 -0
- package/src/scripts/extract-ts-types.ts +773 -0
- package/src/scripts/generate-client-worker.ts +15 -0
- package/src/scripts/generate-client.test.ts +69 -0
- package/src/scripts/generate-client.ts +86 -0
- package/src/scripts/generate-spec-cli.test.ts +106 -0
- package/src/scripts/generate-spec-cli.ts +92 -0
- package/src/scripts/generate-spec.test.ts +99 -0
- package/src/scripts/generate-spec.ts +833 -0
- package/src/scripts/index.ts +21 -0
- package/src/scripts/sandbox.ts +230 -0
- package/src/scripts/telemetry-cli.ts +10 -0
- package/src/scripts/telemetry.test.ts +239 -0
- package/src/scripts/telemetry.ts +173 -0
- package/src/scripts/validate-spec.ts +199 -0
- package/src/server/index.ts +5 -0
- package/src/server/withAuth.test.ts +348 -0
- package/src/server/withAuth.ts +323 -0
- package/src/telemetry/client.ts +226 -0
- package/src/telemetry/config-writer.ts +52 -0
- package/src/telemetry/consent-commands.test.ts +196 -0
- package/src/telemetry/consent.ts +154 -0
- package/src/telemetry/environment.ts +100 -0
- package/src/telemetry/identifiers.ts +94 -0
- package/src/telemetry/index.ts +11 -0
- package/src/telemetry/telemetry.test.ts +1343 -0
- package/src/telemetry/trackCommand.ts +110 -0
- package/src/telemetry/types.ts +92 -0
- package/src/types.ts +33 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Generate an OpenRPC 1.3.2 specification document from backend ApiNamespace exports.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1: Introspect each `ApiNamespace`, discover methods, emit base types.
|
|
7
|
+
* Phase 2: Detect Zod schemas referenced in method bodies and emit precise
|
|
8
|
+
* JSON Schema types via Zod 4's built-in `toJSONSchema()`.
|
|
9
|
+
*
|
|
10
|
+
* The mechanism mirrors `generate-client.ts`: dynamically import the backend
|
|
11
|
+
* via `pathToFileURL`, discover `ApiNamespace` exports via `API_NAMESPACE_MARKER`,
|
|
12
|
+
* and reflect on handler methods.
|
|
13
|
+
*
|
|
14
|
+
* @see docs/native-clients/codegen-design.md — IR design decision and authoring progression
|
|
15
|
+
* @see docs/native-clients/schema-generation-guide-for-devs.md — customer-facing guide and `x-blocks-*` reference
|
|
16
|
+
*/
|
|
17
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { dirname, join } from 'path';
|
|
19
|
+
import { pathToFileURL } from 'url';
|
|
20
|
+
import { API_NAMESPACE_MARKER } from '../api.js';
|
|
21
|
+
import { extractMethodTypes, extractSkipCodegenMethods } from './extract-ts-types.js';
|
|
22
|
+
import { validateSpec } from './validate-spec.js';
|
|
23
|
+
// ── Zod detection ───────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Check if a value is a Zod schema (Zod 4+).
|
|
26
|
+
* Zod 4 schemas have a `_zod` property with `def`, `parse`, etc.
|
|
27
|
+
*/
|
|
28
|
+
function isZodSchema(val) {
|
|
29
|
+
return val != null && typeof val === 'object' && val._zod != null && typeof val._zod === 'object';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Try to convert a Zod schema to JSON Schema using Zod 4's built-in `toJSONSchema()`.
|
|
33
|
+
* Returns null if the conversion fails or Zod is not available.
|
|
34
|
+
*/
|
|
35
|
+
function zodToJsonSchema(zodModule, schema) {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof zodModule?.toJSONSchema !== 'function')
|
|
38
|
+
return null;
|
|
39
|
+
const raw = zodModule.toJSONSchema(schema);
|
|
40
|
+
// Strip the $schema field — OpenRPC components don't need it
|
|
41
|
+
const { $schema, ...rest } = raw;
|
|
42
|
+
return rest;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract the meta `id` from a Zod schema's JSON Schema output.
|
|
50
|
+
* Zod 4's `.meta({ id: 'Post' })` emits `"id": "Post"` at the top level.
|
|
51
|
+
*/
|
|
52
|
+
function getSchemaMetaId(jsonSchema) {
|
|
53
|
+
const id = jsonSchema.id;
|
|
54
|
+
if (typeof id === 'string' && id.length > 0)
|
|
55
|
+
return id;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Capitalize first letter: "kvGet" → "KvGet" */
|
|
59
|
+
function capitalize(s) {
|
|
60
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Collect all Zod schemas reachable from the backend module.
|
|
64
|
+
*
|
|
65
|
+
* Walks exported values looking for anything with `_zod`. Also walks
|
|
66
|
+
* one level into plain objects (e.g., an exported `schemas` map).
|
|
67
|
+
* Returns a map of variable-name → Zod schema instance.
|
|
68
|
+
*/
|
|
69
|
+
function collectZodSchemas(backend) {
|
|
70
|
+
const schemas = new Map();
|
|
71
|
+
for (const [name, value] of Object.entries(backend)) {
|
|
72
|
+
if (name.startsWith('_'))
|
|
73
|
+
continue;
|
|
74
|
+
if (isZodSchema(value)) {
|
|
75
|
+
schemas.set(name, value);
|
|
76
|
+
}
|
|
77
|
+
else if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
|
78
|
+
// Walk one level into plain objects (e.g., exported schema maps)
|
|
79
|
+
for (const [subName, subValue] of Object.entries(value)) {
|
|
80
|
+
if (isZodSchema(subValue)) {
|
|
81
|
+
schemas.set(subName, subValue);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return schemas;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Scan a function's source text for `.parse(` calls that reference known
|
|
90
|
+
* Zod schema variable names. Returns the first matching schema, or null.
|
|
91
|
+
*
|
|
92
|
+
* Matches patterns like:
|
|
93
|
+
* - `GetPostInput.parse(rawInput)`
|
|
94
|
+
* - `GetPostInput.safeParse(rawInput)`
|
|
95
|
+
* - `schema.parse(input)`
|
|
96
|
+
*/
|
|
97
|
+
function findZodParseSchema(fnSource, knownSchemas) {
|
|
98
|
+
for (const [name] of knownSchemas) {
|
|
99
|
+
// Match: schemaName.parse( or schemaName.safeParse(
|
|
100
|
+
const pattern = new RegExp(`\\b${escapeRegex(name)}\\s*\\.\\s*(?:safe)?[Pp]arse\\s*\\(`);
|
|
101
|
+
if (pattern.test(fnSource)) {
|
|
102
|
+
return knownSchemas.get(name) ?? null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function escapeRegex(s) {
|
|
108
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
109
|
+
}
|
|
110
|
+
// ── Param name extraction ───────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Extract parameter names from a function using `Function.toString()`.
|
|
113
|
+
*
|
|
114
|
+
* Handles:
|
|
115
|
+
* - `async foo(a, b, c) { ... }`
|
|
116
|
+
* - `async (a, b) => { ... }`
|
|
117
|
+
* - `function foo(a, b) { ... }`
|
|
118
|
+
* - Destructured params: `{ key, value }` → single param named `param0`
|
|
119
|
+
* - Default values: `a = 5` → `a`
|
|
120
|
+
* - Rest params: `...args` → `args`
|
|
121
|
+
* - Optional params: `a?` → `a` (TS syntax erased, but sometimes visible)
|
|
122
|
+
*/
|
|
123
|
+
function extractParamNames(fn) {
|
|
124
|
+
const src = fn.toString();
|
|
125
|
+
// Match the parameter list between the first ( and its matching )
|
|
126
|
+
const openParen = src.indexOf('(');
|
|
127
|
+
if (openParen === -1)
|
|
128
|
+
return [];
|
|
129
|
+
// Find matching close paren, accounting for nested parens
|
|
130
|
+
let depth = 0;
|
|
131
|
+
let closeParen = -1;
|
|
132
|
+
for (let i = openParen; i < src.length; i++) {
|
|
133
|
+
if (src[i] === '(')
|
|
134
|
+
depth++;
|
|
135
|
+
else if (src[i] === ')') {
|
|
136
|
+
depth--;
|
|
137
|
+
if (depth === 0) {
|
|
138
|
+
closeParen = i;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (closeParen === -1)
|
|
144
|
+
return [];
|
|
145
|
+
const paramStr = src.substring(openParen + 1, closeParen).trim();
|
|
146
|
+
if (!paramStr)
|
|
147
|
+
return [];
|
|
148
|
+
// Split on commas, but respect nested braces/brackets/parens
|
|
149
|
+
const params = [];
|
|
150
|
+
let current = '';
|
|
151
|
+
let nestDepth = 0;
|
|
152
|
+
for (const ch of paramStr) {
|
|
153
|
+
if (ch === '(' || ch === '{' || ch === '[')
|
|
154
|
+
nestDepth++;
|
|
155
|
+
else if (ch === ')' || ch === '}' || ch === ']')
|
|
156
|
+
nestDepth--;
|
|
157
|
+
else if (ch === ',' && nestDepth === 0) {
|
|
158
|
+
params.push(current.trim());
|
|
159
|
+
current = '';
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
current += ch;
|
|
163
|
+
}
|
|
164
|
+
if (current.trim())
|
|
165
|
+
params.push(current.trim());
|
|
166
|
+
return params.map((raw, i) => {
|
|
167
|
+
// Destructured: { key, value } → param{i}
|
|
168
|
+
if (raw.startsWith('{') || raw.startsWith('['))
|
|
169
|
+
return `param${i}`;
|
|
170
|
+
// Rest: ...args → args
|
|
171
|
+
if (raw.startsWith('...'))
|
|
172
|
+
raw = raw.slice(3);
|
|
173
|
+
// Strip default value: a = 5 → a
|
|
174
|
+
const eqIdx = raw.indexOf('=');
|
|
175
|
+
if (eqIdx !== -1)
|
|
176
|
+
raw = raw.substring(0, eqIdx);
|
|
177
|
+
// Strip TS type annotation: a: string → a (visible in some runtimes)
|
|
178
|
+
const colonIdx = raw.indexOf(':');
|
|
179
|
+
if (colonIdx !== -1)
|
|
180
|
+
raw = raw.substring(0, colonIdx);
|
|
181
|
+
// Strip optional marker: a? → a
|
|
182
|
+
raw = raw.replace(/\?$/, '');
|
|
183
|
+
return raw.trim();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const defaultLoader = (url) => import(url);
|
|
187
|
+
export async function generateSpec(foundationPath, loader = defaultLoader) {
|
|
188
|
+
// Reuse the same global-collector pattern as generate-client.ts so that
|
|
189
|
+
// Building Block constructors don't fail during import.
|
|
190
|
+
const collectorAlreadyActive = Array.isArray(globalThis.__BLOCKS_CLIENT_MIDDLEWARE__);
|
|
191
|
+
if (!collectorAlreadyActive) {
|
|
192
|
+
globalThis.__BLOCKS_CLIENT_MIDDLEWARE__ = [];
|
|
193
|
+
}
|
|
194
|
+
const backend = await loader(pathToFileURL(foundationPath).href);
|
|
195
|
+
if (!collectorAlreadyActive) {
|
|
196
|
+
delete globalThis.__BLOCKS_CLIENT_MIDDLEWARE__;
|
|
197
|
+
}
|
|
198
|
+
// Try to import Zod from the backend's dependency tree.
|
|
199
|
+
// If Zod isn't installed, Phase 2 features are skipped gracefully.
|
|
200
|
+
let zodModule = null;
|
|
201
|
+
try {
|
|
202
|
+
zodModule = await import('zod');
|
|
203
|
+
// Zod 4 re-exports from a nested module; normalize
|
|
204
|
+
if (zodModule.z && typeof zodModule.z.toJSONSchema === 'function') {
|
|
205
|
+
zodModule = zodModule.z;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Zod not available — all schemas will be { type: "unknown" }
|
|
210
|
+
}
|
|
211
|
+
// Collect Zod schemas from module exports
|
|
212
|
+
const knownSchemas = collectZodSchemas(backend);
|
|
213
|
+
// Extract TypeScript type info from the source file (AST-based).
|
|
214
|
+
// Used as fallback for methods without Zod schemas — gives base types
|
|
215
|
+
// (string, number, boolean, object shapes) instead of "unknown".
|
|
216
|
+
let tsTypes = new Map();
|
|
217
|
+
try {
|
|
218
|
+
tsTypes = extractMethodTypes(foundationPath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// TS parsing failed — fall back to unknown types
|
|
222
|
+
}
|
|
223
|
+
// Pure-AST scan for `@blocksSkipCodegen`-tagged methods. Runs even when
|
|
224
|
+
// `extractMethodTypes` blows up on a real-world TS program (a common
|
|
225
|
+
// scenario: missing tsconfig paths, third-party `.d.ts` mismatches).
|
|
226
|
+
let skipCodegenNames = new Set();
|
|
227
|
+
try {
|
|
228
|
+
skipCodegenNames = extractSkipCodegenMethods(foundationPath);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// AST parse failed — nothing to skip.
|
|
232
|
+
}
|
|
233
|
+
const methods = [];
|
|
234
|
+
const componentSchemas = {};
|
|
235
|
+
for (const [exportName, exportValue] of Object.entries(backend)) {
|
|
236
|
+
if (exportName.startsWith('_'))
|
|
237
|
+
continue;
|
|
238
|
+
const namespaceName = exportValue?.[API_NAMESPACE_MARKER];
|
|
239
|
+
if (typeof namespaceName !== 'string')
|
|
240
|
+
continue;
|
|
241
|
+
// Use the export name for the qualified method name — this matches
|
|
242
|
+
// how the dev server and Lambda handler register APIs (by export name,
|
|
243
|
+
// not the ApiNamespace() first argument).
|
|
244
|
+
const routingName = exportName;
|
|
245
|
+
// ApiNamespace is a function (context) => { method1, method2, ... }
|
|
246
|
+
// Call it with a stub context to get the method map.
|
|
247
|
+
const stubContext = createStubContext();
|
|
248
|
+
let methodMap;
|
|
249
|
+
try {
|
|
250
|
+
const result = typeof exportValue === 'function' ? exportValue(stubContext) : exportValue;
|
|
251
|
+
methodMap = result && typeof result === 'object' ? result : {};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// If the handler throws during stub invocation, skip this namespace
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const [methodName, methodFn] of Object.entries(methodMap)) {
|
|
258
|
+
if (typeof methodFn !== 'function')
|
|
259
|
+
continue;
|
|
260
|
+
// `@blocksSkipCodegen` is a JSDoc tag the customer puts on a method
|
|
261
|
+
// they want callable from the generated TS client (Proxy-based,
|
|
262
|
+
// doesn't enumerate) but absent from the OpenRPC spec — so native
|
|
263
|
+
// generators (Swift / Kotlin) never emit a binding for it.
|
|
264
|
+
// Mock-only / dev-only helpers like `getLastCode` use this.
|
|
265
|
+
//
|
|
266
|
+
// Two sources because `extractMethodTypes` can fail-soft on
|
|
267
|
+
// type-resolution errors and silently empty its Map — we always
|
|
268
|
+
// want the JSDoc tag to be honored, so the AST-only scan acts as
|
|
269
|
+
// a safety net.
|
|
270
|
+
if (tsTypes.get(methodName)?.skipCodegen || skipCodegenNames.has(methodName))
|
|
271
|
+
continue;
|
|
272
|
+
const qualifiedName = `${routingName}.${methodName}`;
|
|
273
|
+
const resultName = capitalize(methodName) + 'Result';
|
|
274
|
+
const paramNames = extractParamNames(methodFn);
|
|
275
|
+
const fnSource = methodFn.toString();
|
|
276
|
+
// Phase 2: try to find a Zod input schema referenced in the method body
|
|
277
|
+
const inputSchema = zodModule ? findZodParseSchema(fnSource, knownSchemas) : null;
|
|
278
|
+
let inputJsonSchema = null;
|
|
279
|
+
if (inputSchema && zodModule) {
|
|
280
|
+
inputJsonSchema = zodToJsonSchema(zodModule, inputSchema);
|
|
281
|
+
}
|
|
282
|
+
let params;
|
|
283
|
+
if (inputJsonSchema && inputJsonSchema.type === 'object' && inputJsonSchema.properties) {
|
|
284
|
+
// Zod object schema detected — emit one param per property
|
|
285
|
+
const required = new Set(Array.isArray(inputJsonSchema.required) ? inputJsonSchema.required : []);
|
|
286
|
+
const metaId = getSchemaMetaId(inputJsonSchema);
|
|
287
|
+
if (metaId) {
|
|
288
|
+
// Named schema → place in components.schemas, reference via $ref
|
|
289
|
+
const { id: _id, additionalProperties: _ap, ...cleanSchema } = inputJsonSchema;
|
|
290
|
+
componentSchemas[metaId] = cleanSchema;
|
|
291
|
+
}
|
|
292
|
+
params = Object.entries(inputJsonSchema.properties).map(([propName, propSchema]) => ({
|
|
293
|
+
name: propName,
|
|
294
|
+
required: required.has(propName),
|
|
295
|
+
schema: cleanJsonSchema(propSchema),
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
else if (inputJsonSchema) {
|
|
299
|
+
// Non-object Zod schema (e.g., z.string()) — single param
|
|
300
|
+
params = paramNames.map((name) => ({
|
|
301
|
+
name,
|
|
302
|
+
required: true,
|
|
303
|
+
schema: cleanJsonSchema(inputJsonSchema),
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// No Zod schema — fall back to TypeScript AST types
|
|
308
|
+
const tsInfo = tsTypes.get(methodName);
|
|
309
|
+
if (tsInfo) {
|
|
310
|
+
params = tsInfo.params.map((p) => ({
|
|
311
|
+
name: p.name,
|
|
312
|
+
required: p.required,
|
|
313
|
+
schema: p.schema.type === 'unknown' ? { type: 'unknown' } : p.schema,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// No TS type info either — emit unknown
|
|
318
|
+
params = paramNames.map((name) => ({
|
|
319
|
+
name,
|
|
320
|
+
required: true,
|
|
321
|
+
schema: { type: 'unknown' },
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Return type: Zod doesn't cover this yet, so use TS types
|
|
326
|
+
const tsInfo = tsTypes.get(methodName);
|
|
327
|
+
const rawResultSchema = tsInfo?.returnType && tsInfo.returnType.type !== 'unknown'
|
|
328
|
+
? tsInfo.returnType
|
|
329
|
+
: { type: 'unknown' };
|
|
330
|
+
// Determine result schema — transferable types get semantic annotations
|
|
331
|
+
let resultSchema;
|
|
332
|
+
if (tsInfo?.transferable) {
|
|
333
|
+
// Emit x-blocks-transferable instead of raw object schema
|
|
334
|
+
const transferableSchema = {
|
|
335
|
+
'x-blocks-transferable': tsInfo.transferable.blocksTag,
|
|
336
|
+
};
|
|
337
|
+
if (tsInfo.transferable.typeArgs.length > 0) {
|
|
338
|
+
// Hoist named type args to components.schemas, build the type-args array
|
|
339
|
+
const typeArgsSchemas = [];
|
|
340
|
+
for (const argSchema of tsInfo.transferable.typeArgs) {
|
|
341
|
+
const argTitle = argSchema.title;
|
|
342
|
+
if (argTitle && argSchema.type === 'object' && argSchema.properties) {
|
|
343
|
+
const { title: _t, ...cleanArg } = argSchema;
|
|
344
|
+
if (!componentSchemas[argTitle]) {
|
|
345
|
+
componentSchemas[argTitle] = cleanArg;
|
|
346
|
+
}
|
|
347
|
+
typeArgsSchemas.push({ $ref: `#/components/schemas/${argTitle}` });
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
typeArgsSchemas.push(argSchema);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
transferableSchema['x-blocks-type-args'] = typeArgsSchemas;
|
|
354
|
+
}
|
|
355
|
+
resultSchema = transferableSchema;
|
|
356
|
+
}
|
|
357
|
+
else if (rawResultSchema.title && rawResultSchema.type === 'object' && rawResultSchema.properties) {
|
|
358
|
+
// Named object type → extract to components.schemas
|
|
359
|
+
const returnTitle = rawResultSchema.title;
|
|
360
|
+
const { title: _t, ...cleanReturn } = rawResultSchema;
|
|
361
|
+
if (!componentSchemas[returnTitle]) {
|
|
362
|
+
componentSchemas[returnTitle] = cleanReturn;
|
|
363
|
+
}
|
|
364
|
+
resultSchema = { $ref: `#/components/schemas/${returnTitle}` };
|
|
365
|
+
}
|
|
366
|
+
else if (rawResultSchema.type === 'array' && rawResultSchema.items) {
|
|
367
|
+
// Check if array items have a named type (e.g., Todo[] → extract Todo)
|
|
368
|
+
const items = rawResultSchema.items;
|
|
369
|
+
const itemsTitle = items.title;
|
|
370
|
+
if (itemsTitle && items.type === 'object' && items.properties) {
|
|
371
|
+
const { title: _t, ...cleanItems } = items;
|
|
372
|
+
if (!componentSchemas[itemsTitle]) {
|
|
373
|
+
componentSchemas[itemsTitle] = cleanItems;
|
|
374
|
+
}
|
|
375
|
+
resultSchema = { type: 'array', items: { $ref: `#/components/schemas/${itemsTitle}` } };
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
resultSchema = rawResultSchema;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
resultSchema = rawResultSchema;
|
|
383
|
+
}
|
|
384
|
+
methods.push({
|
|
385
|
+
name: qualifiedName,
|
|
386
|
+
params,
|
|
387
|
+
result: {
|
|
388
|
+
name: resultName,
|
|
389
|
+
schema: resultSchema,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Sort methods alphabetically for stable output
|
|
395
|
+
methods.sort((a, b) => a.name.localeCompare(b.name));
|
|
396
|
+
// Post-process step 1: regroup nested-union arms that share a primary
|
|
397
|
+
// discriminator. Run BEFORE hoisting so the regrouped wrapper picks up
|
|
398
|
+
// the named title (if any) in one go and codegen sees a single arm per
|
|
399
|
+
// action name.
|
|
400
|
+
for (const method of methods) {
|
|
401
|
+
for (const param of method.params) {
|
|
402
|
+
param.schema = regroupSharedDiscriminator(param.schema);
|
|
403
|
+
}
|
|
404
|
+
method.result.schema = regroupSharedDiscriminator(method.result.schema);
|
|
405
|
+
}
|
|
406
|
+
for (const [name, schema] of Object.entries(componentSchemas)) {
|
|
407
|
+
componentSchemas[name] = regroupSharedDiscriminator(schema);
|
|
408
|
+
}
|
|
409
|
+
// Post-process step 2: walk all schemas and hoist named types (objects with `title`)
|
|
410
|
+
// to components.schemas with $ref pointers. This catches named types nested
|
|
411
|
+
// inside other schemas (e.g., AuthUser inside AuthState.user oneOf).
|
|
412
|
+
for (const method of methods) {
|
|
413
|
+
for (const param of method.params) {
|
|
414
|
+
param.schema = hoistNamedSchemas(param.schema, componentSchemas);
|
|
415
|
+
}
|
|
416
|
+
method.result.schema = hoistNamedSchemas(method.result.schema, componentSchemas);
|
|
417
|
+
}
|
|
418
|
+
// Also walk component schemas themselves (e.g., AuthState contains AuthAction contains AuthField)
|
|
419
|
+
for (const [name, schema] of Object.entries(componentSchemas)) {
|
|
420
|
+
componentSchemas[name] = hoistNamedSchemas(schema, componentSchemas, name);
|
|
421
|
+
}
|
|
422
|
+
const doc = {
|
|
423
|
+
openrpc: '1.3.2',
|
|
424
|
+
info: { title: 'api', version: '1.0.0' },
|
|
425
|
+
methods,
|
|
426
|
+
};
|
|
427
|
+
if (Object.keys(componentSchemas).length > 0) {
|
|
428
|
+
doc.components = { schemas: componentSchemas };
|
|
429
|
+
}
|
|
430
|
+
return doc;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Recursively walk a JSON Schema tree and hoist any named object types
|
|
434
|
+
* (those with a `title` property) to `componentSchemas` with `$ref` pointers.
|
|
435
|
+
*
|
|
436
|
+
* This ensures types like `AuthUser` (nested inside `AuthState.user`) get
|
|
437
|
+
* extracted to `components.schemas` so native codegen produces named types
|
|
438
|
+
* instead of anonymous inline structs.
|
|
439
|
+
*
|
|
440
|
+
* @param skipTitle - Don't hoist the schema itself if its title matches this
|
|
441
|
+
* (prevents a component schema from replacing itself with a self-$ref).
|
|
442
|
+
*/
|
|
443
|
+
function hoistNamedSchemas(schema, componentSchemas, skipTitle) {
|
|
444
|
+
if (!schema || typeof schema !== 'object')
|
|
445
|
+
return schema;
|
|
446
|
+
// If this is a $ref, nothing to do
|
|
447
|
+
if (schema.$ref)
|
|
448
|
+
return schema;
|
|
449
|
+
// If this is a named object type, hoist it (unless it's the schema we're currently inside
|
|
450
|
+
// or it's a built-in JS type that shouldn't become a component schema)
|
|
451
|
+
const BUILTIN_TYPES = new Set(['Date', 'RegExp', 'Error', 'Map', 'Set', 'Promise', 'ArrayBuffer', 'URL']);
|
|
452
|
+
const title = schema.title;
|
|
453
|
+
if (title && title !== skipTitle && !BUILTIN_TYPES.has(title) && schema.type === 'object' && schema.properties) {
|
|
454
|
+
const { title: _t, ...cleanSchema } = schema;
|
|
455
|
+
if (!componentSchemas[title]) {
|
|
456
|
+
componentSchemas[title] = cleanSchema;
|
|
457
|
+
// Recursively hoist within the newly added schema
|
|
458
|
+
componentSchemas[title] = hoistNamedSchemas(cleanSchema, componentSchemas, title);
|
|
459
|
+
}
|
|
460
|
+
return { $ref: `#/components/schemas/${title}` };
|
|
461
|
+
}
|
|
462
|
+
// Walk properties
|
|
463
|
+
if (schema.properties) {
|
|
464
|
+
const newProps = {};
|
|
465
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
466
|
+
newProps[key] = hoistNamedSchemas(val, componentSchemas, skipTitle);
|
|
467
|
+
}
|
|
468
|
+
schema = { ...schema, properties: newProps };
|
|
469
|
+
}
|
|
470
|
+
// Walk array items
|
|
471
|
+
if (schema.items) {
|
|
472
|
+
if (Array.isArray(schema.items)) {
|
|
473
|
+
schema = { ...schema, items: schema.items.map((i) => hoistNamedSchemas(i, componentSchemas, skipTitle)) };
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
schema = { ...schema, items: hoistNamedSchemas(schema.items, componentSchemas, skipTitle) };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Walk oneOf
|
|
480
|
+
if (Array.isArray(schema.oneOf)) {
|
|
481
|
+
schema = { ...schema, oneOf: schema.oneOf.map((s) => hoistNamedSchemas(s, componentSchemas, skipTitle)) };
|
|
482
|
+
}
|
|
483
|
+
// Walk additionalProperties
|
|
484
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
485
|
+
schema = { ...schema, additionalProperties: hoistNamedSchemas(schema.additionalProperties, componentSchemas, skipTitle) };
|
|
486
|
+
}
|
|
487
|
+
return schema;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Strip Zod-generated noise from a schema for cleaner OpenRPC output.
|
|
491
|
+
* Removes `$schema` and `id` (meta) fields. Preserves
|
|
492
|
+
* `additionalProperties` because it carries open-shape semantics for
|
|
493
|
+
* intersections like `T & Record<string, V>` (e.g. Cognito sign-up's
|
|
494
|
+
* custom-attribute payload) that consumer codegen needs to honor.
|
|
495
|
+
*/
|
|
496
|
+
function cleanJsonSchema(schema) {
|
|
497
|
+
const { $schema, id, ...rest } = schema;
|
|
498
|
+
return rest;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* If a `oneOf` arose from the TS pattern `{ action: K } & U[K]` where `U[K]`
|
|
502
|
+
* is itself a union (e.g. Cognito's `confirmSignIn` challenge variants),
|
|
503
|
+
* the intersection distributes — producing N flat sibling arms that all
|
|
504
|
+
* share a primary discriminator (`action: 'confirmSignIn'`) plus a few
|
|
505
|
+
* shared base fields. Native codegen sees N look-alike structs and falls
|
|
506
|
+
* back to numeric suffixes (`ConfirmSignIn_1`, `ConfirmSignIn_2`, …).
|
|
507
|
+
*
|
|
508
|
+
* This pass walks each `oneOf` and, for any group of arms whose primary
|
|
509
|
+
* discriminator collapses to the same single literal value, regroups them
|
|
510
|
+
* under one outer arm with a nested `oneOf` over the remaining
|
|
511
|
+
* (truly-distinguishing) per-arm fields. A secondary discriminator field —
|
|
512
|
+
* one whose literal value differs across the group — is kept per-arm so
|
|
513
|
+
* the inner `oneOf` stays a discriminated union.
|
|
514
|
+
*/
|
|
515
|
+
function regroupSharedDiscriminator(schema) {
|
|
516
|
+
if (!schema || typeof schema !== 'object')
|
|
517
|
+
return schema;
|
|
518
|
+
// Recurse first so nested oneOfs are normalized bottom-up.
|
|
519
|
+
if (schema.properties) {
|
|
520
|
+
const newProps = {};
|
|
521
|
+
for (const [k, v] of Object.entries(schema.properties)) {
|
|
522
|
+
newProps[k] = regroupSharedDiscriminator(v);
|
|
523
|
+
}
|
|
524
|
+
schema = { ...schema, properties: newProps };
|
|
525
|
+
}
|
|
526
|
+
if (schema.items) {
|
|
527
|
+
if (Array.isArray(schema.items)) {
|
|
528
|
+
schema = { ...schema, items: schema.items.map((s) => regroupSharedDiscriminator(s)) };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
schema = { ...schema, items: regroupSharedDiscriminator(schema.items) };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
535
|
+
schema = { ...schema, additionalProperties: regroupSharedDiscriminator(schema.additionalProperties) };
|
|
536
|
+
}
|
|
537
|
+
if (!Array.isArray(schema.oneOf))
|
|
538
|
+
return schema;
|
|
539
|
+
const arms = schema.oneOf.map((a) => regroupSharedDiscriminator(a));
|
|
540
|
+
// Group arms by their primary discriminator literal value. We pick the
|
|
541
|
+
// discriminator as the field that has a single-literal `const`/`enum`
|
|
542
|
+
// value AND appears with the same literal across every member of a
|
|
543
|
+
// candidate group. Fall back: most-common-literal field name.
|
|
544
|
+
const primary = findPrimaryDiscriminator(arms);
|
|
545
|
+
if (!primary)
|
|
546
|
+
return { ...schema, oneOf: arms };
|
|
547
|
+
const groups = new Map();
|
|
548
|
+
const ungrouped = [];
|
|
549
|
+
for (const arm of arms) {
|
|
550
|
+
const lit = singleLiteralValue(arm, primary);
|
|
551
|
+
if (lit === undefined) {
|
|
552
|
+
ungrouped.push(arm);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const list = groups.get(lit) ?? [];
|
|
556
|
+
list.push(arm);
|
|
557
|
+
groups.set(lit, list);
|
|
558
|
+
}
|
|
559
|
+
const out = [...ungrouped];
|
|
560
|
+
for (const [literal, members] of groups) {
|
|
561
|
+
if (members.length < 2) {
|
|
562
|
+
out.push(...members);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
// Compute fields shared across every member with the same shape
|
|
566
|
+
// (same single-literal value or same schema). Any field whose
|
|
567
|
+
// single-literal value DIFFERS across members must stay per-arm.
|
|
568
|
+
const sharedFieldNames = new Set(sharedRequiredProperties(members));
|
|
569
|
+
// Drop fields that have differing single-literals — those are the
|
|
570
|
+
// secondary discriminator and must remain on each inner arm.
|
|
571
|
+
for (const name of [...sharedFieldNames]) {
|
|
572
|
+
const valuesAcrossGroup = new Set();
|
|
573
|
+
let hasNonLiteral = false;
|
|
574
|
+
for (const m of members) {
|
|
575
|
+
const v = singleLiteralValue(m, name);
|
|
576
|
+
if (v === undefined) {
|
|
577
|
+
hasNonLiteral = true;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
valuesAcrossGroup.add(v);
|
|
581
|
+
}
|
|
582
|
+
if (hasNonLiteral)
|
|
583
|
+
continue;
|
|
584
|
+
if (valuesAcrossGroup.size > 1)
|
|
585
|
+
sharedFieldNames.delete(name);
|
|
586
|
+
}
|
|
587
|
+
const sharedProps = {};
|
|
588
|
+
const sharedRequired = [];
|
|
589
|
+
const memberProps0 = members[0].properties ?? {};
|
|
590
|
+
const memberRequired0 = members[0].required ?? [];
|
|
591
|
+
for (const name of sharedFieldNames) {
|
|
592
|
+
if (memberProps0[name])
|
|
593
|
+
sharedProps[name] = memberProps0[name];
|
|
594
|
+
if (memberRequired0.includes(name))
|
|
595
|
+
sharedRequired.push(name);
|
|
596
|
+
}
|
|
597
|
+
// Each inner arm gets the per-arm subset (everything not in shared).
|
|
598
|
+
const innerArms = members.map((m) => {
|
|
599
|
+
const props = m.properties ?? {};
|
|
600
|
+
const req = m.required ?? [];
|
|
601
|
+
const innerProps = {};
|
|
602
|
+
const innerReq = [];
|
|
603
|
+
for (const [k, v] of Object.entries(props)) {
|
|
604
|
+
if (sharedFieldNames.has(k))
|
|
605
|
+
continue;
|
|
606
|
+
innerProps[k] = v;
|
|
607
|
+
if (req.includes(k))
|
|
608
|
+
innerReq.push(k);
|
|
609
|
+
}
|
|
610
|
+
const inner = { type: 'object', properties: innerProps };
|
|
611
|
+
if (innerReq.length > 0)
|
|
612
|
+
inner.required = innerReq;
|
|
613
|
+
return inner;
|
|
614
|
+
});
|
|
615
|
+
const grouped = {
|
|
616
|
+
type: 'object',
|
|
617
|
+
properties: sharedProps,
|
|
618
|
+
...(sharedRequired.length > 0 ? { required: sharedRequired } : {}),
|
|
619
|
+
oneOf: innerArms,
|
|
620
|
+
};
|
|
621
|
+
// preserve any x-blocks-* annotations that were on each arm by
|
|
622
|
+
// promoting the first member's (they all matched on the
|
|
623
|
+
// primary discriminator literal anyway)
|
|
624
|
+
const m0 = members[0];
|
|
625
|
+
const annotationKeys = Object.keys(m0).filter((k) => k.startsWith('x-blocks-'));
|
|
626
|
+
for (const ak of annotationKeys)
|
|
627
|
+
grouped[ak] = m0[ak];
|
|
628
|
+
out.push(grouped);
|
|
629
|
+
}
|
|
630
|
+
const next = { ...schema, oneOf: out };
|
|
631
|
+
return next;
|
|
632
|
+
}
|
|
633
|
+
function findPrimaryDiscriminator(arms) {
|
|
634
|
+
if (arms.length === 0)
|
|
635
|
+
return null;
|
|
636
|
+
const counts = new Map();
|
|
637
|
+
for (const arm of arms) {
|
|
638
|
+
const props = arm.properties ?? {};
|
|
639
|
+
for (const [name, sub] of Object.entries(props)) {
|
|
640
|
+
const lit = singleLiteralValueFromSchema(sub);
|
|
641
|
+
if (lit === undefined)
|
|
642
|
+
continue;
|
|
643
|
+
let m = counts.get(name);
|
|
644
|
+
if (!m) {
|
|
645
|
+
m = new Map();
|
|
646
|
+
counts.set(name, m);
|
|
647
|
+
}
|
|
648
|
+
m.set(lit, (m.get(lit) ?? 0) + 1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
let best = null;
|
|
652
|
+
let bestScore = 0;
|
|
653
|
+
for (const [name, valueCounts] of counts) {
|
|
654
|
+
// score = max group size for this field (bigger collapses first)
|
|
655
|
+
let maxGroup = 0;
|
|
656
|
+
for (const c of valueCounts.values())
|
|
657
|
+
if (c > maxGroup)
|
|
658
|
+
maxGroup = c;
|
|
659
|
+
if (maxGroup > bestScore) {
|
|
660
|
+
bestScore = maxGroup;
|
|
661
|
+
best = name;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return bestScore >= 2 ? best : null;
|
|
665
|
+
}
|
|
666
|
+
function singleLiteralValue(arm, field) {
|
|
667
|
+
const props = arm.properties ?? {};
|
|
668
|
+
const sub = props[field];
|
|
669
|
+
if (!sub)
|
|
670
|
+
return undefined;
|
|
671
|
+
return singleLiteralValueFromSchema(sub);
|
|
672
|
+
}
|
|
673
|
+
function singleLiteralValueFromSchema(sub) {
|
|
674
|
+
if (typeof sub.const === 'string')
|
|
675
|
+
return sub.const;
|
|
676
|
+
if (Array.isArray(sub.enum) && sub.enum.length === 1 && typeof sub.enum[0] === 'string') {
|
|
677
|
+
return sub.enum[0];
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
function sharedRequiredProperties(members) {
|
|
682
|
+
if (members.length === 0)
|
|
683
|
+
return [];
|
|
684
|
+
const first = members[0];
|
|
685
|
+
const firstProps = first.properties ?? {};
|
|
686
|
+
const candidate = new Set(Object.keys(firstProps));
|
|
687
|
+
for (let i = 1; i < members.length; i++) {
|
|
688
|
+
const props = members[i].properties ?? {};
|
|
689
|
+
for (const name of [...candidate]) {
|
|
690
|
+
if (!(name in props))
|
|
691
|
+
candidate.delete(name);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return [...candidate];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Generate the OpenRPC spec, validate it, and write it to disk as `blocks.spec.json`.
|
|
698
|
+
* Logs validation warnings but does not fail — the spec is still written.
|
|
699
|
+
*/
|
|
700
|
+
export async function writeSpec(foundationPath, outputPath, loader) {
|
|
701
|
+
const doc = await generateSpec(foundationPath, loader);
|
|
702
|
+
// Populate servers array from deployed config if available
|
|
703
|
+
const projectRoot = dirname(dirname(foundationPath));
|
|
704
|
+
const configPath = join(projectRoot, '.blocks-sandbox', 'config.json');
|
|
705
|
+
if (existsSync(configPath)) {
|
|
706
|
+
try {
|
|
707
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
708
|
+
if (config.apiUrl) {
|
|
709
|
+
doc.servers = [{ name: config.environment || 'default', url: config.apiUrl }];
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
713
|
+
}
|
|
714
|
+
// Validate conformance
|
|
715
|
+
const errors = validateSpec(doc);
|
|
716
|
+
if (errors.length > 0) {
|
|
717
|
+
console.warn(`⚠️ OpenRPC spec has ${errors.length} conformance issue(s):`);
|
|
718
|
+
for (const err of errors) {
|
|
719
|
+
console.warn(` ${err.path}: ${err.message}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
723
|
+
writeFileSync(outputPath, JSON.stringify(doc, null, '\t'));
|
|
724
|
+
}
|
|
725
|
+
// ── Stub context ────────────────────────────────────────────────────────────
|
|
726
|
+
/**
|
|
727
|
+
* Create a minimal stub `BlocksContext` so that `ApiNamespace` handler functions
|
|
728
|
+
* can be invoked for method discovery without a real HTTP request.
|
|
729
|
+
*/
|
|
730
|
+
function createStubContext() {
|
|
731
|
+
const headers = new Headers();
|
|
732
|
+
return {
|
|
733
|
+
request: {
|
|
734
|
+
headers,
|
|
735
|
+
body: null,
|
|
736
|
+
json: async () => ({}),
|
|
737
|
+
text: async () => '',
|
|
738
|
+
params: {},
|
|
739
|
+
},
|
|
740
|
+
response: {
|
|
741
|
+
headers: new Headers(),
|
|
742
|
+
status: 200,
|
|
743
|
+
send: () => { },
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
}
|