@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
package/src/raw-route.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { BlocksContext } from './api.js';
|
|
5
|
+
import type { ScopeParent } from './common/index.js';
|
|
6
|
+
import { BLOCKS_NAMESPACE, BLOCKS_RPC_PREFIX } from './constants.js';
|
|
7
|
+
|
|
8
|
+
// ── Public types ────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
11
|
+
|
|
12
|
+
export interface RawRouteOptions {
|
|
13
|
+
method: HttpMethod;
|
|
14
|
+
/**
|
|
15
|
+
* URL path pattern. When provided, used exactly as given.
|
|
16
|
+
* When omitted, derived from the scope-chain IDs.
|
|
17
|
+
*
|
|
18
|
+
* - `/health` — exact match
|
|
19
|
+
* - `/users/{id}` — named path parameter (captures one segment)
|
|
20
|
+
* - `/v1/*` — wildcard (captures everything after prefix)
|
|
21
|
+
*
|
|
22
|
+
* **Scope-chain derivation:** If omitted, the path is built by collecting
|
|
23
|
+
* each ancestor scope's `id` (excluding the root BlocksStack) and joining
|
|
24
|
+
* with `/`. For example, `BlocksStack('app') → Scope('v1') → RawRoute('health')`
|
|
25
|
+
* yields `/v1/health`.
|
|
26
|
+
*/
|
|
27
|
+
path?: string;
|
|
28
|
+
handler: (context: BlocksContext) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Typed error constants for RawRoute. Use with `isBlocksError()` in catch blocks.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* try {
|
|
37
|
+
* new RawRoute(scope, 'dup', { method: 'GET', path: '/health', handler });
|
|
38
|
+
* } catch (e: unknown) {
|
|
39
|
+
* if (isBlocksError(e, RawRouteErrors.DuplicateRoute)) {
|
|
40
|
+
* // same method+path already registered
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const RawRouteErrors = {
|
|
46
|
+
DuplicateRoute: 'DuplicateRouteException',
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
// ── Scope-chain path derivation ─────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Derive a URL path from the scope chain.
|
|
53
|
+
*
|
|
54
|
+
* Walks the parent chain from `scope` up to (but NOT including) the root
|
|
55
|
+
* BlocksStack or the top-level user Scope, collects each intermediate scope's
|
|
56
|
+
* `id`, URL-encodes segments, and joins with `/`. The RawRoute's own `id`
|
|
57
|
+
* is appended as the final segment.
|
|
58
|
+
*
|
|
59
|
+
* A scope is "root-level" if its parent has no `parent` property (i.e., it's
|
|
60
|
+
* the BlocksStack or the fallback `{ id: ... }` sentinel). Root-level scopes
|
|
61
|
+
* are excluded from the path — they represent the application boundary, not
|
|
62
|
+
* a URL segment.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```
|
|
66
|
+
* Scope('app') → RawRoute('health') → /health
|
|
67
|
+
* Scope('app') → Scope('v1') → RawRoute('users') → /v1/users
|
|
68
|
+
* Scope('app') → Scope('api') → Scope('v2') → RawRoute('items') → /api/v2/items
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function derivePathFromScope(scope: ScopeParent, id: string): string {
|
|
72
|
+
const segments: string[] = [];
|
|
73
|
+
|
|
74
|
+
let current: ScopeParent | undefined = scope;
|
|
75
|
+
while (current && 'parent' in current && current.parent) {
|
|
76
|
+
const parentNode: ScopeParent = (current as { parent: ScopeParent }).parent;
|
|
77
|
+
const parentIsRoot = !('parent' in parentNode) || !(parentNode as any).parent;
|
|
78
|
+
if (parentIsRoot) break;
|
|
79
|
+
segments.unshift(encodeURIComponent(current.id));
|
|
80
|
+
current = parentNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
segments.push(encodeURIComponent(id));
|
|
84
|
+
return '/' + segments.join('/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the effective path for a RawRoute — uses the explicit `path` if
|
|
89
|
+
* provided, otherwise derives it from the scope chain.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveRoutePath(scope: ScopeParent, id: string, options: RawRouteOptions): string {
|
|
92
|
+
return options.path ?? derivePathFromScope(scope, id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Route registry ──────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface RegisteredRoute {
|
|
98
|
+
method: string;
|
|
99
|
+
/** Normalized path (trailing slashes removed, double slashes collapsed). */
|
|
100
|
+
path: string;
|
|
101
|
+
/** Compiled regex for matching incoming request paths. */
|
|
102
|
+
pattern: RegExp;
|
|
103
|
+
/** Extracted parameter names in capture-group order. */
|
|
104
|
+
paramNames: string[];
|
|
105
|
+
handler: (context: BlocksContext) => Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const routeRegistry: RegisteredRoute[] = [];
|
|
109
|
+
let registrationLocked = false;
|
|
110
|
+
|
|
111
|
+
const VALID_METHODS = new Set<string>(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
112
|
+
|
|
113
|
+
/** Prevent further route registration. Call after initialization is complete. */
|
|
114
|
+
export function lockRouteRegistry(): void {
|
|
115
|
+
registrationLocked = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Re-allow route registration (useful in tests after lock). */
|
|
119
|
+
export function unlockRouteRegistry(): void {
|
|
120
|
+
registrationLocked = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register a raw HTTP route for runtime dispatch.
|
|
125
|
+
*
|
|
126
|
+
* **Internal.** The public API is `new RawRoute(scope, id, options)`.
|
|
127
|
+
* Path must be resolved before calling this function.
|
|
128
|
+
*
|
|
129
|
+
* @throws If registration is locked (after handler creation).
|
|
130
|
+
* @throws If the HTTP method is invalid.
|
|
131
|
+
* @throws If the path is under the reserved namespace (`/aws-blocks` or `/aws-blocks/api/*`).
|
|
132
|
+
* @throws {RawRouteErrors.DuplicateRoute} If the same method+path is registered twice.
|
|
133
|
+
*/
|
|
134
|
+
export function registerRoute(options: RawRouteOptions & { path: string }): void {
|
|
135
|
+
if (registrationLocked) {
|
|
136
|
+
throw new Error('Routes must be registered during initialization. Cannot register routes after handler creation.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!VALID_METHODS.has(options.method)) {
|
|
140
|
+
throw new Error(`Invalid HTTP method: ${options.method}. Must be one of: ${Array.from(VALID_METHODS).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalizedPath = options.path.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
144
|
+
if (normalizedPath === '/') {
|
|
145
|
+
throw new Error("Cannot register RawRoute at '/' — root path is not supported (API Gateway proxy resource doesn't handle root). Use a sub-path like '/health'.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
149
|
+
if (lowerPath === BLOCKS_NAMESPACE || lowerPath === BLOCKS_RPC_PREFIX || lowerPath.startsWith(`${BLOCKS_RPC_PREFIX}/`)) {
|
|
150
|
+
throw new Error(`Cannot register RawRoute at ${BLOCKS_NAMESPACE} or ${BLOCKS_RPC_PREFIX}/* — these paths are reserved for RPC dispatch`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const existing = routeRegistry.find(
|
|
154
|
+
(r) => r.method === options.method && r.path === normalizedPath,
|
|
155
|
+
);
|
|
156
|
+
if (existing) {
|
|
157
|
+
const err = new Error(
|
|
158
|
+
`${RawRouteErrors.DuplicateRoute}: ${options.method} ${normalizedPath} is already registered`,
|
|
159
|
+
);
|
|
160
|
+
err.name = RawRouteErrors.DuplicateRoute;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { pattern, paramNames } = compilePath(normalizedPath);
|
|
165
|
+
|
|
166
|
+
routeRegistry.push({
|
|
167
|
+
method: options.method,
|
|
168
|
+
path: normalizedPath,
|
|
169
|
+
pattern,
|
|
170
|
+
paramNames,
|
|
171
|
+
handler: options.handler,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Return a read-only snapshot of all registered routes. */
|
|
176
|
+
export function getRegisteredRoutes(): readonly RegisteredRoute[] {
|
|
177
|
+
return routeRegistry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Remove all registered routes and unlock registration (used by tests). */
|
|
181
|
+
export function clearRouteRegistry(): void {
|
|
182
|
+
routeRegistry.length = 0;
|
|
183
|
+
registrationLocked = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Match an incoming HTTP request against registered routes.
|
|
188
|
+
*
|
|
189
|
+
* @returns The matched route and extracted path parameters, or `null` if no match.
|
|
190
|
+
*/
|
|
191
|
+
export function matchRoute(
|
|
192
|
+
method: string,
|
|
193
|
+
path: string,
|
|
194
|
+
): { route: RegisteredRoute; params: Record<string, string> } | null {
|
|
195
|
+
// Normalize incoming path: collapse double slashes
|
|
196
|
+
path = path.replace(/\/+/g, '/');
|
|
197
|
+
|
|
198
|
+
for (const route of routeRegistry) {
|
|
199
|
+
if (route.method !== method) continue;
|
|
200
|
+
const match = route.pattern.exec(path);
|
|
201
|
+
if (match) {
|
|
202
|
+
return { route, params: extractParams(route, match) };
|
|
203
|
+
}
|
|
204
|
+
// Retry without trailing slash for non-root paths (e.g. /health/ → /health)
|
|
205
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
206
|
+
const trimmed = path.slice(0, -1);
|
|
207
|
+
const retryMatch = route.pattern.exec(trimmed);
|
|
208
|
+
if (retryMatch) {
|
|
209
|
+
return { route, params: extractParams(route, retryMatch) };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function extractParams(route: RegisteredRoute, match: RegExpExecArray): Record<string, string> {
|
|
217
|
+
const params: Record<string, string> = Object.create(null);
|
|
218
|
+
route.paramNames.forEach((name, i) => {
|
|
219
|
+
const raw = match[i + 1];
|
|
220
|
+
|
|
221
|
+
if (name === '*') {
|
|
222
|
+
// Wildcards capture raw paths — no decoding (prevents path traversal)
|
|
223
|
+
params[name] = raw;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Named params get decoded
|
|
228
|
+
try {
|
|
229
|
+
params[name] = decodeURIComponent(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
// Invalid percent encoding — use raw value
|
|
232
|
+
params[name] = raw;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return params;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Path compilation ────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
const REGEX_SPECIAL = new Set(['.', '+', '?', '^', '$', '|', '(', ')', '[', ']', '\\']);
|
|
241
|
+
const BLOCKED_PARAM_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Compile a path pattern into a RegExp and extract parameter names.
|
|
245
|
+
*
|
|
246
|
+
* Supported syntax:
|
|
247
|
+
* - `/health` → exact match
|
|
248
|
+
* - `/users/{id}` → captures one path segment as `id`
|
|
249
|
+
* - `/v1/*` → captures everything after `/v1/` as `*`
|
|
250
|
+
* - `/a/{x}/b/{y}` → multiple named parameters
|
|
251
|
+
*/
|
|
252
|
+
export function compilePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
253
|
+
if (!path.startsWith('/')) {
|
|
254
|
+
throw new Error('Path must start with /');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Normalize: collapse double slashes
|
|
258
|
+
path = path.replace(/\/+/g, '/');
|
|
259
|
+
|
|
260
|
+
// Remove trailing slash (except root '/')
|
|
261
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
262
|
+
path = path.slice(0, -1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const paramNames: string[] = [];
|
|
266
|
+
let regexStr = '';
|
|
267
|
+
let i = 0;
|
|
268
|
+
let wildcardCount = 0;
|
|
269
|
+
|
|
270
|
+
while (i < path.length) {
|
|
271
|
+
if (path[i] === '{') {
|
|
272
|
+
const end = path.indexOf('}', i);
|
|
273
|
+
if (end === -1) {
|
|
274
|
+
throw new Error(`Unclosed '{' in path pattern: ${path}`);
|
|
275
|
+
}
|
|
276
|
+
const name = path.substring(i + 1, end);
|
|
277
|
+
if (!name || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Invalid parameter name '${name}' in path '${path}'. Parameter names must be valid JavaScript identifiers (start with letter or underscore, contain only letters, digits, underscores).`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (BLOCKED_PARAM_NAMES.has(name)) {
|
|
283
|
+
throw new Error(`Parameter name '${name}' is reserved and cannot be used in path '${path}'.`);
|
|
284
|
+
}
|
|
285
|
+
paramNames.push(name);
|
|
286
|
+
regexStr += '([^/]+)';
|
|
287
|
+
i = end + 1;
|
|
288
|
+
} else if (path[i] === '}') {
|
|
289
|
+
throw new Error(`Unexpected '}' without matching '{' in path pattern: ${path}`);
|
|
290
|
+
} else if (path[i] === '*') {
|
|
291
|
+
wildcardCount++;
|
|
292
|
+
if (wildcardCount > 1) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Path '${path}' contains multiple wildcards. Only one wildcard (*) is allowed per route, and it must be the last segment.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (i !== path.length - 1) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Path '${path}' contains multiple wildcards. Only one wildcard (*) is allowed per route, and it must be the last segment.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
paramNames.push('*');
|
|
303
|
+
regexStr += '(.*)';
|
|
304
|
+
i++;
|
|
305
|
+
} else if (REGEX_SPECIAL.has(path[i])) {
|
|
306
|
+
regexStr += '\\' + path[i];
|
|
307
|
+
i++;
|
|
308
|
+
} else {
|
|
309
|
+
regexStr += path[i];
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const fullPattern = '^' + regexStr + '$';
|
|
315
|
+
return { pattern: new RegExp(fullPattern), paramNames };
|
|
316
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { redactForLogging, redactToJson, REDACTED } from './redact.js';
|
|
7
|
+
|
|
8
|
+
describe('redactForLogging', () => {
|
|
9
|
+
it('redacts a top-level password field', () => {
|
|
10
|
+
const out = redactForLogging({ username: 'alice', password: 'hunter2' });
|
|
11
|
+
assert.deepEqual(out, { username: 'alice', password: REDACTED });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('redacts every sensitive credential/token field name', () => {
|
|
15
|
+
const out = redactForLogging({
|
|
16
|
+
password: 'p',
|
|
17
|
+
newPassword: 'np',
|
|
18
|
+
session: 's',
|
|
19
|
+
accessToken: 'at',
|
|
20
|
+
refreshToken: 'rt',
|
|
21
|
+
idToken: 'it',
|
|
22
|
+
sharedSecret: 'ss',
|
|
23
|
+
clientSecret: 'cs',
|
|
24
|
+
apiKey: 'ak',
|
|
25
|
+
authorization: 'Bearer x',
|
|
26
|
+
code: '123456',
|
|
27
|
+
credentials: 'c',
|
|
28
|
+
});
|
|
29
|
+
for (const v of Object.values(out as Record<string, unknown>)) {
|
|
30
|
+
assert.equal(v, REDACTED);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('matches field names case-insensitively', () => {
|
|
35
|
+
const out = redactForLogging({ PassWord: 'x', SESSION: 'y' }) as Record<string, unknown>;
|
|
36
|
+
assert.equal(out.PassWord, REDACTED);
|
|
37
|
+
assert.equal(out.SESSION, REDACTED);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('leaves non-sensitive fields untouched', () => {
|
|
41
|
+
const input = { username: 'bob', email: 'b@example.com', count: 3, ok: true };
|
|
42
|
+
assert.deepEqual(redactForLogging(input), input);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('walks nested objects', () => {
|
|
46
|
+
const out = redactForLogging({
|
|
47
|
+
action: 'confirmSignIn',
|
|
48
|
+
payload: { session: 'tok', code: '999' },
|
|
49
|
+
});
|
|
50
|
+
assert.deepEqual(out, {
|
|
51
|
+
action: 'confirmSignIn',
|
|
52
|
+
payload: { session: REDACTED, code: REDACTED },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('walks arrays (e.g. positional RPC args)', () => {
|
|
57
|
+
const out = redactForLogging([
|
|
58
|
+
{ action: 'signIn', username: 'a', password: 'secret' },
|
|
59
|
+
{ action: 'confirmSignIn', session: 'tok', code: '111' },
|
|
60
|
+
]);
|
|
61
|
+
assert.deepEqual(out, [
|
|
62
|
+
{ action: 'signIn', username: 'a', password: REDACTED },
|
|
63
|
+
{ action: 'confirmSignIn', session: REDACTED, code: REDACTED },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('preserves null/undefined sensitive fields without masking', () => {
|
|
68
|
+
const out = redactForLogging({ password: null, session: undefined }) as Record<string, unknown>;
|
|
69
|
+
assert.equal(out.password, null);
|
|
70
|
+
assert.equal(out.session, undefined);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns primitives unchanged', () => {
|
|
74
|
+
assert.equal(redactForLogging('just a string'), 'just a string');
|
|
75
|
+
assert.equal(redactForLogging(42), 42);
|
|
76
|
+
assert.equal(redactForLogging(null), null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not mutate the input object', () => {
|
|
80
|
+
const input = { password: 'keep-me', username: 'alice' };
|
|
81
|
+
redactForLogging(input);
|
|
82
|
+
assert.equal(input.password, 'keep-me');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Regression: the auth state machine ships secrets as form-field
|
|
86
|
+
// descriptors `{ name: 'session', defaultValue: '<token>' }`. The secret
|
|
87
|
+
// sits under `defaultValue`, NOT under a key literally named `session`, so
|
|
88
|
+
// plain key-name redaction walks past it. These guard the descriptor branch.
|
|
89
|
+
it('redacts defaultValue of a sensitive AuthField descriptor', () => {
|
|
90
|
+
const out = redactForLogging({
|
|
91
|
+
name: 'session',
|
|
92
|
+
label: 'Session',
|
|
93
|
+
type: 'hidden',
|
|
94
|
+
defaultValue: 'COGNITO_SESSION_TOKEN',
|
|
95
|
+
}) as Record<string, unknown>;
|
|
96
|
+
assert.equal(out.name, 'session');
|
|
97
|
+
assert.equal(out.defaultValue, REDACTED);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('redacts session + sharedSecret in a full confirmingSignIn AuthState', () => {
|
|
101
|
+
const authState = {
|
|
102
|
+
state: 'confirmingSignIn',
|
|
103
|
+
actions: [{
|
|
104
|
+
name: 'confirmSignIn',
|
|
105
|
+
label: 'Set Up Authenticator',
|
|
106
|
+
fields: [
|
|
107
|
+
{ name: 'session', label: 'Session', type: 'hidden', required: true, defaultValue: 'SESSION_TOKEN_abc' },
|
|
108
|
+
{ name: 'sharedSecret', label: 'Shared Secret', type: 'hidden', required: true, defaultValue: 'JBSWY3DPEHPK3PXP' },
|
|
109
|
+
{ name: 'code', label: 'Code', type: 'text', required: true },
|
|
110
|
+
],
|
|
111
|
+
}],
|
|
112
|
+
};
|
|
113
|
+
const json = JSON.stringify(redactForLogging(authState));
|
|
114
|
+
assert.ok(!json.includes('SESSION_TOKEN_abc'), 'session token must not leak');
|
|
115
|
+
assert.ok(!json.includes('JBSWY3DPEHPK3PXP'), 'TOTP shared secret must not leak');
|
|
116
|
+
// Non-secret descriptor fields are preserved for debuggability.
|
|
117
|
+
assert.ok(json.includes('confirmSignIn'));
|
|
118
|
+
assert.ok(json.includes('Set Up Authenticator'));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('leaves defaultValue untouched for a non-sensitive field (e.g. echoed username)', () => {
|
|
122
|
+
const out = redactForLogging({
|
|
123
|
+
name: 'username',
|
|
124
|
+
type: 'hidden',
|
|
125
|
+
defaultValue: 'alice',
|
|
126
|
+
}) as Record<string, unknown>;
|
|
127
|
+
assert.equal(out.defaultValue, 'alice');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles circular references', () => {
|
|
131
|
+
const a: Record<string, unknown> = { username: 'alice' };
|
|
132
|
+
a.self = a;
|
|
133
|
+
const out = redactForLogging(a) as Record<string, unknown>;
|
|
134
|
+
assert.equal(out.username, 'alice');
|
|
135
|
+
assert.equal(out.self, '[Circular]');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('detects cycles nested deeper than the root', () => {
|
|
139
|
+
const root: Record<string, unknown> = { level: 1 };
|
|
140
|
+
const child: Record<string, unknown> = { level: 2, parent: root };
|
|
141
|
+
root.child = child;
|
|
142
|
+
const out = redactForLogging(root) as any;
|
|
143
|
+
assert.equal(out.child.level, 2);
|
|
144
|
+
assert.equal(out.child.parent, '[Circular]');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Regression: `seen` must track the ancestor PATH, not every node ever
|
|
148
|
+
// visited. A shared (non-cyclic) reference reached twice via sibling
|
|
149
|
+
// branches must be emitted in full both times — a visited-set would wrongly
|
|
150
|
+
// flag the second one as '[Circular]' and drop real data from the log.
|
|
151
|
+
it('does not flag shared (non-circular) references as circular', () => {
|
|
152
|
+
const shared = { region: 'us-east-1', count: 2 };
|
|
153
|
+
const out = redactForLogging({ a: shared, b: shared }) as Record<string, unknown>;
|
|
154
|
+
assert.deepEqual(out.a, shared);
|
|
155
|
+
assert.deepEqual(out.b, shared);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('emits a shared reference repeated in an array, not [Circular]', () => {
|
|
159
|
+
const shared = { id: 'x' };
|
|
160
|
+
const out = redactForLogging([shared, shared, shared]) as unknown[];
|
|
161
|
+
assert.deepEqual(out, [shared, shared, shared]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('still redacts secrets inside a shared reference each time', () => {
|
|
165
|
+
const shared = { username: 'u', password: 'secret' };
|
|
166
|
+
const out = redactForLogging({ first: shared, second: shared }) as any;
|
|
167
|
+
assert.equal(out.first.password, REDACTED);
|
|
168
|
+
assert.equal(out.second.password, REDACTED);
|
|
169
|
+
assert.equal(out.second.username, 'u');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Regression: `code` is exact-match only, so substring siblings like
|
|
173
|
+
// statusCode/errorCode stay visible — they carry no secret and masking them
|
|
174
|
+
// defeats the debugging purpose of the logs.
|
|
175
|
+
it('redacts an exact `code` field but not `code`-substring fields', () => {
|
|
176
|
+
const out = redactForLogging({
|
|
177
|
+
code: '123456',
|
|
178
|
+
statusCode: 200,
|
|
179
|
+
errorCode: 'E_BAD',
|
|
180
|
+
countryCode: 'US',
|
|
181
|
+
zipCode: '98101',
|
|
182
|
+
qrCode: 'data:image/png',
|
|
183
|
+
}) as Record<string, unknown>;
|
|
184
|
+
assert.equal(out.code, REDACTED);
|
|
185
|
+
assert.equal(out.statusCode, 200);
|
|
186
|
+
assert.equal(out.errorCode, 'E_BAD');
|
|
187
|
+
assert.equal(out.countryCode, 'US');
|
|
188
|
+
assert.equal(out.zipCode, '98101');
|
|
189
|
+
assert.equal(out.qrCode, 'data:image/png');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('redactToJson', () => {
|
|
194
|
+
it('serializes a redacted copy', () => {
|
|
195
|
+
const json = redactToJson({ username: 'alice', password: 'hunter2' });
|
|
196
|
+
assert.equal(json, JSON.stringify({ username: 'alice', password: REDACTED }));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns a safe placeholder on serialization failure', () => {
|
|
200
|
+
// BigInt is not JSON-serializable and survives redaction (not a sensitive key).
|
|
201
|
+
assert.equal(redactToJson({ big: 10n }), '[unserializable]');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not leak secrets even when truncation would apply downstream', () => {
|
|
205
|
+
const json = redactToJson([{ action: 'signIn', username: 'u', password: 'p'.repeat(50) }]);
|
|
206
|
+
assert.ok(!json.includes('pppp'));
|
|
207
|
+
assert.ok(json.includes(REDACTED));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('redacts secrets in a real JSON-RPC request body shape', () => {
|
|
211
|
+
const body = {
|
|
212
|
+
jsonrpc: '2.0',
|
|
213
|
+
method: 'authApi.setAuthState',
|
|
214
|
+
params: [{ action: 'signIn', username: 'a', password: 'hunter2' }],
|
|
215
|
+
id: 1,
|
|
216
|
+
};
|
|
217
|
+
const json = redactToJson(body);
|
|
218
|
+
assert.ok(!json.includes('hunter2'));
|
|
219
|
+
assert.ok(json.includes(REDACTED));
|
|
220
|
+
assert.ok(json.includes('setAuthState'), 'method name preserved for debugging');
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sensitive-field redaction for logging.
|
|
6
|
+
*
|
|
7
|
+
* The dev server logs RPC request/response bodies to help debug cross-stack
|
|
8
|
+
* wire-format mismatches. Those bodies carry credentials (sign-in passwords,
|
|
9
|
+
* new-password challenge responses) and bearer-style secrets (Cognito challenge
|
|
10
|
+
* `session` tokens, MFA shared secrets, OTP codes). Logging them verbatim puts
|
|
11
|
+
* plaintext passwords and reusable tokens in the log stream. This module masks
|
|
12
|
+
* those fields before anything is logged, without mutating the value that
|
|
13
|
+
* actually flows over the wire.
|
|
14
|
+
*
|
|
15
|
+
* Two complementary rules, because auth payloads hide secrets two ways:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Direct keys** — `{ password: '...', session: '...' }`. Redacted by
|
|
18
|
+
* field NAME, matched case-insensitively (substring).
|
|
19
|
+
*
|
|
20
|
+
* 2. **Form-field descriptors** — the auth state machine ships secrets as
|
|
21
|
+
* `{ name: 'session', defaultValue: '<token>' }` (see
|
|
22
|
+
* `@aws-blocks/auth-common` AuthField). Here the secret sits under the
|
|
23
|
+
* innocuous key `defaultValue`; the sensitive word is the *value* of a
|
|
24
|
+
* sibling `name`. Rule 1 alone would walk straight past it. So when an
|
|
25
|
+
* object carries a sensitive `name`, its `defaultValue` is redacted too.
|
|
26
|
+
*
|
|
27
|
+
* The wire payload is never altered — only the logged copy is scrubbed.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The string substituted for any sensitive value. */
|
|
31
|
+
export const REDACTED = '[REDACTED]' as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Field-name fragments whose values must never be logged. Matched
|
|
35
|
+
* case-insensitively as a SUBSTRING, so prefixed/suffixed variants
|
|
36
|
+
* (`accessToken`, `refreshToken`, `idToken`, `apiKey`, `clientSecret`) all
|
|
37
|
+
* match without enumerating every spelling. Keep aligned with the auth action
|
|
38
|
+
* payloads in `@aws-blocks/auth-common` plus the generic credential vocabulary.
|
|
39
|
+
*/
|
|
40
|
+
const SENSITIVE_KEY_PARTS: readonly string[] = [
|
|
41
|
+
'password',
|
|
42
|
+
'session',
|
|
43
|
+
'token',
|
|
44
|
+
'secret',
|
|
45
|
+
'credential',
|
|
46
|
+
'apikey',
|
|
47
|
+
'authorization',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Field names that are sensitive only as an EXACT match. The OTP / verification
|
|
52
|
+
* code the auth state machine sends is literally named `code` (see
|
|
53
|
+
* `@aws-blocks/auth-common` confirmSignIn payloads). Matching it as a substring
|
|
54
|
+
* would mask innocuous fields like `statusCode`, `errorCode`, `countryCode`,
|
|
55
|
+
* `zipCode`, `qrCode` — log noise that defeats the debugging purpose of these
|
|
56
|
+
* logs without protecting anything. So `code` is exact-match only.
|
|
57
|
+
*/
|
|
58
|
+
const SENSITIVE_KEY_EXACT: ReadonlySet<string> = new Set(['code']);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a field name should be redacted: an exact-match against
|
|
62
|
+
* {@link SENSITIVE_KEY_EXACT}, or a case-insensitive substring match against
|
|
63
|
+
* {@link SENSITIVE_KEY_PARTS}.
|
|
64
|
+
*/
|
|
65
|
+
function isSensitiveKey(key: string): boolean {
|
|
66
|
+
const lower = key.toLowerCase();
|
|
67
|
+
if (SENSITIVE_KEY_EXACT.has(lower)) return true;
|
|
68
|
+
return SENSITIVE_KEY_PARTS.some((part) => lower.includes(part));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return a deep copy of `value` with the value of every sensitive field
|
|
73
|
+
* replaced by {@link REDACTED}. Arrays and nested objects are walked; the
|
|
74
|
+
* input is never mutated. Circular references are emitted as `'[Circular]'`.
|
|
75
|
+
*
|
|
76
|
+
* Primitives are returned unchanged — only object/array *fields* are matched
|
|
77
|
+
* against the sensitive-key list, since a bare string has no field name to
|
|
78
|
+
* key off.
|
|
79
|
+
*
|
|
80
|
+
* `seen` tracks the current ancestor PATH, not every node ever visited: each
|
|
81
|
+
* node is added before its children are walked and removed afterwards. A
|
|
82
|
+
* plain visited-set would flag the second appearance of a *shared* (non-cyclic)
|
|
83
|
+
* reference as `'[Circular]'` — e.g. `{ a: shared, b: shared }` — silently
|
|
84
|
+
* dropping real data from the log. Only a reference still open on the stack is
|
|
85
|
+
* a genuine cycle.
|
|
86
|
+
*/
|
|
87
|
+
export function redactForLogging(value: unknown, seen: WeakSet<object> = new WeakSet()): unknown {
|
|
88
|
+
if (value === null || typeof value !== 'object') {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (seen.has(value)) {
|
|
93
|
+
return '[Circular]';
|
|
94
|
+
}
|
|
95
|
+
seen.add(value);
|
|
96
|
+
|
|
97
|
+
let out: unknown;
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
out = value.map((item) => redactForLogging(item, seen));
|
|
100
|
+
} else {
|
|
101
|
+
const obj = value as Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
// Rule 2: form-field descriptor `{ name: <sensitive>, defaultValue: <secret> }`.
|
|
104
|
+
// The secret hides under `defaultValue` while the sensitive word is the value
|
|
105
|
+
// of `name` — so the direct-key pass below never sees it. Detect the shape and
|
|
106
|
+
// redact `defaultValue` explicitly.
|
|
107
|
+
const masksDefaultValue =
|
|
108
|
+
typeof obj.name === 'string'
|
|
109
|
+
&& isSensitiveKey(obj.name)
|
|
110
|
+
&& 'defaultValue' in obj;
|
|
111
|
+
|
|
112
|
+
const result: Record<string, unknown> = {};
|
|
113
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
114
|
+
if (isSensitiveKey(key) || (key === 'defaultValue' && masksDefaultValue)) {
|
|
115
|
+
// Preserve presence/absence so logs still show the field was sent,
|
|
116
|
+
// without revealing it. Skip masking explicit null/undefined so an
|
|
117
|
+
// absent secret doesn't look like a populated one.
|
|
118
|
+
result[key] = val === null || val === undefined ? val : REDACTED;
|
|
119
|
+
} else {
|
|
120
|
+
result[key] = redactForLogging(val, seen);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
out = result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pop this node off the ancestor path now that its subtree is fully walked,
|
|
127
|
+
// so sibling branches can reference the same object without false `[Circular]`.
|
|
128
|
+
seen.delete(value);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Convenience for log call sites: redact `value` and serialize it to a JSON
|
|
134
|
+
* string. Returns a safe placeholder instead of throwing if serialization
|
|
135
|
+
* fails (e.g. a BigInt slips through), so logging can never crash a request.
|
|
136
|
+
*/
|
|
137
|
+
export function redactToJson(value: unknown): string {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.stringify(redactForLogging(value));
|
|
140
|
+
} catch {
|
|
141
|
+
return '[unserializable]';
|
|
142
|
+
}
|
|
143
|
+
}
|