@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,597 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { compilePath, registerRoute, matchRoute, getRegisteredRoutes, clearRouteRegistry, lockRouteRegistry, unlockRouteRegistry, RawRouteErrors, derivePathFromScope, resolveRoutePath, } from './raw-route.js';
|
|
6
|
+
const noop = async (_ctx) => { };
|
|
7
|
+
/** Create a null-prototype object for comparing with params from matchRoute. */
|
|
8
|
+
function nullProto(obj) {
|
|
9
|
+
return Object.assign(Object.create(null), obj);
|
|
10
|
+
}
|
|
11
|
+
// Ensure clean slate before every test across all describe blocks
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
clearRouteRegistry();
|
|
14
|
+
});
|
|
15
|
+
// ── compilePath ─────────────────────────────────────────────────────────────
|
|
16
|
+
describe('compilePath', () => {
|
|
17
|
+
it('compiles exact path', () => {
|
|
18
|
+
const { pattern, paramNames } = compilePath('/health');
|
|
19
|
+
assert.deepStrictEqual(paramNames, []);
|
|
20
|
+
assert.ok(pattern.test('/health'));
|
|
21
|
+
assert.ok(!pattern.test('/health/'));
|
|
22
|
+
assert.ok(!pattern.test('/'));
|
|
23
|
+
assert.ok(!pattern.test('/healthz'));
|
|
24
|
+
});
|
|
25
|
+
it('compiles root path', () => {
|
|
26
|
+
const { pattern, paramNames } = compilePath('/');
|
|
27
|
+
assert.deepStrictEqual(paramNames, []);
|
|
28
|
+
assert.ok(pattern.test('/'));
|
|
29
|
+
assert.ok(!pattern.test('/anything'));
|
|
30
|
+
});
|
|
31
|
+
it('compiles single named parameter', () => {
|
|
32
|
+
const { pattern, paramNames } = compilePath('/users/{id}');
|
|
33
|
+
assert.deepStrictEqual(paramNames, ['id']);
|
|
34
|
+
const match = pattern.exec('/users/123');
|
|
35
|
+
assert.ok(match);
|
|
36
|
+
assert.strictEqual(match[1], '123');
|
|
37
|
+
assert.ok(!pattern.test('/users/'));
|
|
38
|
+
assert.ok(!pattern.test('/users'));
|
|
39
|
+
assert.ok(!pattern.test('/users/123/extra'));
|
|
40
|
+
});
|
|
41
|
+
it('compiles multiple named parameters', () => {
|
|
42
|
+
const { pattern, paramNames } = compilePath('/orgs/{orgId}/members/{memberId}');
|
|
43
|
+
assert.deepStrictEqual(paramNames, ['orgId', 'memberId']);
|
|
44
|
+
const match = pattern.exec('/orgs/acme/members/alice');
|
|
45
|
+
assert.ok(match);
|
|
46
|
+
assert.strictEqual(match[1], 'acme');
|
|
47
|
+
assert.strictEqual(match[2], 'alice');
|
|
48
|
+
});
|
|
49
|
+
it('compiles wildcard path', () => {
|
|
50
|
+
const { pattern, paramNames } = compilePath('/v1/*');
|
|
51
|
+
assert.deepStrictEqual(paramNames, ['*']);
|
|
52
|
+
const match = pattern.exec('/v1/anything/deep/nested');
|
|
53
|
+
assert.ok(match);
|
|
54
|
+
assert.strictEqual(match[1], 'anything/deep/nested');
|
|
55
|
+
// With (.*), /v1/ matches with empty wildcard capture
|
|
56
|
+
const emptyMatch = pattern.exec('/v1/');
|
|
57
|
+
assert.ok(emptyMatch);
|
|
58
|
+
assert.strictEqual(emptyMatch[1], '');
|
|
59
|
+
assert.ok(!pattern.test('/v1'));
|
|
60
|
+
});
|
|
61
|
+
it('compiles path with named param followed by wildcard', () => {
|
|
62
|
+
const { pattern, paramNames } = compilePath('/api/{version}/*');
|
|
63
|
+
assert.deepStrictEqual(paramNames, ['version', '*']);
|
|
64
|
+
const match = pattern.exec('/api/v2/users/list');
|
|
65
|
+
assert.ok(match);
|
|
66
|
+
assert.strictEqual(match[1], 'v2');
|
|
67
|
+
assert.strictEqual(match[2], 'users/list');
|
|
68
|
+
});
|
|
69
|
+
it('escapes regex-special characters in path', () => {
|
|
70
|
+
const { pattern } = compilePath('/file.json');
|
|
71
|
+
assert.ok(pattern.test('/file.json'));
|
|
72
|
+
assert.ok(!pattern.test('/fileXjson'));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// ── registerRoute + getRegisteredRoutes ─────────────────────────────────────
|
|
76
|
+
describe('registerRoute', () => {
|
|
77
|
+
it('registers a route', () => {
|
|
78
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
79
|
+
const routes = getRegisteredRoutes();
|
|
80
|
+
assert.strictEqual(routes.length, 1);
|
|
81
|
+
assert.strictEqual(routes[0].method, 'GET');
|
|
82
|
+
assert.strictEqual(routes[0].path, '/health');
|
|
83
|
+
});
|
|
84
|
+
it('registers multiple distinct routes', () => {
|
|
85
|
+
registerRoute({ method: 'GET', path: '/a', handler: noop });
|
|
86
|
+
registerRoute({ method: 'POST', path: '/a', handler: noop });
|
|
87
|
+
registerRoute({ method: 'GET', path: '/b', handler: noop });
|
|
88
|
+
assert.strictEqual(getRegisteredRoutes().length, 3);
|
|
89
|
+
});
|
|
90
|
+
it('throws DuplicateRouteException for same method+path', () => {
|
|
91
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
92
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/health', handler: noop }), (err) => {
|
|
93
|
+
assert.strictEqual(err.name, RawRouteErrors.DuplicateRoute);
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it('allows same path with different methods (no duplicate)', () => {
|
|
98
|
+
registerRoute({ method: 'GET', path: '/users', handler: noop });
|
|
99
|
+
registerRoute({ method: 'POST', path: '/users', handler: noop });
|
|
100
|
+
assert.strictEqual(getRegisteredRoutes().length, 2);
|
|
101
|
+
});
|
|
102
|
+
it('throws DuplicateRouteException when trailing-slash variant is registered second', () => {
|
|
103
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
104
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/health/', handler: noop }), (err) => {
|
|
105
|
+
assert.strictEqual(err.name, RawRouteErrors.DuplicateRoute);
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it('throws DuplicateRouteException when non-trailing-slash variant is registered second', () => {
|
|
110
|
+
registerRoute({ method: 'GET', path: '/health/', handler: noop });
|
|
111
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/health', handler: noop }), (err) => {
|
|
112
|
+
assert.strictEqual(err.name, RawRouteErrors.DuplicateRoute);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
it('throws DuplicateRouteException for paths differing only by double slashes', () => {
|
|
117
|
+
registerRoute({ method: 'POST', path: '/api/users', handler: noop });
|
|
118
|
+
assert.throws(() => registerRoute({ method: 'POST', path: '/api//users', handler: noop }), (err) => {
|
|
119
|
+
assert.strictEqual(err.name, RawRouteErrors.DuplicateRoute);
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('stores normalized path in registry (no trailing slash)', () => {
|
|
124
|
+
registerRoute({ method: 'GET', path: '/items/', handler: noop });
|
|
125
|
+
const routes = getRegisteredRoutes();
|
|
126
|
+
assert.strictEqual(routes[0].path, '/items');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// ── matchRoute ──────────────────────────────────────────────────────────────
|
|
130
|
+
describe('matchRoute', () => {
|
|
131
|
+
it('matches exact path', () => {
|
|
132
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
133
|
+
const result = matchRoute('GET', '/health');
|
|
134
|
+
assert.ok(result);
|
|
135
|
+
assert.deepStrictEqual(result.params, nullProto({}));
|
|
136
|
+
assert.strictEqual(result.route.path, '/health');
|
|
137
|
+
});
|
|
138
|
+
it('returns null for non-matching path', () => {
|
|
139
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
140
|
+
assert.strictEqual(matchRoute('GET', '/other'), null);
|
|
141
|
+
});
|
|
142
|
+
it('returns null for non-matching method', () => {
|
|
143
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
144
|
+
assert.strictEqual(matchRoute('POST', '/health'), null);
|
|
145
|
+
});
|
|
146
|
+
it('extracts named params', () => {
|
|
147
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: noop });
|
|
148
|
+
const result = matchRoute('GET', '/users/42');
|
|
149
|
+
assert.ok(result);
|
|
150
|
+
assert.deepStrictEqual(result.params, nullProto({ id: '42' }));
|
|
151
|
+
});
|
|
152
|
+
it('extracts multiple named params', () => {
|
|
153
|
+
registerRoute({ method: 'GET', path: '/orgs/{orgId}/members/{memberId}', handler: noop });
|
|
154
|
+
const result = matchRoute('GET', '/orgs/acme/members/bob');
|
|
155
|
+
assert.ok(result);
|
|
156
|
+
assert.deepStrictEqual(result.params, nullProto({ orgId: 'acme', memberId: 'bob' }));
|
|
157
|
+
});
|
|
158
|
+
it('extracts wildcard param', () => {
|
|
159
|
+
registerRoute({ method: 'GET', path: '/files/*', handler: noop });
|
|
160
|
+
const result = matchRoute('GET', '/files/docs/readme.md');
|
|
161
|
+
assert.ok(result);
|
|
162
|
+
assert.deepStrictEqual(result.params, nullProto({ '*': 'docs/readme.md' }));
|
|
163
|
+
});
|
|
164
|
+
it('decodes URI-encoded path segments', () => {
|
|
165
|
+
registerRoute({ method: 'GET', path: '/users/{name}', handler: noop });
|
|
166
|
+
const result = matchRoute('GET', '/users/John%20Doe');
|
|
167
|
+
assert.ok(result);
|
|
168
|
+
assert.deepStrictEqual(result.params, nullProto({ name: 'John Doe' }));
|
|
169
|
+
});
|
|
170
|
+
it('matches first registered route when multiple could match', () => {
|
|
171
|
+
const handler1 = async (ctx) => { ctx.response.send('first'); };
|
|
172
|
+
const handler2 = async (ctx) => { ctx.response.send('second'); };
|
|
173
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: handler1 });
|
|
174
|
+
registerRoute({ method: 'GET', path: '/users/*', handler: handler2 });
|
|
175
|
+
const result = matchRoute('GET', '/users/42');
|
|
176
|
+
assert.ok(result);
|
|
177
|
+
assert.strictEqual(result.route.handler, handler1);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ── clearRouteRegistry ──────────────────────────────────────────────────────
|
|
181
|
+
describe('clearRouteRegistry', () => {
|
|
182
|
+
it('removes all registered routes', () => {
|
|
183
|
+
registerRoute({ method: 'GET', path: '/a', handler: noop });
|
|
184
|
+
registerRoute({ method: 'POST', path: '/b', handler: noop });
|
|
185
|
+
assert.strictEqual(getRegisteredRoutes().length, 2);
|
|
186
|
+
clearRouteRegistry();
|
|
187
|
+
assert.strictEqual(getRegisteredRoutes().length, 0);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ── RawRouteErrors ──────────────────────────────────────────────────────────
|
|
191
|
+
describe('RawRouteErrors', () => {
|
|
192
|
+
it('has DuplicateRoute error constant', () => {
|
|
193
|
+
assert.strictEqual(RawRouteErrors.DuplicateRoute, 'DuplicateRouteException');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
// ── FIX 1: Path traversal via wildcard decoding ─────────────────────────────
|
|
197
|
+
describe('path traversal prevention', () => {
|
|
198
|
+
it('wildcard params are NOT decoded (%2F stays as %2F)', () => {
|
|
199
|
+
registerRoute({ method: 'GET', path: '/files/*', handler: noop });
|
|
200
|
+
const result = matchRoute('GET', '/files/..%2F..%2Fetc%2Fpasswd');
|
|
201
|
+
assert.ok(result);
|
|
202
|
+
assert.strictEqual(result.params['*'], '..%2F..%2Fetc%2Fpasswd');
|
|
203
|
+
});
|
|
204
|
+
it('named params ARE decoded (%20 → space)', () => {
|
|
205
|
+
registerRoute({ method: 'GET', path: '/users/{name}', handler: noop });
|
|
206
|
+
const result = matchRoute('GET', '/users/John%20Doe');
|
|
207
|
+
assert.ok(result);
|
|
208
|
+
assert.strictEqual(result.params.name, 'John Doe');
|
|
209
|
+
});
|
|
210
|
+
it('invalid percent encoding does not crash (graceful fallback)', () => {
|
|
211
|
+
registerRoute({ method: 'GET', path: '/items/{id}', handler: noop });
|
|
212
|
+
const result = matchRoute('GET', '/items/%ZZ');
|
|
213
|
+
assert.ok(result);
|
|
214
|
+
assert.strictEqual(result.params.id, '%ZZ');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
// ── FIX 2: Wildcard matches empty path ──────────────────────────────────────
|
|
218
|
+
describe('wildcard empty path matching', () => {
|
|
219
|
+
it('/files/* matches /files/ (empty wildcard → "")', () => {
|
|
220
|
+
registerRoute({ method: 'GET', path: '/files/*', handler: noop });
|
|
221
|
+
const result = matchRoute('GET', '/files/');
|
|
222
|
+
assert.ok(result);
|
|
223
|
+
assert.strictEqual(result.params['*'], '');
|
|
224
|
+
});
|
|
225
|
+
it('/files/* matches /files/readme.md (normal wildcard)', () => {
|
|
226
|
+
registerRoute({ method: 'GET', path: '/files/*', handler: noop });
|
|
227
|
+
const result = matchRoute('GET', '/files/readme.md');
|
|
228
|
+
assert.ok(result);
|
|
229
|
+
assert.strictEqual(result.params['*'], 'readme.md');
|
|
230
|
+
});
|
|
231
|
+
it('/files/* does NOT match /files (no trailing slash, no wildcard segment)', () => {
|
|
232
|
+
registerRoute({ method: 'GET', path: '/files/*', handler: noop });
|
|
233
|
+
const result = matchRoute('GET', '/files');
|
|
234
|
+
assert.strictEqual(result, null);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// ── FIX 3: Registry lock ────────────────────────────────────────────────────
|
|
238
|
+
describe('registry lock', () => {
|
|
239
|
+
it('registerRoute() after lockRouteRegistry() throws', () => {
|
|
240
|
+
lockRouteRegistry();
|
|
241
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/locked', handler: noop }), (err) => {
|
|
242
|
+
assert.ok(err.message.includes('Cannot register routes after handler creation'));
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
it('clearRouteRegistry() unlocks registration', () => {
|
|
247
|
+
lockRouteRegistry();
|
|
248
|
+
clearRouteRegistry();
|
|
249
|
+
assert.doesNotThrow(() => {
|
|
250
|
+
registerRoute({ method: 'GET', path: '/unlocked', handler: noop });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
it('unlockRouteRegistry() re-allows registration', () => {
|
|
254
|
+
lockRouteRegistry();
|
|
255
|
+
unlockRouteRegistry();
|
|
256
|
+
assert.doesNotThrow(() => {
|
|
257
|
+
registerRoute({ method: 'GET', path: '/unlocked2', handler: noop });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// ── FIX 4: Path normalization ───────────────────────────────────────────────
|
|
262
|
+
describe('path normalization', () => {
|
|
263
|
+
it('/health/ matches route registered as /health', () => {
|
|
264
|
+
registerRoute({ method: 'GET', path: '/health', handler: noop });
|
|
265
|
+
const result = matchRoute('GET', '/health/');
|
|
266
|
+
assert.ok(result);
|
|
267
|
+
assert.strictEqual(result.route.path, '/health');
|
|
268
|
+
});
|
|
269
|
+
it('/a//b matches route registered as /a/b', () => {
|
|
270
|
+
registerRoute({ method: 'GET', path: '/a/b', handler: noop });
|
|
271
|
+
const result = matchRoute('GET', '/a//b');
|
|
272
|
+
assert.ok(result);
|
|
273
|
+
assert.strictEqual(result.route.path, '/a/b');
|
|
274
|
+
});
|
|
275
|
+
it('compilePath normalizes double slashes', () => {
|
|
276
|
+
const { pattern } = compilePath('/a//b');
|
|
277
|
+
assert.ok(pattern.test('/a/b'));
|
|
278
|
+
});
|
|
279
|
+
it('compilePath removes trailing slash', () => {
|
|
280
|
+
const { pattern } = compilePath('/health/');
|
|
281
|
+
assert.ok(pattern.test('/health'));
|
|
282
|
+
});
|
|
283
|
+
it('path without leading / throws in compilePath', () => {
|
|
284
|
+
assert.throws(() => compilePath('health'), (err) => {
|
|
285
|
+
assert.ok(err.message.includes('Path must start with /'));
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
// ── FIX 5: Runtime HTTP method validation ───────────────────────────────────
|
|
291
|
+
describe('HTTP method validation', () => {
|
|
292
|
+
it('rejects invalid HTTP method', () => {
|
|
293
|
+
assert.throws(() => registerRoute({ method: 'INVALID', path: '/test', handler: noop }), (err) => {
|
|
294
|
+
assert.ok(err.message.includes('Invalid HTTP method: INVALID'));
|
|
295
|
+
return true;
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it('accepts all valid HTTP methods', () => {
|
|
299
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
300
|
+
methods.forEach((method, i) => {
|
|
301
|
+
assert.doesNotThrow(() => {
|
|
302
|
+
registerRoute({ method, path: `/m${i}`, handler: noop });
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
// ── FIX 6: Reserve /aws-blocks namespace ────────────────────────────────────
|
|
308
|
+
describe('/aws-blocks namespace reservation', () => {
|
|
309
|
+
it('registering /aws-blocks/api throws', () => {
|
|
310
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/aws-blocks/api', handler: noop }), (err) => {
|
|
311
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
it('registering /aws-blocks/api/foo throws', () => {
|
|
316
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/aws-blocks/api/foo', handler: noop }), (err) => {
|
|
317
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
318
|
+
return true;
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
it('registering /aws-blocks/api/ (trailing slash) throws', () => {
|
|
322
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/aws-blocks/api/', handler: noop }), (err) => {
|
|
323
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
324
|
+
return true;
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
it('rejects /AWS-BLOCKS/API (case-insensitive)', () => {
|
|
328
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/AWS-BLOCKS/API', handler: noop }), (err) => {
|
|
329
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
it('rejects /Aws-Blocks/Api/foo (mixed case)', () => {
|
|
334
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/Aws-Blocks/Api/foo', handler: noop }), (err) => {
|
|
335
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
336
|
+
return true;
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
it('rejects /aWs-bLoCks/ApI/ (case-insensitive with trailing slash)', () => {
|
|
340
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/aWs-bLoCks/ApI/', handler: noop }), (err) => {
|
|
341
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
it('allows /aws-blocks/dashboard (not under /aws-blocks/api)', () => {
|
|
346
|
+
assert.doesNotThrow(() => {
|
|
347
|
+
registerRoute({ method: 'GET', path: '/aws-blocks/dashboard', handler: noop });
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
it('allows /aws-blocks/health (not under /aws-blocks/api)', () => {
|
|
351
|
+
assert.doesNotThrow(() => {
|
|
352
|
+
registerRoute({ method: 'GET', path: '/aws-blocks/health', handler: noop });
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
it('rejects /aws-blocks (exact namespace path)', () => {
|
|
356
|
+
assert.throws(() => registerRoute({ method: 'POST', path: '/aws-blocks', handler: noop }), (err) => {
|
|
357
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
it('rejects /AWS-BLOCKS (case-insensitive namespace)', () => {
|
|
362
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/AWS-BLOCKS', handler: noop }), (err) => {
|
|
363
|
+
assert.ok(err.message.includes('reserved for RPC dispatch'));
|
|
364
|
+
return true;
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
it('allows /aws-blocksary (not the reserved prefix)', () => {
|
|
368
|
+
assert.doesNotThrow(() => {
|
|
369
|
+
registerRoute({ method: 'GET', path: '/aws-blocksary', handler: noop });
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
it('allows /aws-blocks/api-docs (not exactly /aws-blocks/api)', () => {
|
|
373
|
+
assert.doesNotThrow(() => {
|
|
374
|
+
registerRoute({ method: 'GET', path: '/aws-blocks/api-docs', handler: noop });
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
// ── FIX 3 (review): Root path rejection ─────────────────────────────────────
|
|
379
|
+
describe('root path rejection', () => {
|
|
380
|
+
it("registering '/' throws", () => {
|
|
381
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/', handler: noop }), (err) => {
|
|
382
|
+
assert.ok(err.message.includes('root path is not supported'));
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
it("registering '//' (normalizes to '/') throws", () => {
|
|
387
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '//', handler: noop }), (err) => {
|
|
388
|
+
assert.ok(err.message.includes('root path is not supported'));
|
|
389
|
+
return true;
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
// ── FIX 5 (review): Unclosed brace validation ──────────────────────────────
|
|
394
|
+
describe('malformed path patterns', () => {
|
|
395
|
+
it("unclosed brace '/users/{id' throws", () => {
|
|
396
|
+
assert.throws(() => compilePath('/users/{id'), (err) => {
|
|
397
|
+
assert.ok(err.message.includes("Unclosed '{'"));
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
it("stray closing brace '/users/id}' throws", () => {
|
|
402
|
+
assert.throws(() => compilePath('/users/id}'), (err) => {
|
|
403
|
+
assert.ok(err.message.includes("Unexpected '}'"));
|
|
404
|
+
return true;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
it("multiple unclosed braces '/a/{x/b/{y' throws", () => {
|
|
408
|
+
assert.throws(() => compilePath('/a/{x/b/{y'), (err) => {
|
|
409
|
+
assert.ok(err.message.includes("Unclosed '{'"));
|
|
410
|
+
return true;
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
it("well-formed braces still work '/users/{id}/posts/{postId}'", () => {
|
|
414
|
+
const { pattern, paramNames } = compilePath('/users/{id}/posts/{postId}');
|
|
415
|
+
assert.deepStrictEqual(paramNames, ['id', 'postId']);
|
|
416
|
+
assert.ok(pattern.test('/users/1/posts/2'));
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
// ── Verify #2: Path separator injection ─────────────────────────────────────
|
|
420
|
+
describe('path separator injection', () => {
|
|
421
|
+
it('named param regex rejects literal / in segment', () => {
|
|
422
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: noop });
|
|
423
|
+
const result = matchRoute('GET', '/users/foo/bar');
|
|
424
|
+
assert.strictEqual(result, null, 'Should not match — / is not allowed in named param');
|
|
425
|
+
});
|
|
426
|
+
it('encoded %2F in URL does not become / before regex match', () => {
|
|
427
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: noop });
|
|
428
|
+
const result = matchRoute('GET', '/users/foo%2Fbar');
|
|
429
|
+
assert.ok(result, 'Should match — %2F is not a literal /');
|
|
430
|
+
assert.strictEqual(result.params.id, 'foo/bar', 'Decoded value should contain /');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
// ── Verify #8: Double encoding bypass ───────────────────────────────────────
|
|
434
|
+
describe('double encoding bypass', () => {
|
|
435
|
+
it('%252F decodes to %2F (not /), no path traversal', () => {
|
|
436
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: noop });
|
|
437
|
+
const result = matchRoute('GET', '/users/%252F');
|
|
438
|
+
assert.ok(result, 'Should match — %252F is a valid segment');
|
|
439
|
+
assert.strictEqual(result.params.id, '%2F', 'Single decode of %252F yields %2F');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
// ── Parameter name validation ───────────────────────────────────────────────
|
|
443
|
+
describe('parameter name validation', () => {
|
|
444
|
+
it('rejects empty braces {}', () => {
|
|
445
|
+
assert.throws(() => compilePath('/users/{}'), (err) => {
|
|
446
|
+
assert.ok(err.message.includes("Invalid parameter name ''"));
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
it('rejects numeric start {123}', () => {
|
|
451
|
+
assert.throws(() => compilePath('/users/{123}'), (err) => {
|
|
452
|
+
assert.ok(err.message.includes("Invalid parameter name '123'"));
|
|
453
|
+
return true;
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
it('rejects dashes {my-param}', () => {
|
|
457
|
+
assert.throws(() => compilePath('/users/{my-param}'), (err) => {
|
|
458
|
+
assert.ok(err.message.includes("Invalid parameter name 'my-param'"));
|
|
459
|
+
return true;
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
it('rejects spaces {with spaces}', () => {
|
|
463
|
+
assert.throws(() => compilePath('/users/{with spaces}'), (err) => {
|
|
464
|
+
assert.ok(err.message.includes("Invalid parameter name 'with spaces'"));
|
|
465
|
+
return true;
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
it('accepts valid name {id}', () => {
|
|
469
|
+
const { paramNames } = compilePath('/users/{id}');
|
|
470
|
+
assert.deepStrictEqual(paramNames, ['id']);
|
|
471
|
+
});
|
|
472
|
+
it('accepts underscore prefix {_private}', () => {
|
|
473
|
+
const { paramNames } = compilePath('/users/{_private}');
|
|
474
|
+
assert.deepStrictEqual(paramNames, ['_private']);
|
|
475
|
+
});
|
|
476
|
+
it('accepts alphanumeric {userId123}', () => {
|
|
477
|
+
const { paramNames } = compilePath('/users/{userId123}');
|
|
478
|
+
assert.deepStrictEqual(paramNames, ['userId123']);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// ── Multiple wildcards validation ───────────────────────────────────────────
|
|
482
|
+
describe('multiple wildcards validation', () => {
|
|
483
|
+
it('rejects multiple wildcards /a/*/b/*', () => {
|
|
484
|
+
assert.throws(() => compilePath('/a/*/b/*'), (err) => {
|
|
485
|
+
assert.ok(err.message.includes('multiple wildcards'));
|
|
486
|
+
return true;
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
it('rejects middle wildcard /a/*/b', () => {
|
|
490
|
+
assert.throws(() => compilePath('/a/*/b'), (err) => {
|
|
491
|
+
assert.ok(err.message.includes('must be the last segment'));
|
|
492
|
+
return true;
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
it('trailing wildcard /v1/* still works', () => {
|
|
496
|
+
const { pattern, paramNames } = compilePath('/v1/*');
|
|
497
|
+
assert.deepStrictEqual(paramNames, ['*']);
|
|
498
|
+
const match = pattern.exec('/v1/anything/deep');
|
|
499
|
+
assert.ok(match);
|
|
500
|
+
assert.strictEqual(match[1], 'anything/deep');
|
|
501
|
+
});
|
|
502
|
+
it('registerRoute rejects multiple wildcards', () => {
|
|
503
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/a/*/b/*', handler: noop }), (err) => {
|
|
504
|
+
assert.ok(err.message.includes('multiple wildcards'));
|
|
505
|
+
return true;
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
it('registerRoute rejects middle wildcard', () => {
|
|
509
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/a/*/b', handler: noop }), (err) => {
|
|
510
|
+
assert.ok(err.message.includes('must be the last segment'));
|
|
511
|
+
return true;
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
// ── FIX 7: Prototype pollution prevention ───────────────────────────────────
|
|
516
|
+
describe('prototype pollution prevention', () => {
|
|
517
|
+
it('rejects __proto__ as param name at registration', () => {
|
|
518
|
+
assert.throws(() => compilePath('/users/{__proto__}'), (err) => {
|
|
519
|
+
assert.ok(err.message.includes("'__proto__' is reserved"));
|
|
520
|
+
return true;
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
it('rejects constructor as param name at registration', () => {
|
|
524
|
+
assert.throws(() => compilePath('/users/{constructor}'), (err) => {
|
|
525
|
+
assert.ok(err.message.includes("'constructor' is reserved"));
|
|
526
|
+
return true;
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
it('rejects prototype as param name at registration', () => {
|
|
530
|
+
assert.throws(() => compilePath('/users/{prototype}'), (err) => {
|
|
531
|
+
assert.ok(err.message.includes("'prototype' is reserved"));
|
|
532
|
+
return true;
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
it('registerRoute rejects __proto__ param', () => {
|
|
536
|
+
assert.throws(() => registerRoute({ method: 'GET', path: '/users/{__proto__}', handler: noop }), (err) => {
|
|
537
|
+
assert.ok(err.message.includes("'__proto__' is reserved"));
|
|
538
|
+
return true;
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
it('params object has no prototype chain', () => {
|
|
542
|
+
registerRoute({ method: 'GET', path: '/users/{id}', handler: noop });
|
|
543
|
+
const result = matchRoute('GET', '/users/42');
|
|
544
|
+
assert.ok(result);
|
|
545
|
+
assert.deepStrictEqual(Object.keys(result.params), ['id']);
|
|
546
|
+
assert.strictEqual(Object.getPrototypeOf(result.params), null);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
// ── Scope-chain path derivation ─────────────────────────────────────────────
|
|
550
|
+
describe('derivePathFromScope', () => {
|
|
551
|
+
// Chain model: root → topScope → [childScopes...] → RawRoute
|
|
552
|
+
// root = sentinel { id } (no parent) — the BlocksStack
|
|
553
|
+
// topScope = user's top-level Scope (parent = root) — excluded from path
|
|
554
|
+
// childScopes = intermediate scopes — included in path
|
|
555
|
+
const root = { id: 'stack' };
|
|
556
|
+
const topScope = { id: 'my-app', parent: root };
|
|
557
|
+
it('direct child of top-level scope → /{id}', () => {
|
|
558
|
+
assert.strictEqual(derivePathFromScope(topScope, 'health'), '/health');
|
|
559
|
+
});
|
|
560
|
+
it('child of root sentinel (no parent) → /{id}', () => {
|
|
561
|
+
assert.strictEqual(derivePathFromScope(root, 'health'), '/health');
|
|
562
|
+
});
|
|
563
|
+
it('nested under one child scope → /{childId}/{id}', () => {
|
|
564
|
+
const child = { id: 'v1', parent: topScope };
|
|
565
|
+
assert.strictEqual(derivePathFromScope(child, 'users'), '/v1/users');
|
|
566
|
+
});
|
|
567
|
+
it('deeply nested → /{a}/{b}/{id}', () => {
|
|
568
|
+
const a = { id: 'api', parent: topScope };
|
|
569
|
+
const b = { id: 'v2', parent: a };
|
|
570
|
+
assert.strictEqual(derivePathFromScope(b, 'items'), '/api/v2/items');
|
|
571
|
+
});
|
|
572
|
+
it('URL-encodes special characters in scope IDs', () => {
|
|
573
|
+
const child = { id: 'my routes', parent: topScope };
|
|
574
|
+
assert.strictEqual(derivePathFromScope(child, 'hello world'), '/my%20routes/hello%20world');
|
|
575
|
+
});
|
|
576
|
+
it('URL-encodes slashes in scope IDs', () => {
|
|
577
|
+
const child = { id: 'a/b', parent: topScope };
|
|
578
|
+
assert.strictEqual(derivePathFromScope(child, 'c/d'), '/a%2Fb/c%2Fd');
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
describe('resolveRoutePath', () => {
|
|
582
|
+
const root = { id: 'stack' };
|
|
583
|
+
const topScope = { id: 'app', parent: root };
|
|
584
|
+
it('explicit path is used when provided', () => {
|
|
585
|
+
const path = resolveRoutePath(topScope, 'health', { method: 'GET', path: '/custom', handler: noop });
|
|
586
|
+
assert.strictEqual(path, '/custom');
|
|
587
|
+
});
|
|
588
|
+
it('derives path when path is omitted', () => {
|
|
589
|
+
const path = resolveRoutePath(topScope, 'health', { method: 'GET', handler: noop });
|
|
590
|
+
assert.strictEqual(path, '/health');
|
|
591
|
+
});
|
|
592
|
+
it('derives nested path when path is omitted', () => {
|
|
593
|
+
const child = { id: 'v1', parent: topScope };
|
|
594
|
+
const path = resolveRoutePath(child, 'users', { method: 'GET', handler: noop });
|
|
595
|
+
assert.strictEqual(path, '/v1/users');
|
|
596
|
+
});
|
|
597
|
+
});
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive-field redaction for logging.
|
|
3
|
+
*
|
|
4
|
+
* The dev server logs RPC request/response bodies to help debug cross-stack
|
|
5
|
+
* wire-format mismatches. Those bodies carry credentials (sign-in passwords,
|
|
6
|
+
* new-password challenge responses) and bearer-style secrets (Cognito challenge
|
|
7
|
+
* `session` tokens, MFA shared secrets, OTP codes). Logging them verbatim puts
|
|
8
|
+
* plaintext passwords and reusable tokens in the log stream. This module masks
|
|
9
|
+
* those fields before anything is logged, without mutating the value that
|
|
10
|
+
* actually flows over the wire.
|
|
11
|
+
*
|
|
12
|
+
* Two complementary rules, because auth payloads hide secrets two ways:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Direct keys** — `{ password: '...', session: '...' }`. Redacted by
|
|
15
|
+
* field NAME, matched case-insensitively (substring).
|
|
16
|
+
*
|
|
17
|
+
* 2. **Form-field descriptors** — the auth state machine ships secrets as
|
|
18
|
+
* `{ name: 'session', defaultValue: '<token>' }` (see
|
|
19
|
+
* `@aws-blocks/auth-common` AuthField). Here the secret sits under the
|
|
20
|
+
* innocuous key `defaultValue`; the sensitive word is the *value* of a
|
|
21
|
+
* sibling `name`. Rule 1 alone would walk straight past it. So when an
|
|
22
|
+
* object carries a sensitive `name`, its `defaultValue` is redacted too.
|
|
23
|
+
*
|
|
24
|
+
* The wire payload is never altered — only the logged copy is scrubbed.
|
|
25
|
+
*/
|
|
26
|
+
/** The string substituted for any sensitive value. */
|
|
27
|
+
export declare const REDACTED: "[REDACTED]";
|
|
28
|
+
/**
|
|
29
|
+
* Return a deep copy of `value` with the value of every sensitive field
|
|
30
|
+
* replaced by {@link REDACTED}. Arrays and nested objects are walked; the
|
|
31
|
+
* input is never mutated. Circular references are emitted as `'[Circular]'`.
|
|
32
|
+
*
|
|
33
|
+
* Primitives are returned unchanged — only object/array *fields* are matched
|
|
34
|
+
* against the sensitive-key list, since a bare string has no field name to
|
|
35
|
+
* key off.
|
|
36
|
+
*
|
|
37
|
+
* `seen` tracks the current ancestor PATH, not every node ever visited: each
|
|
38
|
+
* node is added before its children are walked and removed afterwards. A
|
|
39
|
+
* plain visited-set would flag the second appearance of a *shared* (non-cyclic)
|
|
40
|
+
* reference as `'[Circular]'` — e.g. `{ a: shared, b: shared }` — silently
|
|
41
|
+
* dropping real data from the log. Only a reference still open on the stack is
|
|
42
|
+
* a genuine cycle.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactForLogging(value: unknown, seen?: WeakSet<object>): unknown;
|
|
45
|
+
/**
|
|
46
|
+
* Convenience for log call sites: redact `value` and serialize it to a JSON
|
|
47
|
+
* string. Returns a safe placeholder instead of throwing if serialization
|
|
48
|
+
* fails (e.g. a BigInt slips through), so logging can never crash a request.
|
|
49
|
+
*/
|
|
50
|
+
export declare function redactToJson(value: unknown): string;
|
|
51
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,sDAAsD;AACtD,eAAO,MAAM,QAAQ,EAAG,YAAqB,CAAC;AAwC9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GAAG,OAAO,CA2C/F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMnD"}
|